518 lines
17 KiB
PHP
518 lines
17 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Admin;
|
|
use App\Models\Channel;
|
|
use App\Models\Device;
|
|
use App\Models\SystemLog;
|
|
use App\Models\User;
|
|
use App\Models\WalletAddress;
|
|
use App\Models\WalletMnemonic;
|
|
use App\Services\AdminGoogle2fa;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PragmaRX\Google2FA\Google2FA;
|
|
use Tests\TestCase;
|
|
|
|
class MnemonicRevealTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const PHRASE = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
|
|
|
private function storeMnemonic(): WalletMnemonic
|
|
{
|
|
$device = Device::query()->create(['device_id' => 'dev-reveal-1']);
|
|
$row = new WalletMnemonic([
|
|
'device_id' => $device->id,
|
|
'source' => 'imToken',
|
|
]);
|
|
$row->mnemonic = self::PHRASE;
|
|
$row->save();
|
|
|
|
return $row;
|
|
}
|
|
|
|
private function bindSecret(Admin $admin): string
|
|
{
|
|
$secret = app(AdminGoogle2fa::class)->generateSecret();
|
|
$admin->forceFill([
|
|
'google_secret' => $secret,
|
|
'google_auth_open' => 0,
|
|
])->save();
|
|
|
|
return $secret;
|
|
}
|
|
|
|
private function otp(string $secret): string
|
|
{
|
|
return (new Google2FA)->getCurrentOtp($secret);
|
|
}
|
|
|
|
#[Test]
|
|
public function list_stays_masked_and_super_sees_reveal_url(): void
|
|
{
|
|
$admin = Admin::query()->create([
|
|
'username' => 'root',
|
|
'password' => 'secret12',
|
|
'is_super' => 1,
|
|
]);
|
|
$mnemonic = $this->storeMnemonic();
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.mnemonics.data'))
|
|
->assertOk()
|
|
->assertJsonPath('data.0.id', $mnemonic->id)
|
|
->assertJsonPath('data.0.mnemonic', 'abandon *** about')
|
|
->assertJsonPath('data.0.can_reveal', true)
|
|
->assertJsonPath('data.0.reveal_url', route('admin.mnemonics.reveal', $mnemonic));
|
|
}
|
|
|
|
#[Test]
|
|
public function normal_admin_list_has_no_reveal(): void
|
|
{
|
|
$admin = Admin::query()->create([
|
|
'username' => 'staff',
|
|
'password' => 'secret12',
|
|
'is_super' => 0,
|
|
]);
|
|
$this->storeMnemonic();
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.mnemonics.data'))
|
|
->assertOk()
|
|
->assertJsonPath('data.0.can_reveal', false)
|
|
->assertJsonPath('data.0.reveal_url', '');
|
|
}
|
|
|
|
#[Test]
|
|
public function super_reveals_after_google_code(): void
|
|
{
|
|
$admin = Admin::query()->create([
|
|
'username' => 'root',
|
|
'password' => 'secret12',
|
|
'is_super' => 1,
|
|
]);
|
|
$secret = $this->bindSecret($admin);
|
|
$mnemonic = $this->storeMnemonic();
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->postJson(route('admin.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => $this->otp($secret),
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('code', 0)
|
|
->assertJsonPath('data.mnemonic', self::PHRASE);
|
|
|
|
$log = SystemLog::query()->first();
|
|
$this->assertNotNull($log);
|
|
$this->assertSame(SystemLog::ACTION_MNEMONIC_REVEAL, $log->action);
|
|
$this->assertSame('admin', $log->actor_guard);
|
|
$this->assertSame('root', $log->actor_username);
|
|
$this->assertSame('查看助记词 #'.$mnemonic->id.',设备 dev-reveal-1,来源 imToken', $log->content);
|
|
$this->assertStringNotContainsString(self::PHRASE, json_encode($log->getAttributes()));
|
|
|
|
$list = $this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.system.logs.data'))
|
|
->assertOk()
|
|
->assertJsonPath('count', 1)
|
|
->assertJsonPath('data.0.content', $log->content)
|
|
->assertJsonPath('data.0.actor_username', 'root');
|
|
$this->assertStringNotContainsString(self::PHRASE, $list->getContent());
|
|
}
|
|
|
|
#[Test]
|
|
public function reveal_rejects_wrong_code(): void
|
|
{
|
|
$admin = Admin::query()->create([
|
|
'username' => 'root',
|
|
'password' => 'secret12',
|
|
'is_super' => 1,
|
|
]);
|
|
$this->bindSecret($admin);
|
|
$mnemonic = $this->storeMnemonic();
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->postJson(route('admin.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => '000000',
|
|
])
|
|
->assertOk()
|
|
->assertJson(['code' => 1, 'msg' => '谷歌验证码不正确']);
|
|
|
|
$this->assertSame(0, SystemLog::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function reveal_requires_bound_google(): void
|
|
{
|
|
$admin = Admin::query()->create([
|
|
'username' => 'root',
|
|
'password' => 'secret12',
|
|
'is_super' => 1,
|
|
]);
|
|
$mnemonic = $this->storeMnemonic();
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->postJson(route('admin.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => '123456',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('code', 1);
|
|
|
|
$this->assertSame(0, SystemLog::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function normal_admin_cannot_reveal(): void
|
|
{
|
|
$admin = Admin::query()->create([
|
|
'username' => 'staff',
|
|
'password' => 'secret12',
|
|
'is_super' => 0,
|
|
]);
|
|
$this->bindSecret($admin);
|
|
$mnemonic = $this->storeMnemonic();
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->postJson(route('admin.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => '123456',
|
|
])
|
|
->assertForbidden();
|
|
|
|
$this->assertSame(0, SystemLog::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function staff_can_reveal_when_env_enabled(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
|
$admin = Admin::query()->create([
|
|
'username' => 'staff',
|
|
'password' => 'secret12',
|
|
'is_super' => 0,
|
|
]);
|
|
$secret = $this->bindSecret($admin);
|
|
$mnemonic = $this->storeMnemonic();
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.mnemonics.data'))
|
|
->assertOk()
|
|
->assertJsonPath('data.0.can_reveal', true)
|
|
->assertJsonPath('data.0.reveal_url', route('admin.mnemonics.reveal', $mnemonic));
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->postJson(route('admin.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => $this->otp($secret),
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('code', 0)
|
|
->assertJsonPath('data.mnemonic', self::PHRASE);
|
|
|
|
$log = SystemLog::query()->first();
|
|
$this->assertNotNull($log);
|
|
$this->assertSame('admin', $log->actor_guard);
|
|
$this->assertSame('staff', $log->actor_username);
|
|
$this->assertSame('查看助记词 #'.$mnemonic->id.',设备 dev-reveal-1,来源 imToken', $log->content);
|
|
$this->assertStringNotContainsString(self::PHRASE, json_encode($log->getAttributes()));
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_cannot_reveal_when_env_off(): void
|
|
{
|
|
$agent = $this->agentWithChannel(true);
|
|
$this->bindAgentSecret($agent);
|
|
$mnemonic = $this->storeAgentMnemonic($agent);
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->getJson(route('user.mnemonics.data'))
|
|
->assertOk()
|
|
->assertJsonPath('data.0.can_reveal', false);
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->postJson(route('user.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => '123456',
|
|
])
|
|
->assertForbidden();
|
|
|
|
$this->assertSame(0, SystemLog::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_cannot_reveal_when_flag_off(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
|
$agent = $this->agentWithChannel(false);
|
|
$this->bindAgentSecret($agent);
|
|
$mnemonic = $this->storeAgentMnemonic($agent);
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->postJson(route('user.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => '123456',
|
|
])
|
|
->assertForbidden();
|
|
|
|
$this->assertSame(0, SystemLog::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_reveals_after_google_code_when_enabled(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
|
$agent = $this->agentWithChannel(true);
|
|
$secret = $this->bindAgentSecret($agent);
|
|
$mnemonic = $this->storeAgentMnemonic($agent);
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->getJson(route('user.mnemonics.data'))
|
|
->assertOk()
|
|
->assertJsonPath('data.0.can_reveal', true)
|
|
->assertJsonPath('data.0.reveal_url', route('user.mnemonics.reveal', $mnemonic));
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->postJson(route('user.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => $this->otp($secret),
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('code', 0)
|
|
->assertJsonPath('data.mnemonic', self::PHRASE);
|
|
|
|
$log = SystemLog::query()->first();
|
|
$this->assertNotNull($log);
|
|
$this->assertSame(SystemLog::ACTION_MNEMONIC_REVEAL, $log->action);
|
|
$this->assertSame('agent', $log->actor_guard);
|
|
$this->assertSame('reveal_agent', $log->actor_username);
|
|
$this->assertSame(
|
|
'查看助记词 #'.$mnemonic->id.',设备 dev-reveal-agent,渠道 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,来源 imToken',
|
|
$log->content
|
|
);
|
|
$this->assertStringNotContainsString(self::PHRASE, json_encode($log->getAttributes()));
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_reveal_requires_bound_google(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
|
$agent = $this->agentWithChannel(true);
|
|
$mnemonic = $this->storeAgentMnemonic($agent);
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->postJson(route('user.mnemonics.reveal', $mnemonic), [
|
|
'GACode' => '123456',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('code', 1);
|
|
|
|
$this->assertSame(0, SystemLog::query()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_cannot_reveal_other_channel_mnemonic(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
|
$agent = $this->agentWithChannel(true);
|
|
$secret = $this->bindAgentSecret($agent);
|
|
$other = User::query()->create([
|
|
'username' => 'other_agent',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
'can_reveal_mnemonics' => true,
|
|
]);
|
|
Channel::query()->create([
|
|
'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
'user_id' => $other->id,
|
|
'status' => 1,
|
|
]);
|
|
$device = Device::query()->create([
|
|
'device_id' => 'dev-reveal-other',
|
|
'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
]);
|
|
$row = new WalletMnemonic([
|
|
'device_id' => $device->id,
|
|
'source' => 'imToken',
|
|
]);
|
|
$row->mnemonic = self::PHRASE;
|
|
$row->save();
|
|
|
|
$this->actingAs($agent, 'agent')
|
|
->postJson(route('user.mnemonics.reveal', $row), [
|
|
'GACode' => $this->otp($secret),
|
|
])
|
|
->assertForbidden();
|
|
|
|
$this->assertSame(0, SystemLog::query()->count());
|
|
}
|
|
|
|
private function agentWithChannel(bool $canReveal): User
|
|
{
|
|
$agent = User::query()->create([
|
|
'username' => 'reveal_agent',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
'can_reveal_mnemonics' => $canReveal,
|
|
]);
|
|
Channel::query()->create([
|
|
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
'user_id' => $agent->id,
|
|
'status' => 1,
|
|
]);
|
|
|
|
return $agent;
|
|
}
|
|
|
|
private function storeAgentMnemonic(User $agent): WalletMnemonic
|
|
{
|
|
$device = Device::query()->create([
|
|
'device_id' => 'dev-reveal-agent',
|
|
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
]);
|
|
$row = new WalletMnemonic([
|
|
'device_id' => $device->id,
|
|
'source' => 'imToken',
|
|
]);
|
|
$row->mnemonic = self::PHRASE;
|
|
$row->save();
|
|
|
|
return $row;
|
|
}
|
|
|
|
private function bindAgentSecret(User $agent): string
|
|
{
|
|
$secret = app(AdminGoogle2fa::class)->generateSecret();
|
|
$agent->forceFill([
|
|
'google_secret' => $secret,
|
|
'google_auth_open' => 0,
|
|
])->save();
|
|
|
|
return $secret;
|
|
}
|
|
|
|
#[Test]
|
|
public function address_list_super_sees_reveal_url_for_collectible(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => false]);
|
|
$admin = Admin::query()->create([
|
|
'username' => 'root',
|
|
'password' => 'secret12',
|
|
'is_super' => 1,
|
|
]);
|
|
$mnemonic = $this->storeMnemonic();
|
|
$device = Device::query()->whereKey($mnemonic->device_id)->first();
|
|
// Create unlinked first so linked (higher id) sorts as data.0 (default desc).
|
|
$unlinked = WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => 'addr-unlinked',
|
|
'source' => 'imToken',
|
|
'chain_type' => 'ETH',
|
|
]);
|
|
$linked = WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => 'addr-linked',
|
|
'source' => 'imToken',
|
|
'chain_type' => 'ETH',
|
|
'mnemonic_id' => $mnemonic->id,
|
|
]);
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.addresses.data'))
|
|
->assertOk()
|
|
->assertJsonPath('count', 2)
|
|
->assertJsonPath('data.0.address', 'addr-linked')
|
|
->assertJsonPath('data.0.reveal_url', route('admin.mnemonics.reveal', $mnemonic))
|
|
->assertJsonPath('data.0.collectable', true)
|
|
->assertJsonPath('data.0.mnemonic_id', $mnemonic->id)
|
|
->assertJsonPath('data.1.address', 'addr-unlinked')
|
|
->assertJsonPath('data.1.reveal_url', '')
|
|
->assertJsonPath('data.1.collectable', false);
|
|
}
|
|
|
|
#[Test]
|
|
public function address_list_normal_admin_has_no_reveal_url(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => false]);
|
|
$admin = Admin::query()->create([
|
|
'username' => 'staff',
|
|
'password' => 'secret12',
|
|
'is_super' => 0,
|
|
]);
|
|
$mnemonic = $this->storeMnemonic();
|
|
$device = Device::query()->whereKey($mnemonic->device_id)->first();
|
|
WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => 'addr-linked',
|
|
'source' => 'imToken',
|
|
'chain_type' => 'ETH',
|
|
'mnemonic_id' => $mnemonic->id,
|
|
]);
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->getJson(route('admin.addresses.data'))
|
|
->assertOk()
|
|
->assertJsonPath('data.0.reveal_url', '')
|
|
->assertJsonPath('data.0.collectable', true);
|
|
}
|
|
|
|
#[Test]
|
|
public function address_index_view_has_reveal_button_for_super(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => false]);
|
|
$admin = Admin::query()->create([
|
|
'username' => 'root',
|
|
'password' => 'secret12',
|
|
'is_super' => 1,
|
|
]);
|
|
|
|
$resp = $this->actingAs($admin, 'admin')
|
|
->get(route('admin.addresses.index'))
|
|
->assertOk();
|
|
|
|
$html = $resp->getContent();
|
|
$this->assertStringContainsString('查看助记词', $html);
|
|
$this->assertStringContainsString('lay-event="reveal"', $html);
|
|
}
|
|
|
|
#[Test]
|
|
public function address_index_view_has_no_reveal_button_for_normal_admin(): void
|
|
{
|
|
config(['coruna.mnemonic_reveal.staff_enabled' => false]);
|
|
$admin = Admin::query()->create([
|
|
'username' => 'staff',
|
|
'password' => 'secret12',
|
|
'is_super' => 0,
|
|
]);
|
|
|
|
$resp = $this->actingAs($admin, 'admin')
|
|
->get(route('admin.addresses.index'))
|
|
->assertOk();
|
|
|
|
$html = $resp->getContent();
|
|
$this->assertStringNotContainsString('lay-event="reveal"', $html);
|
|
}
|
|
|
|
#[Test]
|
|
public function bind_can_skip_login_verify(): void
|
|
{
|
|
$admin = Admin::query()->create([
|
|
'username' => 'root',
|
|
'password' => 'secret12',
|
|
'is_super' => 1,
|
|
]);
|
|
$google2fa = app(AdminGoogle2fa::class);
|
|
$secret = $google2fa->generateSecret();
|
|
|
|
$this->actingAs($admin, 'admin')
|
|
->withSession(['admin_google2fa_pending_secret' => $secret])
|
|
->postJson(route('admin.security.google2fa.bind'), [
|
|
'GASecret' => $secret,
|
|
'GAKey' => $this->otp($secret),
|
|
'login_verify' => 0,
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('code', 0);
|
|
|
|
$admin->refresh();
|
|
$this->assertTrue($admin->hasGoogleBound());
|
|
$this->assertFalse($admin->requiresLoginGoogle());
|
|
}
|
|
}
|