|
|
@@ -0,0 +1,137 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Tests\Integration;
|
|
|
+
|
|
|
+use App\Http\Controllers\admin\SiteConfiguration;
|
|
|
+use App\Services\SiteConfigurationService;
|
|
|
+use Illuminate\Config\Repository;
|
|
|
+use Illuminate\Database\Capsule\Manager;
|
|
|
+use Illuminate\Events\Dispatcher;
|
|
|
+use Illuminate\Foundation\Application;
|
|
|
+use Illuminate\Http\JsonResponse;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Routing\Router;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Facade;
|
|
|
+use Illuminate\Translation\ArrayLoader;
|
|
|
+use Illuminate\Translation\Translator;
|
|
|
+use Illuminate\Validation\Factory;
|
|
|
+use Illuminate\Validation\ValidationException;
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
+
|
|
|
+class SiteConfigurationTest extends TestCase
|
|
|
+{
|
|
|
+ private Application $app;
|
|
|
+ private SiteConfigurationService $service;
|
|
|
+
|
|
|
+ protected function setUp(): void
|
|
|
+ {
|
|
|
+ Facade::clearResolvedInstances(); $this->app = new Application(dirname(__DIR__, 2));
|
|
|
+ $this->app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']]));
|
|
|
+ Facade::setFacadeApplication($this->app);
|
|
|
+ $db = new Manager($this->app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
|
|
|
+ $db->setEventDispatcher(new Dispatcher($this->app)); $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('log', new \Psr\Log\NullLogger());
|
|
|
+ $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new class {
|
|
|
+ public function json($data, $status = 200, $headers = [], $options = 0) { return new JsonResponse($data, $status, $headers, $options); }
|
|
|
+ });
|
|
|
+ $request = Request::create('/admin/site/config'); $request->user = (object) ['id' => 1, 'username' => 'admin']; $this->app->instance('request', $request);
|
|
|
+ DB::statement('CREATE TABLE bot_operation_audits (id INTEGER PRIMARY KEY AUTOINCREMENT,resource_type TEXT,resource_id INTEGER,action TEXT,changes TEXT,operator_id INTEGER,operator_name TEXT,created_at TEXT)');
|
|
|
+ DB::statement('CREATE TABLE bot_menus (id INTEGER PRIMARY KEY,title TEXT)');
|
|
|
+ DB::table('menus')->insert(['id' => 1, 'title' => '前端已有菜单']);
|
|
|
+ (require dirname(__DIR__, 2) . '/database/migrations/2026_10_27_120000_create_site_settings.php')->up();
|
|
|
+ $this->service = new SiteConfigurationService();
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function tearDown(): void { DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); }
|
|
|
+
|
|
|
+ private function emptyConfig(): array { return ['translations' => ['zh' => ['content' => ''], 'en' => ['content' => ''], 'vi' => ['content' => '']], 'domains' => []]; }
|
|
|
+
|
|
|
+ public function test_defaults_and_migration_do_not_require_a_separate_configuration_record_or_touch_menus(): void
|
|
|
+ {
|
|
|
+ $this->assertSame($this->emptyConfig(), $this->service->get());
|
|
|
+ $this->assertSame(1, DB::table('menus')->count());
|
|
|
+ $migration = require dirname(__DIR__, 2) . '/database/migrations/2026_10_27_120000_create_site_settings.php';
|
|
|
+ $migration->down();
|
|
|
+ $this->assertSame('前端已有菜单', DB::table('menus')->value('title'));
|
|
|
+ $migration->up();
|
|
|
+ $this->assertSame($this->emptyConfig(), $this->service->get());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_both_fields_save_together_preserve_domain_order_and_return_normalized_values(): void
|
|
|
+ {
|
|
|
+ $result = $this->service->save(['translations' => ['zh' => ['content' => ' 欢迎体验 '], 'en' => ['content' => ' Welcome '], 'vi' => ['content' => ' Chào mừng ']], 'domains' => ['example.com', 'https://EXAMPLE.com/', 'HTTP://BACKUP.EXAMPLE:8080/']]);
|
|
|
+ $this->assertSame(['translations' => ['zh' => ['content' => '欢迎体验'], 'en' => ['content' => 'Welcome'], 'vi' => ['content' => 'Chào mừng']], 'domains' => ['https://example.com', 'http://backup.example:8080']], $result);
|
|
|
+ $this->assertSame($result, $this->service->get());
|
|
|
+ $this->assertSame(1, DB::table('operation_audits')->count());
|
|
|
+ $this->service->save($result);
|
|
|
+ $this->assertSame(1, DB::table('operation_audits')->count());
|
|
|
+ $this->assertSame($this->emptyConfig(), $this->service->save(['translations' => [], 'domains' => []]));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** @dataProvider invalidPayloads */
|
|
|
+ public function test_invalid_configuration_does_not_partially_save(array $payload): void
|
|
|
+ {
|
|
|
+ try { $this->service->save($payload); $this->fail('Expected validation failure'); }
|
|
|
+ catch (ValidationException $e) { $this->assertNotEmpty($e->validator->errors()->all()); }
|
|
|
+ $this->assertSame($this->emptyConfig(), $this->service->get());
|
|
|
+ $this->assertSame(0, DB::table('operation_audits')->count());
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function invalidPayloads(): array
|
|
|
+ {
|
|
|
+ $base = ['translations' => ['zh' => ['content' => '测试']], 'domains' => []];
|
|
|
+ return [[['domains' => []]], [['translations' => []]],
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => str_repeat('字', 2001)]]])],
|
|
|
+ [array_replace($base, ['domains' => ['primary' => 'https://example.com']])],
|
|
|
+ [array_replace($base, ['domains' => array_fill(0, 101, 'https://example.com')])],
|
|
|
+ [array_replace($base, ['domains' => ['']])], [array_replace($base, ['domains' => [123]])],
|
|
|
+ [array_replace($base, ['domains' => ['ftp://example.com']])],
|
|
|
+ [array_replace($base, ['domains' => ['javascript:alert(1)']])],
|
|
|
+ [array_replace($base, ['domains' => ['https://user:pass@example.com']])],
|
|
|
+ [array_replace($base, ['domains' => ['https://example.com/path']])],
|
|
|
+ [array_replace($base, ['domains' => ['https://example.com?x=1']])],
|
|
|
+ [array_replace($base, ['domains' => ['https://example.com/#fragment']])],
|
|
|
+ [array_replace($base, ['translations' => ['fr' => ['content' => 'French']]])],
|
|
|
+ [array_replace($base, ['translations' => ['zh' => '不是对象']])],
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['title' => '未知字段']]])]];
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_translations_are_independent_and_full_replacement_clears_omitted_languages(): void
|
|
|
+ {
|
|
|
+ $this->service->save(['translations' => ['zh' => ['content' => '中文'], 'en' => ['content' => 'English'], 'vi' => ['content' => 'Tiếng Việt']], 'domains' => ['example.com']]);
|
|
|
+ $this->assertSame(3, DB::table('site_setting_translations')->count());
|
|
|
+ $next = $this->service->save(['translations' => ['zh' => ['content' => '新中文']], 'domains' => ['shared.example']]);
|
|
|
+ $this->assertSame(['zh' => ['content' => '新中文'], 'en' => ['content' => ''], 'vi' => ['content' => '']], $next['translations']);
|
|
|
+ $this->assertSame(['https://shared.example'], $next['domains']);
|
|
|
+ $this->assertSame(1, DB::table('site_setting_translations')->count());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_audit_failure_rolls_back_both_configuration_fields(): void
|
|
|
+ {
|
|
|
+ $before = $this->service->save(['translations' => ['zh' => ['content' => '原内容']], 'domains' => ['example.com']]);
|
|
|
+ DB::statement('DROP TABLE bot_operation_audits');
|
|
|
+ try { $this->service->save(['translations' => ['zh' => ['content' => '新内容']], 'domains' => ['new.example']]); $this->fail('Expected audit failure'); }
|
|
|
+ catch (\Illuminate\Database\QueryException $e) { $this->assertSame($before, $this->service->get()); }
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_one_admin_path_supports_read_and_write_with_authentication_and_permission_checks(): void
|
|
|
+ {
|
|
|
+ $router = new Router(new Dispatcher($this->app), $this->app); $this->app->instance('router', $router);
|
|
|
+ $router->group(['prefix' => 'admin'], function () { require dirname(__DIR__, 2) . '/routes/admin.php'; });
|
|
|
+ $get = $router->getRoutes()->match(Request::create('/admin/site/config', 'GET'));
|
|
|
+ $post = $router->getRoutes()->match(Request::create('/admin/site/config', 'POST'));
|
|
|
+ foreach ([$get, $post] as $route) {
|
|
|
+ $this->assertContains('admin.jwt', $route->middleware()); $this->assertContains('check.button.uri', $route->middleware());
|
|
|
+ }
|
|
|
+ $this->assertContains('operations.admin', $post->middleware());
|
|
|
+ $this->assertSame('admin/site/config/save', $post->defaults['permission_uri']);
|
|
|
+ $response = (new SiteConfiguration())->show($this->service);
|
|
|
+ $this->assertSame(0, $response->getData(true)['code']);
|
|
|
+ $this->assertSame($this->emptyConfig(), $response->getData(true)['data']);
|
|
|
+ $this->assertStringContainsString('no-store', $response->headers->get('Cache-Control'));
|
|
|
+ }
|
|
|
+}
|