|
@@ -43,12 +43,13 @@ class SiteConfigurationTest extends TestCase
|
|
|
DB::statement('CREATE TABLE bot_menus (id INTEGER PRIMARY KEY,title TEXT)');
|
|
DB::statement('CREATE TABLE bot_menus (id INTEGER PRIMARY KEY,title TEXT)');
|
|
|
DB::table('menus')->insert(['id' => 1, 'title' => '前端已有菜单']);
|
|
DB::table('menus')->insert(['id' => 1, 'title' => '前端已有菜单']);
|
|
|
(require dirname(__DIR__, 2) . '/database/migrations/2026_10_27_120000_create_site_settings.php')->up();
|
|
(require dirname(__DIR__, 2) . '/database/migrations/2026_10_27_120000_create_site_settings.php')->up();
|
|
|
|
|
+ (require dirname(__DIR__, 2) . '/database/migrations/2026_10_31_120000_add_contents_to_site_setting_translations.php')->up();
|
|
|
$this->service = new SiteConfigurationService();
|
|
$this->service = new SiteConfigurationService();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
protected function tearDown(): void { DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); }
|
|
protected function tearDown(): void { DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); }
|
|
|
|
|
|
|
|
- private function emptyConfig(): array { return ['translations' => ['zh' => ['content' => ''], 'en' => ['content' => ''], 'vi' => ['content' => '']], 'domains' => []]; }
|
|
|
|
|
|
|
+ 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
|
|
public function test_defaults_and_migration_do_not_require_a_separate_configuration_record_or_touch_menus(): void
|
|
|
{
|
|
{
|
|
@@ -58,13 +59,14 @@ class SiteConfigurationTest extends TestCase
|
|
|
$migration->down();
|
|
$migration->down();
|
|
|
$this->assertSame('前端已有菜单', DB::table('menus')->value('title'));
|
|
$this->assertSame('前端已有菜单', DB::table('menus')->value('title'));
|
|
|
$migration->up();
|
|
$migration->up();
|
|
|
|
|
+ (require dirname(__DIR__, 2) . '/database/migrations/2026_10_31_120000_add_contents_to_site_setting_translations.php')->up();
|
|
|
$this->assertSame($this->emptyConfig(), $this->service->get());
|
|
$this->assertSame($this->emptyConfig(), $this->service->get());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public function test_both_fields_save_together_preserve_domain_order_and_return_normalized_values(): void
|
|
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);
|
|
|
|
|
|
|
+ $result = $this->service->save(['translations' => ['zh' => ['content' => [' 欢迎体验 ', ' 第二条 ', ' ', '', '欢迎体验']], 'en' => ['content' => [' Welcome ', 'Second item']], 'vi' => ['content' => [' Chào mừng ']]], 'domains' => ['example.com', 'https://EXAMPLE.com/', 'HTTP://BACKUP.EXAMPLE:8080/']]);
|
|
|
|
|
+ $this->assertSame(['translations' => ['zh' => ['content' => ['欢迎体验', '第二条', '欢迎体验']], 'en' => ['content' => ['Welcome', 'Second item']], 'vi' => ['content' => ['Chào mừng']]], 'domains' => ['https://example.com', 'http://backup.example:8080']], $result);
|
|
|
$this->assertSame($result, $this->service->get());
|
|
$this->assertSame($result, $this->service->get());
|
|
|
$this->assertSame(1, DB::table('operation_audits')->count());
|
|
$this->assertSame(1, DB::table('operation_audits')->count());
|
|
|
$this->service->save($result);
|
|
$this->service->save($result);
|
|
@@ -83,9 +85,17 @@ class SiteConfigurationTest extends TestCase
|
|
|
|
|
|
|
|
public static function invalidPayloads(): array
|
|
public static function invalidPayloads(): array
|
|
|
{
|
|
{
|
|
|
- $base = ['translations' => ['zh' => ['content' => '测试']], 'domains' => []];
|
|
|
|
|
|
|
+ $base = ['translations' => ['zh' => ['content' => ['测试']]], 'domains' => []];
|
|
|
return [[['domains' => []]], [['translations' => []]],
|
|
return [[['domains' => []]], [['translations' => []]],
|
|
|
- [array_replace($base, ['translations' => ['zh' => ['content' => str_repeat('字', 2001)]]])],
|
|
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => [str_repeat('字', 2001)]]]])],
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => '旧字符串']]])],
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => '']]])],
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => null]]])],
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => [123]]]])],
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => [false]]]])],
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => [['nested']]]]])],
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => ['first' => '文字']]]])],
|
|
|
|
|
+ [array_replace($base, ['translations' => ['zh' => ['content' => array_fill(0, 101, '文字')]]])],
|
|
|
[array_replace($base, ['domains' => ['primary' => 'https://example.com']])],
|
|
[array_replace($base, ['domains' => ['primary' => 'https://example.com']])],
|
|
|
[array_replace($base, ['domains' => array_fill(0, 101, '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' => ['']])], [array_replace($base, ['domains' => [123]])],
|
|
@@ -95,29 +105,91 @@ class SiteConfigurationTest extends TestCase
|
|
|
[array_replace($base, ['domains' => ['https://example.com/path']])],
|
|
[array_replace($base, ['domains' => ['https://example.com/path']])],
|
|
|
[array_replace($base, ['domains' => ['https://example.com?x=1']])],
|
|
[array_replace($base, ['domains' => ['https://example.com?x=1']])],
|
|
|
[array_replace($base, ['domains' => ['https://example.com/#fragment']])],
|
|
[array_replace($base, ['domains' => ['https://example.com/#fragment']])],
|
|
|
- [array_replace($base, ['translations' => ['fr' => ['content' => 'French']]])],
|
|
|
|
|
|
|
+ [array_replace($base, ['translations' => ['fr' => ['content' => ['French']]]])],
|
|
|
[array_replace($base, ['translations' => ['zh' => '不是对象']])],
|
|
[array_replace($base, ['translations' => ['zh' => '不是对象']])],
|
|
|
[array_replace($base, ['translations' => ['zh' => ['title' => '未知字段']]])]];
|
|
[array_replace($base, ['translations' => ['zh' => ['title' => '未知字段']]])]];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public function test_translations_are_independent_and_full_replacement_clears_omitted_languages(): void
|
|
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->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());
|
|
$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']);
|
|
|
|
|
|
|
+ $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(['https://shared.example'], $next['domains']);
|
|
|
$this->assertSame(1, DB::table('site_setting_translations')->count());
|
|
$this->assertSame(1, DB::table('site_setting_translations')->count());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public function test_audit_failure_rolls_back_both_configuration_fields(): void
|
|
public function test_audit_failure_rolls_back_both_configuration_fields(): void
|
|
|
{
|
|
{
|
|
|
- $before = $this->service->save(['translations' => ['zh' => ['content' => '原内容']], 'domains' => ['example.com']]);
|
|
|
|
|
|
|
+ $before = $this->service->save(['translations' => ['zh' => ['content' => ['原内容', '原第二条']]], 'domains' => ['example.com']]);
|
|
|
DB::statement('DROP TABLE bot_operation_audits');
|
|
DB::statement('DROP TABLE bot_operation_audits');
|
|
|
- try { $this->service->save(['translations' => ['zh' => ['content' => '新内容']], 'domains' => ['new.example']]); $this->fail('Expected audit failure'); }
|
|
|
|
|
|
|
+ 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()); }
|
|
catch (\Illuminate\Database\QueryException $e) { $this->assertSame($before, $this->service->get()); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ public function test_existing_single_text_survives_migration_and_becomes_one_array_item(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $migration = require dirname(__DIR__, 2) . '/database/migrations/2026_10_31_120000_add_contents_to_site_setting_translations.php';
|
|
|
|
|
+ $migration->down();
|
|
|
|
|
+ DB::table('site_setting_translations')->insert([
|
|
|
|
|
+ ['site_setting_id' => 1, 'language' => 'zh', 'content' => '["原有内容"]'],
|
|
|
|
|
+ ['site_setting_id' => 1, 'language' => 'en', 'content' => ''],
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $migration->up();
|
|
|
|
|
+ $config = $this->service->get();
|
|
|
|
|
+ $this->assertSame(['["原有内容"]'], $config['translations']['zh']['content']);
|
|
|
|
|
+ $this->assertSame([], $config['translations']['en']['content']);
|
|
|
|
|
+ $config['translations']['zh']['content'][] = '新增一条';
|
|
|
|
|
+ $this->assertSame($config, $this->service->save($config));
|
|
|
|
|
+ $this->assertSame($config, $this->service->get());
|
|
|
|
|
+ $stored = DB::table('site_setting_translations')->where('language', 'zh')->first();
|
|
|
|
|
+ $this->assertSame('["原有内容"]', $stored->content);
|
|
|
|
|
+ $this->assertSame($config['translations']['zh']['content'], json_decode($stored->contents, true));
|
|
|
|
|
+ $migration->down();
|
|
|
|
|
+ $this->assertSame('["原有内容"]', DB::table('site_setting_translations')->where('language', 'zh')->value('content'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_maximum_list_round_trips_and_an_explicit_empty_list_clears_one_language(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $items = array_fill(0, 100, str_repeat('字', 2000));
|
|
|
|
|
+ $result = $this->service->save(['translations' => ['zh' => ['content' => $items], 'en' => ['content' => ['English']]], 'domains' => []]);
|
|
|
|
|
+ $this->assertSame($items, $result['translations']['zh']['content']);
|
|
|
|
|
+ $this->assertSame($result, $this->service->get());
|
|
|
|
|
+ $result['translations']['zh']['content'] = [];
|
|
|
|
|
+ $this->assertSame($result, $this->service->save($result));
|
|
|
|
|
+ $this->assertSame($result, $this->service->get());
|
|
|
|
|
+ $this->assertSame(['English'], $result['translations']['en']['content']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_invalid_list_does_not_change_an_existing_configuration(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $before = $this->service->save(['translations' => ['zh' => ['content' => ['第一条', '第二条']]], 'domains' => ['example.com']]);
|
|
|
|
|
+ try {
|
|
|
|
|
+ $this->service->save(['translations' => ['zh' => ['content' => ['first' => '错误列表']]], 'domains' => ['new.example']]);
|
|
|
|
|
+ $this->fail('Expected validation failure');
|
|
|
|
|
+ } catch (ValidationException $e) {
|
|
|
|
|
+ $this->assertSame($before, $this->service->get());
|
|
|
|
|
+ $this->assertSame(1, DB::table('operation_audits')->count());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_admin_save_after_request_normalization_ignores_blank_items(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $request = Request::create('/admin/site/config', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], json_encode([
|
|
|
|
|
+ 'translations' => ['zh' => ['content' => [' 第一条 ', '', ' ', '第二条']]], 'domains' => [],
|
|
|
|
|
+ ], JSON_UNESCAPED_UNICODE));
|
|
|
|
|
+ $response = (new \App\Http\Middleware\TrimStrings())->handle($request, function ($request) {
|
|
|
|
|
+ return (new \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull())->handle($request, function ($request) {
|
|
|
|
|
+ $this->app->instance('request', $request);
|
|
|
|
|
+ return (new SiteConfiguration())->save($this->service);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ $this->assertSame(0, $response->getData(true)['code']);
|
|
|
|
|
+ $this->assertSame(['第一条', '第二条'], $response->getData(true)['data']['translations']['zh']['content']);
|
|
|
|
|
+ $this->assertSame(['第一条', '第二条'], $this->service->get()['translations']['zh']['content']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public function test_one_admin_path_supports_read_and_write_with_authentication_and_permission_checks(): void
|
|
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 = new Router(new Dispatcher($this->app), $this->app); $this->app->instance('router', $router);
|