XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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&amp;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.False(browser.Capabilities.BlockScriptsWhenCspIsDeclared);
        Assert.True(browser.Capabilities.NonceStrictDynamicCspProfile);
        Assert.False(browser.Capabilities.FullContentSecurityPolicy);
    }

    [Fact]
    public void NosniffNonJavaScriptExternalMimeAndModuleScriptsAreReportedWithoutExecutingThem()
    {
        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>", noSniff: true);
        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, bool noSniff = false) : IWebTransport
    {
        public List<Uri> Requests { get; } = [];
        public WebResponse Get(Uri address, BrowserLimits limits)
        {
            Requests.Add(address);
            return new(finalAddress ?? address, 200, mediaType, html, validated, NoSniff: noSniff);
        }
    }
}