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); } }