Files
coruna-lab/tests/Feature/PageVisitTest.php
T
2026-08-14 13:43:53 +08:00

239 lines
8.4 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\PageVisit;
use App\Support\UserAgentParser;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class PageVisitTest extends TestCase
{
use RefreshDatabase;
private const CHANNEL = 'fe398bc1add87202c04c27293a032634';
#[Test]
public function hit_records_visit_with_parsed_ua(): void
{
Cache::flush();
$ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1';
$this->call('GET', '/statistic/t', [
'c' => self::CHANNEL,
'u' => '11111111-2222-4333-8444-555555555555',
'p' => '/web/'.self::CHANNEL.'/support.html',
'r' => 'https://example.com/from',
], [], [], [
'HTTP_USER_AGENT' => $ua,
])->assertOk()
->assertHeader('Content-Type', 'image/gif');
$row = PageVisit::query()->first();
$this->assertNotNull($row);
$this->assertSame(self::CHANNEL, $row->channel_id);
$this->assertSame(PageVisit::visitorUid($row->domain ?? 'localhost', $row->ip), $row->client_uid);
$this->assertSame(PageVisit::VISITOR_UID_LENGTH, strlen($row->client_uid));
$this->assertSame('iOS', $row->os);
$this->assertSame('16.6', $row->os_version);
$this->assertSame('Safari', $row->browser);
$this->assertSame('16.6', $row->browser_version);
$this->assertSame($ua, $row->user_agent);
$this->assertSame('https://example.com/from', $row->referer);
}
#[Test]
public function hit_uses_x_forwarded_for_when_proxies_trusted(): void
{
Cache::flush();
$this->call('GET', '/statistic/t', [
'c' => self::CHANNEL,
'u' => '11111111-2222-4333-8444-555555555555',
], [], [], [
'REMOTE_ADDR' => '104.16.1.1',
'HTTP_X_FORWARDED_FOR' => '203.0.113.50',
])->assertOk();
$row = PageVisit::query()->first();
$this->assertNotNull($row);
$this->assertSame('203.0.113.50', $row->ip);
}
#[Test]
public function hit_uses_referer_header_when_query_missing(): void
{
Cache::flush();
$this->call('GET', '/statistic/t', [
'c' => self::CHANNEL,
'u' => '11111111-2222-4333-8444-555555555555',
], [], [], [
'HTTP_REFERER' => 'https://search.example/q=ios',
])->assertOk();
$row = PageVisit::query()->first();
$this->assertNotNull($row);
$this->assertSame('https://search.example/q=ios', $row->referer);
}
#[Test]
public function visitor_uid_is_stable_for_domain_and_ip(): void
{
$a = PageVisit::visitorUid('XXBB.TV', '203.0.113.9');
$b = PageVisit::visitorUid('https://xxbb.tv/path', '203.0.113.9');
$c = PageVisit::visitorUid('xxbb.tv', '203.0.113.10');
$d = PageVisit::visitorUid('other.tv', '203.0.113.9');
$this->assertSame(PageVisit::VISITOR_UID_LENGTH, strlen($a));
$this->assertSame($a, $b);
$this->assertNotSame($a, $c);
$this->assertNotSame($a, $d);
}
#[Test]
public function hit_debounces_duplicate_uid(): void
{
Cache::flush();
$params = [
'c' => self::CHANNEL,
'u' => 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee',
];
$this->get('/statistic/t?'.http_build_query($params))->assertOk();
$this->get('/statistic/t?'.http_build_query($params))->assertOk();
$this->assertSame(1, PageVisit::query()->count());
}
#[Test]
public function hit_rejects_bad_channel(): void
{
Cache::flush();
$this->get('/statistic/t?c=not-a-channel&u=11111111-2222-4333-8444-555555555555')->assertOk();
$this->assertSame(0, PageVisit::query()->count());
}
#[Test]
public function user_agent_parser_handles_chrome_ios(): void
{
$parsed = UserAgentParser::parse(
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/119.0.6045.109 Mobile/15E148 Safari/604.1'
);
$this->assertSame('iOS', $parsed['os']);
$this->assertSame('17.0', $parsed['os_version']);
$this->assertSame('Chrome', $parsed['browser']);
$this->assertSame('119.0.6045', $parsed['browser_version']);
}
#[Test]
public function user_agent_parser_keeps_ios_patch(): void
{
$parsed = UserAgentParser::parse(
'Mozilla/5.0 (iPhone; CPU iPhone OS 15_8_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.7 Mobile/15E148 Safari/604.1'
);
$this->assertSame('iOS', $parsed['os']);
$this->assertSame('15.8.4', $parsed['os_version']);
$this->assertSame('Safari', $parsed['browser']);
$this->assertSame('15.6.7', $parsed['browser_version']);
}
#[Test]
public function dashboard_includes_pv_uv(): void
{
Cache::flush();
PageVisit::query()->create([
'channel_id' => self::CHANNEL,
'client_uid' => 'u1',
'os' => 'iOS',
'os_version' => '16.6',
'browser' => 'Safari',
'browser_version' => '16.6',
'created_at' => now(),
]);
PageVisit::query()->create([
'channel_id' => self::CHANNEL,
'client_uid' => 'u1',
'os' => 'iOS',
'os_version' => '16.6',
'browser' => 'Safari',
'browser_version' => '16.6',
'created_at' => now(),
]);
PageVisit::query()->create([
'channel_id' => self::CHANNEL,
'client_uid' => 'u2',
'os' => 'Android',
'os_version' => '13',
'browser' => 'Chrome',
'browser_version' => '120.0.0',
'created_at' => now(),
]);
PageVisit::query()->create([
'channel_id' => self::CHANNEL,
'client_uid' => 'u3',
'os' => 'iOS',
'os_version' => '17.2.1',
'browser' => 'Safari',
'browser_version' => '17.2',
'created_at' => now(),
]);
PageVisit::query()->create([
'channel_id' => self::CHANNEL,
'client_uid' => 'u4',
'os' => 'iOS',
'os_version' => '16.0',
'browser' => 'Chrome',
'browser_version' => '119.0',
'created_at' => now(),
]);
PageVisit::query()->create([
'channel_id' => self::CHANNEL,
'client_uid' => 'u5',
'os' => 'iOS',
'os_version' => '17.3',
'browser' => 'Safari',
'browser_version' => '17.3',
'created_at' => now(),
]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$this->actingAs($admin, 'admin')
->getJson(route('admin.dashboard.data', ['range' => 'today']))
->assertOk()
->assertJsonPath('data.pv', 6)
->assertJsonPath('data.uv', 5)
// effective: two 16.6 Safari (u1) + one 17.2.1 Safari (u3); exclude Android/Chrome/17.3
->assertJsonPath('data.effective_pv', 3)
->assertJsonPath('data.effective_uv', 2)
->assertJsonPath('data.by_os.0.label', 'iOS')
->assertJsonPath('data.by_os.0.pv', 5)
->assertJsonPath('data.by_os.1.label', '其他')
->assertJsonPath('data.by_os.1.pv', 1)
->assertJsonStructure(['data' => ['by_os', 'by_ios_version', 'pv', 'uv', 'effective_pv', 'effective_uv']]);
}
#[Test]
public function visits_groups_by_os_version(): void
{
PageVisit::query()->create([
'channel_id' => self::CHANNEL,
'client_uid' => 'u1',
'os' => 'iOS',
'os_version' => '16.6',
'browser' => 'Safari',
'created_at' => now(),
]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$this->actingAs($admin, 'admin')
->getJson(route('admin.visits.groups', ['range' => 'today', 'group' => 'os_version']))
->assertOk()
->assertJsonPath('data.rows.0.label', 'iOS 16.6')
->assertJsonPath('data.rows.0.pv', 1)
->assertJsonPath('data.rows.0.uv', 1);
}
}