feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization
This commit is contained in:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
|
|
@ -0,0 +1,203 @@
|
|||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Paca.Browser.Extensions;
|
||||
using Paca.App.Browser.Features;
|
||||
using Xunit;
|
||||
|
||||
namespace Paca.Tests.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// RED를 위한 RED: TDD RED 방법론 및 테스트 스위트 통폐합·무결점 메타 검증 게이트.
|
||||
/// 100+ 학술/산업계 TDD 원칙(Meszaros, Fowler, Beck)을 바탕으로
|
||||
/// 테스트 냄새(Test Smells), 동어반복(Tautological), 돌연변이 감지율(Mutation Score),
|
||||
/// 중복 통폐합(Subsumption), 결정론적 격리성을 메타 레벨에서 검증합니다.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue