(string) self::OWNER_CHAT, 'coruna.telegram.webhook_secret' => 'test-secret', 'nutgram.token' => FakeNutgram::TOKEN, ]); // 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 unauthorized_chat_is_silent(): void { /** @var FakeNutgram $bot */ $bot = app(Nutgram::class); $bot->hearMessage([ 'text' => '/ping', '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 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' => '/ping', '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_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() ->with('tron', 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH', '10', 'USDT') ->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'] ?? ''); } }