namespace XFESystem.Browser.Tests; public sealed class BrowserServiceTests { [Fact] public void BuiltInStartPageActuallyExecutesJavaScriptWithoutANetworkAdapter() { var browser = new BrowserService(); var page = browser.Navigate("about:Start"); Assert.Equal("XFESystem Browser", page.Title); Assert.Contains("JavaScript ready: 12", page.Text, StringComparison.Ordinal); Assert.DoesNotContain("JavaScript pending", page.Text, StringComparison.Ordinal); Assert.DoesNotContain(" """); var browser = new BrowserService(transport); var page = browser.Navigate("https://example.test/Docs/Index"); Assert.Equal("After", page.Title); Assert.Contains("Hello 6", page.Text, StringComparison.Ordinal); Assert.Contains("New body", page.Text, StringComparison.Ordinal); Assert.Contains("https://example.test/Next?x=1&y=2", page.Text, StringComparison.Ordinal); Assert.Contains("DOM updated 42", page.Text, StringComparison.Ordinal); Assert.DoesNotContain("Old heading", page.Text, StringComparison.Ordinal); Assert.Single(transport.Requests); } [Fact] public void ExplicitDomPreservesIdentityAndSupportsCreatingAppendingAndRemovingElements() { var browser = new BrowserService(new TestTransport("DOM
")); browser.Navigate("http://example.test/"); var result = browser.ExecuteJavaScript("const p = document.createElement('p'); p.id = 'New'; p.textContent = 'Added'; document.getElementById('Target').appendChild(p); p === document.getElementById('New')"); Assert.Equal("true", result); Assert.Contains("Added", browser.RefreshDocument().Text, StringComparison.Ordinal); Assert.Equal("undefined", browser.ExecuteJavaScript("document.querySelector('#New').remove()")); Assert.DoesNotContain("Added", browser.RefreshDocument().Text, StringComparison.Ordinal); } [Fact] public void DomExposesNoReflectionAndUnsafeSchemesDoNotBecomeNumberedLinks() { var browser = new BrowserService(new TestTransport("

Content

BadFile")); var page = browser.Navigate("http://example.test/"); Assert.DoesNotContain("Links:", page.Text, StringComparison.Ordinal); Assert.Equal("undefined", browser.ExecuteJavaScript("typeof document.getElementById('Value').GetType")); Assert.Equal("null", browser.ExecuteJavaScript("document.getElementById('Missing')")); } [Fact] public void NewNavigationUsesAFreshEngineAndCannotReadPreviousPageVariables() { var browser = new BrowserService(new TestTransport("

Page

")); browser.Navigate("http://first.test/"); Assert.Equal("42", browser.ExecuteJavaScript("var secret = 42; secret")); browser.Navigate("http://second.test/"); Assert.Equal("undefined", browser.ExecuteJavaScript("typeof secret")); } [Fact] public void LocalJavaScriptPreviewHasNoPageDomOrVariablesAndDoesNotMutateThePage() { var browser = new BrowserService(new TestTransport("

Page content

")); browser.Navigate("http://example.test/"); browser.ExecuteJavaScript("var pageSecret = 42"); Assert.Equal("undefined / undefined", browser.PreviewJavaScript("typeof document + ' / ' + typeof pageSecret")); Assert.Equal("42", browser.PreviewJavaScript("var local = 42; local")); Assert.Equal("undefined", browser.PreviewJavaScript("typeof local")); Assert.Contains("Page content", browser.RefreshDocument().Text, StringComparison.Ordinal); Assert.Equal("undefined", browser.ExecuteJavaScript("typeof local")); } [Fact] public void MetaCspBlocksAutomaticScriptsRatherThanIgnoringThePagePolicy() { var browser = new BrowserService(new TestTransport("

Original

")); var page = browser.Navigate("http://example.test/"); Assert.Contains("Original", page.Text, StringComparison.Ordinal); Assert.DoesNotContain("Forbidden", page.Text, StringComparison.Ordinal); Assert.Contains("Automatic scripts blocked", page.Text, StringComparison.Ordinal); Assert.True(browser.Capabilities.BlockScriptsWhenCspIsDeclared); Assert.False(browser.Capabilities.FullContentSecurityPolicy); } [Fact] public void NonJavaScriptExternalMimeAndModuleScriptsAreReportedWithoutExecutingThem() { var transport = new TestTransport("

Original

"); var page = new BrowserService(transport).Navigate("http://example.test/"); Assert.Contains("Inline ran", page.Text, StringComparison.Ordinal); Assert.Contains("unsupported MIME type: text/html", page.Text, StringComparison.Ordinal); Assert.Contains("Module scripts are not supported", page.Text, StringComparison.Ordinal); Assert.Equal(2, transport.Requests.Count); } [Fact] public void MissingTransportAndUnverifiedHttpsFailClearlyWithoutHostDiskFallback() { Assert.Contains("no HTTP/TLS transport", new BrowserService().Navigate("https://example.test/").Text, StringComparison.Ordinal); var insecure = new TestTransport("

Unverified

", validated: false); var page = new BrowserService(insecure).Navigate("https://example.test/"); Assert.Equal("Navigation failed", page.Title); Assert.Contains("certificate validation", page.Text, StringComparison.Ordinal); Assert.DoesNotContain("Unverified", page.Text, StringComparison.Ordinal); } [Theory] [InlineData("file:///C:/Secret.txt")] [InlineData("javascript:alert(1)")] [InlineData("ftp://example.test/")] [InlineData("https://User:Password@example.test/")] [InlineData("https://example.test/\u001b[31m")] public void NavigationRejectsUnsupportedOrCredentialBearingUrlsBeforeTransport(string address) { var transport = new TestTransport("

Unexpected

"); Assert.Equal("Navigation failed", new BrowserService(transport).Navigate(address).Title); Assert.Empty(transport.Requests); } [Fact] public void HttpsCannotDowngradeThroughAnInjectedTransport() { var transport = new TestTransport("

Downgraded

", finalAddress: new Uri("http://example.test/")); var page = new BrowserService(transport).Navigate("https://example.test/"); Assert.Equal("Navigation failed", page.Title); Assert.Contains("downgrade", page.Text, StringComparison.Ordinal); } [Fact] public void PlainTextDoesNotAccidentallyExecuteHtmlOrScripts() { var page = new BrowserService(new TestTransport("", mediaType: "text/plain")) .Navigate("http://example.test/Readme"); Assert.Equal("", page.Text); } [Fact] public void MarkdownPreviewUsesWebsiteRendererWithTablesFencesAndSanitization() { var preview = new BrowserService().PreviewMarkdown("# Title\n\n| A | B |\n|---|---|\n| One | Two |\n\n```js\nconsole.log(42);\n```\n"); Assert.Contains("Title", preview, StringComparison.Ordinal); Assert.Contains("One", preview, StringComparison.Ordinal); Assert.Contains("console.log(42);", preview, StringComparison.Ordinal); Assert.DoesNotContain("alert('Bad')", preview, StringComparison.Ordinal); Assert.DoesNotContain("X

", 100))), new BrowserLimits { MaximumDomNodes = 20 }); Assert.Equal("Navigation failed", nodes.Navigate("http://example.test/").Title); var scripts = new BrowserService(new TestTransport(""), new BrowserLimits { MaximumScripts = 1 }); Assert.Equal("Navigation failed", scripts.Navigate("http://example.test/").Title); var output = new BrowserService(new TestTransport(new string('X', 100)), new BrowserLimits { MaximumOutputCharacters = 40 }); Assert.Equal("Navigation failed", output.Navigate("http://example.test/").Title); } [Fact] public void AnInfiniteInlineScriptReturnsADiagnosticAndStopsFollowingScripts() { var browser = new BrowserService(new TestTransport("

Before

"), new BrowserLimits { MaximumStatements = 100 }); var page = browser.Navigate("http://example.test/"); Assert.Contains("Before", page.Text, StringComparison.Ordinal); Assert.Contains("JavaScript error:", page.Text, StringComparison.Ordinal); Assert.DoesNotContain("After", page.Text, StringComparison.Ordinal); } [Fact] public void DomMutationsCannotExceedNodeLimitsAndRollbackOnFailure() { var browser = new BrowserService(new TestTransport("
Before
"), new BrowserLimits { MaximumDomNodes = 12 }); browser.Navigate("http://example.test/"); var result = browser.ExecuteJavaScript("document.getElementById('Target').innerHTML = '

X

'.repeat(100)"); Assert.Contains("JavaScript error:", result, StringComparison.Ordinal); Assert.Contains("Before", browser.RefreshDocument().Text, StringComparison.Ordinal); } [Fact] public void AttributeAndDetachedElementTextCannotBypassTheCombinedDomBudget() { var browser = new BrowserService(new TestTransport("
Before
"), new BrowserLimits { MaximumHtmlCharacters = 300 }); browser.Navigate("http://example.test/"); var attribute = browser.ExecuteJavaScript("const target = document.getElementById('Target'); target.setAttribute('First', 'A'.repeat(180)); target.setAttribute('Second', 'B'.repeat(180));"); Assert.Contains("JavaScript error:", attribute, StringComparison.Ordinal); Assert.Equal("null", browser.ExecuteJavaScript("target.getAttribute('Second')")); var detached = browser.ExecuteJavaScript("const detached = document.createElement('p'); detached.textContent = 'C'.repeat(200)"); Assert.Contains("JavaScript error:", detached, StringComparison.Ordinal); Assert.Equal("", browser.ExecuteJavaScript("detached.textContent")); Assert.Contains("Before", browser.RefreshDocument().Text, StringComparison.Ordinal); } private sealed class TestTransport(string html, bool validated = true, string mediaType = "text/html", Uri? finalAddress = null) : IWebTransport { public List Requests { get; } = []; public WebResponse Get(Uri address, BrowserLimits limits) { Requests.Add(address); return new(finalAddress ?? address, 200, mediaType, html, validated); } } }