| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- <?php
- namespace Tests\Integration;
- use App\Http\Controllers\admin\Config;
- use App\Http\Controllers\admin\TelegramChannel;
- use App\Http\Middleware\CheckButtonPermission;
- use App\Services\ChannelService;
- use App\Services\TelegramChannelService;
- use GuzzleHttp\Client;
- use GuzzleHttp\Handler\MockHandler;
- use GuzzleHttp\HandlerStack;
- use GuzzleHttp\Middleware;
- use GuzzleHttp\Psr7\Response;
- use Illuminate\Config\Repository;
- use Illuminate\Contracts\Routing\ResponseFactory;
- use Illuminate\Database\Capsule\Manager;
- use Illuminate\Database\Schema\Blueprint;
- use Illuminate\Encryption\Encrypter;
- use Illuminate\Filesystem\FilesystemServiceProvider;
- use Illuminate\Foundation\Application;
- use Illuminate\Http\Request;
- use Illuminate\Routing\Route;
- use Illuminate\Support\Facades\Crypt;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Facade;
- use Illuminate\Support\Facades\Schema;
- use Illuminate\Support\Facades\Storage;
- use Illuminate\Translation\ArrayLoader;
- use Illuminate\Translation\Translator;
- use Illuminate\Validation\Factory;
- use PHPUnit\Framework\TestCase;
- use Telegram\Bot\Api;
- use Telegram\Bot\HttpClients\GuzzleHttpClient;
- use Tests\Support\JsonResponseFactory;
- class TelegramChannelTest extends TestCase
- {
- private Application $app;
- private TelegramChannelService $service;
- private MockHandler $telegram;
- private array $requests = [];
- protected function setUp(): void
- {
- Facade::clearResolvedInstances();
- $this->app = new Application(dirname(__DIR__, 2));
- $this->app->instance('config', new Repository([
- 'app' => ['locale' => 'zh'],
- 'filesystems' => ['disks' => ['public' => [
- 'driver' => 'local', 'root' => sys_get_temp_dir(), 'url' => 'https://admin.example.com/storage',
- ]]],
- ]));
- Facade::setFacadeApplication($this->app);
- $db = new Manager($this->app);
- $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
- $db->setAsGlobal();
- $db->bootEloquent();
- $this->app->instance('db', $db->getDatabaseManager());
- $this->app->bind('db.schema', fn () => $db->schema());
- $translator = new Translator(new ArrayLoader(), 'zh');
- $this->app->instance('translator', $translator);
- $this->app->instance('validator', new Factory($translator, $this->app));
- $this->app->instance(ResponseFactory::class, new JsonResponseFactory());
- Request::macro('validate', function (array $rules) {
- return app('validator')->make($this->all(), $rules)->validate();
- });
- Schema::create('config', function (Blueprint $table) {
- $table->id();
- $table->string('field')->unique();
- $table->text('val');
- $table->integer('group_id')->default(2);
- });
- $this->telegram = new MockHandler();
- $stack = HandlerStack::create($this->telegram);
- $stack->push(Middleware::history($this->requests));
- $client = new GuzzleHttpClient(new Client(['handler' => $stack]));
- $this->app->instance(Api::class, new Api('1:fixture', false, $client));
- $this->app->instance('encrypter', new Encrypter(random_bytes(32), 'aes-256-cbc'));
- $this->service = new class($client) extends TelegramChannelService {
- public function __construct(private GuzzleHttpClient $client) {}
- protected function telegram(#[\SensitiveParameter] ?string $token): Api
- {
- return $token === null ? parent::telegram(null) : new Api($token, false, $this->client);
- }
- };
- }
- protected function tearDown(): void
- {
- DB::disconnect();
- Facade::clearResolvedInstances();
- parent::tearDown();
- }
- private function migrate(?array $legacy = null): void
- {
- if ($legacy !== null) {
- DB::table('config')->insert(['field' => 'channel_message', 'val' => json_encode($legacy)]);
- }
- (require database_path('migrations/2026_11_13_150000_create_telegram_channels.php'))->up();
- (require database_path('migrations/2026_11_13_150200_add_bot_credentials_to_telegram_channels.php'))->up();
- }
- private function request(array $data = [], string $method = 'POST', int $userId = 1): Request
- {
- $request = TelegramChannelTestRequest::create('/admin/telegramChannel', $method, $data);
- $request->user = (object) ['id' => $userId];
- $this->app->instance('request', $request);
- return $request;
- }
- private function telegramResult(mixed $result): void
- {
- $this->telegram->append(new Response(200, [], json_encode(['ok' => true, 'result' => $result])));
- }
- private function telegramChannel(int $id, ?string $username = null): void
- {
- $this->telegramResult(['id' => $id, 'type' => 'channel', 'username' => $username, 'title' => '频道']);
- }
- private function message(string $chatId, string $text): array
- {
- return [
- 'chatId' => $chatId, 'type' => 'text', 'text' => $text,
- 'button' => [[['text' => '网站', 'url' => 'https://example.com']]],
- ];
- }
- public function test_migrates_old_image_message_and_does_not_overwrite_it_when_saving_another_channel(): void
- {
- $legacy = [
- 'chatId' => 'OriginalChannel', 'image' => 'https://example.com/menu.jpg', 'text' => '旧文案',
- 'button' => [[['text' => '网站', 'url' => 'https://example.com']]],
- ];
- $this->migrate($legacy);
- $this->assertSame([], $this->requests);
- $default = $this->service->list()[0];
- $this->assertTrue($default['is_default']);
- $this->telegramChannel(-1001234567890);
- $this->request($this->message('-1001234567890', '第二个频道') + ['isSend' => false]);
- $this->assertSame(0, (new Config())->sendChannelMessage($this->service)->getData(true)['code']);
- $this->assertCount(1, $this->requests);
- $this->assertCount(2, $this->service->list());
- $this->request(['field' => 'channel_message'], 'GET');
- $controller = new Config();
- $value = $controller->get()->getData(true)['data']['channel_message'];
- $this->assertSame('originalchannel', $value['chatId']);
- $this->assertSame('image', $value['type']);
- $this->assertSame($legacy['button'], $value['button']);
- $this->assertSame($value, $controller->getAll()->getData(true)['data']['channel_message']);
- $this->assertSame(json_encode($legacy), DB::table('config')->value('val'));
- }
- public function test_quarantines_local_debug_url_and_denies_generic_write_without_channel_permission(): void
- {
- $legacy = [
- 'chatId' => 'test', 'type' => 'image', 'text' => 'test',
- 'image' => 'http://127.0.0.1/_ignition/execute-solution', 'video' => '',
- ];
- $this->migrate($legacy);
- $this->assertSame([], $this->service->list());
- $this->assertNull(ChannelService::createRoomNotice('missing'));
- $this->request(['field' => 'channel_message', 'val' => json_encode($legacy)]);
- $this->assertNotSame(0, (new Config())->setField()->getData(true)['code']);
- $this->installPermissions();
- $this->request(['field' => 'channel_message', 'val' => $this->message('@another', '禁止')], userId: 2);
- $this->assertSame(101009, (new Config())->setField()->getData(true)['code']);
- $this->assertSame(json_encode($legacy), DB::table('config')->value('val'));
- $this->assertSame([], $this->requests);
- }
- public function test_edits_one_channel_and_routes_room_notices_only_to_the_enabled_default(): void
- {
- $this->migrate();
- Schema::create('rooms', function (Blueprint $table) {
- $table->string('room_id');
- $table->string('game_name');
- $table->integer('rounds');
- $table->integer('base_score');
- $table->integer('participants');
- });
- DB::table('rooms')->insert(['room_id' => '123', 'game_name' => '游戏', 'rounds' => 3, 'base_score' => 10, 'participants' => 4]);
- $this->telegramChannel(-1001000000001, 'first_channel');
- $first = $this->service->save($this->message('@first_channel', '一'));
- $this->telegramChannel(-1001000000002, 'second_channel');
- $second = $this->service->save(['chatId' => '@second_channel']);
- $this->assertNull($second['type']);
- $request = $this->request(['id' => $second['id']]);
- $this->assertSame(-3, (new TelegramChannel())->publish($request, $this->service)->getData(true)['code']);
- $this->service->setDefault($second['id']);
- $this->assertSame('-1001000000002', ChannelService::createRoomNotice('123')['chat_id']);
- $this->service->save($this->message('@second_channel', '二') + ['id' => $second['id'], 'enabled' => false]);
- $this->assertSame('一', $this->service->detail($first['id'])['text']);
- $this->assertSame('二', $this->service->detail($second['id'])['text']);
- $this->assertNull(ChannelService::createRoomNotice('123'));
- $this->service->setDefault($first['id']);
- $this->service->delete($first['id']);
- $this->assertNull(ChannelService::createRoomNotice('123'));
- $this->assertCount(2, $this->requests);
- }
- public function test_publishes_only_to_selected_channel_and_reports_pin_failure_separately(): void
- {
- $this->migrate();
- $this->telegramChannel(-1001000000001, 'first_channel');
- $first = $this->service->save($this->message('@first_channel', '一'));
- $this->service->setDefault($first['id']);
- $this->telegramChannel(-1001000000002);
- $second = $this->service->save($this->message('-1001000000002', '二'));
- $this->telegramResult(['message_id' => 37]);
- $this->telegram->append(new Response(400, [], '{"ok":false,"error_code":400,"description":"pin rejected"}'));
- $request = $this->request(['id' => $second['id'], 'isTop' => true]);
- $result = (new TelegramChannel())->publish($request, $this->service)->getData(true);
- $this->assertSame(0, $result['code']);
- $this->assertSame(['sent' => true, 'pinned' => false, 'message_id' => 37], array_intersect_key($result['data'], array_flip(['sent', 'pinned', 'message_id'])));
- $this->assertCount(4, $this->requests);
- parse_str((string) $this->requests[2]['request']->getBody(), $sent);
- $this->assertSame('-1001000000002', $sent['chat_id']);
- $this->assertSame('二', $sent['text']);
- $this->assertSame($second['button'], json_decode($sent['reply_markup'], true)['inline_keyboard']);
- parse_str((string) $this->requests[3]['request']->getBody(), $pin);
- $this->assertSame(['chat_id' => '-1001000000002', 'message_id' => '37'], $pin);
- }
- private function installPermissions(): void
- {
- Schema::create('menus', function (Blueprint $table) {
- $table->id();
- $table->unsignedBigInteger('parent_id')->default(0);
- $table->string('title')->default('');
- $table->string('uri');
- $table->string('permission_name')->nullable();
- $table->integer('type');
- $table->integer('sort')->default(0);
- $table->integer('status')->default(1);
- $table->timestamps();
- });
- Schema::create('role_menu', function (Blueprint $table) {
- $table->unsignedBigInteger('menu_id');
- $table->unsignedBigInteger('role_id');
- $table->primary(['menu_id', 'role_id']);
- });
- Schema::create('roles', function (Blueprint $table) {
- $table->id();
- });
- Schema::create('role_user', function (Blueprint $table) {
- $table->unsignedBigInteger('user_id');
- $table->unsignedBigInteger('role_id');
- });
- $previous = DB::table('menus')->insertGetId(['uri' => 'admin/config/sendChannelMessage', 'type' => 2]);
- DB::table('role_menu')->insert(['menu_id' => $previous, 'role_id' => 8]);
- (require database_path('migrations/2026_11_13_150100_add_telegram_channel_permissions.php'))->up();
- }
- public function test_old_permission_is_carried_forward_and_channel_ids_are_deduplicated(): void
- {
- $this->migrate();
- $this->installPermissions();
- $role = DB::table('role_menu')->join('menus', 'menus.id', '=', 'role_menu.menu_id')->where('role_id', 8);
- $this->assertContains(TelegramChannelService::MANAGE, $role->pluck('uri')->all());
- $this->assertContains(TelegramChannelService::PUBLISH, $role->pluck('uri')->all());
- DB::table('role_user')->insert(['user_id' => 2, 'role_id' => 8]);
- $this->telegramChannel(-1001000000001, 'first_channel');
- $this->request(['field' => 'channel_message', 'val' => $this->message('@first_channel', '允许')], userId: 2);
- $this->assertSame(0, (new Config())->setField()->getData(true)['code']);
- $this->telegramChannel(-1001000000001, 'first_channel');
- $request = $this->request(['chatId' => '-1001000000001']);
- $this->assertSame(-3, (new TelegramChannel())->save($request, $this->service)->getData(true)['code']);
- $this->assertCount(1, $this->service->list());
- }
- public function test_blank_channel_name_defaults_on_create_and_is_preserved_on_edit(): void
- {
- $this->migrate();
- $this->telegramChannel(-1001000000001, 'first_channel');
- $request = $this->request(['chatId' => '@first_channel', 'name' => ' ']);
- $controller = new TelegramChannel();
- $saved = $controller->save($request, $this->service)->getData(true);
- $this->assertSame(0, $saved['code']);
- $this->assertSame('频道', $saved['data']['name']);
- $edit = ['id' => $saved['data']['id'], 'chatId' => '@first_channel'];
- $request = $this->request($edit + ['name' => ' 后台自定义名称 ']);
- $this->assertSame('后台自定义名称', $controller->save($request, $this->service)->getData(true)['data']['name']);
- $request = $this->request($edit + ['name' => null]);
- $this->assertSame('后台自定义名称', $controller->save($request, $this->service)->getData(true)['data']['name']);
- $this->assertCount(1, $this->requests);
- }
- public function test_channel_tokens_are_encrypted_and_the_correct_bot_publishes_each_channel(): void
- {
- $this->migrate();
- $firstToken = '111:fixture_first';
- $secondToken = '222:fixture_second';
- $this->telegramResult(['id' => 111, 'is_bot' => true, 'username' => 'first_bot', 'first_name' => '机器人一']);
- $this->telegramChannel(-1001000000001, 'first_channel');
- $request = $this->request($this->message('@first_channel', '一') + [
- 'bot_username' => '@FIRST_BOT', 'bot_token' => $firstToken,
- ]);
- $saved = (new TelegramChannel())->save($request, $this->service)->getData(true);
- $this->assertSame(0, $saved['code']);
- $first = $saved['data'];
- $this->assertSame('first_bot', $first['bot_username']);
- $this->assertSame('机器人一', $first['bot_name']);
- $this->assertTrue($first['has_bot_token']);
- $this->assertArrayNotHasKey('bot_token', $first);
- $encrypted = DB::table('telegram_channels')->where('id', $first['id'])->value('bot_token');
- $this->assertNotSame($firstToken, $encrypted);
- $this->assertSame($firstToken, Crypt::decryptString($encrypted));
- $this->telegramResult(['id' => 222, 'is_bot' => true, 'username' => 'second_bot', 'first_name' => '机器人二']);
- $this->telegramChannel(-1001000000002, 'second_channel');
- $second = $this->service->save($this->message('@second_channel', '二') + ['bot_token' => $secondToken]);
- $this->service->save(['id' => $first['id'], 'chatId' => $first['chatId'], 'name' => '改名', 'bot_token' => null]);
- $this->assertSame($encrypted, DB::table('telegram_channels')->where('id', $first['id'])->value('bot_token'));
- $this->telegramResult(['message_id' => 10]);
- $this->telegramResult(true);
- $this->telegramResult(['message_id' => 20]);
- $this->assertTrue($this->service->publish($first['id'], true)['pinned']);
- $this->assertTrue($this->service->publish($second['id'], false)['sent']);
- $paths = array_map(fn ($entry) => $entry['request']->getUri()->getPath(), $this->requests);
- $this->assertSame([
- "/bot$firstToken/getMe", "/bot$firstToken/getChat", "/bot$secondToken/getMe", "/bot$secondToken/getChat",
- "/bot$firstToken/sendMessage", "/bot$firstToken/pinChatMessage", "/bot$secondToken/sendMessage",
- ], $paths);
- $this->assertSame('1:fixture', $this->app->make(Api::class)->getAccessToken());
- $this->assertArrayNotHasKey('bot_token', $this->service->list()[0]);
- }
- public function test_wrong_bot_username_does_not_replace_existing_token(): void
- {
- $this->migrate();
- $this->telegramResult(['id' => 111, 'is_bot' => true, 'username' => 'first_bot', 'first_name' => '机器人一']);
- $this->telegramChannel(-1001000000001, 'first_channel');
- $first = $this->service->save($this->message('@first_channel', '一') + ['bot_token' => '111:fixture_first']);
- $before = DB::table('telegram_channels')->where('id', $first['id'])->value('bot_token');
- $this->telegramResult(['id' => 222, 'is_bot' => true, 'username' => 'second_bot', 'first_name' => '机器人二']);
- $request = $this->request([
- 'id' => $first['id'], 'chatId' => $first['chatId'],
- 'bot_username' => 'first_bot', 'bot_token' => '222:fixture_second',
- ]);
- $result = (new TelegramChannel())->save($request, $this->service)->getData(true);
- $this->assertSame(-3, $result['code']);
- $this->assertStringContainsString('不匹配', $result['msg']);
- $this->assertStringNotContainsString('222:fixture_second', $result['msg']);
- $this->assertSame($before, DB::table('telegram_channels')->where('id', $first['id'])->value('bot_token'));
- }
- public function test_publish_permission_is_independent_from_save_permission(): void
- {
- $this->migrate();
- $this->installPermissions();
- DB::table('roles')->insert(['id' => 9]);
- DB::table('role_user')->insert(['user_id' => 2, 'role_id' => 9]);
- $saveButton = DB::table('menus')->where('uri', TelegramChannelService::MANAGE)->value('id');
- DB::table('role_menu')->insert(['menu_id' => $saveButton, 'role_id' => 9]);
- $request = $this->request(userId: 2);
- $middleware = new CheckButtonPermission();
- $save = (new Route('POST', TelegramChannelService::MANAGE, fn () => null))->defaults('require_active_button', true);
- $request->setRouteResolver(fn () => $save);
- $this->assertTrue($middleware->handle($request, fn () => true));
- $publish = (new Route('POST', TelegramChannelService::PUBLISH, fn () => null))->defaults('require_active_button', true);
- $request->setRouteResolver(fn () => $publish);
- $this->assertSame(-1, $middleware->handle($request, fn () => true)->getData(true)['code']);
- $this->assertSame([], $this->requests);
- }
- public function test_external_media_url_is_passed_to_telegram_without_downloading_it(): void
- {
- $this->migrate();
- $this->app->register(FilesystemServiceProvider::class);
- $this->telegramChannel(-1001000000001, 'first_channel');
- $media = 'https://files.example.test/photo.jpg';
- $channel = $this->service->save(['chatId' => '@first_channel', 'type' => 'image', 'image' => $media]);
- $this->telegramResult(['message_id' => 18]);
- $this->assertTrue($this->service->publish($channel['id'], false)['sent']);
- $this->assertStringEndsWith('/sendPhoto', $this->requests[1]['request']->getUri()->getPath());
- parse_str((string) $this->requests[1]['request']->getBody(), $sent);
- $this->assertSame($media, $sent['photo']);
- }
- public function test_admin_upload_is_sent_as_a_file_and_migration_can_be_retried(): void
- {
- $this->migrate();
- $this->app->register(FilesystemServiceProvider::class);
- $disk = Storage::disk('public');
- $file = 'videos/' . bin2hex(random_bytes(16)) . '.mp4';
- $disk->put($file, 'test video');
- try {
- $this->telegramChannel(-1001000000001, 'first_channel');
- $channel = $this->service->save([
- 'chatId' => '@first_channel', 'type' => 'video', 'video' => $disk->url($file),
- ]);
- $this->service->setDefault($channel['id']);
- (require database_path('migrations/2026_11_13_150000_create_telegram_channels.php'))->up();
- $this->assertTrue($this->service->detail($channel['id'])['is_default']);
- $this->telegram->append(function ($request) {
- $this->assertStringEndsWith('/sendVideo', $request->getUri()->getPath());
- $this->assertStringStartsWith('multipart/form-data;', $request->getHeaderLine('Content-Type'));
- $this->assertStringContainsString('test video', (string) $request->getBody());
- return new Response(200, [], '{"ok":true,"result":{"message_id":18}}');
- });
- $this->assertTrue($this->service->publish($channel['id'], false)['sent']);
- } finally {
- $disk->delete($file);
- }
- }
- }
- class TelegramChannelTestRequest extends Request
- {
- public object $user;
- }
|