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<script>alert(1)</script>", "<Title>");
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);
}
}
}
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<script>alert(1)</script>", "<Title>");
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);
}
}
}