286 lines
11 KiB
C#
286 lines
11 KiB
C#
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"]));
|
||
}
|
||
}
|