From 4a7c8b31c5d8602f4d86e2fa73a0ede7c9d544cc Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:47:34 -0500 Subject: [PATCH] tests: add test that validates the metadata of all posts --- test/Blog.Tests/Unit/PostMetadataTests.cs | 95 +++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 test/Blog.Tests/Unit/PostMetadataTests.cs diff --git a/test/Blog.Tests/Unit/PostMetadataTests.cs b/test/Blog.Tests/Unit/PostMetadataTests.cs new file mode 100644 index 0000000..a449215 --- /dev/null +++ b/test/Blog.Tests/Unit/PostMetadataTests.cs @@ -0,0 +1,95 @@ +using System.Text.Json; + +using Markdig; +using Markdig.Syntax; + +namespace Blog.Tests.Unit; + +[TestFixture] +public class PostMetadataTests +{ + private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); + + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + }; + + private const string MetaFence = "meta"; + + + [Test] + public void PostsDirectory_ShouldContainIndexFiles() + { + var postsDir = GetPostsDirectory(); + var files = Directory.GetFiles(postsDir, "index.md", SearchOption.AllDirectories); + + files.Should().NotBeEmpty("The wwwroot/posts directory should contain blog posts with index.md files."); + } + + [TestCaseSource(nameof(GetPostIndexFiles))] + public async Task IndexMd_Metadata_CanBeProperlyParsedAndRead(string filePath) + { + var markdown = await File.ReadAllTextAsync(filePath); + var document = Markdown.Parse(markdown, MarkdownPipeline); + + var metadataBlock = document + .OfType() + .FirstOrDefault(block => + block.Arguments is not null && + block.Arguments.Contains(MetaFence) + ); + + metadataBlock.Should().NotBeNull($"file '{filePath}' should contain a fenced code block with '{MetaFence}' argument."); + + var jsonContent = metadataBlock.Lines.ToString(); + + jsonContent.Should().NotBeNullOrWhiteSpace($"metadata code block in '{filePath}' should not be empty."); + + Post? post = null; + + Action deserializeAction = () => post = JsonSerializer.Deserialize(jsonContent, JsonSerializerOptions); + + deserializeAction.Should().NotThrow($"JSON metadata in '{filePath}' should be valid JSON matching the Post record schema."); + + post.Should().NotBeNull($"deserialized Post from '{filePath}' should not be null."); + post.Title.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have a title."); + post.Lead.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have a lead summary."); + post.PublishedAt.Should().NotBe(default, $"post in '{filePath}' must have a valid published date."); + post.OpenGraphImage.Should().NotBeNullOrWhiteSpace($"post in '{filePath}' must have an openGraphImage path."); + } + + private static string GetPostsDirectory() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + + while (current is not null) + { + var candidate = Path.Combine(current.FullName, "src", "Blog", "wwwroot", "posts"); + + if (Directory.Exists(candidate)) + { + return candidate; + } + + current = current.Parent; + } + + throw new DirectoryNotFoundException($"Could not locate 'src/Blog/wwwroot/posts' starting from '{AppContext.BaseDirectory}'."); + } + + private static IEnumerable GetPostIndexFiles() + { + var postsDir = GetPostsDirectory(); + var files = Directory.GetFiles(postsDir, "index.md", SearchOption.AllDirectories); + + foreach (var file in files) + { + var relativePath = Path.GetRelativePath(postsDir, file); + yield return new TestCaseData(file).SetName($"PostMetadata_{relativePath.Replace('\\', '/')}"); + } + } +} \ No newline at end of file