releases/tests/Paca.Tests/ModernBrowsingMilestone4Phase104Tests.cs

267 lines
11 KiB
C#

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<string>();
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<CredentialAuditItem>
{
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"));
}
}