267 lines
11 KiB
C#
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"));
|
|
}
|
|
}
|