확장이 훅으로 등록한 카탈로그에서 고른 값은 그 확장이 사라져도 저장값으로 남는다. 그 상태에서 값만 보고 판정하면 이미 제공 불가한 항목이 사용자 화면에 그대로 노출된다. 예외도 경고도 로그도 남지 않아 관리자 화면과 나란히 보지 않으면 드러나지 않는다. - 결제수단에 지정한 PG, 현금영수증 발급사, 검색엔진 드라이버 세 축에 같은 판정을 적용. 공개 응답에서는 제거하고 관리자 응답에는 표시를 남긴다 — 운영자가 고쳐야 할 대상이라 감추면 복구 경로가 사라진다. 서버 검증도 같은 판정을 공유해 화면 우회 제출을 막는다. - 설정 단건 저장이 벌크 저장과 다른 경로를 타던 문제를 코어·이커머스·게시판에서 정리. 저장은 성공하고 파생 값만 갱신되지 않아 어느 화면에서 저장했느냐로 결과가 갈렸다. - 단건 저장의 값 형태 계약 신설. 빈 값으로 되돌릴 수 있게 하고, 타입은 defaults.json 의 기본값을 SSoT 로 삼는다. 해석 불가한 입력은 조용히 캐스팅하지 않고 거절한다. - 드라이버 셀렉트를 서버 카탈로그에 바인딩하고, 저장값이 카탈로그에 없으면 그 값을 드러내는 안내를 8곳 전수에 붙인다. 종전에는 빈 칸으로만 보여 무엇이 저장됐는지 알 수 없었다. 시나리오 매니페스트 8종을 면제 없이 신설했다. `audit:allow` 는 cross product 와 effects 검사를 함께 끄므로, 면제가 걸린 매니페스트에 선언을 추가하면 검증이 회복되지 않는다. 그 아래에서 대응 테스트가 없는 effect 이름 2건이 살아남아 있었고, 함께 정리했다.
432 lines
15 KiB
PHP
432 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Board\Tests\Unit;
|
|
|
|
use App\Contracts\Repositories\NotificationDefinitionRepositoryInterface;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\File;
|
|
use Modules\Sirsoft\Board\Services\BoardPermissionService;
|
|
use Modules\Sirsoft\Board\Services\BoardSettingsService;
|
|
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
|
|
|
|
/**
|
|
* BoardSettingsService 단위 테스트
|
|
*
|
|
* ModuleSettingsInterface 구현을 검증합니다.
|
|
*/
|
|
class BoardSettingsServiceTest extends ModuleTestCase
|
|
{
|
|
private BoardSettingsService $service;
|
|
|
|
private string $storagePath;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$permissionService = $this->createMock(BoardPermissionService::class);
|
|
$notificationDefinitionRepository = $this->createMock(NotificationDefinitionRepositoryInterface::class);
|
|
// report_policy 저장 시 호출되는 알림 정의 동기화 경로가 빈 컬렉션을 안전하게 다루도록 기본 stub
|
|
$notificationDefinitionRepository->method('getByExtension')
|
|
->willReturn(new Collection);
|
|
$this->service = new BoardSettingsService($permissionService, $notificationDefinitionRepository);
|
|
// 테스트 격리 경로 — 운영 설정(storage/app/modules/...)을 지우거나 덮어쓰지 않는다
|
|
$this->storagePath = storage_path('framework/testing/modules/sirsoft-board/settings');
|
|
|
|
// 테스트 전 저장소 정리
|
|
if (File::isDirectory($this->storagePath)) {
|
|
File::deleteDirectory($this->storagePath);
|
|
}
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
// 테스트 후 저장소 정리
|
|
if (File::isDirectory($this->storagePath)) {
|
|
File::deleteDirectory($this->storagePath);
|
|
}
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
// ========================================
|
|
// getSettingsDefaultsPath 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* defaults.json 경로가 정상적으로 반환되는지 확인
|
|
*/
|
|
public function test_get_settings_defaults_path_returns_valid_path(): void
|
|
{
|
|
$path = $this->service->getSettingsDefaultsPath();
|
|
|
|
$this->assertNotNull($path);
|
|
$this->assertFileExists($path);
|
|
$this->assertStringEndsWith('defaults.json', $path);
|
|
}
|
|
|
|
// ========================================
|
|
// getAllSettings 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* 저장된 설정이 없으면 defaults.json의 기본값을 반환하는지 확인
|
|
*/
|
|
public function test_get_all_settings_returns_defaults_when_no_saved_settings(): void
|
|
{
|
|
$settings = $this->service->getAllSettings();
|
|
|
|
$this->assertIsArray($settings);
|
|
$this->assertArrayHasKey('basic_defaults', $settings);
|
|
$this->assertArrayHasKey('report_policy', $settings);
|
|
$this->assertArrayHasKey('spam_security', $settings);
|
|
}
|
|
|
|
/**
|
|
* basic_defaults 카테고리의 기본값이 올바른지 확인
|
|
*/
|
|
public function test_get_all_settings_has_correct_basic_defaults(): void
|
|
{
|
|
$settings = $this->service->getAllSettings();
|
|
$basicDefaults = $settings['basic_defaults'];
|
|
|
|
$this->assertEquals('basic', $basicDefaults['type']);
|
|
$this->assertEquals(20, $basicDefaults['per_page']);
|
|
$this->assertEquals(15, $basicDefaults['per_page_mobile']);
|
|
$this->assertEquals(5, $basicDefaults['max_reply_depth']);
|
|
$this->assertEquals(10, $basicDefaults['max_comment_depth']);
|
|
$this->assertTrue($basicDefaults['use_comment']);
|
|
$this->assertTrue($basicDefaults['use_reply']);
|
|
$this->assertTrue($basicDefaults['show_view_count']);
|
|
$this->assertTrue($basicDefaults['notify_admin_on_post']);
|
|
$this->assertTrue($basicDefaults['notify_author']);
|
|
}
|
|
|
|
/**
|
|
* report_policy 카테고리의 기본값이 올바른지 확인
|
|
*/
|
|
public function test_get_all_settings_has_correct_report_policy(): void
|
|
{
|
|
$settings = $this->service->getAllSettings();
|
|
$reportPolicy = $settings['report_policy'];
|
|
|
|
$this->assertEquals(5, $reportPolicy['auto_hide_threshold']);
|
|
$this->assertEquals('both', $reportPolicy['auto_hide_target']);
|
|
$this->assertEquals(10, $reportPolicy['daily_report_limit']);
|
|
}
|
|
|
|
/**
|
|
* spam_security 카테고리의 기본값이 올바른지 확인
|
|
*/
|
|
public function test_get_all_settings_has_correct_spam_security(): void
|
|
{
|
|
$settings = $this->service->getAllSettings();
|
|
$spamSecurity = $settings['spam_security'];
|
|
|
|
$this->assertEquals(0, $spamSecurity['post_cooldown_seconds']);
|
|
$this->assertEquals(86400, $spamSecurity['view_count_cache_ttl']);
|
|
}
|
|
|
|
/**
|
|
* blocked_keywords가 basic_defaults에 포함되는지 확인
|
|
*/
|
|
public function test_get_all_settings_has_blocked_keywords_in_basic_defaults(): void
|
|
{
|
|
$settings = $this->service->getAllSettings();
|
|
$basicDefaults = $settings['basic_defaults'];
|
|
|
|
$this->assertArrayHasKey('blocked_keywords', $basicDefaults);
|
|
$this->assertIsArray($basicDefaults['blocked_keywords']);
|
|
$this->assertNotEmpty($basicDefaults['blocked_keywords']);
|
|
}
|
|
|
|
/**
|
|
* 결과가 캐싱되는지 확인 (두 번째 호출 시 동일 객체)
|
|
*/
|
|
public function test_get_all_settings_caches_result(): void
|
|
{
|
|
$first = $this->service->getAllSettings();
|
|
$second = $this->service->getAllSettings();
|
|
|
|
$this->assertSame($first, $second);
|
|
}
|
|
|
|
// ========================================
|
|
// getSettings (카테고리별) 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* 특정 카테고리 설정을 조회할 수 있는지 확인
|
|
*/
|
|
public function test_get_settings_returns_category_settings(): void
|
|
{
|
|
$basicDefaults = $this->service->getSettings('basic_defaults');
|
|
|
|
$this->assertIsArray($basicDefaults);
|
|
$this->assertArrayHasKey('per_page', $basicDefaults);
|
|
$this->assertEquals(20, $basicDefaults['per_page']);
|
|
}
|
|
|
|
/**
|
|
* 존재하지 않는 카테고리는 빈 배열 반환
|
|
*/
|
|
public function test_get_settings_returns_empty_for_unknown_category(): void
|
|
{
|
|
$result = $this->service->getSettings('nonexistent');
|
|
|
|
$this->assertIsArray($result);
|
|
$this->assertEmpty($result);
|
|
}
|
|
|
|
// ========================================
|
|
// getSetting (단일 키) 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* dot notation으로 설정값을 조회할 수 있는지 확인
|
|
*/
|
|
public function test_get_setting_with_dot_notation(): void
|
|
{
|
|
$perPage = $this->service->getSetting('basic_defaults.per_page');
|
|
|
|
$this->assertEquals(20, $perPage);
|
|
}
|
|
|
|
/**
|
|
* 존재하지 않는 키에 대해 기본값 반환
|
|
*/
|
|
public function test_get_setting_returns_default_for_unknown_key(): void
|
|
{
|
|
$result = $this->service->getSetting('basic_defaults.nonexistent', 'fallback');
|
|
|
|
$this->assertEquals('fallback', $result);
|
|
}
|
|
|
|
// ========================================
|
|
// setSetting 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* 개별 설정값을 저장할 수 있는지 확인
|
|
*/
|
|
public function test_set_setting_saves_value(): void
|
|
{
|
|
$result = $this->service->setSetting('basic_defaults.per_page', 30);
|
|
|
|
$this->assertTrue($result);
|
|
|
|
// 단건 저장이 자기 캐시를 비우므로 수동 초기화 없이 신값이 조회된다 (공개 #114 동종)
|
|
$perPage = $this->service->getSetting('basic_defaults.per_page');
|
|
$this->assertEquals(30, $perPage);
|
|
}
|
|
|
|
/**
|
|
* 설정이 파일에 실제로 저장되는지 확인
|
|
*/
|
|
public function test_set_setting_persists_to_file(): void
|
|
{
|
|
$this->service->setSetting('report_policy.auto_hide_threshold', 10);
|
|
|
|
$filePath = $this->storagePath.'/report_policy.json';
|
|
$this->assertFileExists($filePath);
|
|
|
|
$content = json_decode(File::get($filePath), true);
|
|
$this->assertEquals(10, $content['auto_hide_threshold']);
|
|
}
|
|
|
|
// ========================================
|
|
// saveSettings (전체 저장) 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* 여러 카테고리를 한 번에 저장할 수 있는지 확인
|
|
*/
|
|
public function test_save_settings_saves_multiple_categories(): void
|
|
{
|
|
$result = $this->service->saveSettings([
|
|
'basic_defaults' => [
|
|
'per_page' => 50,
|
|
'type' => 'gallery',
|
|
],
|
|
'report_policy' => [
|
|
'auto_hide_threshold' => 3,
|
|
],
|
|
]);
|
|
|
|
$this->assertTrue($result);
|
|
|
|
// 캐시 초기화 후 검증
|
|
$this->service->clearCache();
|
|
$settings = $this->service->getAllSettings();
|
|
|
|
$this->assertEquals(50, $settings['basic_defaults']['per_page']);
|
|
$this->assertEquals('gallery', $settings['basic_defaults']['type']);
|
|
$this->assertEquals(3, $settings['report_policy']['auto_hide_threshold']);
|
|
}
|
|
|
|
/**
|
|
* _meta 등 메타 키는 저장하지 않는지 확인
|
|
*/
|
|
public function test_save_settings_ignores_meta_keys(): void
|
|
{
|
|
$result = $this->service->saveSettings([
|
|
'_meta' => ['version' => '2.0.0'],
|
|
'_tab' => 'basic',
|
|
'basic_defaults' => ['per_page' => 25],
|
|
]);
|
|
|
|
$this->assertTrue($result);
|
|
|
|
// _meta 파일이 생성되지 않아야 함
|
|
$this->assertFileDoesNotExist($this->storagePath.'/_meta.json');
|
|
$this->assertFileDoesNotExist($this->storagePath.'/_tab.json');
|
|
}
|
|
|
|
/**
|
|
* 카테고리 값이 배열이 아닌 경우 무시되는지 확인
|
|
*/
|
|
public function test_save_settings_ignores_non_array_category_values(): void
|
|
{
|
|
$result = $this->service->saveSettings([
|
|
'basic_defaults' => ['per_page' => 25],
|
|
'invalid_key' => 'string_value',
|
|
]);
|
|
|
|
$this->assertTrue($result);
|
|
$this->assertFileDoesNotExist($this->storagePath.'/invalid_key.json');
|
|
}
|
|
|
|
/**
|
|
* 저장 후 캐시가 초기화되는지 확인
|
|
*/
|
|
public function test_save_settings_clears_cache(): void
|
|
{
|
|
// 캐시를 채움
|
|
$this->service->getAllSettings();
|
|
|
|
// 저장 후 캐시가 초기화되어야 함
|
|
$this->service->saveSettings([
|
|
'basic_defaults' => ['per_page' => 99],
|
|
]);
|
|
|
|
// 새로운 인스턴스처럼 동작해야 함
|
|
$settings = $this->service->getAllSettings();
|
|
$this->assertEquals(99, $settings['basic_defaults']['per_page']);
|
|
}
|
|
|
|
// ========================================
|
|
// getFrontendSettings 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* 프론트엔드 설정이 올바른 구조로 반환되는지 확인
|
|
*
|
|
* defaults.json의 frontend_schema 모든 카테고리가 expose: false이므로
|
|
* getFrontendSettings()는 빈 배열을 반환해야 합니다.
|
|
*/
|
|
public function test_get_frontend_settings_returns_exposed_categories(): void
|
|
{
|
|
$frontendSettings = $this->service->getFrontendSettings();
|
|
|
|
$this->assertIsArray($frontendSettings);
|
|
// 모든 카테고리가 expose: false → 프론트엔드에 노출되는 카테고리 없음
|
|
$this->assertEmpty($frontendSettings);
|
|
}
|
|
|
|
/**
|
|
* 프론트엔드 설정에 실제 값이 포함되는지 확인
|
|
*
|
|
* defaults.json의 frontend_schema 모든 카테고리가 expose: false이므로
|
|
* getFrontendSettings()는 빈 배열을 반환해야 합니다.
|
|
*/
|
|
public function test_get_frontend_settings_contains_actual_values(): void
|
|
{
|
|
$frontendSettings = $this->service->getFrontendSettings();
|
|
|
|
// 모든 카테고리가 expose: false → 빈 배열 반환
|
|
$this->assertIsArray($frontendSettings);
|
|
$this->assertEmpty($frontendSettings);
|
|
}
|
|
|
|
// ========================================
|
|
// clearCache 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* 캐시 초기화 후 fresh한 데이터를 가져오는지 확인
|
|
*/
|
|
public function test_clear_cache_forces_fresh_load(): void
|
|
{
|
|
// 첫 번째 조회로 캐시 생성
|
|
$first = $this->service->getAllSettings();
|
|
|
|
// 파일 직접 수정 (서비스를 경유하지 않아 캐시가 갱신되지 않는 상태를 만든다)
|
|
File::ensureDirectoryExists($this->storagePath);
|
|
File::put(
|
|
$this->storagePath.'/basic_defaults.json',
|
|
json_encode(['per_page' => 77], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
|
);
|
|
$this->assertEquals(
|
|
$first['basic_defaults']['per_page'],
|
|
$this->service->getAllSettings()['basic_defaults']['per_page'],
|
|
'캐시 초기화 전인데 파일 변경이 조회에 반영되었습니다.'
|
|
);
|
|
|
|
// 캐시 초기화
|
|
$this->service->clearCache();
|
|
|
|
// 수정된 값이 반영되어야 함
|
|
$settings = $this->service->getAllSettings();
|
|
$this->assertEquals(77, $settings['basic_defaults']['per_page']);
|
|
}
|
|
|
|
// ========================================
|
|
// 저장 후 기본값 병합 테스트
|
|
// ========================================
|
|
|
|
/**
|
|
* 저장된 설정이 기본값과 올바르게 병합되는지 확인
|
|
*
|
|
* 주의: saveSettings 는 Toggle/체크박스 OFF UX 대응으로
|
|
* 카테고리 범위 내 누락된 boolean 필드를 false 로 강제 저장한다
|
|
* (commit 6b46b1ce7 "Toggle/체크박스 저장 버그 수정"). 따라서 partial save
|
|
* 시 non-boolean 기본값만 유지되고 boolean 은 모두 false 가 된다.
|
|
* 이 테스트는 기본값 병합 로직의 non-boolean 경로만 검증한다.
|
|
*/
|
|
public function test_saved_settings_merge_with_defaults(): void
|
|
{
|
|
// 일부 필드만 저장
|
|
$this->service->saveSettings([
|
|
'basic_defaults' => ['per_page' => 40],
|
|
]);
|
|
|
|
$this->service->clearCache();
|
|
$settings = $this->service->getAllSettings();
|
|
|
|
// 저장된 값 반영
|
|
$this->assertEquals(40, $settings['basic_defaults']['per_page']);
|
|
|
|
// 저장되지 않은 non-boolean 기본값은 유지
|
|
$this->assertEquals('basic', $settings['basic_defaults']['type']);
|
|
$this->assertEquals(15, $settings['basic_defaults']['per_page_mobile']);
|
|
|
|
// 저장되지 않은 boolean 필드는 false 로 저장됨 (Toggle/체크박스 OFF UX 대응)
|
|
$this->assertFalse($settings['basic_defaults']['use_comment']);
|
|
}
|
|
|
|
/**
|
|
* default_board_permissions 기본값이 정상적으로 포함되는지 확인
|
|
*/
|
|
public function test_basic_defaults_includes_default_board_permissions(): void
|
|
{
|
|
$settings = $this->service->getAllSettings();
|
|
$permissions = $settings['basic_defaults']['default_board_permissions'] ?? null;
|
|
|
|
$this->assertNotNull($permissions);
|
|
$this->assertIsArray($permissions);
|
|
$this->assertArrayHasKey('posts.read', $permissions);
|
|
$this->assertContains('admin', $permissions['posts.read']);
|
|
$this->assertContains('guest', $permissions['posts.read']);
|
|
}
|
|
}
|