fix: link old

This commit is contained in:
hashbro
2026-09-14 01:09:01 +08:00
parent b49755c900
commit d5ae0bf6ff
29 changed files with 1584 additions and 212 deletions
+1 -5
View File
@@ -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]
+222
View File
@@ -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;
}
}
+3 -1
View File
@@ -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
+1 -5
View File
@@ -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]
+120
View File
@@ -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);
}
}
+2
View File
@@ -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)
-2
View File
@@ -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);
}