releases/tests/Paca.Tests/ModernBrowsingMilestone5Phase105Tests.cs

412 lines
18 KiB
C#

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<ScopedSearchItem>
{
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 = @"<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<!-- Main content -->
<div class=""container"" id=""main"">
<p>Welcome to PACA &amp; fast web!</p>
</div>
</body>
</html>";
var formattedHtml = ViewSourceHelper.GenerateSourceHtml(rawHtml, "https://example.com/demo");
// Assertions
Assert.NotNull(formattedHtml);
Assert.Contains("<!DOCTYPE html>", 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("<div class=\"container\"", formattedHtml); // Raw unescaped div must not exist directly in body
Assert.Contains("&lt;div", formattedHtml); // Properly escaped
}
[Fact]
public async Task Gate136_BookmarkSanitizer_DeduplicatesBookmarksAndDetectsDeadLinks()
{
// Arrange
var bookmarks = new List<BookmarkItem>
{
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<HttpRequestMessage, HttpResponseMessage> _handler;
public MockHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
{
_handler = handler;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(_handler(request));
}
}
}