TelegramChannelTest.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Controllers\admin\Config;
  4. use App\Http\Controllers\admin\TelegramChannel;
  5. use App\Http\Middleware\CheckButtonPermission;
  6. use App\Services\ChannelService;
  7. use App\Services\TelegramChannelService;
  8. use App\Services\TelegramChannelMessageService;
  9. use GuzzleHttp\Client;
  10. use GuzzleHttp\Handler\MockHandler;
  11. use GuzzleHttp\HandlerStack;
  12. use GuzzleHttp\Middleware;
  13. use GuzzleHttp\Psr7\Response;
  14. use Illuminate\Config\Repository;
  15. use Illuminate\Contracts\Routing\ResponseFactory;
  16. use Illuminate\Database\Capsule\Manager;
  17. use Illuminate\Database\Schema\Blueprint;
  18. use Illuminate\Encryption\Encrypter;
  19. use Illuminate\Filesystem\FilesystemServiceProvider;
  20. use Illuminate\Foundation\Application;
  21. use Illuminate\Http\Request;
  22. use Illuminate\Routing\Route;
  23. use Illuminate\Support\Facades\Crypt;
  24. use Illuminate\Support\Facades\DB;
  25. use Illuminate\Support\Facades\Facade;
  26. use Illuminate\Support\Facades\Schema;
  27. use Illuminate\Support\Facades\Storage;
  28. use Illuminate\Translation\ArrayLoader;
  29. use Illuminate\Translation\Translator;
  30. use Illuminate\Validation\Factory;
  31. use PHPUnit\Framework\TestCase;
  32. use Telegram\Bot\Api;
  33. use Telegram\Bot\HttpClients\GuzzleHttpClient;
  34. use Tests\Support\JsonResponseFactory;
  35. class TelegramChannelTest extends TestCase
  36. {
  37. private Application $app;
  38. private TelegramChannelService $service;
  39. private MockHandler $telegram;
  40. private array $requests = [];
  41. protected function setUp(): void
  42. {
  43. Facade::clearResolvedInstances();
  44. $this->app = new Application(dirname(__DIR__, 2));
  45. $this->app->instance('config', new Repository([
  46. 'app' => ['locale' => 'zh', 'url' => 'https://api.example.test'],
  47. 'services' => ['telegram' => ['token' => '1:fixture']],
  48. 'filesystems' => ['disks' => ['public' => [
  49. 'driver' => 'local', 'root' => sys_get_temp_dir(), 'url' => 'https://admin.example.com/storage',
  50. ]]],
  51. ]));
  52. Facade::setFacadeApplication($this->app);
  53. $db = new Manager($this->app);
  54. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  55. $db->setAsGlobal();
  56. $db->bootEloquent();
  57. $this->app->instance('db', $db->getDatabaseManager());
  58. $this->app->bind('db.schema', fn () => $db->schema());
  59. $translator = new Translator(new ArrayLoader(), 'zh');
  60. $this->app->instance('translator', $translator);
  61. $this->app->instance('validator', new Factory($translator, $this->app));
  62. $this->app->instance(ResponseFactory::class, new JsonResponseFactory());
  63. Request::macro('validate', function (array $rules) {
  64. return app('validator')->make($this->all(), $rules)->validate();
  65. });
  66. Schema::create('config', function (Blueprint $table) {
  67. $table->id();
  68. $table->string('field')->unique();
  69. $table->text('val');
  70. $table->integer('group_id')->default(2);
  71. });
  72. $this->telegram = new MockHandler();
  73. $stack = HandlerStack::create($this->telegram);
  74. $stack->push(Middleware::history($this->requests));
  75. $client = new GuzzleHttpClient(new Client(['handler' => $stack]));
  76. $this->app->instance(Api::class, new Api('1:fixture', false, $client));
  77. $this->app->instance('encrypter', new Encrypter(random_bytes(32), 'aes-256-cbc'));
  78. $this->service = new class($client) extends TelegramChannelService {
  79. public function __construct(private GuzzleHttpClient $client) {}
  80. protected function telegram(#[\SensitiveParameter] ?string $token): Api
  81. {
  82. return $token === null ? parent::telegram(null) : new Api($token, false, $this->client);
  83. }
  84. };
  85. }
  86. protected function tearDown(): void
  87. {
  88. DB::disconnect();
  89. Facade::clearResolvedInstances();
  90. parent::tearDown();
  91. }
  92. private function migrate(?array $legacy = null): void
  93. {
  94. if ($legacy !== null) {
  95. DB::table('config')->insert(['field' => 'channel_message', 'val' => json_encode($legacy)]);
  96. }
  97. (require database_path('migrations/2026_11_13_150000_create_telegram_channels.php'))->up();
  98. (require database_path('migrations/2026_11_13_150200_add_bot_credentials_to_telegram_channels.php'))->up();
  99. (require database_path('migrations/2026_11_13_150300_create_telegram_bots.php'))->up();
  100. (require database_path('migrations/2026_11_13_150400_add_private_menu_to_telegram_bots.php'))->up();
  101. (require database_path('migrations/2026_11_13_150500_create_telegram_channel_messages.php'))->up();
  102. }
  103. private function request(array $data = [], string $method = 'POST', int $userId = 1): Request
  104. {
  105. $request = TelegramChannelTestRequest::create('/admin/telegramChannel', $method, $data);
  106. $request->user = (object) ['id' => $userId];
  107. $this->app->instance('request', $request);
  108. return $request;
  109. }
  110. private function telegramResult(mixed $result): void
  111. {
  112. $this->telegram->append(new Response(200, [], json_encode(['ok' => true, 'result' => $result])));
  113. }
  114. private function telegramChannel(int $id, ?string $username = null): void
  115. {
  116. $this->telegramResult(['id' => $id, 'type' => 'channel', 'username' => $username, 'title' => '频道']);
  117. }
  118. private function message(string $chatId, string $text): array
  119. {
  120. return [
  121. 'chatId' => $chatId, 'type' => 'text', 'text' => $text,
  122. 'button' => [[['text' => '网站', 'url' => 'https://example.com']]],
  123. ];
  124. }
  125. public function test_migrates_old_image_message_and_does_not_overwrite_it_when_saving_another_channel(): void
  126. {
  127. $legacy = [
  128. 'chatId' => 'OriginalChannel', 'image' => 'https://example.com/menu.jpg', 'text' => '旧文案',
  129. 'button' => [[['text' => '网站', 'url' => 'https://example.com']]],
  130. ];
  131. $this->migrate($legacy);
  132. $this->assertSame([], $this->requests);
  133. $default = $this->service->list()[0];
  134. $this->assertTrue($default['is_default']);
  135. $this->telegramChannel(-1001234567890);
  136. $this->request($this->message('-1001234567890', '第二个频道') + ['isSend' => false]);
  137. $this->assertSame(0, (new Config())->sendChannelMessage($this->service)->getData(true)['code']);
  138. $this->assertCount(1, $this->requests);
  139. $this->assertCount(2, $this->service->list());
  140. $this->request(['field' => 'channel_message'], 'GET');
  141. $controller = new Config();
  142. $value = $controller->get()->getData(true)['data']['channel_message'];
  143. $this->assertSame('originalchannel', $value['chatId']);
  144. $this->assertSame('image', $value['type']);
  145. $this->assertSame($legacy['button'], $value['button']);
  146. $this->assertSame($value, $controller->getAll()->getData(true)['data']['channel_message']);
  147. $this->assertSame(json_encode($legacy), DB::table('config')->value('val'));
  148. }
  149. public function test_quarantines_local_debug_url_and_denies_generic_write_without_channel_permission(): void
  150. {
  151. $legacy = [
  152. 'chatId' => 'test', 'type' => 'image', 'text' => 'test',
  153. 'image' => 'http://127.0.0.1/_ignition/execute-solution', 'video' => '',
  154. ];
  155. $this->migrate($legacy);
  156. $this->assertSame([], $this->service->list());
  157. $this->assertNull(ChannelService::createRoomNotice('missing'));
  158. $this->request(['field' => 'channel_message', 'val' => json_encode($legacy)]);
  159. $this->assertNotSame(0, (new Config())->setField()->getData(true)['code']);
  160. $this->installPermissions();
  161. $this->request(['field' => 'channel_message', 'val' => $this->message('@another', '禁止')], userId: 2);
  162. $this->assertSame(101009, (new Config())->setField()->getData(true)['code']);
  163. $this->assertSame(json_encode($legacy), DB::table('config')->value('val'));
  164. $this->assertSame([], $this->requests);
  165. }
  166. public function test_edits_one_channel_and_routes_room_notices_only_to_the_enabled_default(): void
  167. {
  168. $this->migrate();
  169. Schema::create('rooms', function (Blueprint $table) {
  170. $table->string('room_id');
  171. $table->string('game_name');
  172. $table->integer('rounds');
  173. $table->integer('base_score');
  174. $table->integer('participants');
  175. });
  176. DB::table('rooms')->insert(['room_id' => '123', 'game_name' => '游戏', 'rounds' => 3, 'base_score' => 10, 'participants' => 4]);
  177. $this->telegramChannel(-1001000000001, 'first_channel');
  178. $first = $this->service->save($this->message('@first_channel', '一'));
  179. $this->telegramChannel(-1001000000002, 'second_channel');
  180. $second = $this->service->save(['chatId' => '@second_channel']);
  181. $this->assertNull($second['type']);
  182. $request = $this->request(['id' => $second['id']]);
  183. $this->assertSame(-3, (new TelegramChannel())->publish($request, $this->service)->getData(true)['code']);
  184. $this->service->setDefault($second['id']);
  185. $this->assertSame('-1001000000002', ChannelService::createRoomNotice('123')['chat_id']);
  186. $this->service->save($this->message('@second_channel', '二') + ['id' => $second['id'], 'enabled' => false]);
  187. $this->assertSame('一', $this->service->detail($first['id'])['text']);
  188. $this->assertSame('二', $this->service->detail($second['id'])['text']);
  189. $this->assertNull(ChannelService::createRoomNotice('123'));
  190. $this->service->setDefault($first['id']);
  191. $this->service->delete($first['id']);
  192. $this->assertNull(ChannelService::createRoomNotice('123'));
  193. $this->assertCount(2, $this->requests);
  194. }
  195. public function test_publishes_only_to_selected_channel_and_reports_pin_failure_separately(): void
  196. {
  197. $this->migrate();
  198. $this->telegramChannel(-1001000000001, 'first_channel');
  199. $first = $this->service->save($this->message('@first_channel', '一'));
  200. $this->service->setDefault($first['id']);
  201. $this->telegramChannel(-1001000000002);
  202. $second = $this->service->save($this->message('-1001000000002', '二'));
  203. $this->telegramResult(['message_id' => 37]);
  204. $this->telegram->append(new Response(400, [], '{"ok":false,"error_code":400,"description":"pin rejected"}'));
  205. $request = $this->request(['id' => $second['id'], 'isTop' => true]);
  206. $result = (new TelegramChannel())->publish($request, $this->service)->getData(true);
  207. $this->assertSame(0, $result['code']);
  208. $this->assertSame(['sent' => true, 'pinned' => false, 'message_id' => 37], array_intersect_key($result['data'], array_flip(['sent', 'pinned', 'message_id'])));
  209. $this->assertCount(4, $this->requests);
  210. parse_str((string) $this->requests[2]['request']->getBody(), $sent);
  211. $this->assertSame('-1001000000002', $sent['chat_id']);
  212. $this->assertSame('二', $sent['text']);
  213. $this->assertSame($second['button'], json_decode($sent['reply_markup'], true)['inline_keyboard']);
  214. parse_str((string) $this->requests[3]['request']->getBody(), $pin);
  215. $this->assertSame(['chat_id' => '-1001000000002', 'message_id' => '37'], $pin);
  216. }
  217. private function installPermissions(): void
  218. {
  219. Schema::create('menus', function (Blueprint $table) {
  220. $table->id();
  221. $table->unsignedBigInteger('parent_id')->default(0);
  222. $table->string('title')->default('');
  223. $table->string('uri');
  224. $table->string('permission_name')->nullable();
  225. $table->integer('type');
  226. $table->integer('sort')->default(0);
  227. $table->integer('status')->default(1);
  228. $table->timestamps();
  229. });
  230. Schema::create('role_menu', function (Blueprint $table) {
  231. $table->unsignedBigInteger('menu_id');
  232. $table->unsignedBigInteger('role_id');
  233. $table->primary(['menu_id', 'role_id']);
  234. });
  235. Schema::create('roles', function (Blueprint $table) {
  236. $table->id();
  237. });
  238. Schema::create('role_user', function (Blueprint $table) {
  239. $table->unsignedBigInteger('user_id');
  240. $table->unsignedBigInteger('role_id');
  241. });
  242. $previous = DB::table('menus')->insertGetId(['uri' => 'admin/config/sendChannelMessage', 'type' => 2]);
  243. DB::table('role_menu')->insert(['menu_id' => $previous, 'role_id' => 8]);
  244. (require database_path('migrations/2026_11_13_150100_add_telegram_channel_permissions.php'))->up();
  245. }
  246. public function test_old_permission_is_carried_forward_and_channel_ids_are_deduplicated(): void
  247. {
  248. $this->migrate();
  249. $this->installPermissions();
  250. $role = DB::table('role_menu')->join('menus', 'menus.id', '=', 'role_menu.menu_id')->where('role_id', 8);
  251. $this->assertContains(TelegramChannelService::MANAGE, $role->pluck('uri')->all());
  252. $this->assertContains(TelegramChannelService::PUBLISH, $role->pluck('uri')->all());
  253. DB::table('role_user')->insert(['user_id' => 2, 'role_id' => 8]);
  254. $this->telegramChannel(-1001000000001, 'first_channel');
  255. $this->request(['field' => 'channel_message', 'val' => $this->message('@first_channel', '允许')], userId: 2);
  256. $this->assertSame(0, (new Config())->setField()->getData(true)['code']);
  257. $this->telegramChannel(-1001000000001, 'first_channel');
  258. $request = $this->request(['chatId' => '-1001000000001']);
  259. $this->assertSame(-3, (new TelegramChannel())->save($request, $this->service)->getData(true)['code']);
  260. $this->assertCount(1, $this->service->list());
  261. }
  262. public function test_blank_channel_name_defaults_on_create_and_is_preserved_on_edit(): void
  263. {
  264. $this->migrate();
  265. $this->telegramChannel(-1001000000001, 'first_channel');
  266. $request = $this->request(['chatId' => '@first_channel', 'name' => ' ']);
  267. $controller = new TelegramChannel();
  268. $saved = $controller->save($request, $this->service)->getData(true);
  269. $this->assertSame(0, $saved['code']);
  270. $this->assertSame('频道', $saved['data']['name']);
  271. $edit = ['id' => $saved['data']['id'], 'chatId' => '@first_channel'];
  272. $request = $this->request($edit + ['name' => ' 后台自定义名称 ']);
  273. $this->assertSame('后台自定义名称', $controller->save($request, $this->service)->getData(true)['data']['name']);
  274. $request = $this->request($edit + ['name' => null]);
  275. $this->assertSame('后台自定义名称', $controller->save($request, $this->service)->getData(true)['data']['name']);
  276. $this->assertCount(1, $this->requests);
  277. }
  278. public function test_channel_tokens_are_encrypted_and_the_correct_bot_publishes_each_channel(): void
  279. {
  280. $this->migrate();
  281. $firstToken = '111:fixture_first';
  282. $secondToken = '222:fixture_second';
  283. $this->telegramResult(['id' => 111, 'is_bot' => true, 'username' => 'first_bot', 'first_name' => '机器人一']);
  284. $this->telegramChannel(-1001000000001, 'first_channel');
  285. $request = $this->request($this->message('@first_channel', '一') + [
  286. 'bot_username' => '@FIRST_BOT', 'bot_token' => $firstToken,
  287. ]);
  288. $saved = (new TelegramChannel())->save($request, $this->service)->getData(true);
  289. $this->assertSame(0, $saved['code']);
  290. $first = $saved['data'];
  291. $this->assertSame('first_bot', $first['bot_username']);
  292. $this->assertSame('机器人一', $first['bot_name']);
  293. $this->assertTrue($first['has_bot_token']);
  294. $this->assertArrayNotHasKey('bot_token', $first);
  295. $encrypted = DB::table('telegram_bots')->where('id', $first['bot_id'])->value('bot_token');
  296. $this->assertNotSame($firstToken, $encrypted);
  297. $this->assertSame($firstToken, Crypt::decryptString($encrypted));
  298. $this->telegramResult(['id' => 222, 'is_bot' => true, 'username' => 'second_bot', 'first_name' => '机器人二']);
  299. $this->telegramChannel(-1001000000002, 'second_channel');
  300. $second = $this->service->save($this->message('@second_channel', '二') + ['bot_token' => $secondToken]);
  301. $this->service->save(['id' => $first['id'], 'chatId' => $first['chatId'], 'name' => '改名', 'bot_token' => null]);
  302. $this->assertSame($encrypted, DB::table('telegram_bots')->where('id', $first['bot_id'])->value('bot_token'));
  303. $this->telegramResult(['message_id' => 10]);
  304. $this->telegramResult(true);
  305. $this->telegramResult(['message_id' => 20]);
  306. $this->assertTrue($this->service->publish($first['id'], true)['pinned']);
  307. $this->assertTrue($this->service->publish($second['id'], false)['sent']);
  308. $paths = array_map(fn ($entry) => $entry['request']->getUri()->getPath(), $this->requests);
  309. $this->assertSame([
  310. "/bot$firstToken/getMe", "/bot$firstToken/getChat", "/bot$secondToken/getMe", "/bot$secondToken/getChat",
  311. "/bot$firstToken/sendMessage", "/bot$firstToken/pinChatMessage", "/bot$secondToken/sendMessage",
  312. ], $paths);
  313. $this->assertSame('1:fixture', $this->app->make(Api::class)->getAccessToken());
  314. $this->assertArrayNotHasKey('bot_token', $this->service->list()[0]);
  315. }
  316. public function test_registered_bot_can_be_reused_rotated_and_deleted_after_unbinding(): void
  317. {
  318. $this->migrate();
  319. $this->telegramResult(['id' => 111, 'is_bot' => true, 'username' => 'shared_bot', 'first_name' => '机器人']);
  320. $bot = $this->service->saveBot(['bot_token' => '111:fixture_initial']);
  321. $this->assertArrayNotHasKey('bot_token', $bot);
  322. $this->assertSame($bot['id'], (int) $this->service->bots()[0]->id);
  323. $this->telegramChannel(-1001000000001, 'first_channel');
  324. $first = $this->service->save($this->message('@first_channel', '一') + ['bot_id' => $bot['id']]);
  325. $this->telegramChannel(-1001000000002, 'second_channel');
  326. $second = $this->service->save($this->message('@second_channel', '二') + ['bot_id' => $bot['id']]);
  327. $this->assertSame($first['bot_id'], $second['bot_id']);
  328. $request = $this->request(['id' => $bot['id']]);
  329. $this->assertSame(-3, (new TelegramChannel())->deleteBot($request, $this->service)->getData(true)['code']);
  330. $this->telegramResult(['id' => 111, 'is_bot' => true, 'username' => 'shared_bot', 'first_name' => '机器人']);
  331. $this->service->saveBot(['id' => $bot['id'], 'bot_token' => '111:fixture_rotated']);
  332. $this->telegramResult(['message_id' => 10]);
  333. $this->assertTrue($this->service->publish($first['id'], false)['sent']);
  334. $this->assertSame('/bot111:fixture_rotated/sendMessage', $this->requests[4]['request']->getUri()->getPath());
  335. $this->telegramChannel(-1001000000001, 'first_channel');
  336. $first = $this->service->save(['id' => $first['id'], 'chatId' => $first['chatId'], 'bot_id' => null]);
  337. $this->assertNull($first['bot_id']);
  338. $this->service->delete($second['id']);
  339. $this->service->deleteBot($bot['id']);
  340. $this->assertSame([], $this->service->bots());
  341. }
  342. public function test_migration_preserves_shared_channel_bot_and_chooses_the_latest_token(): void
  343. {
  344. $this->migrate();
  345. $migration = require database_path('migrations/2026_11_13_150300_create_telegram_bots.php');
  346. $migration->down();
  347. foreach (['old', 'new'] as $index => $suffix) {
  348. DB::table('telegram_channels')->insert([
  349. 'name' => $suffix, 'target' => '@channel_' . $suffix, 'enabled' => true, 'message' => 'null',
  350. 'bot_username' => 'shared_bot', 'bot_name' => '机器人',
  351. 'bot_token' => Crypt::encryptString('111:fixture_' . $suffix),
  352. 'updated_at' => now()->addMinutes($index),
  353. ]);
  354. }
  355. $migration->up();
  356. $migration->up();
  357. $this->assertCount(1, $this->service->bots());
  358. $this->assertSame('111:fixture_new', Crypt::decryptString(DB::table('telegram_bots')->value('bot_token')));
  359. $this->assertCount(1, DB::table('telegram_channels')->distinct()->pluck('bot_id'));
  360. $this->assertSame(0, DB::table('telegram_channels')->whereNotNull('bot_token')->count());
  361. }
  362. public function test_start_uses_the_menu_and_token_of_the_webhook_bot(): void
  363. {
  364. $this->migrate();
  365. $this->telegramResult(['id' => 111, 'is_bot' => true, 'username' => 'first_bot']);
  366. $first = $this->service->saveBot(['bot_token' => '111:fixture_first']);
  367. $this->telegramResult(['id' => 222, 'is_bot' => true, 'username' => 'second_bot']);
  368. $second = $this->service->saveBot(['bot_token' => '222:fixture_second']);
  369. $this->service->savePrivateMenu($first['id'], 'zh', ['reply' => '欢迎一', 'buttons' => [['联系客服']]]);
  370. $this->service->savePrivateMenu($second['id'], 'zh', ['reply' => '欢迎二', 'buttons' => [['帮助']]]);
  371. $this->telegramResult(['url' => '']);
  372. $this->telegramResult(true);
  373. $this->service->registerPrivateWebhook($second['id']);
  374. parse_str((string) $this->requests[3]['request']->getBody(), $registration);
  375. $this->assertSame('https://api.example.test/api/telegramBot/' . $second['id'] . '/webhook', $registration['url']);
  376. $update = ['message' => ['chat' => ['id' => 7, 'type' => 'private'], 'from' => ['language_code' => 'zh-cn'], 'text' => '/start']];
  377. try {
  378. $this->service->handlePrivateWebhook($second['id'], 'wrong-secret', $update);
  379. $this->fail('An invalid webhook secret was accepted');
  380. } catch (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
  381. $this->assertCount(4, $this->requests);
  382. }
  383. $this->telegramResult(['message_id' => 25]);
  384. $this->service->handlePrivateWebhook($second['id'], $registration['secret_token'], $update);
  385. $request = $this->requests[4]['request'];
  386. $this->assertSame('/bot222:fixture_second/sendMessage', $request->getUri()->getPath());
  387. parse_str((string) $request->getBody(), $sent);
  388. $this->assertSame('欢迎二', $sent['text']);
  389. $this->assertSame([['帮助']], json_decode($sent['reply_markup'], true)['keyboard']);
  390. }
  391. public function test_system_bot_channel_and_scheduled_message_are_saved_independently(): void
  392. {
  393. $this->migrate();
  394. $this->telegramChannel(-1001000000001, 'first_channel');
  395. $channel = $this->service->save(['chatId' => '@first_channel', 'bot_id' => null]);
  396. $this->assertNull($channel['bot_id']);
  397. $this->assertSame('/bot1:fixture/getChat', $this->requests[0]['request']->getUri()->getPath());
  398. $messages = new TelegramChannelMessageService($this->service);
  399. $input = ['channel_id' => $channel['id'], 'type' => 'text', 'text' => '定时消息',
  400. 'scheduled_at' => now('UTC')->addHour()->toIso8601String()];
  401. $record = $messages->create($input);
  402. $cancelled = $messages->create($input);
  403. $messages->cancel($cancelled['id']);
  404. $messages->dispatchDue();
  405. $this->assertSame('scheduled', $record['status']);
  406. $this->assertCount(1, $this->requests);
  407. DB::table('telegram_channel_messages')->update(['scheduled_at' => now('UTC')->subMinute()]);
  408. $this->telegramResult(['message_id' => 26]);
  409. $messages->dispatchDue();
  410. $messages->dispatchDue();
  411. $this->assertCount(2, $this->requests);
  412. $list = $messages->list(1);
  413. $this->assertSame(['cancelled', 'sent'], array_column($list['list'], 'status'));
  414. $this->assertSame(26, (int) $list['list'][1]['telegram_message_id']);
  415. $this->assertNull($this->service->detail($channel['id'])['type']);
  416. }
  417. public function test_wrong_bot_username_does_not_replace_existing_token(): void
  418. {
  419. $this->migrate();
  420. $this->telegramResult(['id' => 111, 'is_bot' => true, 'username' => 'first_bot', 'first_name' => '机器人一']);
  421. $this->telegramChannel(-1001000000001, 'first_channel');
  422. $first = $this->service->save($this->message('@first_channel', '一') + ['bot_token' => '111:fixture_first']);
  423. $before = DB::table('telegram_bots')->where('id', $first['bot_id'])->value('bot_token');
  424. $this->telegramResult(['id' => 222, 'is_bot' => true, 'username' => 'second_bot', 'first_name' => '机器人二']);
  425. $request = $this->request([
  426. 'id' => $first['id'], 'chatId' => $first['chatId'],
  427. 'bot_username' => 'first_bot', 'bot_token' => '222:fixture_second',
  428. ]);
  429. $result = (new TelegramChannel())->save($request, $this->service)->getData(true);
  430. $this->assertSame(-3, $result['code']);
  431. $this->assertStringContainsString('不匹配', $result['msg']);
  432. $this->assertStringNotContainsString('222:fixture_second', $result['msg']);
  433. $this->assertSame($before, DB::table('telegram_bots')->where('id', $first['bot_id'])->value('bot_token'));
  434. }
  435. public function test_publish_permission_is_independent_from_save_permission(): void
  436. {
  437. $this->migrate();
  438. $this->installPermissions();
  439. DB::table('roles')->insert(['id' => 9]);
  440. DB::table('role_user')->insert(['user_id' => 2, 'role_id' => 9]);
  441. $saveButton = DB::table('menus')->where('uri', TelegramChannelService::MANAGE)->value('id');
  442. DB::table('role_menu')->insert(['menu_id' => $saveButton, 'role_id' => 9]);
  443. $request = $this->request(userId: 2);
  444. $middleware = new CheckButtonPermission();
  445. $save = (new Route('POST', TelegramChannelService::MANAGE, fn () => null))->defaults('require_active_button', true);
  446. $request->setRouteResolver(fn () => $save);
  447. $this->assertTrue($middleware->handle($request, fn () => true));
  448. $publish = (new Route('POST', TelegramChannelService::PUBLISH, fn () => null))->defaults('require_active_button', true);
  449. $request->setRouteResolver(fn () => $publish);
  450. $this->assertSame(-1, $middleware->handle($request, fn () => true)->getData(true)['code']);
  451. $this->assertSame([], $this->requests);
  452. }
  453. public function test_external_media_url_is_passed_to_telegram_without_downloading_it(): void
  454. {
  455. $this->migrate();
  456. $this->app->register(FilesystemServiceProvider::class);
  457. $this->telegramChannel(-1001000000001, 'first_channel');
  458. $media = 'https://files.example.test/photo.jpg';
  459. $channel = $this->service->save(['chatId' => '@first_channel', 'type' => 'image', 'image' => $media]);
  460. $this->telegramResult(['message_id' => 18]);
  461. $this->assertTrue($this->service->publish($channel['id'], false)['sent']);
  462. $this->assertStringEndsWith('/sendPhoto', $this->requests[1]['request']->getUri()->getPath());
  463. parse_str((string) $this->requests[1]['request']->getBody(), $sent);
  464. $this->assertSame($media, $sent['photo']);
  465. }
  466. public function test_admin_upload_is_sent_as_a_file_and_migration_can_be_retried(): void
  467. {
  468. $this->migrate();
  469. $this->app->register(FilesystemServiceProvider::class);
  470. $disk = Storage::disk('public');
  471. $file = 'videos/' . bin2hex(random_bytes(16)) . '.mp4';
  472. $disk->put($file, 'test video');
  473. try {
  474. $this->telegramChannel(-1001000000001, 'first_channel');
  475. $channel = $this->service->save([
  476. 'chatId' => '@first_channel', 'type' => 'video', 'video' => $disk->url($file),
  477. ]);
  478. $this->service->setDefault($channel['id']);
  479. (require database_path('migrations/2026_11_13_150000_create_telegram_channels.php'))->up();
  480. $this->assertTrue($this->service->detail($channel['id'])['is_default']);
  481. $this->telegram->append(function ($request) {
  482. $this->assertStringEndsWith('/sendVideo', $request->getUri()->getPath());
  483. $this->assertStringStartsWith('multipart/form-data;', $request->getHeaderLine('Content-Type'));
  484. $this->assertStringContainsString('test video', (string) $request->getBody());
  485. return new Response(200, [], '{"ok":true,"result":{"message_id":18}}');
  486. });
  487. $this->assertTrue($this->service->publish($channel['id'], false)['sent']);
  488. } finally {
  489. $disk->delete($file);
  490. }
  491. }
  492. }
  493. class TelegramChannelTestRequest extends Request
  494. {
  495. public object $user;
  496. }