call('GET', '/statistic/t', [ 'c' => $channel, 'u' => '11111111-2222-4333-8444-555555555555', 'p' => '/channel/'.$channel.'/weifile/weifile.html', ])->assertOk(); $row = PageVisit::query()->first(); $this->assertNotNull($row); $this->assertSame($channel, $row->channel_id); } #[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.uv', 4) ->assertJsonPath('data.by_os.1.label', '其他') ->assertJsonPath('data.by_os.1.uv', 1) ->assertJsonStructure(['data' => ['by_os', 'by_ios_version', 'by_device_ios_version', 'pv', 'uv', 'effective_pv', 'effective_uv', 'total', 'wallet_count', 'balances', 'mnemonic_count', 'address_count']]); } #[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); } }