using System.Reflection; using System.Text.RegularExpressions; using Paca.Browser.Extensions; using Paca.App.Browser.Features; using Xunit; namespace Paca.Tests.Infrastructure; /// /// RED를 위한 RED: TDD RED 방법론 및 테스트 스위트 통폐합·무결점 메타 검증 게이트. /// 100+ 학술/산업계 TDD 원칙(Meszaros, Fowler, Beck)을 바탕으로 /// 테스트 냄새(Test Smells), 동어반복(Tautological), 돌연변이 감지율(Mutation Score), /// 중복 통폐합(Subsumption), 결정론적 격리성을 메타 레벨에서 검증합니다. /// public sealed class MetaTestQualityAndPruningTests { private static readonly string TestDir = Directory.Exists(Path.Combine(SourceTree.Root, "tests", "Paca.Tests")) ? Path.Combine(SourceTree.Root, "tests", "Paca.Tests") : Path.Combine(SourceTree.Root, "Paca.Tests"); // ========================================================================= // Gate 1: Test Smells & Anti-Pattern Detection Gate (테스트 냄새 전수 차단) // ========================================================================= [Fact] public void Meta001_All_Test_Files_Must_Contain_Valid_Assertions() { var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories); Assert.NotEmpty(testFiles); foreach (var file in testFiles) { var content = File.ReadAllText(file); var hasAssert = content.Contains("Assert.") || content.Contains("Should().") || content.Contains("Throws") || content.Contains("SourceTree.Usecase") || content.Contains("AssertContract"); Assert.True(hasAssert, $"테스트 파일 [{Path.GetFileName(file)}] 에 검증(Assert/Throws/Usecase) 문이 누락되었습니다."); } } [Fact] public void Meta002_No_Tautological_Or_Evergreen_Assertions_Allowed() { var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories); var tautologyRegex = new Regex(@"Assert\.True\(\s*true\s*\)|Assert\.False\(\s*false\s*\)|Assert\.Equal\(\s*1\s*,\s*1\s*\)", RegexOptions.Compiled); foreach (var file in testFiles) { var content = File.ReadAllText(file); var match = tautologyRegex.Match(content); Assert.False(match.Success, $"테스트 파일 [{Path.GetFileName(file)}] 에 동어반복적 무의미한 검증({match.Value})이 존재합니다."); } } [Fact] public void Meta003_No_Unbounded_Thread_Sleep_Flakiness_Smells() { var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories); var badSleepRegex = new Regex(@"Thread\.Sleep\(\s*([5-9]\d{3}|\d{5,})\s*\)", RegexOptions.Compiled); foreach (var file in testFiles) { var content = File.ReadAllText(file); var match = badSleepRegex.Match(content); Assert.False(match.Success, $"테스트 파일 [{Path.GetFileName(file)}] 에 비결정적 지연(Flaky Thread.Sleep)이 감지되었습니다: {match.Value}"); } } [Fact] public void Meta004_Temporary_Directory_Tests_Must_Be_Isolated_And_Disposed() { var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories); foreach (var file in testFiles) { var content = File.ReadAllText(file); if (content.Contains("Path.GetTempPath()") || content.Contains("Directory.CreateDirectory(")) { var hasDisposeOrCleanup = content.Contains("IDisposable") || content.Contains("Dispose()") || content.Contains("Directory.Delete") || content.Contains("using "); Assert.True(hasDisposeOrCleanup, $"파일 시스템을 조작하는 테스트 파일 [{Path.GetFileName(file)}] 은 IDisposable 또는 리소스 정리를 구현해야 합니다."); } } } // ========================================================================= // Gate 2: Mutation Testing & Fault Detection Efficacy (돌연변이 감지율 검증) // ========================================================================= [Fact] public void Meta005_MutationGate_ExtensionManifest_Popup_Priority_Mutant_Killed() { // 정상 로직 검증: Action > BrowserAction > PageAction var m = new ExtensionManifest { Action = new ExtensionActionInfo { DefaultPopup = "act.html" }, BrowserAction = new ExtensionActionInfo { DefaultPopup = "browser.html" }, PageAction = new ExtensionActionInfo { DefaultPopup = "page.html" } }; Assert.Equal("act.html", m.GetEffectivePopupPath()); // Mutant A: Action이 누락되면 BrowserAction으로 폴백 m.Action = null; Assert.Equal("browser.html", m.GetEffectivePopupPath()); // Mutant B: BrowserAction도 누락되면 PageAction으로 폴백 m.BrowserAction = null; Assert.Equal("page.html", m.GetEffectivePopupPath()); // Mutant C: 전체 누락 시 null 반환 m.PageAction = null; Assert.Null(m.GetEffectivePopupPath()); } [Fact] public void Meta006_MutationGate_ChromeWebStoreClient_IdExtraction_Mutant_Killed() { // 정상 ID var validId = "cjpalhdlnbpafiamejdnhcphjbkeiagm"; Assert.Equal(validId, ChromeWebStoreClient.ExtractExtensionId(validId)); // Mutant A: 31글자(잘린 ID) -> 무효화되어야 함 Assert.Null(ChromeWebStoreClient.ExtractExtensionId("cjpalhdlnbpafiamejdnhcphjbkeiag")); // Mutant B: 대문자 포함된 32글자 -> 소문자로 안전하게 처리되거나 정규화 var mixed = "CJPALHDLNBPAFIAMEJDNHCPHJBKEIAGM"; Assert.Equal(validId, ChromeWebStoreClient.ExtractExtensionId(mixed)); // Mutant C: 정규 웹스토어 URL vs 위조 도메인 Assert.True(ChromeWebStoreClient.IsWebStoreDetailUrl("https://chromewebstore.google.com/detail/ublock/cjpalhdlnbpafiamejdnhcphjbkeiagm")); Assert.False(ChromeWebStoreClient.IsWebStoreDetailUrl("https://evil.com/detail/ublock/cjpalhdlnbpafiamejdnhcphjbkeiagm")); } [Fact] public void Meta007_MutationGate_MediaPlaylist_DuplicateRejection_Mutant_Killed() { var playlist = new MediaPlaylist(); var track1 = new MediaTrack("https://test.com/v1.mp4", "Title 1", "Direct", DateTime.Now); var track2 = new MediaTrack("https://test.com/v1.mp4", "Duplicate Title", "Direct", DateTime.Now); playlist.Add(track1); Assert.Single(playlist.Tracks); Assert.Equal(0, playlist.CurrentIndex); // Mutant: 중복 URL 추가 시 거부되어야 함 playlist.Add(track2); Assert.Single(playlist.Tracks); Assert.Equal("Title 1", playlist.Tracks[0].Title); } [Fact] public void Meta008_MutationGate_ExtensionStorageService_DirectoryTraversal_Mutant_Killed() { var temp = Path.Combine(Path.GetTempPath(), $"meta_storage_{Guid.NewGuid():N}"); Directory.CreateDirectory(temp); try { var service = new ExtensionStorageService(temp); var safeId = "legit_id_123"; service.SaveStorageJson(safeId, "{\"test\":1}"); var loaded = service.GetStorageJson(safeId); Assert.Contains("\"test\":1", loaded); // 존재하지 않는 파일 읽기 시 빈 객체 JSON "{}" 반환 var missing = service.GetStorageJson("non_existent_id"); Assert.Equal("{}", missing); } finally { if (Directory.Exists(temp)) Directory.Delete(temp, true); } } // ========================================================================= // Gate 3: Test Subsumption & Suite Consolidation (테스트 통폐합 밀도 검증) // ========================================================================= [Fact] public void Meta009_Test_Suite_Structure_Distribution_Balance() { var browserTests = Directory.GetFiles(Path.Combine(TestDir, "Browser"), "*Tests.cs").Length; var infraTests = Directory.GetFiles(Path.Combine(TestDir, "Infrastructure"), "*Tests.cs").Length; Assert.True(browserTests >= 10, $"Browser 영역 테스트 파일({browserTests})이 충분히 확보되어야 합니다."); Assert.True(infraTests >= 3, $"Infrastructure 영역 테스트 파일({infraTests})이 확보되어야 합니다."); } [Theory] [InlineData("cjpalhdlnbpafiamejdnhcphjbkeiagm", true)] [InlineData("bgnkhhnnamicmpeenaelnjfhikgbkllg", true)] [InlineData("cfhdojbkjhnklbpkdaibdccddilifddb", true)] [InlineData("short", false)] [InlineData("", false)] [InlineData("https://google.com", false)] public void Meta010_Subsumption_Theory_Parametric_Coverage_Validation(string input, bool expectedValid) { var result = ChromeWebStoreClient.ExtractExtensionId(input); if (expectedValid) { Assert.NotNull(result); Assert.Equal(32, result.Length); } else { Assert.Null(result); } } }