using System; using System.Collections.Generic; using System.IO; using System.Linq; using Paca.Browser.Security; using Xunit; namespace Paca.Tests; public sealed class ModernBrowsingMilestone4Phase104Tests : IDisposable { private readonly string _tempDir; public ModernBrowsingMilestone4Phase104Tests() { _tempDir = Path.Combine(Path.GetTempPath(), "paca_m4_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } public void Dispose() { try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } catch { } } [Fact] public void Gate124_ContainerTabManager_CreatesAndPartitionsContainers() { // Arrange var configPath = Path.Combine(_tempDir, "containers.json"); var manager = new ContainerTabManager(configPath); // Act 1: Verify default containers var defaults = manager.GetAllContainers(); Assert.Contains(defaults, c => c.Name == "Personal"); Assert.Contains(defaults, c => c.Name == "Work"); Assert.Contains(defaults, c => c.Name == "Banking"); Assert.Contains(defaults, c => c.Name == "Shopping"); // Act 2: Create custom container var travelContainer = manager.CreateContainer("Travel", colorHex: "#9333EA", icon: "plane"); Assert.NotNull(travelContainer); Assert.Equal("Travel", travelContainer.Name); Assert.False(string.IsNullOrWhiteSpace(travelContainer.PartitionId)); // Act 3: Partition verification var personal = manager.GetContainerByName("Personal"); Assert.NotNull(personal); Assert.NotEqual(personal.PartitionId, travelContainer.PartitionId); // Act 4: Save & Reload manager.Save(); var reloaded = new ContainerTabManager(configPath); Assert.NotNull(reloaded.GetContainerByName("Travel")); } [Fact] public void Gate125_SslCertificateInspector_ExtractsAndEvaluatesCertDetails() { // Arrange var inspector = new SslCertificateInspector(); var validCert = new SslCertDetails( Subject: "CN=example.com, O=Example Inc", Issuer: "CN=DigiCert Global Root CA", ValidFrom: DateTime.UtcNow.AddDays(-30), ValidTo: DateTime.UtcNow.AddDays(300), Protocol: "TLS 1.3", CipherAlgorithm: "AES-GCM-256", SubjectAlternativeNames: new[] { "example.com", "www.example.com" }); var expiredCert = new SslCertDetails( Subject: "CN=expired.badssl.com", Issuer: "CN=BadSSL Intermediate", ValidFrom: DateTime.UtcNow.AddDays(-400), ValidTo: DateTime.UtcNow.AddDays(-10), Protocol: "TLS 1.2", CipherAlgorithm: "AES-CBC-128", SubjectAlternativeNames: new[] { "expired.badssl.com" }); // Act 1: Valid cert evaluation var validEval = inspector.Evaluate(validCert, hostName: "example.com"); Assert.Equal(SslSecurityStatus.Secure, validEval.Status); Assert.True(validEval.IsTls13); Assert.Empty(validEval.Warnings); // Act 2: Expired cert evaluation var expiredEval = inspector.Evaluate(expiredCert, hostName: "expired.badssl.com"); Assert.Equal(SslSecurityStatus.Insecure, expiredEval.Status); Assert.Contains(expiredEval.Warnings, w => w.Contains("만료")); } [Fact] public void Gate126_HttpsUpgradePolicy_UpgradesHttpAndEnforcesHttpsOnlyMode() { // Arrange var configPath = Path.Combine(_tempDir, "https_policy.json"); var policy = new HttpsUpgradePolicy(configPath); // Act 1: Default HTTPS upgrade var res1 = policy.EvaluateNavigation("http://example.com/login"); Assert.Equal(HttpsNavigationAction.UpgradeToHttps, res1.Action); Assert.Equal("https://example.com/login", res1.TargetUrl); // Act 2: HTTPS URL remains as is var res2 = policy.EvaluateNavigation("https://secure.site.com/dashboard"); Assert.Equal(HttpsNavigationAction.Allow, res2.Action); Assert.Equal("https://secure.site.com/dashboard", res2.TargetUrl); // Act 3: Exception whitelist for legacy internal domains policy.AddHttpException("internal.local"); var res3 = policy.EvaluateNavigation("http://internal.local/status"); Assert.Equal(HttpsNavigationAction.Allow, res3.Action); Assert.Equal("http://internal.local/status", res3.TargetUrl); // Act 4: Save & Reload policy.Save(); var reloaded = new HttpsUpgradePolicy(configPath); var resReload = reloaded.EvaluateNavigation("http://internal.local/status"); Assert.Equal(HttpsNavigationAction.Allow, resReload.Action); } [Fact] public void Gate127_ClearOnExitManager_ExecutesConfiguredClearRulesOnShutdown() { // Arrange var configPath = Path.Combine(_tempDir, "clear_on_exit.json"); var manager = new ClearOnExitManager(configPath); // Configure options manager.Options = new ClearOnExitOptions( ClearCookies: true, ClearCache: true, ClearHistory: false, ClearPasswords: false, ClearDownloads: true); manager.Save(); // Act: Execute clear actions with mock tracker var executedActions = new List(); manager.ExecuteClear( clearCookiesAction: () => executedActions.Add("cookies"), clearCacheAction: () => executedActions.Add("cache"), clearHistoryAction: () => executedActions.Add("history"), clearPasswordsAction: () => executedActions.Add("passwords"), clearDownloadsAction: () => executedActions.Add("downloads")); // Assert Assert.Contains("cookies", executedActions); Assert.Contains("cache", executedActions); Assert.Contains("downloads", executedActions); Assert.DoesNotContain("history", executedActions); Assert.DoesNotContain("passwords", executedActions); // Persistence check var reloaded = new ClearOnExitManager(configPath); Assert.True(reloaded.Options.ClearCookies); Assert.False(reloaded.Options.ClearHistory); } [Fact] public void Gate128_PasswordSecurityAuditor_DetectsWeakReusedAndBreachedPasswords() { // Arrange var auditor = new PasswordSecurityAuditor(); var credentials = new List { new("site1.com", "admin", "123456"), new("site2.com", "user1", "Company2026!"), new("site3.com", "user2", "Company2026!"), new("site4.com", "expert", "9#Xp$2mK!vL8@zQ7") }; // Act var report = auditor.AuditCredentials(credentials); // Assert 1: Weak password detection Assert.Contains(report.WeakCredentials, c => c.Domain == "site1.com"); // Assert 2: Reused password detection (Company2026! reused in site2 and site3) Assert.Equal(2, report.ReusedCredentials.Count); Assert.Contains(report.ReusedCredentials, c => c.Domain == "site2.com"); Assert.Contains(report.ReusedCredentials, c => c.Domain == "site3.com"); // Assert 3: Safe password Assert.DoesNotContain(report.WeakCredentials, c => c.Domain == "site4.com"); Assert.DoesNotContain(report.ReusedCredentials, c => c.Domain == "site4.com"); } [Fact] public void Gate129_QuietPermissionPromptEngine_TransformsNoisyModalsToSubtleBadges() { // Arrange var engine = new QuietPermissionPromptEngine(quietModeEnabled: true); // Act 1: Initial prompt is quiet var prompt1 = engine.EvaluatePrompt("https://news.example.com", BrowserPermissionKind.Notifications); Assert.Equal(PermissionPromptStyle.QuietAddressBarIcon, prompt1.Style); Assert.False(prompt1.IsAutoBlocked); // Act 2: Dismiss 3 times triggers auto-block engine.RecordUserDismiss("news.example.com"); engine.RecordUserDismiss("news.example.com"); engine.RecordUserDismiss("news.example.com"); var promptBlocked = engine.EvaluatePrompt("https://news.example.com", BrowserPermissionKind.Notifications); Assert.True(promptBlocked.IsAutoBlocked); Assert.Equal(PermissionPromptStyle.SilentBlock, promptBlocked.Style); } [Fact] public void Gate130_HardwareDeviceIndicatorCoordinator_TracksActiveDevicesAndMutes() { // Arrange var coordinator = new HardwareDeviceIndicatorCoordinator(); // Act 1: Start mic and camera coordinator.SetDeviceUsage(tabId: "tab-meet-1", HardwareDeviceKind.Microphone, isActive: true); coordinator.SetDeviceUsage(tabId: "tab-meet-1", HardwareDeviceKind.Camera, isActive: true); // Assert 1 var status = coordinator.GetStatus("tab-meet-1"); Assert.True(status.HasActiveDevices); Assert.Contains(HardwareDeviceKind.Microphone, status.ActiveDevices); Assert.Contains(HardwareDeviceKind.Camera, status.ActiveDevices); Assert.Contains("마이크", status.SummaryDescription); Assert.Contains("카메라", status.SummaryDescription); // Act 2: Hardware killswitch / revoke coordinator.RevokeAllAccess("tab-meet-1"); var revoked = coordinator.GetStatus("tab-meet-1"); Assert.False(revoked.HasActiveDevices); Assert.Empty(revoked.ActiveDevices); } [Fact] public void Gate131_ExternalProtocolHandlerGate_GuardsProtocolExecutionsAndManagesWhitelist() { // Arrange var configPath = Path.Combine(_tempDir, "protocol_gate.json"); var gate = new ExternalProtocolHandlerGate(configPath); // Act 1: Dangerous protocols are permanently blocked var cmdAction = gate.EvaluateProtocolLaunch("https://malicious.com", "cmd:///c calc.exe"); Assert.Equal(ProtocolLaunchDecision.Blocked, cmdAction); var psAction = gate.EvaluateProtocolLaunch("https://malicious.com", "powershell://run"); Assert.Equal(ProtocolLaunchDecision.Blocked, psAction); // Act 2: Unregistered safe protocol prompts user var slackPrompt = gate.EvaluateProtocolLaunch("https://slack.com", "slack://channel?team=T1"); Assert.Equal(ProtocolLaunchDecision.PromptUser, slackPrompt); // Act 3: Whitelist domain + protocol gate.AllowProtocolForDomain("slack.com", "slack"); var slackAllowed = gate.EvaluateProtocolLaunch("https://slack.com", "slack://channel?team=T1"); Assert.Equal(ProtocolLaunchDecision.Allowed, slackAllowed); // Act 4: Save & Reload gate.Save(); var reloaded = new ExternalProtocolHandlerGate(configPath); Assert.Equal(ProtocolLaunchDecision.Allowed, reloaded.EvaluateProtocolLaunch("https://slack.com", "slack://channel?team=T1")); } }