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("<script>", page.Text, StringComparison.Ordinal);
Assert.True(browser.Capabilities.CssLayout);
Assert.False(browser.Capabilities.NativeNetworkTransport);
Assert.False(browser.Capabilities.ProcessIsolation);
}
[Fact]
public void InlineScriptChangesRealHtmlDomTitleTextAndLinks()
{
var transport = new TestTransport("""
<title>Before</title><h1 id="Heading">Old heading</h1><div id="Body">Old</div>
<a href="../Next?x=1&y=2">Next page</a>
<script>
document.title = 'After';
document.getElementById('Heading').textContent = 'Hello ' + [1,2,3].reduce((a,b) => a+b, 0);
document.querySelector('#Body').innerHTML = '<p>New <strong>body</strong></p>';
console.log('DOM updated', 42);
</script>
""");
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("<title>DOM</title><div id='Target'></div>"));
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("<p id='Value'>Content</p><a href='javascript:alert(1)'>Bad</a><a href='file:///C:/Secret'>File</a>"));
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("<p>Page</p>"));
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("<p id='Secret'>Page content</p>"));
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("<meta http-equiv='Content-Security-Policy' content=\"script-src 'none'\"><p id='Value'>Original</p><script>document.getElementById('Value').textContent='Forbidden'</script>"));
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("<p id='Result'>Original</p><script src='Other.js'></script><script type='module'>document.title='Wrong'</script><script type='application/json'>{\"Foo\":1}</script><script>document.getElementById('Result').textContent='Inline ran'</script>");
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("<p>Unverified</p>", 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("<p>Unexpected</p>");
Assert.Equal("Navigation failed", new BrowserService(transport).Navigate(address).Title);
Assert.Empty(transport.Requests);
}
[Fact]
public void HttpsCannotDowngradeThroughAnInjectedTransport()
{
var transport = new TestTransport("<p>Downgraded</p>", 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("<script>while(true){}</script>", mediaType: "text/plain"))
.Navigate("http://example.test/Readme");
Assert.Equal("<script>while(true){}</script>", 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<script>alert('Bad')</script>");
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("<script", preview, StringComparison.Ordinal);
}
[Fact]
public void OversizedHtmlDomScriptsAndPageOutputAreRejected()
{
var html = new BrowserService(new TestTransport(new string('X', 1_001)), new BrowserLimits { MaximumHtmlCharacters = 1_000 });
Assert.Equal("Navigation failed", html.Navigate("http://example.test/").Title);
var nodes = new BrowserService(new TestTransport(string.Concat(Enumerable.Repeat("<p>X</p>", 100))), new BrowserLimits { MaximumDomNodes = 20 });
Assert.Equal("Navigation failed", nodes.Navigate("http://example.test/").Title);
var scripts = new BrowserService(new TestTransport("<script>1</script><script>2</script>"), 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("<p id='Value'>Before</p><script>while(true){}</script><script>document.getElementById('Value').textContent='After'</script>"),
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("<div id='Target'>Before</div>"), new BrowserLimits { MaximumDomNodes = 12 });
browser.Navigate("http://example.test/");
var result = browser.ExecuteJavaScript("document.getElementById('Target').innerHTML = '<p>X</p>'.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("<div id='Target'>Before</div>"), 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<Uri> Requests { get; } = [];
public WebResponse Get(Uri address, BrowserLimits limits)
{
Requests.Add(address);
return new(finalAddress ?? address, 200, mediaType, html, validated);
}
}
}
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("<script>", page.Text, StringComparison.Ordinal);
Assert.True(browser.Capabilities.CssLayout);
Assert.False(browser.Capabilities.NativeNetworkTransport);
Assert.False(browser.Capabilities.ProcessIsolation);
}
[Fact]
public void InlineScriptChangesRealHtmlDomTitleTextAndLinks()
{
var transport = new TestTransport("""
<title>Before</title><h1 id="Heading">Old heading</h1><div id="Body">Old</div>
<a href="../Next?x=1&y=2">Next page</a>
<script>
document.title = 'After';
document.getElementById('Heading').textContent = 'Hello ' + [1,2,3].reduce((a,b) => a+b, 0);
document.querySelector('#Body').innerHTML = '<p>New <strong>body</strong></p>';
console.log('DOM updated', 42);
</script>
""");
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("<title>DOM</title><div id='Target'></div>"));
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("<p id='Value'>Content</p><a href='javascript:alert(1)'>Bad</a><a href='file:///C:/Secret'>File</a>"));
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("<p>Page</p>"));
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("<p id='Secret'>Page content</p>"));
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("<meta http-equiv='Content-Security-Policy' content=\"script-src 'none'\"><p id='Value'>Original</p><script>document.getElementById('Value').textContent='Forbidden'</script>"));
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("<p id='Result'>Original</p><script src='Other.js'></script><script type='module'>document.title='Wrong'</script><script type='application/json'>{\"Foo\":1}</script><script>document.getElementById('Result').textContent='Inline ran'</script>");
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("<p>Unverified</p>", 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("<p>Unexpected</p>");
Assert.Equal("Navigation failed", new BrowserService(transport).Navigate(address).Title);
Assert.Empty(transport.Requests);
}
[Fact]
public void HttpsCannotDowngradeThroughAnInjectedTransport()
{
var transport = new TestTransport("<p>Downgraded</p>", 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("<script>while(true){}</script>", mediaType: "text/plain"))
.Navigate("http://example.test/Readme");
Assert.Equal("<script>while(true){}</script>", 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<script>alert('Bad')</script>");
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("<script", preview, StringComparison.Ordinal);
}
[Fact]
public void OversizedHtmlDomScriptsAndPageOutputAreRejected()
{
var html = new BrowserService(new TestTransport(new string('X', 1_001)), new BrowserLimits { MaximumHtmlCharacters = 1_000 });
Assert.Equal("Navigation failed", html.Navigate("http://example.test/").Title);
var nodes = new BrowserService(new TestTransport(string.Concat(Enumerable.Repeat("<p>X</p>", 100))), new BrowserLimits { MaximumDomNodes = 20 });
Assert.Equal("Navigation failed", nodes.Navigate("http://example.test/").Title);
var scripts = new BrowserService(new TestTransport("<script>1</script><script>2</script>"), 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("<p id='Value'>Before</p><script>while(true){}</script><script>document.getElementById('Value').textContent='After'</script>"),
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("<div id='Target'>Before</div>"), new BrowserLimits { MaximumDomNodes = 12 });
browser.Navigate("http://example.test/");
var result = browser.ExecuteJavaScript("document.getElementById('Target').innerHTML = '<p>X</p>'.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("<div id='Target'>Before</div>"), 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<Uri> Requests { get; } = [];
public WebResponse Get(Uri address, BrowserLimits limits)
{
Requests.Add(address);
return new(finalAddress ?? address, 200, mediaType, html, validated);
}
}
}