412 lines
18 KiB
C#
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 & 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("<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));
|
|
}
|
|
}
|
|
}
|