$slug, 'name' => ['ko' => '규칙 테스트 게시판', 'en' => 'Rules Test Board'], 'is_active' => true, 'secret_mode' => 'disabled', 'blocked_keywords' => [], 'max_comment_depth' => 3, 'categories' => [], ]; } // ========================================== // 헬퍼 // ========================================== private function runRule(\Illuminate\Contracts\Validation\ValidationRule $rule, mixed $value): array { $failures = []; $rule->validate('field', $value, function (string $msg) use (&$failures) { $failures[] = $msg; }); return $failures; } // ========================================== // BlockedKeywordsRule // ========================================== /** * 금지 키워드가 없으면 항상 통과합니다. */ public function test_blocked_keywords_passes_when_no_keywords(): void { $rule = new BlockedKeywordsRule([]); $this->assertEmpty($this->runRule($rule, '어떤 내용이든 통과')); } /** * null 키워드 목록이면 통과합니다. */ public function test_blocked_keywords_passes_when_keywords_null(): void { $rule = new BlockedKeywordsRule(null); $this->assertEmpty($this->runRule($rule, '스팸')); } /** * 금지 키워드가 포함되면 fail합니다. */ public function test_blocked_keywords_fails_when_keyword_found(): void { $rule = new BlockedKeywordsRule(['스팸', '광고']); $failures = $this->runRule($rule, '스팸 게시글 내용'); $this->assertNotEmpty($failures); } /** * 대소문자를 무시하여 검사합니다. */ public function test_blocked_keywords_is_case_insensitive(): void { $rule = new BlockedKeywordsRule(['SPAM']); $failures = $this->runRule($rule, 'spam 내용'); $this->assertNotEmpty($failures); } /** * 금지 키워드가 없는 내용은 통과합니다. */ public function test_blocked_keywords_passes_when_no_match(): void { $rule = new BlockedKeywordsRule(['스팸']); $this->assertEmpty($this->runRule($rule, '정상적인 게시글 내용입니다.')); } /** * 값이 문자열이 아니면 통과합니다. */ public function test_blocked_keywords_passes_when_value_not_string(): void { $rule = new BlockedKeywordsRule(['스팸']); $this->assertEmpty($this->runRule($rule, 123)); } // ========================================== // PermissionRolesRequiredRule // ========================================== /** * permissions 배열에서 roles가 비어있으면 fail합니다. */ public function test_permission_roles_required_fails_when_roles_empty(): void { $rule = new PermissionRolesRequiredRule(); $value = [ 'posts_read' => ['roles' => []], 'posts_write' => ['roles' => ['user']], ]; $failures = $this->runRule($rule, $value); $this->assertNotEmpty($failures); } /** * 모든 permission에 role이 있으면 통과합니다. */ public function test_permission_roles_required_passes_when_all_have_roles(): void { $rule = new PermissionRolesRequiredRule(); $value = [ 'posts_read' => ['roles' => ['guest', 'user']], 'posts_write' => ['roles' => ['user']], ]; $this->assertEmpty($this->runRule($rule, $value)); } /** * 값이 배열이 아니면 통과합니다. */ public function test_permission_roles_required_passes_when_not_array(): void { $rule = new PermissionRolesRequiredRule(); $this->assertEmpty($this->runRule($rule, 'not-an-array')); } /** * roles 누락 메시지는 권한 키 대신 다국어 라벨을 노출합니다. * * 회귀: posts.read-secret(사용자) 라벨 누락으로 키가 그대로 노출되던 문제 (이슈 #413-20) */ public function test_permission_roles_required_uses_localized_label_for_read_secret(): void { $rule = new PermissionRolesRequiredRule(); $value = [ 'posts.read-secret' => ['roles' => []], ]; $failures = $this->runRule($rule, $value); $this->assertNotEmpty($failures); // 원본 키가 그대로 노출되면 안 됨 $this->assertStringNotContainsString('posts.read-secret', $failures[0]); $this->assertStringNotContainsString('posts_read-secret', $failures[0]); // 다국어 라벨이 노출되어야 함 $this->assertStringContainsString( __('sirsoft-board::validation.permission_names.posts.read-secret'), $failures[0] ); } // ========================================== // SlugUniqueRule // ========================================== /** * 예약된 slug 'boards'는 fail합니다. */ public function test_slug_unique_fails_for_reserved_slug(): void { $rule = new SlugUniqueRule(); $failures = $this->runRule($rule, 'boards'); $this->assertNotEmpty($failures); } /** * 이미 존재하는 slug는 fail합니다. */ public function test_slug_unique_fails_for_existing_slug(): void { $rule = new SlugUniqueRule(); // $this->board->slug는 이미 DB에 존재 $failures = $this->runRule($rule, $this->board->slug); $this->assertNotEmpty($failures); } /** * 새로운 slug는 통과합니다. */ public function test_slug_unique_passes_for_new_slug(): void { $rule = new SlugUniqueRule(); $this->assertEmpty($this->runRule($rule, 'completely-new-unique-slug-xyz')); } // ========================================== // CooldownRule // ========================================== /** * cooldownSeconds가 0이면 항상 통과합니다. */ public function test_cooldown_rule_passes_when_disabled(): void { $rule = new CooldownRule('post', 0, $this->board->slug); $this->assertEmpty($this->runRule($rule, null)); } /** * 쿨다운 캐시가 없으면 통과합니다. */ public function test_cooldown_rule_passes_when_no_cache(): void { // 캐시 초기화 Cache::flush(); $rule = new CooldownRule('post', 60, $this->board->slug); $this->assertEmpty($this->runRule($rule, null)); } // ========================================== // CategoryInUseRule // ========================================== /** * 제거 카테고리가 없으면 통과합니다. */ public function test_category_in_use_passes_when_no_removal(): void { $rule = new CategoryInUseRule( $this->board->slug, ['tech', 'news'], ['tech', 'news', 'sports'] // 추가만 있음 ); $this->assertEmpty($this->runRule($rule, null)); } /** * 제거 카테고리가 있어도 파티션 테이블이 없으면 통과합니다. */ public function test_category_in_use_passes_when_table_not_exists(): void { // board_posts_{slug} 파티션 테이블 없음 (비파티션 환경) $rule = new CategoryInUseRule( 'nonexistent-board-xyz', ['tech', 'news'], ['tech'] // news 제거 ); $this->assertEmpty($this->runRule($rule, null)); } // ========================================== // CommentValidationRule // ========================================== /** * blinded 게시글에 댓글 작성 → fail */ public function test_comment_validation_fails_for_blinded_post(): void { $postId = $this->createTestPost(['status' => 'blinded']); $rule = new CommentValidationRule($this->board->slug, 'post'); $failures = $this->runRule($rule, $postId); $this->assertNotEmpty($failures); } /** * 소프트 삭제된 게시글에 댓글 작성 → fail */ public function test_comment_validation_fails_for_deleted_post(): void { $postId = $this->createTestPost(); // soft delete \Illuminate\Support\Facades\DB::table('board_posts') ->where('id', $postId) ->update(['deleted_at' => now()]); $rule = new CommentValidationRule($this->board->slug, 'post'); $failures = $this->runRule($rule, $postId); $this->assertNotEmpty($failures); } /** * 존재하지 않는 게시글 ID → fail */ public function test_comment_validation_fails_for_nonexistent_post(): void { $rule = new CommentValidationRule($this->board->slug, 'post'); $failures = $this->runRule($rule, 99999999); $this->assertNotEmpty($failures); } /** * 정상 게시글에 댓글 작성 → 통과 */ public function test_comment_validation_passes_for_published_post(): void { $postId = $this->createTestPost(['status' => 'published']); $rule = new CommentValidationRule($this->board->slug, 'post'); $this->assertEmpty($this->runRule($rule, $postId)); } /** * 값이 비어있으면 통과합니다 (nullable). */ public function test_comment_validation_passes_for_empty_value(): void { $rule = new CommentValidationRule($this->board->slug, 'post'); $this->assertEmpty($this->runRule($rule, null)); } }