using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Paca.Browser.Search; using Xunit; namespace Paca.Tests; public sealed class ModernBrowsingMilestone5Phase105Tests : IDisposable { private readonly string _tempDir; public ModernBrowsingMilestone5Phase105Tests() { _tempDir = Path.Combine(Path.GetTempPath(), "paca_m5_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } public void Dispose() { try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } catch { } } [Fact] public void Gate132_OmniboxKeywordEngine_ResolvesKeywordSearchTriggersAndManagesCustomKeywords() { // Arrange var configPath = Path.Combine(_tempDir, "keywords.json"); var engine = new OmniboxKeywordEngine(configPath); // Act 1: Built-in keyword triggers Assert.True(engine.TryResolve("yt newjeans", out var ytMatch)); Assert.NotNull(ytMatch); Assert.Equal("yt", ytMatch.Keyword); Assert.Equal("newjeans", ytMatch.SearchTerms); Assert.Equal("https://www.youtube.com/results?search_query=newjeans", ytMatch.ResolvedUrl); Assert.True(engine.TryResolve("w artificial intelligence", out var wikiMatch)); Assert.NotNull(wikiMatch); Assert.Equal("w", wikiMatch.Keyword); Assert.Equal("artificial intelligence", wikiMatch.SearchTerms); Assert.Contains("wikipedia.org", wikiMatch.ResolvedUrl); Assert.Contains("artificial+intelligence", wikiMatch.ResolvedUrl); Assert.True(engine.TryResolve("g quantum computing", out var gMatch)); Assert.NotNull(gMatch); Assert.Equal("g", gMatch.Keyword); Assert.Contains("google.com/search?q=quantum+computing", gMatch.ResolvedUrl); // Act 2: Case insensitivity Assert.True(engine.TryResolve("YT ive", out var upperMatch)); Assert.NotNull(upperMatch); Assert.Equal("yt", upperMatch.Keyword); Assert.Equal("ive", upperMatch.SearchTerms); // Act 3: Pure URLs or non-keyword strings should not trigger Assert.False(engine.TryResolve("https://youtube.com/watch", out _)); Assert.False(engine.TryResolve("yt.com", out _)); Assert.False(engine.TryResolve("unregisteredkeyword test", out _)); // Act 4: Custom keyword registration and persistence engine.RegisterKeyword("mdn", "MDN Web Docs", "https://developer.mozilla.org/en-US/search?q={searchTerms}"); Assert.True(engine.TryResolve("mdn flexbox", out var mdnMatch)); Assert.NotNull(mdnMatch); Assert.Equal("mdn", mdnMatch.Keyword); Assert.Equal("https://developer.mozilla.org/en-US/search?q=flexbox", mdnMatch.ResolvedUrl); engine.Save(); var reloaded = new OmniboxKeywordEngine(configPath); Assert.True(reloaded.TryResolve("mdn grid", out var reloadedMdn)); Assert.NotNull(reloadedMdn); Assert.Equal("https://developer.mozilla.org/en-US/search?q=grid", reloadedMdn.ResolvedUrl); // Act 5: Removal Assert.True(reloaded.RemoveKeyword("mdn")); Assert.False(reloaded.TryResolve("mdn grid", out _)); } [Fact] public void Gate133_OmniboxScopeFilter_ParsesScopesAndRoutesScopedQueries() { // Arrange var filter = new OmniboxScopeFilter(); var sampleItems = new List { new("PACA Settings", "chrome://settings", OmniboxSearchScope.Tabs), new("YouTube Music", "https://music.youtube.com", OmniboxSearchScope.Tabs), new("GitHub PACA Browser", "https://github.com/paca/browser", OmniboxSearchScope.Bookmarks), new("PACA Developer Guide", "https://paca.dev/docs", OmniboxSearchScope.Bookmarks), new("Naver News Article", "https://news.naver.com/123", OmniboxSearchScope.History), new("Wikipedia Search Result", "https://en.wikipedia.org/wiki/PACA", OmniboxSearchScope.History) }; // Act 1: Scope parsing tests var pTabs = filter.Parse("@tabs youtube"); Assert.True(pTabs.IsScoped); Assert.Equal(OmniboxSearchScope.Tabs, pTabs.Scope); Assert.Equal("youtube", pTabs.Query); var pTabsKo = filter.Parse("@탭 설정"); Assert.True(pTabsKo.IsScoped); Assert.Equal(OmniboxSearchScope.Tabs, pTabsKo.Scope); Assert.Equal("설정", pTabsKo.Query); var pBook = filter.Parse("@bookmarks dev"); Assert.True(pBook.IsScoped); Assert.Equal(OmniboxSearchScope.Bookmarks, pBook.Scope); Assert.Equal("dev", pBook.Query); var pHist = filter.Parse("@history naver"); Assert.True(pHist.IsScoped); Assert.Equal(OmniboxSearchScope.History, pHist.Scope); Assert.Equal("naver", pHist.Query); var pPlain = filter.Parse("general search query"); Assert.False(pPlain.IsScoped); Assert.Equal(OmniboxSearchScope.None, pPlain.Scope); // Act 2: Routing and filtering var routedTabs = filter.RouteAndFilter("@tabs music", sampleItems); Assert.Single(routedTabs); Assert.Equal("YouTube Music", routedTabs[0].Title); var routedBookmarks = filter.RouteAndFilter("@북마크 dev", sampleItems); Assert.Single(routedBookmarks); Assert.Equal("PACA Developer Guide", routedBookmarks[0].Title); var routedHistory = filter.RouteAndFilter("@기록 naver", sampleItems); Assert.Single(routedHistory); Assert.Equal("Naver News Article", routedHistory[0].Title); var unscopedMatches = filter.RouteAndFilter("PACA", sampleItems); Assert.Equal(4, unscopedMatches.Count); // Settings, GitHub PACA, PACA Developer Guide, Wikipedia } [Fact] public void Gate134_SecurePaymentVault_EncryptsCardAndMasksSensitiveData() { // Arrange var vaultPath = Path.Combine(_tempDir, "payment_vault.json"); var vault = new SecurePaymentVault(vaultPath); // Act 1: Masking and Card Issuer Detection Helpers Assert.Equal("•••• •••• •••• 4444", CardMaskHelper.MaskCardNumber("4111222233334444")); Assert.Equal("•••• •••• •••• 1234", CardMaskHelper.MaskCardNumber("4111-2222-3333-1234")); Assert.Equal("•••••••••••5678", CardMaskHelper.MaskCardNumber("378282246315678")); // Amex 15-digit Assert.Equal("Visa", CardMaskHelper.DetectIssuer("4111222233334444")); Assert.Equal("Mastercard", CardMaskHelper.DetectIssuer("5500000000000004")); Assert.Equal("American Express", CardMaskHelper.DetectIssuer("340000000000000")); Assert.Equal("•••", CardMaskHelper.MaskCvv("789")); Assert.Equal("••••", CardMaskHelper.MaskCvv("1234")); // Act 2: Card Vault Add & DPAPI Encryption var card = vault.AddCard( cardholderName: "Hong Gildong", rawCardNumber: "4111222233334444", expMonth: "12", expYear: "2028", rawCvv: "999", nickname: "Primary Visa"); Assert.NotNull(card); Assert.NotEqual("4111222233334444", card.EncryptedCardNumber); Assert.NotEqual("999", card.EncryptedCvv); Assert.Equal("Visa", card.Issuer); // Act 3: Decrypt and Mask verification Assert.Equal("4111222233334444", vault.GetDecryptedCardNumber(card)); Assert.Equal("999", vault.GetDecryptedCvv(card)); Assert.Equal("•••• •••• •••• 4444", vault.GetMaskedCardNumber(card)); // Act 4: Address Autofill Record var address = new AutofillAddressRecord( Id: Guid.NewGuid().ToString("N"), FullName: "Hong Gildong", AddressLine1: "123 Teheran-ro", AddressLine2: "Suite 500", City: "Seoul", StateOrProvince: "Gangnam-gu", PostalCode: "06234", Country: "KR", PhoneNumber: "010-1234-5678"); vault.AddAddress(address); Assert.Single(vault.GetAllAddresses()); // Act 5: Save & Reload vault.Save(); var reloaded = new SecurePaymentVault(vaultPath); Assert.Single(reloaded.GetAllCards()); Assert.Single(reloaded.GetAllAddresses()); var reloadedCard = reloaded.GetAllCards()[0]; Assert.Equal("4111222233334444", reloaded.GetDecryptedCardNumber(reloadedCard)); Assert.Equal("999", reloaded.GetDecryptedCvv(reloadedCard)); // Act 6: Removal Assert.True(reloaded.RemoveCard(reloadedCard.Id)); Assert.Empty(reloaded.GetAllCards()); } [Fact] public void Gate135_ViewSourceHelper_GeneratesSyntaxHighlightedHtmlAndHandlesUrlRedirection() { // Act 1: URL Redirection Helpers Assert.True(ViewSourceHelper.IsViewSourceUrl("view-source:https://example.com")); Assert.True(ViewSourceHelper.IsViewSourceUrl("VIEW-SOURCE:https://example.com/test")); Assert.False(ViewSourceHelper.IsViewSourceUrl("https://example.com")); Assert.Equal("https://example.com/page.html", ViewSourceHelper.ExtractTargetUrl("view-source:https://example.com/page.html")); Assert.Equal("view-source:https://example.com", ViewSourceHelper.WrapInViewSourceUrl("https://example.com")); // Act 2: Generate Syntax-Highlighted HTML Wrapper var rawHtml = @" Hello World

Welcome to PACA & fast web!

"; var formattedHtml = ViewSourceHelper.GenerateSourceHtml(rawHtml, "https://example.com/demo"); // Assertions Assert.NotNull(formattedHtml); Assert.Contains("", formattedHtml); Assert.Contains("view-source:https://example.com/demo", formattedHtml); Assert.Contains("line-number", formattedHtml); Assert.Contains("html-tag", formattedHtml); Assert.Contains("html-attr-name", formattedHtml); Assert.Contains("html-comment", formattedHtml); Assert.Contains("Consolas", formattedHtml); Assert.DoesNotContain("
{ new("1", "Example Site", "https://example.com/path/?utm_source=twitter&ref=123#section1", DateTime.UtcNow.AddDays(-10)), new("2", "Example Site Duplicate", "https://example.com:443/path?utm_campaign=spring", DateTime.UtcNow.AddDays(-5)), new("3", "GitHub", "https://github.com/", DateTime.UtcNow.AddDays(-2)), new("4", "Dead Page", "https://example.com/404-not-found", DateTime.UtcNow.AddDays(-1)), new("5", "Malformed Link", "htp:/invalid-url", DateTime.UtcNow.AddDays(-1)) }; // Act 1: URL Normalization var norm1 = BookmarkSanitizer.NormalizeUrl("HTTPS://Example.com:443/path/?utm_source=twitter&ref=123#section1"); var norm2 = BookmarkSanitizer.NormalizeUrl("https://example.com/path"); Assert.Equal(norm1, norm2); // Act 2: Deduplication var duplicates = BookmarkSanitizer.FindDuplicates(bookmarks); Assert.NotEmpty(duplicates); var deduplicated = BookmarkSanitizer.Deduplicate(bookmarks); Assert.Equal(4, deduplicated.Count); // duplicate entry removed, keeping oldest or first Assert.Contains(deduplicated, b => b.Id == "1"); Assert.DoesNotContain(deduplicated, b => b.Id == "2"); // Act 3: Dead link detection with Mock HttpMessageHandler var mockHandler = new MockHttpMessageHandler(req => { if (req.RequestUri?.AbsolutePath.Contains("404-not-found") == true) { return new HttpResponseMessage(HttpStatusCode.NotFound); } return new HttpResponseMessage(HttpStatusCode.OK); }); var report = await BookmarkSanitizer.CheckDeadLinksAsync(deduplicated, mockHandler); Assert.Equal(2, report.DeadLinks.Count); // 404 dead link and malformed link Assert.Contains(report.DeadLinks, d => d.Bookmark.Id == "4" && d.StatusCode == 404); Assert.Contains(report.DeadLinks, d => d.Bookmark.Id == "5" && d.IsMalformed); Assert.Equal(2, report.ValidBookmarks.Count); // Example Site and GitHub } [Fact] public void Gate137_SiteZoomPersistenceStore_PersistsGlobalAndPerSiteZoomLevels() { // Arrange var configPath = Path.Combine(_tempDir, "zoom_settings.json"); var store = new SiteZoomPersistenceStore(configPath, defaultZoom: 1.0); // Act 1: Global default zoom Assert.Equal(1.0, store.DefaultZoomLevel); Assert.Equal(1.0, store.GetZoomForSite("https://news.ycombinator.com")); store.DefaultZoomLevel = 1.25; Assert.Equal(1.25, store.GetZoomForSite("https://news.ycombinator.com")); // Act 2: Site specific overrides & host normalization store.SetZoomForSite("https://news.ycombinator.com/item?id=123", 1.50); store.SetZoomForSite("https://github.com/trending", 0.90); Assert.Equal(1.50, store.GetZoomForSite("https://news.ycombinator.com/news")); Assert.Equal(1.50, store.GetZoomForSite("news.ycombinator.com")); Assert.Equal(0.90, store.GetZoomForSite("github.com")); Assert.Equal(1.25, store.GetZoomForSite("https://wikipedia.org")); // Fallback to global default // Act 3: Clamp boundaries (0.25 to 5.0) store.SetZoomForSite("https://huge.site.com", 10.0); Assert.Equal(5.0, store.GetZoomForSite("https://huge.site.com")); store.SetZoomForSite("https://tiny.site.com", 0.05); Assert.Equal(0.25, store.GetZoomForSite("https://tiny.site.com")); // Act 4: Reset per-site zoom store.ResetZoomForSite("https://news.ycombinator.com"); Assert.Equal(1.25, store.GetZoomForSite("https://news.ycombinator.com")); // Act 5: Persistence & Reload store.SetZoomForSite("https://reddit.com", 1.10); store.Save(); var reloaded = new SiteZoomPersistenceStore(configPath); Assert.Equal(1.25, reloaded.DefaultZoomLevel); Assert.Equal(1.10, reloaded.GetZoomForSite("https://reddit.com")); Assert.Equal(1.25, reloaded.GetZoomForSite("https://unknown.com")); } [Fact] public void Gate138_HardRefreshEngine_EvaluatesShortcutsAndGeneratesClearCacheReloadRequests() { // Act 1: Shortcut evaluation // Normal refresh: F5 (Ctrl=false, Shift=false) Assert.Equal(RefreshType.Normal, HardRefreshEngine.EvaluateRefresh(ctrl: false, shift: false, isF5: true, isR: false)); // Normal refresh: Ctrl+R (Ctrl=true, Shift=false) Assert.Equal(RefreshType.Normal, HardRefreshEngine.EvaluateRefresh(ctrl: true, shift: false, isF5: false, isR: true)); // Hard refresh: Ctrl+F5 Assert.Equal(RefreshType.HardClearCache, HardRefreshEngine.EvaluateRefresh(ctrl: true, shift: false, isF5: true, isR: false)); // Hard refresh: Shift+F5 Assert.Equal(RefreshType.HardClearCache, HardRefreshEngine.EvaluateRefresh(ctrl: false, shift: true, isF5: true, isR: false)); // Hard refresh: Ctrl+Shift+R Assert.Equal(RefreshType.HardClearCache, HardRefreshEngine.EvaluateRefresh(ctrl: true, shift: true, isF5: false, isR: true)); // Irrelevant keys Assert.Equal(RefreshType.None, HardRefreshEngine.EvaluateRefresh(ctrl: true, shift: false, isF5: false, isR: false)); Assert.Equal(RefreshType.None, HardRefreshEngine.EvaluateRefresh(ctrl: false, shift: false, isF5: false, isR: false)); // Act 2: Request Generation var hardReq = HardRefreshEngine.CreateRequest("https://example.com/dashboard", RefreshType.HardClearCache); Assert.Equal(RefreshType.HardClearCache, hardReq.Type); Assert.True(hardReq.ClearMemoryCache); Assert.True(hardReq.ClearDiskCache); Assert.True(hardReq.BypassServiceWorker); Assert.Equal("https://example.com/dashboard", hardReq.TargetUrl); Assert.Contains("no-cache", hardReq.Headers["Cache-Control"]); Assert.Equal("no-cache", hardReq.Headers["Pragma"]); var normalReq = HardRefreshEngine.CreateRequest("https://example.com/dashboard", RefreshType.Normal); Assert.Equal(RefreshType.Normal, normalReq.Type); Assert.False(normalReq.ClearMemoryCache); Assert.False(normalReq.ClearDiskCache); Assert.False(normalReq.BypassServiceWorker); // Act 3: Execution Coordinator var cacheCleared = false; var reloaded = false; HardRefreshEngine.Execute( RefreshType.HardClearCache, clearCacheAction: () => cacheCleared = true, reloadAction: () => reloaded = true); Assert.True(cacheCleared); Assert.True(reloaded); var normalCacheCleared = false; var normalReloaded = false; HardRefreshEngine.Execute( RefreshType.Normal, clearCacheAction: () => normalCacheCleared = true, reloadAction: () => normalReloaded = true); Assert.False(normalCacheCleared); Assert.True(normalReloaded); } private sealed class MockHttpMessageHandler : HttpMessageHandler { private readonly Func _handler; public MockHttpMessageHandler(Func handler) { _handler = handler; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return Task.FromResult(_handler(request)); } } }