feat: ds
This commit is contained in:
@@ -563,6 +563,8 @@ class C2ApiTest extends TestCase
|
||||
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($ks);
|
||||
$this->assertSame('aes-128-ctr', $ks->raw_json['crypto']['cipher'] ?? null);
|
||||
$this->assertSame('imToken', $ks->source);
|
||||
$this->assertSame(0, (int) $ks->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -815,6 +817,7 @@ class C2ApiTest extends TestCase
|
||||
->assertOk()
|
||||
->assertSee('钱包地址')
|
||||
->assertSee('助记词')
|
||||
->assertSee('钥匙串')
|
||||
->assertSee('相册')
|
||||
->assertSee('已装 APP')
|
||||
->assertSee('备忘录')
|
||||
@@ -826,5 +829,19 @@ class C2ApiTest extends TestCase
|
||||
$this->getJson(route('admin.devices.tabData', [$device, 'tab' => 'apps']))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
|
||||
WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => '',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => ['kind' => 'sandbox'],
|
||||
]);
|
||||
$this->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.0.source', '未知')
|
||||
->assertJsonPath('data.0.decrypted', 0)
|
||||
->assertJsonPath('data.0.kind', '沙盒文件')
|
||||
->assertJsonPath('data.0.item_count', 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,851 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceApp;
|
||||
use App\Models\DeviceEvent;
|
||||
use App\Models\DsBeaconTask;
|
||||
use App\Models\DsChainLog;
|
||||
use App\Models\Note;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\Photo;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletKeystore;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\CorunaCrypto;
|
||||
use App\Services\DsBeaconQueue;
|
||||
use App\Services\EthKeystore;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DarkSwordC2ApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const DS_LHU = '69DD25B2CA8B5682BA2470D77124E2FC';
|
||||
|
||||
private const XXBB_D = '000C30D83CD0402E';
|
||||
|
||||
private const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function log_post_returns_accepted(): void
|
||||
{
|
||||
$this->postJson('/api/ds/log', ['text' => 'hello', 'source' => 'pe_mpd'])
|
||||
->assertOk()
|
||||
->assertExactJson(['status' => 'accepted']);
|
||||
$this->assertSame(0, DsChainLog::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function log_with_stage_writes_chain_log_and_dedups(): void
|
||||
{
|
||||
$payload = [
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'stage' => 'loader',
|
||||
'progress' => 18,
|
||||
'label' => 'loader',
|
||||
'channelCode' => 'BODOZR5F613N9',
|
||||
];
|
||||
$this->postJson('/api/ds/log', $payload)->assertOk()->assertJson(['status' => 'accepted']);
|
||||
$this->postJson('/api/ds/log', $payload)->assertOk();
|
||||
|
||||
$this->assertSame(1, DsChainLog::query()->count());
|
||||
$row = DsChainLog::query()->first();
|
||||
$this->assertSame('50624FE26CC4A0DF689EAEA117557C3E', $row->client_uid);
|
||||
$this->assertSame('loader', $row->stage);
|
||||
$this->assertSame(18, $row->progress);
|
||||
$this->assertSame('BODOZR5F613N9', $row->channel_id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function log_infers_stage_and_ignores_noise(): void
|
||||
{
|
||||
$this->postJson('/api/ds/log', [
|
||||
'text' => 'malloc ok 0x1234',
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
])->assertOk();
|
||||
$this->assertSame(0, DsChainLog::query()->count());
|
||||
|
||||
$this->postJson('/api/ds/log', [
|
||||
'text' => 'pe_main_start',
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
])->assertOk();
|
||||
$row = DsChainLog::query()->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame('pe', $row->stage);
|
||||
$this->assertSame(86, $row->progress);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function next_chain_does_not_steal_ds_api_or_logs(): void
|
||||
{
|
||||
$this->getJson('/api/ds/chain-targets?ios=18.6')
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
'chain' => 'darksword',
|
||||
]);
|
||||
|
||||
$this->get('/log.html?text=lab')
|
||||
->assertOk()
|
||||
->assertSee('ok', false);
|
||||
|
||||
$this->get('/api/ds/log?text=lab')
|
||||
->assertOk()
|
||||
->assertSee('ok', false);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pe_stage_get_writes_file_log_and_chain_row(): void
|
||||
{
|
||||
$this->get('/api/ds/pe-stage/s1_launchd?deviceUUID=50624FE26CC4A0DF689EAEA117557C3E')
|
||||
->assertOk()
|
||||
->assertSee('ok', false);
|
||||
|
||||
$row = DsChainLog::query()->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame('50624FE26CC4A0DF689EAEA117557C3E', $row->client_uid);
|
||||
$this->assertSame('pe', $row->stage);
|
||||
$this->assertSame(86, $row->progress);
|
||||
$this->assertSame('pe_stage:s1_launchd', $row->label);
|
||||
|
||||
$this->get('/api/ds/pe-stage/s5_c2.js?deviceUUID=50624FE26CC4A0DF689EAEA117557C3E')
|
||||
->assertOk();
|
||||
$this->assertSame(2, DsChainLog::query()->count());
|
||||
$this->assertSame('pe_stage:s5_c2', DsChainLog::query()->orderByDesc('id')->first()->label);
|
||||
$this->assertSame(94, DsChainLog::query()->orderByDesc('id')->first()->progress);
|
||||
|
||||
$this->get('/api/ds/pe-stage/s2_keychain')
|
||||
->assertOk()
|
||||
->assertSee('ok', false);
|
||||
$this->assertSame(2, DsChainLog::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function plaintext_a_ingests_darksword_device(): void
|
||||
{
|
||||
$this->postJson('/a', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'machine' => 'iPhone15,2',
|
||||
'ios_version' => '18.6',
|
||||
'ip' => '192.168.31.77',
|
||||
'source' => 'c2_agent',
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame(Device::FAMILY_DARKSWORD, $device->family);
|
||||
$this->assertSame('iPhone15,2', $device->device_model);
|
||||
$this->assertSame('18.6', $device->ios_version);
|
||||
$this->assertSame('192.168.31.77', $device->ip);
|
||||
$this->assertNull($device->channel_id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function shared_path_with_x_ts_still_uses_xxbb_ack(): void
|
||||
{
|
||||
$this->xxbbPost('/a', [
|
||||
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
||||
'd' => self::XXBB_D,
|
||||
'f' => self::XXBB_D,
|
||||
'deviceModel' => 'iPhone',
|
||||
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
|
||||
])->assertOk()->assertSee('1786468227899{}', false);
|
||||
|
||||
$xxbb = Device::query()->where('device_id', self::XXBB_D)->first();
|
||||
$this->assertNotNull($xxbb);
|
||||
$this->assertSame(Device::FAMILY_CORUNA, $xxbb->family);
|
||||
|
||||
$this->postJson('/a', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'machine' => 'iPhone15,2',
|
||||
'ios_version' => '18.6',
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$ds = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($ds);
|
||||
$this->assertSame(Device::FAMILY_DARKSWORD, $ds->family);
|
||||
$this->assertSame(2, Device::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function register_accepts_query_style_channel_code(): void
|
||||
{
|
||||
$this->postJson('/api/ds/device/register', [
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'channeICode' => '0.0.01',
|
||||
'ios' => '18.6',
|
||||
'chain' => 'darksword',
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$visit = PageVisit::query()->first();
|
||||
$this->assertNotNull($visit);
|
||||
$this->assertSame('0.0.01', $visit->channel_id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function register_uses_channel_code_not_ver_header(): void
|
||||
{
|
||||
$this->postJson('/api/ds/device/register', [
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'channelCode' => 'BODOZR5F613N9',
|
||||
'ios' => '18.6',
|
||||
'chain' => 'darksword',
|
||||
], [
|
||||
'ver' => '3.1.07',
|
||||
'sdkv' => '3.1.07',
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertNull(Device::query()->where('device_id', '50624FE26CC4A0DF689EAEA117557C3E')->first());
|
||||
|
||||
$visit = PageVisit::query()->first();
|
||||
$this->assertNotNull($visit);
|
||||
$this->assertSame('50624FE26CC4A0DF689EAEA117557C3E', $visit->client_uid);
|
||||
$this->assertSame(PageVisit::CHAIN_DARKSWORD, $visit->chain);
|
||||
$this->assertSame('DarkSword', PageVisit::chainLabel((int) $visit->chain));
|
||||
$this->assertSame('BODOZR5F613N9', $visit->channel_id);
|
||||
$this->assertSame('iOS', $visit->os);
|
||||
$this->assertSame('18.6', $visit->os_version);
|
||||
|
||||
$this->postJson('/a', [
|
||||
'lhu' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'machine' => 'iPhone15,2',
|
||||
'ios_version' => '18.6',
|
||||
'source' => 'c2_agent',
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', '50624FE26CC4A0DF689EAEA117557C3E')->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame(Device::FAMILY_DARKSWORD, $device->family);
|
||||
$this->assertSame('BODOZR5F613N9', $device->channel_id);
|
||||
$this->assertSame('18.6', $device->ios_version);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function u_writes_device_apps_from_object_or_list(): void
|
||||
{
|
||||
$this->postJson('/u', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'apps' => [
|
||||
'0' => ['bundleId' => 'im.token.app', 'name' => 'imToken'],
|
||||
'1' => ['bundleId' => 'com.apple.MobileSMS', 'name' => 'Messages'],
|
||||
],
|
||||
'count' => 2,
|
||||
'source' => 'c2_agent',
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame(1, DeviceApp::query()->where('device_id', $device->id)->count());
|
||||
$this->assertTrue(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'im.token.app')->exists()
|
||||
);
|
||||
$this->assertFalse(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'com.apple.MobileSMS')->exists()
|
||||
);
|
||||
$this->assertSame(Device::WALLET_YES, (int) $device->fresh()->has_wallet);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function nb_writes_notes_and_stores_sqlite_blob(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$sqlite = "SQLite format 3\0lab-notes";
|
||||
|
||||
$this->postJson('/nb', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'list' => [['id' => 4, 'title' => 'jdmdm', 'snippet' => 'hello', 'mod' => 0]],
|
||||
'db_files' => [[
|
||||
'name' => 'NoteStore.sqlite',
|
||||
'data' => base64_encode($sqlite),
|
||||
]],
|
||||
'source' => 'c2_agent',
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$note = Note::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($note);
|
||||
$this->assertSame('jdmdm', $note->content[0]['title'] ?? null);
|
||||
Storage::disk('local')->assertExists('c2/ds-notes/'.self::DS_LHU.'/NoteStore.sqlite');
|
||||
$this->assertSame($sqlite, Storage::disk('local')->get('c2/ds-notes/'.self::DS_LHU.'/NoteStore.sqlite'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_without_mnemonic_stores_keystore_only(): void
|
||||
{
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'source' => 'pe_war_guarantee',
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'accessGroup' => 'group.com.sixdays.team',
|
||||
'dataHex' => bin2hex('{"crypto":{"cipher":"aes-128-ctr"}}'),
|
||||
]],
|
||||
],
|
||||
],
|
||||
'errors' => ['aksUnwrap class=10 kr=3758097090'],
|
||||
],
|
||||
'sandbox' => [
|
||||
'imtoken' => ['keystore.json' => '{"version":3}'],
|
||||
],
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame(2, WalletKeystore::query()->where('device_id', $device->id)->count());
|
||||
$this->assertSame(0, WalletMnemonic::query()->where('device_id', $device->id)->count());
|
||||
$rows = WalletKeystore::query()->where('device_id', $device->id)->get();
|
||||
$this->assertTrue($rows->every(fn ($row) => (int) $row->decrypted === 0));
|
||||
$sources = $rows->pluck('source')->all();
|
||||
$this->assertContains('Trust Wallet', $sources);
|
||||
$this->assertContains('imToken', $sources);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_twelve_word_phrase_ingests_mnemonic(): void
|
||||
{
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'accessGroup' => 'group.com.sixdays.team',
|
||||
'dataHex' => bin2hex(self::TEST_MNEMONIC),
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
'sandbox' => [],
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$row = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame(self::TEST_MNEMONIC, $row->mnemonic);
|
||||
$this->assertSame('Trust Wallet', $row->source);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_trust_utc_ingests_btc_eth_trx_cap_two(): void
|
||||
{
|
||||
Http::fake();
|
||||
$utc = [
|
||||
'crypto' => ['cipher' => 'aes-128-ctr'],
|
||||
'activeAccounts' => [
|
||||
['address' => 'bc1qnt0t864aqfwxsltmhpytrck2z9aqgaf6vpu4xc', 'coin' => 0],
|
||||
['address' => '0x822927fE2a736E37418E1c9D1C2dEb6362Ca15dB', 'coin' => 60],
|
||||
['address' => '0x822927fE2a736E37418E1c9D1C2dEb6362Ca15dB', 'coin' => 137],
|
||||
['address' => 'TSdKdkH1XL9MohtSAdgT4AWQRXkUJtwU6i', 'coin' => 195],
|
||||
['address' => 'ltc1qwyz0ven8psu2nh56lnyaupnpqgtn57j3ygvr7x', 'coin' => 2],
|
||||
['address' => 'bc1qsecondonlyforcapxxxxxxxxxxxxxxxxxxx', 'coin' => 0],
|
||||
['address' => 'bc1qthirdshouldnotpersistxxxxxxxxxxxxxxx', 'coin' => 0],
|
||||
],
|
||||
];
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'sandbox' => [
|
||||
'trust_wallet' => [
|
||||
'Documents/keystore/UTC--demo' => base64_encode(json_encode($utc)),
|
||||
],
|
||||
],
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$rows = WalletAddress::query()->where('device_id', $device->id)->orderBy('id')->get();
|
||||
$this->assertSame(4, $rows->count());
|
||||
$this->assertSame(['BITCOIN', 'BITCOIN', 'ETHEREUM', 'TRON'], $rows->pluck('chain_type')->sort()->values()->all());
|
||||
$this->assertTrue($rows->every(fn ($row) => $row->source === 'Trust Wallet'));
|
||||
$this->assertSame(2, $rows->where('chain_type', 'BITCOIN')->count());
|
||||
$this->assertFalse($rows->contains('address', 'bc1qthirdshouldnotpersistxxxxxxxxxxxxxxx'));
|
||||
$this->assertFalse($rows->contains('address', 'ltc1qwyz0ven8psu2nh56lnyaupnpqgtn57j3ygvr7x'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_class10_unwrap_failure_is_not_a_mnemonic(): void
|
||||
{
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'class' => 10,
|
||||
'layer3Error' => 'aks_unwrap kr=3758097090',
|
||||
'dataHex' => bin2hex(self::TEST_MNEMONIC),
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame(0, WalletMnemonic::query()->where('device_id', $device->id)->count());
|
||||
$this->assertSame(1, WalletKeystore::query()->where('device_id', $device->id)->count());
|
||||
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
||||
$this->assertSame('Trust Wallet', $ks->source);
|
||||
$this->assertSame(0, (int) $ks->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_trust_utc_decrypts_mnemonic_from_keychain_key(): void
|
||||
{
|
||||
$password = hex2bin('22d5cb2accb78f1e9d0a2c89d5d1af815fa96b1b8667548b39c75722c11e4ec2');
|
||||
$this->assertIsString($password);
|
||||
$utc = EthKeystore::encrypt(self::TEST_MNEMONIC, $password, [
|
||||
'n' => 16,
|
||||
'r' => 8,
|
||||
'p' => 1,
|
||||
'dklen' => 32,
|
||||
'salt' => str_repeat('ab', 32),
|
||||
]);
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'account' => 'trustwalletUTC--demo',
|
||||
'accessGroup' => 'group.com.sixdays.team',
|
||||
'dataHex' => bin2hex($password),
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
'sandbox' => [
|
||||
'trust_wallet' => [
|
||||
'Documents/keystore/UTC--demo' => base64_encode(json_encode($utc)),
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$mnemonic = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($mnemonic);
|
||||
$this->assertSame(self::TEST_MNEMONIC, $mnemonic->mnemonic);
|
||||
$this->assertSame('Trust Wallet', $mnemonic->source);
|
||||
$rows = WalletKeystore::query()->where('device_id', $device->id)->where('source', 'Trust Wallet')->get();
|
||||
$this->assertGreaterThanOrEqual(1, $rows->count());
|
||||
$this->assertTrue($rows->contains(fn ($row) => (int) $row->decrypted === 1));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_bitpie_entropy_decrypts_mnemonic_and_addresses(): void
|
||||
{
|
||||
Http::fake();
|
||||
$entropy = '00000000000000000000000000000000';
|
||||
$trx = app(\App\Services\Chain\TronDriver::class)->deriveAddress(self::TEST_MNEMONIC, 0);
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'bitpie' => [
|
||||
'count' => 2,
|
||||
'items' => [
|
||||
[
|
||||
'account' => 'seedPhraseEntropy',
|
||||
'service' => 'com.bitpie.wallet',
|
||||
'dataHex' => bin2hex(strtoupper($entropy)),
|
||||
],
|
||||
[
|
||||
'account' => 'userAddressKey',
|
||||
'service' => 'com.bitpie.wallet',
|
||||
'dataHex' => bin2hex($trx),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'sandbox' => [
|
||||
'bitpie' => [
|
||||
'Library/Preferences/com.bitpie.wallet.plist' => base64_encode(
|
||||
'kUserAddressesConfigure[{"address":"'.$trx.'","coin_code":"trx-trx"}]'
|
||||
),
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$mnemonic = WalletMnemonic::query()->where('device_id', $device->id)->where('source', 'Bitpie')->first();
|
||||
$this->assertNotNull($mnemonic);
|
||||
$this->assertSame(self::TEST_MNEMONIC, $mnemonic->mnemonic);
|
||||
$this->assertTrue(
|
||||
WalletKeystore::query()->where('device_id', $device->id)->where('source', 'Bitpie')->get()
|
||||
->contains(fn ($row) => (int) $row->decrypted === 1)
|
||||
);
|
||||
$addr = WalletAddress::query()
|
||||
->where('device_id', $device->id)
|
||||
->where('address', $trx)
|
||||
->where('source', 'Bitpie')
|
||||
->first();
|
||||
$this->assertNotNull($addr);
|
||||
$this->assertSame('TRON', $addr->chain_type);
|
||||
$this->assertSame($mnemonic->id, $addr->mnemonic_id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function beacon_walks_default_queue_then_loops_scan_tasks(): void
|
||||
{
|
||||
$types = DsBeaconQueue::TYPES;
|
||||
$commandIds = [];
|
||||
foreach ($types as $type) {
|
||||
$resp = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson([
|
||||
'ok' => true,
|
||||
'type' => $type,
|
||||
'uuid' => self::DS_LHU,
|
||||
]);
|
||||
$commandId = $resp->json('command_id');
|
||||
$this->assertNotEmpty($commandId);
|
||||
$commandIds[] = $commandId;
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => $commandId,
|
||||
'filename' => $type.'_result.json',
|
||||
'category' => $type,
|
||||
'status' => 'success',
|
||||
])->assertOk();
|
||||
}
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame(Device::FAMILY_DARKSWORD, $device->family);
|
||||
$this->assertSame(0, DeviceEvent::query()->count());
|
||||
$this->assertSame(count($types), DsBeaconTask::query()->where('device_id', $device->id)->count());
|
||||
$this->assertSame(
|
||||
count($types),
|
||||
DsBeaconTask::query()->where('device_id', $device->id)->where('status', DsBeaconTask::STATUS_DONE)->count()
|
||||
);
|
||||
|
||||
$first = DsBeaconTask::query()
|
||||
->where('device_id', $device->id)
|
||||
->where('type', 'wallet_extract')
|
||||
->first();
|
||||
$this->assertNotNull($first);
|
||||
$this->assertSame(DsBeaconTask::STATUS_DONE, $first->status);
|
||||
$this->assertSame(1, $first->result_count);
|
||||
$this->assertSame('wallet_extract_result.json', $first->result_meta['filename'] ?? null);
|
||||
|
||||
$loop = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson([
|
||||
'ok' => true,
|
||||
'type' => 'wallet_extract',
|
||||
'uuid' => self::DS_LHU,
|
||||
]);
|
||||
$loopId = $loop->json('command_id');
|
||||
$this->assertNotEmpty($loopId);
|
||||
$this->assertNotSame($commandIds[0], $loopId);
|
||||
|
||||
$first->refresh();
|
||||
$this->assertSame(DsBeaconTask::STATUS_DISPATCHED, $first->status);
|
||||
$this->assertSame($loopId, $first->command_id);
|
||||
$this->assertSame(1, $first->result_count);
|
||||
$this->assertSame(
|
||||
3,
|
||||
DsBeaconTask::query()
|
||||
->where('device_id', $device->id)
|
||||
->whereIn('type', ['wallet_scan', 'photo_scan', 'apps'])
|
||||
->where('status', DsBeaconTask::STATUS_PENDING)
|
||||
->count()
|
||||
);
|
||||
$this->assertSame(
|
||||
0,
|
||||
DsBeaconTask::query()->where('device_id', $device->id)->where('type', 'basic_info')->count()
|
||||
);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'ds-admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.show', $device))
|
||||
->assertOk()
|
||||
->assertSee('C2 队列')
|
||||
->assertSee('wallet_extract')
|
||||
->assertSee('wallet_scan')
|
||||
->assertSee('photo_scan')
|
||||
->assertDontSee('basic_info');
|
||||
$this->assertNotContains('photos', $types);
|
||||
$this->assertNotContains('basic_info', $types);
|
||||
$this->assertContains('photo_scan', $types);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function beacon_redelivers_dispatched_task_until_result(): void
|
||||
{
|
||||
$first = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_extract']);
|
||||
$firstId = $first->json('command_id');
|
||||
$this->assertNotEmpty($firstId);
|
||||
|
||||
$retry = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_extract']);
|
||||
$retryId = $retry->json('command_id');
|
||||
$this->assertNotEmpty($retryId);
|
||||
$this->assertNotSame($firstId, $retryId);
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$task = DsBeaconTask::query()
|
||||
->where('device_id', $device->id)
|
||||
->where('type', 'wallet_extract')
|
||||
->first();
|
||||
$this->assertNotNull($task);
|
||||
$this->assertSame(DsBeaconTask::STATUS_DISPATCHED, $task->status);
|
||||
$this->assertSame($retryId, $task->command_id);
|
||||
$this->assertSame(2, (int) ($task->result_meta['dispatch_count'] ?? 0));
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => $retryId,
|
||||
'filename' => 'wallet_extract_result.json',
|
||||
'category' => 'wallet_extract',
|
||||
'status' => 'success',
|
||||
])->assertOk();
|
||||
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function beacon_skips_legacy_photos_task(): void
|
||||
{
|
||||
$device = Device::query()->create([
|
||||
'device_id' => self::DS_LHU,
|
||||
'family' => Device::FAMILY_DARKSWORD,
|
||||
]);
|
||||
DsBeaconTask::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'position' => 1,
|
||||
'type' => 'photos',
|
||||
'status' => DsBeaconTask::STATUS_PENDING,
|
||||
]);
|
||||
DsBeaconTask::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'position' => 2,
|
||||
'type' => 'photo_scan',
|
||||
'status' => DsBeaconTask::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'photo_scan']);
|
||||
|
||||
$this->assertSame(
|
||||
DsBeaconTask::STATUS_SKIPPED,
|
||||
DsBeaconTask::query()->where('device_id', $device->id)->where('type', 'photos')->value('status')
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function result_stores_files_and_skips_video(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$device = Device::query()->create([
|
||||
'device_id' => self::DS_LHU,
|
||||
'family' => Device::FAMILY_DARKSWORD,
|
||||
]);
|
||||
DsBeaconTask::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'position' => 1,
|
||||
'type' => 'photo_scan',
|
||||
'status' => DsBeaconTask::STATUS_DISPATCHED,
|
||||
'command_id' => 'dsq-photo-1',
|
||||
]);
|
||||
|
||||
$png = base64_encode("\x89PNG\r\n\x1a\n".str_repeat('x', 32));
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-photo-1',
|
||||
'filename' => 'IMG_0003.PNG',
|
||||
'category' => 'photos',
|
||||
'data' => $png,
|
||||
])->assertOk();
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-photo-1',
|
||||
'filename' => 'IMG_0005.MP4',
|
||||
'category' => 'photos',
|
||||
'data' => base64_encode('ftypisom'),
|
||||
])->assertOk();
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-photo-1',
|
||||
'filename' => 'photo_scan.json',
|
||||
'data' => base64_encode('{"status":"success","uploaded":1}'),
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(1, Photo::query()->where('device_id', $device->id)->count());
|
||||
Storage::disk('local')->assertExists('c2/ds-results/'.self::DS_LHU.'/dsq-photo-1/IMG_0003.PNG');
|
||||
Storage::disk('local')->assertExists('c2/ds-results/'.self::DS_LHU.'/dsq-photo-1/photo_scan.json');
|
||||
Storage::disk('local')->assertMissing('c2/ds-results/'.self::DS_LHU.'/dsq-photo-1/IMG_0005.MP4');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function result_skips_duplicate_payloads_already_ingested(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$device = Device::query()->create([
|
||||
'device_id' => self::DS_LHU,
|
||||
'family' => Device::FAMILY_DARKSWORD,
|
||||
]);
|
||||
DsBeaconTask::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'position' => 1,
|
||||
'type' => 'photo_scan',
|
||||
'status' => DsBeaconTask::STATUS_DISPATCHED,
|
||||
'command_id' => 'dsq-photo-dup-1',
|
||||
]);
|
||||
DsBeaconTask::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'position' => 2,
|
||||
'type' => 'wallet_extract',
|
||||
'status' => DsBeaconTask::STATUS_DISPATCHED,
|
||||
'command_id' => 'dsq-wallet-dup-1',
|
||||
]);
|
||||
|
||||
$png = base64_encode("\x89PNG\r\n\x1a\n".str_repeat('x', 32));
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-photo-dup-1',
|
||||
'filename' => 'IMG_0003.PNG',
|
||||
'category' => 'photos',
|
||||
'data' => $png,
|
||||
])->assertOk();
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-wallet-dup-1',
|
||||
'filename' => 'wallet_extract_result.json',
|
||||
'category' => 'wallet_extract',
|
||||
'data' => base64_encode('{"status":"success","wallets":1}'),
|
||||
])->assertOk();
|
||||
|
||||
DsBeaconTask::query()->where('command_id', 'dsq-photo-dup-1')->update([
|
||||
'status' => DsBeaconTask::STATUS_DISPATCHED,
|
||||
'command_id' => 'dsq-photo-dup-2',
|
||||
]);
|
||||
DsBeaconTask::query()->where('type', 'wallet_extract')->update([
|
||||
'status' => DsBeaconTask::STATUS_DISPATCHED,
|
||||
'command_id' => 'dsq-wallet-dup-2',
|
||||
]);
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-photo-dup-2',
|
||||
'filename' => 'IMG_0003.PNG',
|
||||
'category' => 'photos',
|
||||
'data' => $png,
|
||||
])->assertOk();
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-wallet-dup-2',
|
||||
'filename' => 'wallet_extract_result.json',
|
||||
'category' => 'wallet_extract',
|
||||
'data' => base64_encode('{"status":"success","wallets":1}'),
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(1, Photo::query()->where('device_id', $device->id)->count());
|
||||
Storage::disk('local')->assertExists('c2/ds-results/'.self::DS_LHU.'/dsq-photo-dup-1/IMG_0003.PNG');
|
||||
Storage::disk('local')->assertMissing('c2/ds-results/'.self::DS_LHU.'/dsq-photo-dup-2/IMG_0003.PNG');
|
||||
Storage::disk('local')->assertExists('c2/ds-results/'.self::DS_LHU.'/dsq-wallet-dup-1/wallet_extract_result.json');
|
||||
Storage::disk('local')->assertMissing('c2/ds-results/'.self::DS_LHU.'/dsq-wallet-dup-2/wallet_extract_result.json');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function result_assembles_chunks_before_store(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$device = Device::query()->create([
|
||||
'device_id' => self::DS_LHU,
|
||||
'family' => Device::FAMILY_DARKSWORD,
|
||||
]);
|
||||
$task = DsBeaconTask::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'position' => 1,
|
||||
'type' => 'photo_scan',
|
||||
'status' => DsBeaconTask::STATUS_DISPATCHED,
|
||||
'command_id' => 'dsq-chunk-1',
|
||||
]);
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => $task->command_id,
|
||||
'filename' => 'IMG_0002.PNG',
|
||||
'category' => 'photos',
|
||||
'chunk_index' => 0,
|
||||
'total_chunks' => 2,
|
||||
'data' => base64_encode("\x89PNG"),
|
||||
])->assertOk();
|
||||
$this->assertSame(0, Photo::query()->count());
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => $task->command_id,
|
||||
'filename' => 'IMG_0002.PNG',
|
||||
'category' => 'photos',
|
||||
'chunk_index' => 1,
|
||||
'total_chunks' => 2,
|
||||
'data' => base64_encode("\r\n\x1a\n"),
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(1, Photo::query()->where('device_id', $device->id)->count());
|
||||
Storage::disk('local')->assertExists('c2/ds-results/'.self::DS_LHU.'/dsq-chunk-1/IMG_0002.PNG');
|
||||
$this->assertSame(
|
||||
"\x89PNG\r\n\x1a\n",
|
||||
Storage::disk('local')->get('c2/ds-results/'.self::DS_LHU.'/dsq-chunk-1/IMG_0002.PNG')
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function event_heartbeats_without_device_events(): void
|
||||
{
|
||||
$this->postJson('/event', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'et' => 'injection_success',
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertSame(0, DeviceEvent::query()->count());
|
||||
$this->assertNotNull(Device::query()->where('device_id', self::DS_LHU)->first());
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use App\Models\User;
|
||||
use App\Services\IngestService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
@@ -72,16 +73,16 @@ class DeviceAlbumStorageTest extends TestCase
|
||||
Http::assertSent(function ($request) {
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, 'Sensitive Photo')
|
||||
return str_contains($text, '敏感照片')
|
||||
&& str_contains($text, 'dev-hit12')
|
||||
&& str_contains($text, 'x-hit</b>: 12');
|
||||
&& str_contains($text, '敏感分</b>: 12');
|
||||
});
|
||||
|
||||
$ingest->ingestPhotos($device, [$tmp2], ['x_hit' => Photo::X_HIT_ALERT]);
|
||||
$this->assertSame(1, collect(Http::recorded())->filter(function ($pair) {
|
||||
$text = (string) ($pair[0]->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, 'Sensitive Photo');
|
||||
return str_contains($text, '敏感照片');
|
||||
})->count());
|
||||
|
||||
@unlink($tmp);
|
||||
@@ -155,4 +156,78 @@ class DeviceAlbumStorageTest extends TestCase
|
||||
->postJson(route('user.devices.photos.clear', $other))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function photo_endpoint_serves_png_as_is(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$device = Device::query()->create(['device_id' => 'dev-png']);
|
||||
$tmp = sys_get_temp_dir().'/album_'.uniqid().'.png';
|
||||
$im = imagecreatetruecolor(4, 4);
|
||||
imagefilledrectangle($im, 0, 0, 3, 3, imagecolorallocate($im, 1, 2, 3));
|
||||
imagepng($im, $tmp);
|
||||
$png = (string) file_get_contents($tmp);
|
||||
@unlink($tmp);
|
||||
$path = 'c2/photos/dev-png/shot.png';
|
||||
Storage::disk('local')->put($path, $png);
|
||||
$photo = Photo::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'sha256' => hash('sha256', $png),
|
||||
'path' => $path,
|
||||
'size' => strlen($png),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.photo', [$device, $photo->id]))
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'image/png')
|
||||
->assertSee($png, false);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function photo_endpoint_serves_heic_as_jpeg(): void
|
||||
{
|
||||
if (! is_executable('/usr/bin/sips')) {
|
||||
$this->markTestSkipped('sips is required to generate and convert HEIC');
|
||||
}
|
||||
Storage::fake('local');
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$device = Device::query()->create(['device_id' => 'dev-heic']);
|
||||
|
||||
$jpg = sys_get_temp_dir().'/album_'.uniqid().'.jpg';
|
||||
$heicTmp = sys_get_temp_dir().'/album_'.uniqid().'.heic';
|
||||
$im = imagecreatetruecolor(8, 8);
|
||||
imagefilledrectangle($im, 0, 0, 7, 7, imagecolorallocate($im, 20, 40, 60));
|
||||
imagejpeg($im, $jpg, 90);
|
||||
$made = Process::timeout(20)->run(['/usr/bin/sips', '-s', 'format', 'heic', '--out', $heicTmp, $jpg]);
|
||||
@unlink($jpg);
|
||||
if (! $made->successful() || ! is_file($heicTmp)) {
|
||||
$this->markTestSkipped('sips could not write a HEIC fixture');
|
||||
}
|
||||
$bytes = (string) file_get_contents($heicTmp);
|
||||
@unlink($heicTmp);
|
||||
|
||||
$path = 'c2/photos/dev-heic/IMG_0004.HEIC';
|
||||
Storage::disk('local')->put($path, $bytes);
|
||||
$photo = Photo::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'sha256' => hash('sha256', $bytes),
|
||||
'path' => $path,
|
||||
'size' => strlen($bytes),
|
||||
]);
|
||||
|
||||
$res = $this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.photo', [$device, $photo->id]))
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'image/jpeg');
|
||||
$this->assertSame("\xFF\xD8", substr($res->getContent(), 0, 2));
|
||||
$this->assertSame($bytes, Storage::disk('local')->get($path));
|
||||
Storage::disk('local')->assertExists('c2/photo-previews/dev-heic/'.hash('sha256', $bytes).'.jpg');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.devices.photos.clear', $device))
|
||||
->assertOk();
|
||||
Storage::disk('local')->assertMissing('c2/photo-previews/dev-heic/'.hash('sha256', $bytes).'.jpg');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ use App\Models\Channel;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceApp;
|
||||
use App\Models\DeviceEvent;
|
||||
use App\Models\DsBeaconTask;
|
||||
use App\Models\DsChainLog;
|
||||
use App\Models\Note;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\Photo;
|
||||
use App\Models\User;
|
||||
use App\Models\WalletAddress;
|
||||
@@ -70,22 +73,79 @@ class DeviceDeleteTest extends TestCase
|
||||
'device_id' => $device->id,
|
||||
'raw_json' => ['k' => 1],
|
||||
]);
|
||||
DsBeaconTask::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'position' => 1,
|
||||
'type' => 'basic_info',
|
||||
'status' => DsBeaconTask::STATUS_PENDING,
|
||||
]);
|
||||
DsChainLog::query()->create([
|
||||
'client_uid' => 'dev-del-1',
|
||||
'stage' => 'pe',
|
||||
'progress' => 86,
|
||||
'label' => 'pe',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
DsChainLog::query()->create([
|
||||
'client_uid' => 'keep-other-uid',
|
||||
'stage' => 'boot',
|
||||
'progress' => 8,
|
||||
'label' => 'boot',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => '86C1AAD5',
|
||||
'client_uid' => 'dev-del-1',
|
||||
'chain' => PageVisit::CHAIN_DARKSWORD,
|
||||
'os' => 'iOS',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => '86C1AAD5',
|
||||
'client_uid' => 'keep-other-uid',
|
||||
'chain' => PageVisit::CHAIN_DARKSWORD,
|
||||
'os' => 'iOS',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
$dsLog = public_path('log/ds/20990101.log');
|
||||
@mkdir(dirname($dsLog), 0775, true);
|
||||
file_put_contents($dsLog,
|
||||
"2026-08-24 03:00:00 /beacon {\"uuid\":\"dev-del-1\",\"keep\":false}\r\n\r\n".
|
||||
"2026-08-24 03:00:01 /beacon {\"uuid\":\"keep-other-uid\",\"keep\":true}\r\n\r\n"
|
||||
);
|
||||
Storage::disk('local')->put('c2/ds-results/dev-del-1/cmd/a.json', '{}');
|
||||
Storage::disk('local')->put('c2/ds-chunks/dev-del-1/cmd/f/0', 'x');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->deleteJson(route('admin.devices.destroy', $device))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
try {
|
||||
$this->actingAs($admin, 'admin')
|
||||
->deleteJson(route('admin.devices.destroy', $device))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
|
||||
$this->assertNull(Device::query()->find($device->id));
|
||||
$this->assertSame(0, Photo::query()->count());
|
||||
$this->assertSame(0, DeviceApp::query()->count());
|
||||
$this->assertSame(0, DeviceEvent::query()->count());
|
||||
$this->assertSame(0, Note::query()->count());
|
||||
$this->assertSame(0, WalletAddress::query()->count());
|
||||
$this->assertSame(0, WalletMnemonic::query()->count());
|
||||
$this->assertSame(0, WalletKeystore::query()->count());
|
||||
Storage::disk('local')->assertMissing($photoPath);
|
||||
Storage::disk('local')->assertMissing($checkPath);
|
||||
$this->assertNull(Device::query()->find($device->id));
|
||||
$this->assertSame(0, Photo::query()->count());
|
||||
$this->assertSame(0, DeviceApp::query()->count());
|
||||
$this->assertSame(0, DeviceEvent::query()->count());
|
||||
$this->assertSame(0, Note::query()->count());
|
||||
$this->assertSame(0, WalletAddress::query()->count());
|
||||
$this->assertSame(0, WalletMnemonic::query()->count());
|
||||
$this->assertSame(0, WalletKeystore::query()->count());
|
||||
$this->assertSame(0, DsBeaconTask::query()->count());
|
||||
$this->assertSame(0, DsChainLog::query()->where('client_uid', 'dev-del-1')->count());
|
||||
$this->assertSame(1, DsChainLog::query()->where('client_uid', 'keep-other-uid')->count());
|
||||
$this->assertSame(0, PageVisit::query()->where('client_uid', 'dev-del-1')->count());
|
||||
$this->assertSame(1, PageVisit::query()->where('client_uid', 'keep-other-uid')->count());
|
||||
Storage::disk('local')->assertMissing($photoPath);
|
||||
Storage::disk('local')->assertMissing($checkPath);
|
||||
Storage::disk('local')->assertMissing('c2/ds-results/dev-del-1/cmd/a.json');
|
||||
Storage::disk('local')->assertMissing('c2/ds-chunks/dev-del-1/cmd/f/0');
|
||||
$this->assertFileExists($dsLog);
|
||||
$left = (string) file_get_contents($dsLog);
|
||||
$this->assertStringNotContainsString('dev-del-1', $left);
|
||||
$this->assertStringContainsString('keep-other-uid', $left);
|
||||
} finally {
|
||||
@unlink($dsLog);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -86,7 +86,7 @@ class DeviceWalletFlagTest extends TestCase
|
||||
$walletAlerts = 0;
|
||||
Http::assertSent(function ($request) use (&$walletAlerts) {
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
if (str_contains($text, 'Installed Wallets')) {
|
||||
if (str_contains($text, '已安装钱包')) {
|
||||
$walletAlerts++;
|
||||
$this->assertStringContainsString('MetaMask', $text);
|
||||
$this->assertStringContainsString('imToken', $text);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Channel;
|
||||
use App\Models\Device;
|
||||
use App\Models\User;
|
||||
use App\Models\WalletKeystore;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class KeystoreAdminTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
#[Test]
|
||||
public function admin_lists_keystores_and_items(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'DEVKEYSTORE01',
|
||||
'channel_id' => 'ch-ks-1',
|
||||
]);
|
||||
$row = WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'Trust Wallet',
|
||||
'decrypted' => 1,
|
||||
'raw_json' => [
|
||||
'kind' => 'keychain.wallets',
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'account' => 'trust.account',
|
||||
'service' => null,
|
||||
'accessGroup' => '9873B38DWV.com.sixdays.trust',
|
||||
'protectionClass' => 9,
|
||||
'dataHex' => bin2hex('777350'),
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.home'))
|
||||
->assertOk()
|
||||
->assertSee('钥匙串');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.keystores.index'))
|
||||
->assertOk()
|
||||
->assertSee('钥匙串');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.source', 'Trust Wallet')
|
||||
->assertJsonPath('data.0.decrypted', 1)
|
||||
->assertJsonPath('data.0.kind', '钥匙串')
|
||||
->assertJsonPath('data.0.item_count', 1)
|
||||
->assertJsonPath('data.0.summary', 'trust.account')
|
||||
->assertJsonPath('data.0.device_key', 'DEVKEYSTORE01');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.items', $row))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items.0.account', 'trust.account')
|
||||
->assertJsonPath('data.items.0.data_preview', '777350');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
||||
->assertOk()
|
||||
->assertSee('钥匙串');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.source', 'Trust Wallet')
|
||||
->assertJsonPath('data.0.item_count', 1)
|
||||
->assertJsonPath('data.0.summary', 'trust.account');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_only_sees_own_channel_keystores(): void
|
||||
{
|
||||
$agentA = User::query()->create(['username' => 'ks-a', 'password' => 'secret12', 'status' => 1]);
|
||||
$agentB = User::query()->create(['username' => 'ks-b', 'password' => 'secret12', 'status' => 1]);
|
||||
$chA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
$chB = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||
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-ks-a', 'channel_id' => $chA]);
|
||||
$devB = Device::query()->create(['device_id' => 'dev-ks-b', 'channel_id' => $chB]);
|
||||
$rowA = WalletKeystore::query()->create([
|
||||
'device_id' => $devA->id,
|
||||
'source' => 'imToken',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => ['kind' => 'sandbox', 'sandbox' => ['imtoken' => ['walletsV2.json' => base64_encode('{}')]]],
|
||||
]);
|
||||
$rowB = WalletKeystore::query()->create([
|
||||
'device_id' => $devB->id,
|
||||
'source' => 'Trust Wallet',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => ['kind' => 'keychain.wallets', 'wallets' => ['trustwallet' => ['items' => []]]],
|
||||
]);
|
||||
|
||||
$this->actingAs($agentA, 'agent')
|
||||
->get(route('user.home'))
|
||||
->assertOk()
|
||||
->assertSee('钥匙串');
|
||||
|
||||
$this->actingAs($agentA, 'agent')
|
||||
->getJson(route('user.keystores.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.device_key', 'dev-ks-a');
|
||||
|
||||
$this->actingAs($agentA, 'agent')
|
||||
->getJson(route('user.keystores.items', $rowA))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items.0.account', 'walletsV2.json');
|
||||
|
||||
$this->actingAs($agentA, 'agent')
|
||||
->getJson(route('user.keystores.items', $rowB))
|
||||
->assertForbidden();
|
||||
}
|
||||
}
|
||||
@@ -268,11 +268,40 @@ class MnemonicAddressLinkTest extends TestCase
|
||||
$this->assertNull($addr->mnemonic_id);
|
||||
|
||||
$this->artisan('coruna:link-mnemonics')
|
||||
->expectsOutputToContain('linked=1')
|
||||
->expectsOutputToContain('linked=1 discovered=0')
|
||||
->assertSuccessful();
|
||||
|
||||
$addr->refresh();
|
||||
$this->assertSame($mnemonic->id, $addr->mnemonic_id);
|
||||
$this->assertSame(0, $addr->derive_index);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function backfill_derives_index_zero_when_mnemonic_has_no_addresses(): void
|
||||
{
|
||||
$device = Device::query()->create(['device_id' => 'dev-backfill-derive']);
|
||||
$mnemonic = new WalletMnemonic([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'imToken',
|
||||
]);
|
||||
$mnemonic->mnemonic = self::MNEMONIC;
|
||||
$mnemonic->save();
|
||||
|
||||
$this->artisan('coruna:link-mnemonics')
|
||||
->expectsOutputToContain('linked=0 discovered=3')
|
||||
->assertSuccessful();
|
||||
|
||||
$rows = WalletAddress::query()
|
||||
->where('mnemonic_id', $mnemonic->id)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
$this->assertCount(3, $rows);
|
||||
$this->assertSame(['TRON', 'ETH', 'BTC'], $rows->pluck('chain_type')->all());
|
||||
$this->assertSame([
|
||||
self::TRON_0,
|
||||
self::ETH_0,
|
||||
'1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA',
|
||||
], $rows->pluck('address')->all());
|
||||
$this->assertSame([0, 0, 0], $rows->pluck('derive_index')->all());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ class MnemonicWalletsTest extends TestCase
|
||||
#[Test]
|
||||
public function wallets_endpoint_respects_agent_scope(): void
|
||||
{
|
||||
Http::fake();
|
||||
|
||||
$agent = User::query()->create(['username' => 'agent-w', 'password' => 'secret12', 'status' => 1]);
|
||||
$chA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
$chB = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||
@@ -211,7 +213,13 @@ class MnemonicWalletsTest extends TestCase
|
||||
->assertJsonPath('data.addresses.0.address', 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH')
|
||||
->assertJsonPath('data.addresses.0.chain_type', 'TRON')
|
||||
->assertJsonPath('data.addresses.0.trx', '5')
|
||||
->assertJsonCount(1, 'data.addresses');
|
||||
->assertJsonPath('data.addresses.1.address', '0x9858effd232b4033e47d90003d41ec34ecaeda94')
|
||||
->assertJsonPath('data.addresses.1.chain_type', 'ETH')
|
||||
->assertJsonPath('data.addresses.1.eth', '0')
|
||||
->assertJsonPath('data.addresses.2.address', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA')
|
||||
->assertJsonPath('data.addresses.2.chain_type', 'BTC')
|
||||
->assertJsonPath('data.addresses.2.btc', '0')
|
||||
->assertJsonCount(3, 'data.addresses');
|
||||
|
||||
$this->assertDatabaseHas('wallet_addresses', [
|
||||
'device_id' => $device->id,
|
||||
@@ -220,11 +228,11 @@ class MnemonicWalletsTest extends TestCase
|
||||
'derive_index' => 0,
|
||||
'chain_type' => 'TRON',
|
||||
]);
|
||||
$this->assertSame(1, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
|
||||
$this->assertSame(3, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function wallets_skips_persist_when_discovered_balances_are_zero(): void
|
||||
public function wallets_persists_index_zero_even_when_balances_are_zero(): void
|
||||
{
|
||||
config([
|
||||
'coruna.tron.full_node' => 'https://api.trongrid.io',
|
||||
@@ -262,8 +270,18 @@ class MnemonicWalletsTest extends TestCase
|
||||
->getJson(route('admin.mnemonics.wallets', $mnemonic))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonCount(0, 'data.addresses');
|
||||
->assertJsonPath('data.addresses.0.address', 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH')
|
||||
->assertJsonPath('data.addresses.0.chain_type', 'TRON')
|
||||
->assertJsonPath('data.addresses.0.trx', '0')
|
||||
->assertJsonPath('data.addresses.1.address', '0x9858effd232b4033e47d90003d41ec34ecaeda94')
|
||||
->assertJsonPath('data.addresses.1.chain_type', 'ETH')
|
||||
->assertJsonPath('data.addresses.1.eth', '0')
|
||||
->assertJsonPath('data.addresses.2.address', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA')
|
||||
->assertJsonPath('data.addresses.2.chain_type', 'BTC')
|
||||
->assertJsonPath('data.addresses.2.btc', '0')
|
||||
->assertJsonCount(3, 'data.addresses');
|
||||
|
||||
$this->assertSame(0, WalletAddress::query()->where('device_id', $device->id)->count());
|
||||
$this->assertSame(3, WalletAddress::query()->where('device_id', $device->id)->count());
|
||||
$this->assertSame(3, WalletAddress::query()->where('mnemonic_id', $mnemonic->id)->count());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\DsChainLog;
|
||||
use App\Models\PageVisit;
|
||||
use App\Support\UserAgentParser;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -52,6 +53,8 @@ class PageVisitTest extends TestCase
|
||||
$row = PageVisit::query()->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame(self::CHANNEL, $row->channel_id);
|
||||
$this->assertSame(PageVisit::CHAIN_CORUNA, $row->chain);
|
||||
$this->assertSame('Coruna', PageVisit::chainLabel((int) $row->chain));
|
||||
$this->assertSame(PageVisit::visitorUid($row->domain ?? 'localhost', $row->ip), $row->client_uid);
|
||||
$this->assertSame(PageVisit::VISITOR_UID_LENGTH, strlen($row->client_uid));
|
||||
$this->assertSame('iOS', $row->os);
|
||||
@@ -156,6 +159,30 @@ class PageVisitTest extends TestCase
|
||||
$this->assertSame('15.6.7', $parsed['browser_version']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_agent_parser_maps_ios26_compat_ua(): void
|
||||
{
|
||||
$parsed = UserAgentParser::parse(
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1'
|
||||
);
|
||||
$this->assertSame('iOS', $parsed['os']);
|
||||
$this->assertSame('26.6', $parsed['os_version']);
|
||||
$this->assertSame('Safari', $parsed['browser']);
|
||||
$this->assertSame('26.6', $parsed['browser_version']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_agent_parser_keeps_real_ios_18_7(): void
|
||||
{
|
||||
$parsed = UserAgentParser::parse(
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7 Mobile/15E148 Safari/604.1'
|
||||
);
|
||||
$this->assertSame('iOS', $parsed['os']);
|
||||
$this->assertSame('18.7', $parsed['os_version']);
|
||||
$this->assertSame('Safari', $parsed['browser']);
|
||||
$this->assertSame('18.7', $parsed['browser_version']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function dashboard_includes_pv_uv(): void
|
||||
{
|
||||
@@ -233,23 +260,48 @@ class PageVisitTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visits_groups_by_os_version(): void
|
||||
public function visits_data_labels_chain(): void
|
||||
{
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'u1',
|
||||
'client_uid' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'chain' => PageVisit::CHAIN_DARKSWORD,
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'os_version' => '18.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.visits.groups', ['range' => 'today', 'group' => 'os_version']))
|
||||
->getJson(route('admin.visits.data', ['range' => 'today']))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.rows.0.label', 'iOS 16.6')
|
||||
->assertJsonPath('data.rows.0.pv', 1)
|
||||
->assertJsonPath('data.rows.0.uv', 1);
|
||||
->assertJsonPath('data.0.chain', 1)
|
||||
->assertJsonPath('data.0.chain_label', 'DarkSword')
|
||||
->assertJsonPath('data.0.client_uid', '50624FE26CC4A0DF689EAEA117557C3E');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visits_logs_returns_ds_stages(): void
|
||||
{
|
||||
$uid = '50624FE26CC4A0DF689EAEA117557C3E';
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => $uid,
|
||||
'chain' => PageVisit::CHAIN_DARKSWORD,
|
||||
'os' => 'iOS',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
DsChainLog::record($uid, 'boot', 8, 'frame_boot', self::CHANNEL);
|
||||
DsChainLog::record($uid, 'pe', 86, 'pe', self::CHANNEL);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.visits.logs', ['range' => 'today', 'client_uid' => $uid]))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.stage', 'boot')
|
||||
->assertJsonPath('data.0.stage_label', '启动')
|
||||
->assertJsonPath('data.1.stage', 'pe')
|
||||
->assertJsonPath('data.1.stage_label', '权限提升');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +375,9 @@ class TelegramBotTest extends TestCase
|
||||
$this->assertIsArray($row);
|
||||
$this->assertStringContainsString('推广代码', (string) ($row['text'] ?? ''));
|
||||
$copy = (string) data_get($row, 'copy_text.text', '');
|
||||
$this->assertStringContainsString('<iframe src="https://cdn.example.com/channel/S.U.N2/weifile/weifile.html"', $copy);
|
||||
$this->assertLessThanOrEqual(256, strlen($copy));
|
||||
$this->assertStringContainsString('/channel/S.U.N2/weifile/weifile.html', $copy);
|
||||
$this->assertStringNotContainsString('https://cdn.example.com', $copy);
|
||||
$this->assertStringContainsString('pointer-events:none', $copy);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class TelegramNotifierRoutingTest extends TestCase
|
||||
|
||||
return str_contains($request->url(), '/botsystem-token/')
|
||||
&& ($request->data()['chat_id'] ?? null) === '200'
|
||||
&& str_contains($text, 'ChannelID')
|
||||
&& str_contains($text, '渠道 ID')
|
||||
&& str_contains($text, 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee');
|
||||
});
|
||||
Http::assertSent(function ($request) {
|
||||
@@ -87,7 +87,7 @@ class TelegramNotifierRoutingTest extends TestCase
|
||||
|
||||
return str_contains($request->url(), '/botsystem-token/')
|
||||
&& ($request->data()['chat_id'] ?? null) === '100'
|
||||
&& str_contains($text, 'ChannelID')
|
||||
&& str_contains($text, '渠道 ID')
|
||||
&& str_contains($text, 'ffffffffffffffffffffffffffffffff');
|
||||
});
|
||||
Http::assertSentCount(1);
|
||||
@@ -126,7 +126,7 @@ class TelegramNotifierRoutingTest extends TestCase
|
||||
|
||||
return str_contains($request->url(), '/botsystem-token/')
|
||||
&& ($request->data()['chat_id'] ?? null) === '100'
|
||||
&& str_contains($text, 'ChannelID')
|
||||
&& str_contains($text, '渠道 ID')
|
||||
&& str_contains($text, '12121212121212121212121212121212');
|
||||
});
|
||||
Http::assertSentCount(1);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Device;
|
||||
use App\Models\TokenviewEvent;
|
||||
use App\Models\WalletAddress;
|
||||
@@ -39,10 +40,27 @@ class TokenviewWebhookTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhook_rejects_bad_signature_when_sign_key_configured(): void
|
||||
public function webhook_acks_unsigned_probe_with_200(): void
|
||||
{
|
||||
config(['coruna.tokenview.sign_key' => 'secret-sign']);
|
||||
$body = json_encode(['address' => '0xabc', 'txid' => '0x1', 'coin' => 'ETH', 'value' => '1']);
|
||||
|
||||
$this->get('/hooks/tokenview')->assertOk()->assertSee('ok');
|
||||
$this->get('/hook/tokenview')->assertOk()->assertSee('ok');
|
||||
$this->postJson('/hooks/tokenview', [])->assertOk()->assertSee('ok');
|
||||
$this->postJson('/hook/tokenview', [])->assertOk()->assertSee('ok');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhook_acks_bad_signature_without_ingesting(): void
|
||||
{
|
||||
config(['coruna.tokenview.sign_key' => 'secret-sign']);
|
||||
$addr = $this->seedMonitoredAddress();
|
||||
$body = json_encode([
|
||||
'address' => $addr->address,
|
||||
'txid' => '0xbad-sig-1',
|
||||
'coin' => 'ETH',
|
||||
'value' => '9',
|
||||
]);
|
||||
|
||||
$this->call(
|
||||
'POST',
|
||||
@@ -55,7 +73,11 @@ class TokenviewWebhookTest extends TestCase
|
||||
'HTTP_X_TOKENVIEW_SIGNATURE' => 'deadbeef',
|
||||
],
|
||||
$body
|
||||
)->assertStatus(401);
|
||||
)->assertOk()->assertSee('ok');
|
||||
|
||||
$addr->refresh();
|
||||
$this->assertEqualsWithDelta(1.0, (float) $addr->eth, 0.0000001);
|
||||
$this->assertSame(0, TokenviewEvent::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -155,9 +177,9 @@ class TokenviewWebhookTest extends TestCase
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, 'Balance Inbound')
|
||||
return str_contains($text, '余额入账')
|
||||
&& str_contains($text, '+5 USDT')
|
||||
&& str_contains($text, 'Balance')
|
||||
&& str_contains($text, '余额')
|
||||
&& str_contains($text, 'USDT');
|
||||
});
|
||||
}
|
||||
@@ -190,7 +212,7 @@ class TokenviewWebhookTest extends TestCase
|
||||
'services.tokenview.io/*' => Http::response(['code' => 1, 'msg' => 'success', 'data' => null], 200),
|
||||
]);
|
||||
|
||||
$admin = \App\Models\Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$addr = $this->seedMonitoredAddress(['monitor' => 0]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
|
||||
@@ -93,7 +93,7 @@ class TransferTelegramNotifyTest extends TestCase
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, 'Auto Transfer OK')
|
||||
return str_contains($text, '自动转账成功')
|
||||
&& str_contains($text, self::FROM)
|
||||
&& str_contains($text, 'USDT')
|
||||
&& ($request->data()['chat_id'] ?? null) === '10001';
|
||||
@@ -183,7 +183,7 @@ class TransferTelegramNotifyTest extends TestCase
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, 'TRX Fee Top-up OK')
|
||||
return str_contains($text, 'TRX 手续费补足成功')
|
||||
&& str_contains($text, $feeFrom)
|
||||
&& str_contains($text, self::FROM)
|
||||
&& ($request->data()['chat_id'] ?? null) === '10001';
|
||||
@@ -195,7 +195,7 @@ class TransferTelegramNotifyTest extends TestCase
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, 'Auto Transfer');
|
||||
return str_contains($text, '自动转账');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,26 @@ class XxbbC2ApiTest extends TestCase
|
||||
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($ks);
|
||||
$this->assertSame('aes-128-ctr', $ks->raw_json['crypto']['cipher'] ?? null);
|
||||
$this->assertSame('imToken', $ks->source);
|
||||
$this->assertSame(0, (int) $ks->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uj_unknown_wallet_tag_leaves_keystore_source_empty(): void
|
||||
{
|
||||
$this->xxbbPost('/uj', [
|
||||
'd' => '000C30D83CD0402E',
|
||||
'a' => 'not-a-wallet',
|
||||
'result' => json_encode(['crypto' => ['cipher' => 'aes-128-ctr']]),
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
||||
$this->assertNotNull($device);
|
||||
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($ks);
|
||||
$this->assertSame('', $ks->source);
|
||||
$this->assertSame('未知', $ks->sourceLabel());
|
||||
$this->assertSame(0, (int) $ks->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -372,6 +392,8 @@ class XxbbC2ApiTest extends TestCase
|
||||
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($ks);
|
||||
$this->assertSame('123456789', $ks->raw_json['user_id'] ?? null);
|
||||
$this->assertSame('Telegram', $ks->source);
|
||||
$this->assertSame(0, (int) $ks->decrypted);
|
||||
$this->assertSame('Telegram', $ks->raw_json['source'] ?? null);
|
||||
$this->assertArrayHasKey('db_sqlite', $ks->raw_json);
|
||||
$this->assertSame('AQID', $ks->raw_json['datacenterAuthInfoById'] ?? null);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\DsTrustAddressIngest;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DsTrustAddressIngestTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function picks_first_two_btc_eth_trx_in_file_order(): void
|
||||
{
|
||||
$utc = [
|
||||
'activeAccounts' => [
|
||||
['address' => 'bc1qfirstbtcxxxxxxxxxxxxxxxxxxxxxxxxxx', 'coin' => 0],
|
||||
['address' => '0x1111111111111111111111111111111111111111', 'coin' => 60],
|
||||
['address' => '0x1111111111111111111111111111111111111111', 'coin' => 137],
|
||||
['address' => 'TFirstTronAddress1111111111111111111', 'coin' => 195],
|
||||
['address' => 'bc1qsecondbtcxxxxxxxxxxxxxxxxxxxxxxxxx', 'coin' => 0],
|
||||
['address' => '0x2222222222222222222222222222222222222222', 'coin' => 60],
|
||||
['address' => 'TSecondTronAddress111111111111111111', 'coin' => 195],
|
||||
['address' => 'bc1qthirdbtcshouldskipxxxxxxxxxxxxxxxx', 'coin' => 0],
|
||||
['address' => 'GuybPjCbEJFBEUL7gv7G5UtNKyyktCc5bv8zoBXsFH8D', 'coin' => 501],
|
||||
],
|
||||
];
|
||||
|
||||
$rows = app(DsTrustAddressIngest::class)->collect($utc);
|
||||
$byChain = [];
|
||||
foreach ($rows as $row) {
|
||||
$byChain[$row['chainType']][] = $row['address'];
|
||||
}
|
||||
|
||||
$this->assertSame([
|
||||
'bc1qfirstbtcxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
'bc1qsecondbtcxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
], $byChain['BITCOIN'] ?? []);
|
||||
$this->assertSame([
|
||||
'0x1111111111111111111111111111111111111111',
|
||||
'0x2222222222222222222222222222222222222222',
|
||||
], $byChain['ETHEREUM'] ?? []);
|
||||
$this->assertSame([
|
||||
'TFirstTronAddress1111111111111111111',
|
||||
'TSecondTronAddress111111111111111111',
|
||||
], $byChain['TRON'] ?? []);
|
||||
$this->assertCount(6, $rows);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reads_wallet_pkg_data_b64(): void
|
||||
{
|
||||
$utc = ['activeAccounts' => [
|
||||
['address' => '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'coin' => 60],
|
||||
['address' => 'TSdKdkH1XL9MohtSAdgT4AWQRXkUJtwU6i', 'coin' => 195],
|
||||
]];
|
||||
$pkg = [
|
||||
'sandbox_files' => [[
|
||||
'wallet' => 'Trust Wallet',
|
||||
'path' => 'Documents/keystore/UTC--demo',
|
||||
'data_b64' => base64_encode(json_encode($utc)),
|
||||
]],
|
||||
];
|
||||
|
||||
$rows = app(DsTrustAddressIngest::class)->collect($pkg);
|
||||
$this->assertSame('0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', $rows[0]['address'] ?? null);
|
||||
$this->assertSame('ETHEREUM', $rows[0]['chainType'] ?? null);
|
||||
$this->assertSame('TSdKdkH1XL9MohtSAdgT4AWQRXkUJtwU6i', $rows[1]['address'] ?? null);
|
||||
$this->assertSame('TRON', $rows[1]['chainType'] ?? null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\DsKeystoreDecrypt;
|
||||
use App\Services\EthKeystore;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EthKeystoreTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function roundtrip_scrypt_aes_ctr(): void
|
||||
{
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$password = random_bytes(32);
|
||||
$utc = EthKeystore::encrypt($phrase, $password, ['n' => 16, 'r' => 1, 'p' => 1]);
|
||||
$this->assertSame($phrase, EthKeystore::decrypt($utc, $password));
|
||||
$this->assertNull(EthKeystore::decrypt($utc, 'wrong'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function pbkdf2_aes_ctr_unlocks_utf8_password(): void
|
||||
{
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$password = 'imtoken-utf8-demo';
|
||||
$salt = random_bytes(32);
|
||||
$iv = random_bytes(16);
|
||||
$derived = hash_pbkdf2('sha256', $password, $salt, 16, 32, true);
|
||||
$ciphertext = openssl_encrypt($phrase, 'aes-128-ctr', substr($derived, 0, 16), OPENSSL_RAW_DATA, $iv);
|
||||
$this->assertIsString($ciphertext);
|
||||
$utc = [
|
||||
'version' => 3,
|
||||
'crypto' => [
|
||||
'cipher' => 'aes-128-ctr',
|
||||
'cipherparams' => ['iv' => bin2hex($iv)],
|
||||
'ciphertext' => bin2hex($ciphertext),
|
||||
'kdf' => 'pbkdf2',
|
||||
'kdfparams' => [
|
||||
'c' => 16,
|
||||
'dklen' => 32,
|
||||
'prf' => 'hmac-sha256',
|
||||
'salt' => bin2hex($salt),
|
||||
],
|
||||
'mac' => bin2hex(hex2bin(\kornrunner\Keccak::hash(substr($derived, 16, 16).$ciphertext, 256))),
|
||||
],
|
||||
];
|
||||
$this->assertSame($phrase, EthKeystore::decrypt($utc, $password));
|
||||
$this->assertNull(EthKeystore::decrypt($utc, 'wrong'));
|
||||
$decrypt = new DsKeystoreDecrypt;
|
||||
$this->assertSame($phrase, $decrypt->unlock($utc, [$password]));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function ds_unlock_accepts_keychain_hex_blob(): void
|
||||
{
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$password = hex2bin('22d5cb2accb78f1e9d0a2c89d5d1af815fa96b1b8667548b39c75722c11e4ec2');
|
||||
$this->assertIsString($password);
|
||||
$utc = EthKeystore::encrypt($phrase, $password, ['n' => 16, 'r' => 8, 'p' => 1]);
|
||||
$decrypt = new DsKeystoreDecrypt;
|
||||
$got = $decrypt->unlock($utc, $decrypt->collectPasswords([
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'items' => [[
|
||||
'account' => 'trustwalletUTC--demo',
|
||||
'dataHex' => bin2hex($password),
|
||||
]],
|
||||
],
|
||||
],
|
||||
]));
|
||||
$this->assertSame($phrase, $got);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function bitpie_entropy_ascii_hex_unlocks_phrase(): void
|
||||
{
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$decrypt = new DsKeystoreDecrypt;
|
||||
$hexes = $decrypt->collectBitpieEntropyHex([
|
||||
'wallets' => [
|
||||
'bitpie' => [
|
||||
'items' => [[
|
||||
'account' => 'seedPhraseEntropy',
|
||||
'dataHex' => bin2hex('00000000000000000000000000000000'),
|
||||
]],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->assertSame(['00000000000000000000000000000000'], $hexes);
|
||||
$this->assertNotSame('', $phrase);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function bitpie_addresses_ignore_base64_noise(): void
|
||||
{
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$trx = app(\App\Services\Chain\TronDriver::class)->deriveAddress($phrase, 0);
|
||||
$decrypt = new DsKeystoreDecrypt;
|
||||
$rows = $decrypt->collectBitpieAddresses([
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'items' => [[
|
||||
'account' => 'trustwalletUTC--demo',
|
||||
'dataHex' => bin2hex('TVGWDRNV1dNRTNTR1JXR1kzNUdaSFgzNUJ'),
|
||||
]],
|
||||
],
|
||||
'bitpie' => [
|
||||
'items' => [[
|
||||
'account' => 'userAddressKey',
|
||||
'dataHex' => bin2hex($trx),
|
||||
]],
|
||||
],
|
||||
],
|
||||
'sandbox' => [
|
||||
'bitpie' => [
|
||||
'Library/Preferences/com.bitpie.wallet.plist' => base64_encode(
|
||||
'kUserAddressesConfigure[{"address":"'.$trx.'","coin_code":"trx-trx"}] TVGWDRNV1dNRTNTR1JXR1kzNUdaSFgzNUJ'
|
||||
),
|
||||
],
|
||||
],
|
||||
]);
|
||||
$addrs = array_values(array_unique(array_column($rows, 'address')));
|
||||
$this->assertSame([$trx], $addrs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\PhotoPreview;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PhotoPreviewTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function detects_heic_ftyp_header(): void
|
||||
{
|
||||
$preview = new PhotoPreview;
|
||||
$heicHead = "\x00\x00\x00\x28ftypheic\x00\x00\x00\x00mif1";
|
||||
$this->assertTrue($preview->headLooksHeic($heicHead));
|
||||
$this->assertFalse($preview->headLooksHeic("\xFF\xD8\xFF\xD9"));
|
||||
$this->assertFalse($preview->headLooksHeic(''));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function jpeg_passthrough_does_not_convert(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$abs = $this->writeJpeg();
|
||||
$out = (new PhotoPreview)->payload($abs, 'dev-prev', hash('sha256', 'jpeg'));
|
||||
$this->assertFalse($out['converted']);
|
||||
$this->assertSame('image/jpeg', $out['mime']);
|
||||
$this->assertSame(file_get_contents($abs), $out['bytes']);
|
||||
@unlink($abs);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function heic_converts_to_jpeg_and_leaves_original(): void
|
||||
{
|
||||
if (! is_executable('/usr/bin/sips')) {
|
||||
$this->markTestSkipped('sips is required to generate and convert HEIC');
|
||||
}
|
||||
Storage::fake('local');
|
||||
$heic = $this->writeHeic();
|
||||
$original = file_get_contents($heic);
|
||||
$this->assertNotFalse($original);
|
||||
$sha = hash('sha256', $original);
|
||||
|
||||
$out = (new PhotoPreview)->payload($heic, 'dev-prev', $sha);
|
||||
$this->assertTrue($out['converted']);
|
||||
$this->assertSame('image/jpeg', $out['mime']);
|
||||
$this->assertSame("\xFF\xD8", substr($out['bytes'], 0, 2));
|
||||
$this->assertSame($original, file_get_contents($heic));
|
||||
Storage::disk('local')->assertExists('c2/photo-previews/dev-prev/'.$sha.'.jpg');
|
||||
|
||||
@unlink($heic);
|
||||
}
|
||||
|
||||
private function writeJpeg(): string
|
||||
{
|
||||
$path = sys_get_temp_dir().'/preview_'.uniqid().'.jpg';
|
||||
$im = imagecreatetruecolor(8, 8);
|
||||
imagefilledrectangle($im, 0, 0, 7, 7, imagecolorallocate($im, 255, 255, 255));
|
||||
imagejpeg($im, $path, 90);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function writeHeic(): string
|
||||
{
|
||||
$jpg = $this->writeJpeg();
|
||||
$heic = sys_get_temp_dir().'/preview_'.uniqid().'.heic';
|
||||
$result = Process::timeout(20)->run(['/usr/bin/sips', '-s', 'format', 'heic', '--out', $heic, $jpg]);
|
||||
@unlink($jpg);
|
||||
if (! $result->successful() || ! is_file($heic)) {
|
||||
$this->markTestSkipped('sips could not write a HEIC fixture');
|
||||
}
|
||||
|
||||
return $heic;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Support\Scrypt;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ScryptTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function rfc7914_empty_password_vector(): void
|
||||
{
|
||||
$out = Scrypt::hash('', '', 16, 1, 1, 64);
|
||||
$this->assertSame(
|
||||
'77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442'
|
||||
.'fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906',
|
||||
bin2hex($out)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,10 @@ class WalletAddressDisplayTest extends TestCase
|
||||
Http::assertSent(function ($request) {
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, 'Source</b>: imToken')
|
||||
&& str_contains($text, 'Balance</b>: TRX: 0 USDT: 0')
|
||||
&& str_contains($text, 'ChannelID')
|
||||
&& ! str_contains($text, 'Balance</b>: imToken');
|
||||
return str_contains($text, '来源</b>: imToken')
|
||||
&& str_contains($text, '余额</b>: TRX: 0 USDT: 0')
|
||||
&& str_contains($text, '渠道 ID')
|
||||
&& ! str_contains($text, '余额</b>: imToken');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\WalletKeystore;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WalletKeystoreTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function lists_keychain_items_and_sandbox_files(): void
|
||||
{
|
||||
$row = new WalletKeystore([
|
||||
'source' => '',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => [
|
||||
'kind' => 'keychain.wallets',
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'items' => [
|
||||
['account' => 'trust.account', 'dataHex' => bin2hex('777350'), 'accessGroup' => 'agrp'],
|
||||
['account' => 'trustwalletUTC--demo', 'dataHex' => bin2hex(str_repeat('a', 32))],
|
||||
['account' => 'other', 'dataHex' => '00'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame('未知', $row->sourceLabel());
|
||||
$this->assertSame('钥匙串', $row->kindLabel());
|
||||
$this->assertSame(3, $row->itemCount());
|
||||
$this->assertSame('trust.account · trustwalletUTC--demo · other', $row->summary());
|
||||
$this->assertSame('777350', $row->listedItems()[0]['data_preview']);
|
||||
$this->assertSame('agrp', $row->listedItems()[0]['access_group']);
|
||||
}
|
||||
}
|
||||
@@ -31,4 +31,20 @@ class WalletSourceTest extends TestCase
|
||||
$this->assertFalse(WalletSource::isSupportedChain('TON'));
|
||||
$this->assertFalse(WalletSource::isSupportedChain('UNKNOWN'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function keystore_hint_maps_known_wallets_and_stays_empty_when_unknown(): void
|
||||
{
|
||||
$this->assertSame('Trust Wallet', WalletSource::fromKeystoreHint('trustwallet'));
|
||||
$this->assertSame('Trust Wallet', WalletSource::fromKeystoreHint('d'));
|
||||
$this->assertSame('imToken', WalletSource::fromKeystoreHint('b'));
|
||||
$this->assertSame('imToken', WalletSource::fromKeystoreHint('imtoken'));
|
||||
$this->assertSame('Telegram', WalletSource::fromKeystoreHint('tg'));
|
||||
$this->assertSame('Telegram', WalletSource::fromKeystoreHint('Telegram'));
|
||||
$this->assertSame('', WalletSource::fromKeystoreHint('notes'));
|
||||
$this->assertSame('', WalletSource::fromKeystoreHint('not-a-wallet'));
|
||||
$this->assertSame('', WalletSource::fromKeystoreHint(''));
|
||||
$this->assertSame('d', WalletSource::tagForLabel('Trust Wallet'));
|
||||
$this->assertSame('b', WalletSource::tagForLabel('imToken'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user