using System; using System.IO; using Paca.Browser; using Xunit; namespace Paca.Tests; public sealed class BrowserStorageAndSessionHardeningPhase7Tests : IDisposable { private readonly string _tempDir; public BrowserStorageAndSessionHardeningPhase7Tests() { _tempDir = Path.Combine(Path.GetTempPath(), "paca_phase7_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } public void Dispose() { try { if (Directory.Exists(_tempDir)) { Directory.Delete(_tempDir, true); } } catch { } } [Fact] public void Gate701_WorkspaceRoutingRules_Preserves_W_Prefixed_Domains_And_Handles_Null() { // Arrange var rulesFile = Path.Combine(_tempDir, "routing_rules.json"); var rules = new WorkspaceRoutingRules(rulesFile); // Act - domains starting with 'w' rules.Add("wired.com", "TechNews"); rules.Add("whatsapp.com", "Messaging"); rules.Add("www.wsj.com", "Finance"); rules.Add("weather.com", "Utility"); // Assert - must NOT corrupt domains like 'wired.com' -> 'ired.com' Assert.Equal("TechNews", rules.Match("https://wired.com/story/ai")); Assert.Equal("TechNews", rules.Match("https://www.wired.com/story/ai")); Assert.Equal("Messaging", rules.Match("https://web.whatsapp.com")); Assert.Equal("Messaging", rules.Match("https://whatsapp.com")); Assert.Equal("Finance", rules.Match("https://wsj.com")); Assert.Equal("Utility", rules.Match("https://weather.com/today")); // Null & Malformed safety Assert.Null(rules.Match(null)); Assert.Null(rules.Match("")); Assert.Null(rules.Match("javascript:alert(1)")); Assert.Null(rules.Match("not-a-url")); } [Fact] public void Gate702_NotesStore_Ensures_Directory_On_Delete_And_Explicit_Save() { // Arrange: deep nested path whose parent directory doesn't exist yet var deepNotesPath = Path.Combine(_tempDir, "nested", "sub", "notes.json"); var store = new NotesStore(deepNotesPath); // Act & Assert 1: Add creates file & directory var id1 = store.Add("First Note", "Content 1"); Assert.True(File.Exists(deepNotesPath)); Assert.Single(store.List()); // Now simulate directory deletion or separate store instance targeting a new path var deepNotesPath2 = Path.Combine(_tempDir, "another", "sub", "notes2.json"); var store2 = new NotesStore(deepNotesPath2); // Act 2: Explicit Save without prior Add should ensure directory and not throw store2.Save(); Assert.True(File.Exists(deepNotesPath2)); // Act 3: Delete should ensure directory and persist store.Delete(id1); Assert.Empty(store.List()); var reloaded = new NotesStore(deepNotesPath); Assert.Empty(reloaded.List()); } [Fact] public void Gate703_TrackerCounter_And_PermissionGate_Handle_Malformed_Urls_And_Null_Safely() { // Arrange var counter = new TrackerCounter(); var gate = new PermissionGate(); // Act & Assert 1: TrackerCounter should not throw UriFormatException on malformed or relative URLs var initialTotal = counter.Total; counter.Record("https://google-analytics.com/collect"); Assert.Equal(initialTotal + 1, counter.Total); Assert.True(counter.ByDomain.ContainsKey("google-analytics.com")); // Malformed URLs must be safely handled without throwing uncaught exceptions counter.Record("malformed-not-an-url"); counter.Record(""); counter.Record("javascript:void(0)"); // Act & Assert 2: PermissionGate handles null or whitespace gracefully var verdict1 = gate.Decide("Camera", ""); Assert.Equal(PermissionVerdict.Ask, verdict1); var verdict2 = gate.Decide("", "https://example.com"); Assert.Equal(PermissionVerdict.Ask, verdict2); gate.ResetOrigin(null!); var allOrigins = gate.GetAllOrigins(); Assert.NotNull(allOrigins); } }