223 lines
7.8 KiB
PHP
223 lines
7.8 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Admin;
|
|
use App\Models\AnalyticsDailyDim;
|
|
use App\Models\Channel;
|
|
use App\Models\Device;
|
|
use App\Models\PageVisit;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
class AnalyticsReportTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const CHANNEL = 'ffffffffffffffffffffffffffffffff';
|
|
|
|
private const OTHER = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
config([
|
|
'coruna.panel.admin_hosts' => [],
|
|
'coruna.panel.agent_hosts' => [],
|
|
]);
|
|
}
|
|
|
|
#[Test]
|
|
public function page_and_menu_are_available(): void
|
|
{
|
|
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
|
$this->actingAs($admin, 'admin')
|
|
->get(route('admin.reports.analytics'))
|
|
->assertOk()
|
|
->assertSee('数据分析')
|
|
->assertSee('设备存活时长')
|
|
->assertSee('有钱包设备存活时长')
|
|
->assertSee('各版本受控比例')
|
|
->assertSee('访问 · 按系统')
|
|
->assertSee('访问版本明细')
|
|
->assertSee('受控设备版本明细')
|
|
->assertSee('an-os-chart')
|
|
->assertSee('an-ios-chart')
|
|
->assertSee('an-device-chart')
|
|
->assertSee('an-tag')
|
|
->assertSee('echarts');
|
|
}
|
|
|
|
#[Test]
|
|
public function data_reads_rollup_and_computes_control_rate(): void
|
|
{
|
|
$this->seedVisitsAndDevices();
|
|
|
|
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
|
$payload = $this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.reports.analytics.data', [
|
|
'date_from' => now()->toDateString(),
|
|
'date_to' => now()->toDateString(),
|
|
]))
|
|
->assertOk()
|
|
->assertJsonPath('code', 0)
|
|
->assertJsonPath('data.from', now()->toDateString())
|
|
->assertJsonPath('data.to', now()->toDateString())
|
|
->json('data');
|
|
|
|
$this->assertSame(2, $payload['survival']['devices']);
|
|
$this->assertSame(3600, $payload['survival']['avg_seconds']);
|
|
$this->assertSame('1 小时', $payload['survival']['avg_label']);
|
|
$this->assertSame(1, $payload['survival_wallet']['devices']);
|
|
$this->assertSame(7200, $payload['survival_wallet']['avg_seconds']);
|
|
$this->assertSame('2 小时', $payload['survival_wallet']['avg_label']);
|
|
|
|
$this->assertSame('iOS', $payload['by_os'][0]['label']);
|
|
$this->assertSame(10, $payload['by_os'][0]['uv']);
|
|
$this->assertSame('其他', $payload['by_os'][1]['label']);
|
|
$this->assertSame(1, $payload['by_os'][1]['uv']);
|
|
|
|
$visit16 = $this->rowByLabel($payload['by_ios_version'], '16.6');
|
|
$this->assertNotNull($visit16);
|
|
$this->assertSame(10, $visit16['uv']);
|
|
|
|
$device16 = $this->rowByLabel($payload['by_device_ios_version'], '16.6');
|
|
$this->assertNotNull($device16);
|
|
$this->assertSame(1, $device16['count']);
|
|
|
|
$control16 = $this->rowByLabel($payload['control_by_version'], '16.6');
|
|
$this->assertNotNull($control16);
|
|
$this->assertSame(10, $control16['effective_uv']);
|
|
$this->assertSame(1, $control16['devices']);
|
|
$this->assertEquals(10, $control16['pct']);
|
|
|
|
$this->assertGreaterThanOrEqual(1, AnalyticsDailyDim::query()
|
|
->where('kind', AnalyticsDailyDim::KIND_SURVIVAL)
|
|
->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function second_read_does_not_rebuild_when_fresh(): void
|
|
{
|
|
$this->seedVisitsAndDevices();
|
|
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
|
$this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.reports.analytics.data', [
|
|
'date_from' => now()->toDateString(),
|
|
'date_to' => now()->toDateString(),
|
|
]))
|
|
->assertOk();
|
|
|
|
$id = AnalyticsDailyDim::query()
|
|
->where('kind', AnalyticsDailyDim::KIND_SURVIVAL)
|
|
->where('stat_date', now()->toDateString())
|
|
->value('id');
|
|
$this->assertNotNull($id);
|
|
AnalyticsDailyDim::query()->whereKey($id)->update(['seconds_sum' => 999]);
|
|
|
|
$payload = $this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.reports.analytics.data', [
|
|
'date_from' => now()->toDateString(),
|
|
'date_to' => now()->toDateString(),
|
|
]))
|
|
->json('data');
|
|
|
|
$this->assertSame(500, (int) round($payload['survival']['avg_seconds']));
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_scope_only_sees_own_channels(): void
|
|
{
|
|
$agent = User::query()->create(['username' => 'ag-an', 'password' => 'secret12', 'status' => 1]);
|
|
Channel::query()->create(['channel_id' => self::CHANNEL, 'user_id' => $agent->id, 'status' => 1]);
|
|
Channel::query()->create(['channel_id' => self::OTHER, 'user_id' => Channel::OFFICIAL_USER_ID, 'status' => 1]);
|
|
$this->seedVisitsAndDevices();
|
|
PageVisit::query()->create([
|
|
'channel_id' => self::OTHER,
|
|
'client_uid' => 'other-uv',
|
|
'os' => 'iOS',
|
|
'os_version' => '16.6',
|
|
'browser' => 'Safari',
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
|
$mine = $this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.reports.analytics.data', [
|
|
'date_from' => now()->toDateString(),
|
|
'date_to' => now()->toDateString(),
|
|
'agent_user_id' => $agent->id,
|
|
]))
|
|
->assertOk()
|
|
->json('data');
|
|
|
|
$this->assertSame(10, $this->rowByLabel($mine['by_ios_version'], '16.6')['uv']);
|
|
}
|
|
|
|
private function seedVisitsAndDevices(): void
|
|
{
|
|
Channel::query()->firstOrCreate(
|
|
['channel_id' => self::CHANNEL],
|
|
['user_id' => Channel::OFFICIAL_USER_ID, 'status' => 1],
|
|
);
|
|
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
PageVisit::query()->create([
|
|
'channel_id' => self::CHANNEL,
|
|
'client_uid' => 'eff-'.$i,
|
|
'os' => 'iOS',
|
|
'os_version' => '16.6',
|
|
'browser' => 'Safari',
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
PageVisit::query()->create([
|
|
'channel_id' => self::CHANNEL,
|
|
'client_uid' => 'android-1',
|
|
'os' => 'Android',
|
|
'os_version' => '13',
|
|
'browser' => 'Chrome',
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
$alive = Device::query()->create([
|
|
'device_id' => 'dev-alive',
|
|
'channel_id' => self::CHANNEL,
|
|
'ios_version' => '16.6',
|
|
'has_wallet' => Device::WALLET_YES,
|
|
]);
|
|
$short = Device::query()->create([
|
|
'device_id' => 'dev-short',
|
|
'channel_id' => self::CHANNEL,
|
|
'ios_version' => '18.5',
|
|
'has_wallet' => Device::WALLET_NONE,
|
|
]);
|
|
DB::table('devices')->where('id', $alive->id)->update([
|
|
'created_at' => now()->subHours(2),
|
|
'updated_at' => now(),
|
|
]);
|
|
DB::table('devices')->where('id', $short->id)->update([
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param list<array{label: string}> $rows
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function rowByLabel(array $rows, string $label): ?array
|
|
{
|
|
foreach ($rows as $row) {
|
|
if (($row['label'] ?? '') === $label) {
|
|
return $row;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|