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:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
267
tests/Paca.Tests/ModernBrowsingMilestone4Phase104Tests.cs
Normal file
267
tests/Paca.Tests/ModernBrowsingMilestone4Phase104Tests.cs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
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"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue