Files
Wiki/test/Wiki.Core.Tests/ArticleBodyTests.cs
T

286 lines
11 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Wiki.Core.Parsing;
namespace Wiki.Core.Tests;
public class ArticleBodyTests
{
private const string WithPrivate = "A\n\n::: private\nx\n:::\n\nB";
[Fact]
public void RedactPrivate_RemovesBlock_KeepsLockedMarker()
{
var result = ArticleBody.RedactPrivate("A\n\n::: private\nsecret\n:::\n\nB");
Assert.Contains("A", result);
Assert.Contains("B", result);
Assert.Contains("::: private-locked", result);
Assert.DoesNotContain("secret", result);
Assert.Equal("A\n\n::: private-locked\n:::\n\nB", result);
}
[Fact]
public void RedactPrivate_UnterminatedBlock_DropsRest()
{
var result = ArticleBody.RedactPrivate("A\n\n::: private\nsecret\n\nmore");
Assert.DoesNotContain("secret", result);
Assert.DoesNotContain("more", result);
Assert.Contains("A", result);
}
[Fact]
public void RedactPrivate_FailsClosedOnContainerLineInsideCode()
{
var result = ArticleBody.RedactPrivate("A\n\n```\n::: private\nsecret\n:::\n```\n\nB");
Assert.StartsWith("A\n\n```\n::: private-locked\n:::", result);
Assert.DoesNotContain("secret", result);
}
[Fact]
public void RedactPrivate_FenceWithoutContainerLines_IsKept()
{
var body = "A\n\n```\ncode\n```\n\nB";
Assert.Equal(body, ArticleBody.RedactPrivate(body));
}
[Theory]
[InlineData("::: private\nx\n:::\u000B\nsecret")]
[InlineData("::: private\nx\n:::\u0085\nsecret")]
[InlineData("::: private\nx\n::: \nsecret")]
[InlineData("::: private\nx\n::: \nsecret")]
[InlineData("::: private\nx\n::: \nsecret")]
[InlineData("A\n\n::: private\nsecret one\n:::\u000B\nsecret two\n:::\n:::\n\nB")]
[InlineData("::: infobox \nsecret\n:::")]
[InlineData("::: private\nsecret\n:::")]
public void RedactPrivate_FailsClosedOnNonAsciiWhitespaceAndBom(string body)
{
Assert.DoesNotContain("secret", ArticleBody.RedactPrivate(body));
}
[Fact]
public void Skeleton_KeepsIndentation()
{
Assert.NotEqual(ArticleBody.Skeleton("::: private\nx\n :::"), ArticleBody.Skeleton("::: private\nx\n:::"));
}
[Theory]
[InlineData(":::\n```\n:::\n::: private\nsecret\n:::\n```")]
[InlineData("::: infobox\n```\n:::\n\n::: private\nsecret\n:::\n```")]
[InlineData("$$\n```\n$$\n\n::: private\nsecret\n:::\n```")]
public void RedactPrivate_FailsClosedOnAnonymousContainersAndFencedColons(string body)
{
Assert.DoesNotContain("secret", ArticleBody.RedactPrivate(body));
}
[Fact]
public void RedactPrivate_FailsClosedOnCloserInsideCodeInsidePrivate()
{
var result = ArticleBody.RedactPrivate("A\n\n::: private\n```\n:::\nsecret\n```\nmore\n:::\n\nB");
Assert.Equal("A\n\n::: private-locked\n:::", result);
}
[Theory]
[InlineData("A\n\n:::private\nsecret\n:::\n\nB")]
[InlineData("A\n\n::: Private\nsecret\n:::\n\nB")]
[InlineData("A\n\n::: unknown-kind\nsecret\n:::\n\nB")]
[InlineData("A\n\n::: infobox\n| k | v |\n::: private\nsecret\n:::\n:::\n\nB")]
[InlineData("A\n\n::: private\n::: infobox\nsecret\n:::\nsecret too\n:::\n\nB")]
public void RedactPrivate_FailsClosedOnVariantsAndNesting(string body)
{
var result = ArticleBody.RedactPrivate(body);
Assert.DoesNotContain("secret", result);
Assert.Contains("B", result);
}
[Theory]
[InlineData("A\n\n::: private\nsecret\n\n :::\nleaked\n:::")]
[InlineData("A\n\n::: private\nsecret\n\t:::\nleaked\n:::")]
[InlineData("A\n\n::: private\nsecret\n:::x\nleaked\n:::")]
[InlineData(":::: private\nsecret\n:::\nleaked\n::::")]
public void RedactPrivate_FailsClosedOnLooseClosers(string body)
{
var result = ArticleBody.RedactPrivate(body);
Assert.DoesNotContain("secret", result);
Assert.DoesNotContain("leaked", result);
}
[Fact]
public void RedactPrivate_IndentedCloserWithinThreeSpaces_Closes()
{
var result = ArticleBody.RedactPrivate("::: private\nsecret\n :::\n\nB");
Assert.DoesNotContain("secret", result);
Assert.EndsWith("\nB", result);
}
[Fact]
public void RedactPrivate_FailsClosedOnBacktickInfoWithBacktick()
{
var result = ArticleBody.RedactPrivate("```ls -la```\n\n::: private\nsecret\n:::");
Assert.DoesNotContain("secret", result);
Assert.StartsWith("```ls -la```", result);
}
[Theory]
[InlineData("- item\n ```\n code\n::: private\nsecret\n:::\n```")]
[InlineData("::: private\n- ```\n :::\n secret\n ```\n:::\n\nB")]
[InlineData("::: private\n ```\n```\n:::\nsecret\n```\n::: private\nsecret\n:::")]
public void RedactPrivate_FailsClosedOnFencesInListItems(string body)
{
Assert.DoesNotContain("secret", ArticleBody.RedactPrivate(body));
}
[Theory]
[InlineData("A\n\n- ::: private\n secret\n :::")]
[InlineData("A\n\n> ::: private\n> secret\n> :::")]
[InlineData("A\n\n1. ::: private\n secret\n :::")]
[InlineData("A\n\n2) ::: private\n secret\n :::")]
[InlineData("A\n\n * > ::: private\n > secret")]
[InlineData("A\n\n ::: infobox\nsecret\n:::")]
public void RedactPrivate_FailsClosedOnContainersInListsAndQuotes(string body)
{
var result = ArticleBody.RedactPrivate(body);
Assert.DoesNotContain("secret", result);
Assert.StartsWith("A\n", result);
}
[Theory]
[InlineData("```\ncode\n\n::: private\nsecret\n:::")]
[InlineData("A\n\n- x\n ```\n code\n\n ::: private\n secret")]
public void RedactPrivate_FailsClosedOnUnterminatedFence(string body)
{
var result = ArticleBody.RedactPrivate(body);
Assert.DoesNotContain("secret", result);
Assert.Contains(ArticleBody.PrivateLocked, result);
}
[Fact]
public void RedactPrivate_FailsClosedOnLoneCarriageReturns()
{
var result = ArticleBody.RedactPrivate("A\r::: private\rsecret\r:::");
Assert.Equal("A\n::: private-locked\n:::", result);
}
[Fact]
public void Reassemble_UnitWithMarkedContainerLine_Throws()
{
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(WithPrivate, ["A", "x\n- :::\nleak", "B"]));
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(WithPrivate, ["A", "x\n> :::\nleak", "B"]));
}
[Fact]
public void PublicBlocks_SkipsPrivateAndNumbersSequentially()
{
Assert.Equal([new BodyBlock(0, "A"), new BodyBlock(1, "B")], ArticleBody.PublicBlocks(WithPrivate));
}
[Fact]
public void PublicBlocks_InfoboxIsOneBlock()
{
var blocks = ArticleBody.PublicBlocks("Lead.\n::: infobox\n| Type | Switch |\n\n| Brand | Shelly |\n:::\n\nAfter.");
Assert.Equal(3, blocks.Count);
Assert.Equal("Lead.", blocks[0].Text);
Assert.Contains("| Type | Switch |", blocks[1].Text);
Assert.Contains("| Brand | Shelly |", blocks[1].Text);
Assert.Equal(new BodyBlock(2, "After."), blocks[2]);
}
[Fact]
public void PublicBlocks_CodeFenceIsPublicContent()
{
var blocks = ArticleBody.PublicBlocks("A\n\n```\nip 192.0.2.1\n\nmore\n```\n\nB");
Assert.Equal(3, blocks.Count);
Assert.Contains("192.0.2.1", blocks[1].Text);
Assert.Contains("more", blocks[1].Text);
}
[Fact]
public void Privatize_WrapsChosenBlocks()
{
var body = "A\n\nB";
var result = ArticleBody.Privatize(body, new HashSet<int> { 1 });
Assert.Equal("A\n\n::: private\nB\n:::", result);
Assert.Equal([new BodyBlock(0, "A")], ArticleBody.PublicBlocks(result));
}
[Fact]
public void Privatize_UnknownIndex_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => ArticleBody.Privatize("A", new HashSet<int> { 3 }));
}
[Fact]
public void TranslationUnits_MarksPrivateUnits()
{
Assert.Equal(
[new TranslationUnit(0, false, "A"), new TranslationUnit(1, true, "x"), new TranslationUnit(2, false, "B")],
ArticleBody.TranslationUnits(WithPrivate));
}
[Fact]
public void TranslationUnits_MermaidFenceIsOneUnit_CodeFenceIsNot()
{
var body = "Intro\n\n```mermaid\ngraph TD\n\nA[Schalter] --> B[Licht]\n```\n\n```yaml\nkey: value\n```\n\nEnd";
var units = ArticleBody.TranslationUnits(body);
Assert.Equal(
[
new TranslationUnit(0, false, "Intro"),
new TranslationUnit(1, false, "graph TD\n\nA[Schalter] --> B[Licht]"),
new TranslationUnit(2, false, "End"),
],
units);
}
[Fact]
public void TranslationUnits_UnterminatedPrivate_RestIsPrivate()
{
var closed = ArticleBody.TranslationUnits("A\n\n::: private\nx\n\ny\n:::\n\nz");
Assert.Equal([false, true, true, false], closed.Select(u => u.Private));
var open = ArticleBody.TranslationUnits("A\n\n::: private\nx\n\ny");
Assert.Equal(
[new TranslationUnit(0, false, "A"), new TranslationUnit(1, true, "x"), new TranslationUnit(2, true, "y")],
open);
}
[Fact]
public void Reassemble_KeepsSkeleton()
{
var body = "# Titel\nA [[heating]]\n\n::: infobox\n| Typ | Schalter |\n:::\n\n::: private\nx\n:::\n\n```mermaid\ngraph TD\nA-->B\n```\n\n```\nverbatim\n```\n\nB";
var units = ArticleBody.TranslationUnits(body);
var texts = units.Select(u => u.Text.ToUpperInvariant().Replace("[[HEATING]]", "[[heating]]")).ToList();
var result = ArticleBody.Reassemble(body, texts);
Assert.Equal(ArticleBody.Skeleton(body), ArticleBody.Skeleton(result));
Assert.Equal(["::: infobox", ":::", "::: private", ":::", "```mermaid", "```", "```", "```"], ArticleBody.Skeleton(body));
Assert.Contains("verbatim", result);
Assert.Contains("\nX\n", result);
Assert.Equal(ArticleBody.PublicBlocks(body).Count, ArticleBody.PublicBlocks(result).Count);
}
[Fact]
public void Reassemble_WrongCount_Throws()
{
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(WithPrivate, ["A", "x"]));
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(WithPrivate, ["A", "x", "B", "C"]));
}
[Fact]
public void Reassemble_UnitWithStructuralLine_Throws()
{
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(WithPrivate, ["A", "x\n:::\nleak", "B"]));
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(WithPrivate, ["A", "x\n```\nleak", "B"]));
}
[Fact]
public void Reassemble_BlankLineInUnit_Throws()
{
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(WithPrivate, ["A\n\nA2", "x", "B"]));
}
[Fact]
public void Reassemble_LinkSetChanged_Throws()
{
var body = "Siehe [[heating]].\n\nB";
Assert.Equal("See [[heating|Heating]].\n\nB2", ArticleBody.Reassemble(body, ["See [[heating|Heating]].", "B2"]));
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(body, ["See heating.", "B2"]));
Assert.Throws<FormatException>(() => ArticleBody.Reassemble(body, ["See [[heating]] [[heating]].", "B2"]));
}
}