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:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,397 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Paca.Browser.SystemIntegration;
using Xunit;
namespace Paca.Tests;
public sealed class ModernBrowsingMilestone7Phase107Tests : IDisposable
{
private readonly string _tempDir;
public ModernBrowsingMilestone7Phase107Tests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "paca_m7_test_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true);
}
catch { }
}
[Fact]
public void Gate145_FilterSubscriptionManager_ManagesSubscriptionLifecycleAndRuleUpdates()
{
// Arrange
var configPath = Path.Combine(_tempDir, "filter_subscriptions.json");
var manager = new FilterSubscriptionManager(configPath);
// Act 1: Default subscriptions: EasyList and List-KR
var subs = manager.GetSubscriptions();
Assert.True(subs.Count >= 2);
var easyList = manager.GetSubscription("easylist");
Assert.NotNull(easyList);
Assert.Equal("EasyList", easyList.Name);
Assert.True(easyList.IsEnabled);
Assert.True(easyList.AutoUpdateEnabled);
var listKr = manager.GetSubscription("list-kr");
Assert.NotNull(listKr);
Assert.Equal("List-KR", listKr.Name);
Assert.True(listKr.IsEnabled);
// Act 2: Auto-update schedule check
var now = DateTime.UtcNow;
Assert.True(manager.NeedsUpdate("easylist", now));
// Act 3: Update subscription rules & count
var rules = new[]
{
"! EasyList comment header",
"||doubleclick.net^",
"||googleadservices.com^",
"##.ad-banner",
"@@||example.com/ad.js"
};
manager.UpdateSubscriptionRules("easylist", rules, now);
easyList = manager.GetSubscription("easylist");
Assert.Equal(4, easyList!.RuleCount);
Assert.Equal(now, easyList.LastUpdated);
Assert.False(manager.NeedsUpdate("easylist", now.AddHours(2)));
Assert.True(manager.NeedsUpdate("easylist", now.AddHours(25)));
// Act 4: Custom subscription registration
var customSub = new FilterSubscription(
Id: "fanboy-social",
Name: "Fanboy's Social Blocking List",
Url: "https://easylist.to/easylist/fanboy-social.txt",
IsEnabled: true,
AutoUpdateEnabled: true,
UpdateInterval: TimeSpan.FromDays(3)
);
manager.AddSubscription(customSub);
manager.UpdateSubscriptionRules("fanboy-social", new[] { "||facebook.com/tr^", "||twitter.com/widgets.js^" }, now);
// Total rule count for enabled subscriptions
Assert.Equal(6, manager.GetTotalRuleCount());
// Disable a subscription
manager.SetSubscriptionEnabled("fanboy-social", false);
Assert.Equal(4, manager.GetTotalRuleCount());
// Act 5: Save & Reload
manager.Save();
var reloaded = new FilterSubscriptionManager(configPath);
Assert.NotNull(reloaded.GetSubscription("fanboy-social"));
Assert.False(reloaded.GetSubscription("fanboy-social")!.IsEnabled);
Assert.Equal(4, reloaded.GetSubscription("easylist")!.RuleCount);
}
[Fact]
public void Gate146_ShieldBreakdownReport_CategorizesTrackersAndAttributesCompanies()
{
// Arrange
var coordinator = new ShieldBreakdownCoordinator();
// Act 1: Direct URL classification
var res1 = coordinator.Classify("https://google-analytics.com/analytics.js");
Assert.Equal(TrackerCategory.Analytics, res1.Category);
Assert.Equal("Google", res1.CompanyName);
var res2 = coordinator.Classify("https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js");
Assert.Equal(TrackerCategory.Advertising, res2.Category);
Assert.Equal("Google", res2.CompanyName);
var res3 = coordinator.Classify("https://connect.facebook.net/en_US/fbevents.js");
Assert.Equal(TrackerCategory.Social, res3.Category);
Assert.Equal("Meta", res3.CompanyName);
var res4 = coordinator.Classify("https://coinhive.com/lib/miner.min.js");
Assert.Equal(TrackerCategory.Cryptomining, res4.Category);
var res5 = coordinator.Classify("https://fp.cdn.fingerprintjs.com/v3");
Assert.Equal(TrackerCategory.Fingerprinting, res5.Category);
// Act 2: Record blocked requests on a website
var pageHost = "news.portal.com";
coordinator.RecordBlockedTracker(pageHost, "https://google-analytics.com/analytics.js");
coordinator.RecordBlockedTracker(pageHost, "https://pagead2.googlesyndication.com/pagead/ads.js");
coordinator.RecordBlockedTracker(pageHost, "https://connect.facebook.net/sdk.js");
coordinator.RecordBlockedTracker(pageHost, "https://adservice.google.com/ads.js");
coordinator.RecordBlockedTracker(pageHost, "https://cdn.unknown-tracker.xyz/pixel.gif");
// Act 3: Generate Breakdown Report
var report = coordinator.GetReportForHost(pageHost);
Assert.NotNull(report);
Assert.Equal(pageHost, report.TargetHost);
Assert.Equal(5, report.TotalBlockedCount);
// Category breakdown
Assert.Equal(2, report.CategoryBreakdown[TrackerCategory.Advertising]);
Assert.Equal(1, report.CategoryBreakdown[TrackerCategory.Analytics]);
Assert.Equal(1, report.CategoryBreakdown[TrackerCategory.Social]);
Assert.Equal(1, report.CategoryBreakdown[TrackerCategory.Unknown]);
// Company breakdown
Assert.Equal(3, report.CompanyBreakdown["Google"]);
Assert.Equal(1, report.CompanyBreakdown["Meta"]);
Assert.Equal(1, report.CompanyBreakdown["Unknown"]);
// Act 4: Clear host stats
coordinator.ClearHostStats(pageHost);
var clearedReport = coordinator.GetReportForHost(pageHost);
Assert.Equal(0, clearedReport.TotalBlockedCount);
}
[Fact]
public void Gate147_DownloadRiskScanner_DetectsDangerousFilesAndEnforcesMotw()
{
// Arrange
var scanner = new DownloadRiskScanner();
// Act 1: Safe files
var safe1 = scanner.Scan("document.pdf", "https://trusted.org/doc.pdf");
Assert.Equal(DownloadRiskSeverity.Safe, safe1.Severity);
Assert.False(safe1.IsBlocked);
Assert.False(safe1.RequiresMotw);
var safe2 = scanner.Scan("image.png");
Assert.Equal(DownloadRiskSeverity.Safe, safe2.Severity);
// Act 2: Low Risk files (Archives / ISOs)
var low1 = scanner.Scan("backup.zip", "https://files.com/backup.zip");
Assert.Equal(DownloadRiskSeverity.LowRisk, low1.Severity);
Assert.False(low1.IsBlocked);
Assert.True(low1.RequiresMotw);
// Act 3: Dangerous Executables & Scripts
var dangerousExe = scanner.Scan("installer.exe", "https://untrusted-site.com/setup.exe");
Assert.Equal(DownloadRiskSeverity.HighRisk, dangerousExe.Severity);
Assert.True(dangerousExe.RequiresMotw);
Assert.False(string.IsNullOrWhiteSpace(dangerousExe.RiskReason));
var dangerousScript = scanner.Scan("script.vbs");
Assert.Equal(DownloadRiskSeverity.HighRisk, dangerousScript.Severity);
var dangerousScr = scanner.Scan("payload.scr");
Assert.Equal(DownloadRiskSeverity.Dangerous, dangerousScr.Severity);
Assert.True(dangerousScr.IsBlocked);
// Act 4: Double Extension Spoofing Detection
var doubleExt = scanner.Scan("invoice.pdf.exe", "https://phishing.com/invoice.pdf.exe");
Assert.Equal(DownloadRiskSeverity.Dangerous, doubleExt.Severity);
Assert.True(doubleExt.IsDoubleExtension);
Assert.Contains("이중 확장자", doubleExt.RiskReason);
// Act 5: Mark of the Web (Zone.Identifier) Generation
var motwContent = DownloadRiskScanner.GenerateZoneIdentifier(
sourceUrl: "https://example.com/downloads/tool.exe",
referrerUrl: "https://example.com/page.html");
Assert.Contains("[ZoneTransfer]", motwContent);
Assert.Contains("ZoneId=3", motwContent);
Assert.Contains("HostUrl=https://example.com/downloads/tool.exe", motwContent);
Assert.Contains("ReferrerUrl=https://example.com/page.html", motwContent);
// Act 6: Whitelisted domain downgrade
scanner.AddTrustedDomain("internal.corp");
var corpExe = scanner.Scan("internal-tool.exe", "https://internal.corp/tool.exe");
Assert.Equal(DownloadRiskSeverity.ModerateRisk, corpExe.Severity);
Assert.False(corpExe.RequiresMotw);
}
[Fact]
public void Gate148_DownloadBandwidthScheduler_ManagesThrottlingAndNightSchedules()
{
// Arrange
var configPath = Path.Combine(_tempDir, "bandwidth_scheduler.json");
var scheduler = new DownloadBandwidthScheduler(configPath);
// Act 1: Default: Unlimited (0)
Assert.Equal(0, scheduler.CurrentLimitBytesPerSecond);
Assert.False(scheduler.IsThrottlingActive);
// Act 2: Static manual throttling limit
scheduler.SetGlobalLimit(1_048_576);
Assert.True(scheduler.IsThrottlingActive);
Assert.Equal(1_048_576, scheduler.GetEffectiveLimit(DateTime.UtcNow));
// Act 3: Night scheduler window configuration: 23:00 to 07:00
scheduler.SetNightSchedule(new NightScheduleConfig(
IsEnabled: true,
StartTime: new TimeOnly(23, 0),
EndTime: new TimeOnly(7, 0),
NightLimitBytesPerSecond: 10_485_760,
DayLimitBytesPerSecond: 524_288,
ActiveDays: new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday }
));
// Test time inside night window on Monday at 02:30 AM
var mondayNight = new DateTime(2026, 9, 14, 2, 30, 0, DateTimeKind.Utc);
Assert.True(scheduler.IsInNightWindow(mondayNight));
Assert.Equal(10_485_760, scheduler.GetEffectiveLimit(mondayNight));
// Test time outside night window on Monday at 14:00 PM
var mondayDay = new DateTime(2026, 9, 14, 14, 0, 0, DateTimeKind.Utc);
Assert.False(scheduler.IsInNightWindow(mondayDay));
Assert.Equal(524_288, scheduler.GetEffectiveLimit(mondayDay));
// Test inactive day (Sunday)
var sundayNight = new DateTime(2026, 9, 13, 2, 30, 0, DateTimeKind.Utc);
Assert.False(scheduler.IsInNightWindow(sundayNight));
Assert.Equal(scheduler.CurrentLimitBytesPerSecond, scheduler.GetEffectiveLimit(sundayNight));
// Act 4: Throttle delay calculation
var delayMs = scheduler.CalculateDelayMs(chunkSizeBytes: 250_000, rateLimitBytesPerSecond: 500_000, elapsedMs: 100);
Assert.Equal(400, delayMs);
// Act 5: Save & Reload
scheduler.Save();
var reloaded = new DownloadBandwidthScheduler(configPath);
Assert.True(reloaded.NightSchedule.IsEnabled);
Assert.Equal(new TimeOnly(23, 0), reloaded.NightSchedule.StartTime);
Assert.Equal(10_485_760, reloaded.NightSchedule.NightLimitBytesPerSecond);
}
[Fact]
public void Gate149_AutoOpenFileTypePolicy_MaintainsWhitelistAndBlocksDangerousExtensions()
{
// Arrange
var configPath = Path.Combine(_tempDir, "auto_open_policy.json");
var policy = new AutoOpenFileTypePolicy(configPath);
// Act 1: Whitelist safe extensions (.pdf, .torrent, .ics)
Assert.True(policy.AllowExtension(".pdf"));
Assert.True(policy.AllowExtension("torrent"));
Assert.True(policy.AllowExtension(".ICS"));
Assert.Contains(".pdf", policy.AllowedExtensions);
Assert.Contains(".torrent", policy.AllowedExtensions);
Assert.Contains(".ics", policy.AllowedExtensions);
// Act 2: Reject dangerous executable & script extensions
Assert.False(policy.AllowExtension(".exe"));
Assert.False(policy.AllowExtension(".bat"));
Assert.False(policy.AllowExtension(".scr"));
Assert.False(policy.AllowExtension(".vbs"));
Assert.False(policy.AllowExtension(".ps1"));
Assert.False(policy.AllowExtension(".cmd"));
Assert.False(policy.AllowExtension(".msi"));
Assert.False(policy.AllowExtension(".jar"));
Assert.DoesNotContain(".exe", policy.AllowedExtensions);
// Act 3: EvaluateAutoOpen
var resPdf = policy.EvaluateAutoOpen("ebook.pdf", fileSizeBytes: 1024);
Assert.Equal(AutoOpenDecision.Allowed, resPdf.Decision);
Assert.True(resPdf.CanOpen);
var resExe = policy.EvaluateAutoOpen("setup.exe", fileSizeBytes: 2048);
Assert.Equal(AutoOpenDecision.BlockedDangerousExtension, resExe.Decision);
Assert.False(resExe.CanOpen);
var resDocx = policy.EvaluateAutoOpen("notes.docx", fileSizeBytes: 1024);
Assert.Equal(AutoOpenDecision.NotInWhitelist, resDocx.Decision);
Assert.False(resDocx.CanOpen);
// Max file size enforcement
policy.MaxAutoOpenFileSizeBytes = 10 * 1024 * 1024;
var resHugePdf = policy.EvaluateAutoOpen("huge.pdf", fileSizeBytes: 15 * 1024 * 1024);
Assert.Equal(AutoOpenDecision.ExceedsMaxFileSize, resHugePdf.Decision);
Assert.False(resHugePdf.CanOpen);
// Act 4: Remove from whitelist
policy.RemoveExtension(".torrent");
Assert.False(policy.EvaluateAutoOpen("movie.torrent", 512).CanOpen);
// Act 5: Save & Reload
policy.Save();
var reloaded = new AutoOpenFileTypePolicy(configPath);
Assert.True(reloaded.EvaluateAutoOpen("manual.pdf", 1024).CanOpen);
Assert.False(reloaded.EvaluateAutoOpen("movie.torrent", 512).CanOpen);
Assert.Equal(10 * 1024 * 1024, reloaded.MaxAutoOpenFileSizeBytes);
}
[Fact]
public void Gate150_PrintPreviewCoordinator_CoordinatesLayoutMarginsAndPageRanges()
{
// Arrange
var coordinator = new PrintPreviewCoordinator();
// Act 1: Default print settings
var defaults = coordinator.CurrentOptions;
Assert.Equal(PrintOrientation.Portrait, defaults.Orientation);
Assert.Equal(PrintMarginType.Default, defaults.MarginType);
Assert.True(defaults.PrintBackgroundGraphics);
Assert.Equal(PrintPaperSize.A4, defaults.PaperSize);
Assert.Equal(100, defaults.ScalePercent);
// Act 2: Margins resolution
var normalMargins = PrintPreviewCoordinator.ResolveMargins(PrintMarginType.Default);
Assert.True(normalMargins.TopMm > 0 && normalMargins.BottomMm > 0);
var zeroMargins = PrintPreviewCoordinator.ResolveMargins(PrintMarginType.None);
Assert.Equal(0, zeroMargins.TopMm);
Assert.Equal(0, zeroMargins.LeftMm);
var minMargins = PrintPreviewCoordinator.ResolveMargins(PrintMarginType.Minimum);
Assert.True(minMargins.TopMm < normalMargins.TopMm);
// Act 3: Page range parsing
var parsedPages = PrintPreviewCoordinator.ParsePageRange("1-3, 5, 8-10", totalPages: 10);
Assert.Equal(new[] { 1, 2, 3, 5, 8, 9, 10 }, parsedPages);
var allPages = PrintPreviewCoordinator.ParsePageRange("All", totalPages: 5);
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, allPages);
var clampedPages = PrintPreviewCoordinator.ParsePageRange("1, 4, 12", totalPages: 5);
Assert.Equal(new[] { 1, 4 }, clampedPages);
// Act 4: Layout & Printable Area calculation (A4 = 210 x 297 mm)
var layout = coordinator.ComputeLayout(new PrintPreviewOptions
{
PaperSize = PrintPaperSize.A4,
Orientation = PrintOrientation.Portrait,
MarginType = PrintMarginType.Custom,
CustomMargins = new PrintMargins(TopMm: 10, BottomMm: 10, LeftMm: 15, RightMm: 15),
ScalePercent = 100
});
Assert.Equal(210, layout.PaperWidthMm);
Assert.Equal(297, layout.PaperHeightMm);
Assert.Equal(180, layout.PrintableWidthMm);
Assert.Equal(277, layout.PrintableHeightMm);
var landscapeLayout = coordinator.ComputeLayout(new PrintPreviewOptions
{
PaperSize = PrintPaperSize.A4,
Orientation = PrintOrientation.Landscape,
MarginType = PrintMarginType.None
});
Assert.Equal(297, landscapeLayout.PaperWidthMm);
Assert.Equal(210, landscapeLayout.PaperHeightMm);
Assert.Equal(297, landscapeLayout.PrintableWidthMm);
Assert.Equal(210, landscapeLayout.PrintableHeightMm);
// Act 5: Options Validation
var validResult = coordinator.Validate(new PrintPreviewOptions { ScalePercent = 100 });
Assert.True(validResult.IsValid);
var invalidScale = coordinator.Validate(new PrintPreviewOptions { ScalePercent = 5 });
Assert.False(invalidScale.IsValid);
Assert.Contains("배율", invalidScale.ErrorMessage);
var invalidMargins = coordinator.Validate(new PrintPreviewOptions
{
MarginType = PrintMarginType.Custom,
CustomMargins = new PrintMargins(-5, 10, 10, 10)
});
Assert.False(invalidMargins.IsValid);
}
}