using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using Xunit; namespace XFESystem.Documents.Tests; public sealed class DocumentPreviewTests { [Fact] public void MarkdownPreviewUsesRealBundledWebsiteAssetsAndRestrictsNetwork() { var preview = new DocumentPreviewBuilder(new()).Markdown("# Document\n", ""); Assert.Contains("<Title>", preview.Html); Assert.DoesNotContain("alert(1)", preview.Html); Assert.Contains("data-manual-doc-init=\"true\"", preview.Html); Assert.Contains("connect-src 'none'", preview.ContentSecurityPolicy); Assert.Contains("base-uri 'none'", preview.ContentSecurityPolicy); foreach (var name in WebsiteAssets.StyleNames.Concat(WebsiteAssets.ScriptNames)) { Assert.Contains(WebsiteAssets.BaseUri + name, preview.Html); Assert.NotEmpty(WebsiteAssets.ReadText(name)); } } [Theory] [InlineData("Test.js", "javascript")] [InlineData("Test.CS", "csharp")] [InlineData("Test.xaml", "xaml")] [InlineData("Test.md", "markdown")] [InlineData("Test.unknown", "plaintext")] public void CodePreviewsEscapeHtmlAndUseWebsiteLanguageMapping(string path, string language) { var preview = new DocumentPreviewBuilder(new()).Code("</code><script>danger()</script>", path); Assert.Contains("language-" + language, preview.Html); Assert.Contains("</code><script>danger()</script>", preview.Html); Assert.DoesNotContain("<script>danger()", preview.Html); } [Fact] public void JavaScriptPreviewHasExplicitIsolationRequirementsAndDoesNotInterpolateExecutableHtml() { const string source = "console.log('中文'); const text = '</script><img src=x onerror=bad()>';"; var plan = DocumentPreviewBuilder.JavaScript(source); Assert.True(plan.RequiresSeparateProcess); Assert.False(plan.AllowNetwork); Assert.False(plan.AllowHostObjects); Assert.Equal("allow-scripts", plan.IframeSandbox); Assert.DoesNotContain("allow-same-origin", plan.IframeSandbox); Assert.DoesNotContain("<img", plan.Html); Assert.Contains("connect-src 'none'", plan.Html); var encoded = Regex.Match(plan.Html, "data:text/javascript;base64,([^\"]+)").Groups[1].Value; Assert.Equal(source, Encoding.UTF8.GetString(Convert.FromBase64String(encoded))); } [Fact] public void JavaScriptPreviewRejectsUnboundedBudgets() { Assert.Throws<ArgumentOutOfRangeException>(() => DocumentPreviewBuilder.JavaScript("", TimeSpan.Zero)); Assert.Throws<ArgumentOutOfRangeException>(() => DocumentPreviewBuilder.JavaScript("", TimeSpan.FromMinutes(1))); Assert.Throws<ArgumentOutOfRangeException>(() => DocumentPreviewBuilder.JavaScript("", memoryLimitBytes: long.MaxValue)); Assert.Throws<ArgumentException>(() => DocumentPreviewBuilder.JavaScript(new string('x', 1024 * 1024 + 1))); } [Fact] public void BundledAssetsMatchTheWebsiteProvenanceManifest() { using var manifest = JsonDocument.Parse(WebsiteAssets.ReadText("SourceManifest.json")); Assert.Equal("95b43e16776aa648bca19d0060d01d3d2a52196e", manifest.RootElement.GetProperty("SourceCommit").GetString()); foreach (var entry in manifest.RootElement.GetProperty("Entries").EnumerateArray()) { var path = entry.GetProperty("SnapshotPath").GetString()!; if (path.StartsWith("../", StringComparison.Ordinal)) continue; using var asset = WebsiteAssets.Open(path); Assert.Equal(entry.GetProperty("SnapshotSha256").GetString(), Convert.ToHexStringLower(SHA256.HashData(asset))); } } [Fact] public void ResourcePathsCannotEscapeTheEmbeddedBundle() { Assert.Throws<ArgumentException>(() => WebsiteAssets.Open("../Secret")); Assert.Throws<ArgumentException>(() => WebsiteAssets.Open("Fonts\\Secret")); Assert.Throws<FileNotFoundException>(() => WebsiteAssets.Open("Missing.txt")); } [Fact] public void MathFontStylesResolveEveryBundledFontWithoutNetworkFallback() { foreach (Match match in Regex.Matches(WebsiteAssets.ReadText("Katex.min.css"), @"url\(([^)]+)\)")) { var fontName = match.Groups[1].Value; Assert.StartsWith("Fonts/", fontName); using var stream = WebsiteAssets.Open(fontName); Assert.True(stream.Length > 0); } } }