using Wiki.Core.Model; using Wiki.Core.Parsing; namespace Wiki.Core.Tests; public class ArticleFileTests { private static readonly Article Sample = new( "de", "wall-switches", "Wandschalter: Konvention", ArticleCategory.System, ArticleVisibility.Public, [new ArticleSource("schalter-konvention-blueprint-system", "abc123"), new ArticleSource("heizung", "def456")], new DateOnly(2026, 10, 24), "claude-opus-5-5", "Lead [[heizung]].\n\n::: private\nx\n:::\n"); private static Document Load(string text, string slug = "de/wall-switches") => DocumentParser.Parse(new DocumentRef(DocumentKind.Article, slug, DocumentParser.PathFor(DocumentKind.Article, slug), "h"), text); [Fact] public void ArticleFile_RoundTrip() { var text = ArticleFile.Build(Sample); Assert.Contains("visibility: public\n", text); Assert.Contains("category: system\n", text); Assert.Contains(" - schalter-konvention-blueprint-system@abc123\n", text); Assert.Contains(" - heizung@def456\n", text); Assert.Equal(Sample, ArticleFile.Parse(Load(text))); } [Theory] [InlineData("A: \"B\"")] [InlineData("Pfad C:\\x: \"y\"")] [InlineData("[x]")] [InlineData("{x}")] [InlineData("a\\\\b: c")] [InlineData("\"quoted\"")] public void ArticleFile_RoundTrip_AmbiguousTitles(string title) { var article = Sample with { Title = title, Model = title }; Assert.Equal(article, ArticleFile.Parse(Load(ArticleFile.Build(article)))); } [Theory] [InlineData("title: \"say \\\"hi\\\" \\\\ C:\\x\"", "say \"hi\" \\ C:\\x")] [InlineData("title: 'it \\\"s'", "it \\\"s")] [InlineData("title: plain \\\" text", "plain \\\" text")] public void Frontmatter_DoubleQuotedEscapes_AreUnescaped(string line, string expected) { var (fields, _, _) = Frontmatter.Parse($"---\n{line}\n---\n"); Assert.Equal(expected, fields["title"].Scalar); } [Theory] [InlineData("visibility: public\n", "")] [InlineData("visibility: public\n", "visibility: secret\n")] [InlineData("visibility: public\n", "visibility:\n")] public void Parse_MissingOrInvalidVisibility_IsPrivate(string from, string to) { var text = ArticleFile.Build(Sample).Replace(from, to); Assert.Equal(ArticleVisibility.Private, ArticleFile.Parse(Load(text)).Visibility); } [Theory] [InlineData("subject: wall-switches\n", "subject: other\n")] [InlineData("subject: wall-switches\n", "")] [InlineData("lang: de\n", "lang: en\n")] [InlineData("category: system\n", "category: 1\n")] [InlineData("category: system\n", "")] [InlineData("generated: 2026-10-24\n", "generated: soon\n")] [InlineData("model: claude-opus-5-5\n", "")] [InlineData(" - heizung@def456\n", " - heizung\n")] public void Parse_BadRequiredKey_Throws(string from, string to) { var text = ArticleFile.Build(Sample).Replace(from, to); Assert.Throws(() => ArticleFile.Parse(Load(text))); } [Fact] public void Parse_UnsupportedLang_Throws() { var text = ArticleFile.Build(Sample).Replace("lang: de\n", "lang: fr\n"); Assert.Throws(() => ArticleFile.Parse(Load(text, "fr/wall-switches"))); } [Fact] public void Parse_NonArticleDocument_Throws() { var doc = DocumentParser.Parse(new DocumentRef(DocumentKind.Wiki, "x", "wiki/x.md", "h"), ArticleFile.Build(Sample)); Assert.Throws(() => ArticleFile.Parse(doc)); } [Fact] public void Build_NewlineInScalar_Throws() { Assert.Throws(() => ArticleFile.Build(Sample with { Title = "T\nvisibility: public" })); Assert.Throws(() => ArticleFile.Build(Sample with { Slug = "../x" })); } }