From 95d488887642c9cb103165143ed96ffab07c4c11 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:31:56 -0500 Subject: [PATCH 1/2] feat: implement rss feed --- .vscode/settings.json | 1 + src/Blog/Blog.csproj | 1 + src/Blog/Program.cs | 69 +++++++++++++++++++++++++++- test/Blog.Tests/EndToEnd/RssTests.cs | 37 +++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 test/Blog.Tests/EndToEnd/RssTests.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index 035fb60..8e6eda8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "Antiforgery", "blazor", + "Entitize", "Hsts", "Markdig", "msedge" diff --git a/src/Blog/Blog.csproj b/src/Blog/Blog.csproj index 49fbe70..3d300cd 100644 --- a/src/Blog/Blog.csproj +++ b/src/Blog/Blog.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Blog/Program.cs b/src/Blog/Program.cs index 0ee5596..79f17f8 100644 --- a/src/Blog/Program.cs +++ b/src/Blog/Program.cs @@ -1,3 +1,8 @@ +using System.ServiceModel.Syndication; +using System.Text; +using System.Xml; +using System.Xml.Linq; + var builder = WebApplication.CreateBuilder(args); builder.Services.ConfigureOptions(); @@ -21,11 +26,73 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAntiforgery(); +app.UseStatusCodePagesWithRedirects("/Error/{0}"); + +app + .MapGet("/rss", async (HttpContext context, IPostService postService) => + { + var req = context.Request; + var url = $"{req.Scheme}://{req.Host}{req.PathBase}"; + + var posts = await postService.GetPostsAsync(); + + var items = posts.Select(post => + { + var uri = new Uri($"{url}/{post.Slug}"); + + var item = new SyndicationItem( + post.Title, + post.Lead, + uri, + post.Slug, + post.PublishedAt + ); + + return item; + }); + + var feed = new SyndicationFeed( + "journal", + "A blog by Stevan Freeborn", + new Uri(url) + ) + { + Items = items, + }; + + XNamespace atom = "http://www.w3.org/2005/Atom"; + feed.ElementExtensions.Add( + new XElement(atom + "link", + new XAttribute("href", url + "/rss"), + new XAttribute("rel", "self"), + new XAttribute("type", "application/rss+xml") + ) + ); + + var settings = new XmlWriterSettings + { + Encoding = Encoding.UTF8, + NewLineHandling = NewLineHandling.Entitize, + Indent = true, + Async = true, + }; + + using var stream = new MemoryStream(); + using var writer = XmlWriter.Create(stream, settings); + + var rssFormatter = new Rss20FeedFormatter(feed, false); + rssFormatter.WriteTo(writer); + await writer.FlushAsync(); + + return Results.File(stream.ToArray(), "application/xml"); + }) + .WithDisplayName("RSS Feed") + .WithDescription("RSS feed for the blog"); + app .MapRazorComponents() .AddInteractiveServerRenderMode(); -app.UseStatusCodePagesWithRedirects("/Error/{0}"); app.Run(); diff --git a/test/Blog.Tests/EndToEnd/RssTests.cs b/test/Blog.Tests/EndToEnd/RssTests.cs new file mode 100644 index 0000000..8493f4d --- /dev/null +++ b/test/Blog.Tests/EndToEnd/RssTests.cs @@ -0,0 +1,37 @@ +using System.ServiceModel.Syndication; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace Blog.Tests.EndToEnd; + +[TestFixture] +public class RssTests : BlogTest +{ + [Test] + public async Task Task_Rss_WhenFetched_ItShouldReturnRssFeed() + { + var response = await Page.APIRequest.GetAsync("/rss"); + response.Status.Should().Be((int)HttpStatusCode.OK); + + var xml = await response.BodyAsync(); + + var streamReader = new StreamReader( + new MemoryStream(xml), + Encoding.UTF8 + ); + + + var settings = new XmlReaderSettings + { + Async = true + }; + + using var xmlReader = XmlReader.Create(streamReader, settings); + var feed = SyndicationFeed.Load(xmlReader); + + feed.Title.Text.Should().Be("journal"); + feed.Description.Text.Should().Be("A blog by Stevan Freeborn"); + feed.Items.Should().HaveCount(2); + } +} \ No newline at end of file From ba65401c8810cbc1e0e65dcac0cef84a12cbcf39 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:33:56 -0500 Subject: [PATCH 2/2] chore: cleanup usings and run dotnet format --- src/Blog/GlobalUsings.cs | 4 ++++ src/Blog/Posts/FilePostService.cs | 14 +++++++------- src/Blog/Program.cs | 9 ++------- test/Blog.Tests/EndToEnd/BlogTest.cs | 2 +- test/Blog.Tests/EndToEnd/CompositeHost.cs | 6 +++--- test/Blog.Tests/EndToEnd/PostTests.cs | 2 +- test/Blog.Tests/EndToEnd/RssTests.cs | 9 ++------- test/Blog.Tests/GlobalUsings.cs | 5 ++++- test/Blog.Tests/Unit/FilePostServiceTests.cs | 6 +++--- 9 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/Blog/GlobalUsings.cs b/src/Blog/GlobalUsings.cs index ed008cf..c884ce6 100644 --- a/src/Blog/GlobalUsings.cs +++ b/src/Blog/GlobalUsings.cs @@ -1,5 +1,9 @@ global using System.IO.Abstractions; +global using System.ServiceModel.Syndication; +global using System.Text; global using System.Text.Json; +global using System.Xml; +global using System.Xml.Linq; global using Blog.Components; global using Blog.Posts; diff --git a/src/Blog/Posts/FilePostService.cs b/src/Blog/Posts/FilePostService.cs index 449618e..e811a60 100644 --- a/src/Blog/Posts/FilePostService.cs +++ b/src/Blog/Posts/FilePostService.cs @@ -16,7 +16,7 @@ class FilePostService( .UseAdvancedExtensions() .UsePrism() .Build(); - + private const string MetaFence = "meta"; private const string IndexFile = "index.md"; @@ -68,12 +68,12 @@ class FilePostService( try { var indexFilePath = _fileSystem.Path.Combine(subDirectory, IndexFile); - + if (_fileSystem.File.Exists(indexFilePath) is false) { continue; } - + var postText = await _fileSystem.File.ReadAllTextAsync(indexFilePath); var (metaContent, document) = ParsePost(postText); @@ -108,7 +108,7 @@ class FilePostService( public async Task GetPostAsync(string slug) { - try + try { var postPath = _fileSystem.Path.Combine(_options.PostsDirectory, slug, IndexFile); @@ -133,10 +133,10 @@ class FilePostService( return null; } - var postWithContent = post with - { + var postWithContent = post with + { Slug = slug, - Content = document.ToHtml(MarkdownPipeline) + Content = document.ToHtml(MarkdownPipeline) }; return postWithContent; diff --git a/src/Blog/Program.cs b/src/Blog/Program.cs index 79f17f8..15a8d8a 100644 --- a/src/Blog/Program.cs +++ b/src/Blog/Program.cs @@ -1,8 +1,3 @@ -using System.ServiceModel.Syndication; -using System.Text; -using System.Xml; -using System.Xml.Linq; - var builder = WebApplication.CreateBuilder(args); builder.Services.ConfigureOptions(); @@ -29,11 +24,11 @@ app.UseAntiforgery(); app.UseStatusCodePagesWithRedirects("/Error/{0}"); app - .MapGet("/rss", async (HttpContext context, IPostService postService) => + .MapGet("/rss", async (HttpContext context, IPostService postService) => { var req = context.Request; var url = $"{req.Scheme}://{req.Host}{req.PathBase}"; - + var posts = await postService.GetPostsAsync(); var items = posts.Select(post => diff --git a/test/Blog.Tests/EndToEnd/BlogTest.cs b/test/Blog.Tests/EndToEnd/BlogTest.cs index 8a4fb0b..1b7ef4a 100644 --- a/test/Blog.Tests/EndToEnd/BlogTest.cs +++ b/test/Blog.Tests/EndToEnd/BlogTest.cs @@ -32,7 +32,7 @@ public class BlogTest : PageTest { await Context.Tracing.StopAsync(new() { - Path = Path.Combine( + Path = Path.Combine( TestContext.CurrentContext.WorkDirectory, "playwright-traces", $"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip" diff --git a/test/Blog.Tests/EndToEnd/CompositeHost.cs b/test/Blog.Tests/EndToEnd/CompositeHost.cs index 5402072..7989fb7 100644 --- a/test/Blog.Tests/EndToEnd/CompositeHost.cs +++ b/test/Blog.Tests/EndToEnd/CompositeHost.cs @@ -6,20 +6,20 @@ public class CompositeHost(IHost testHost, IHost kestrelHost) : IHost private readonly IHost _kestrelHost = kestrelHost; public IServiceProvider Services => _testHost.Services; - + public void Dispose() { _testHost.Dispose(); _kestrelHost.Dispose(); GC.SuppressFinalize(this); } - + public async Task StartAsync(CancellationToken cancellationToken = default) { await _testHost.StartAsync(cancellationToken); await _kestrelHost.StartAsync(cancellationToken); } - + public async Task StopAsync(CancellationToken cancellationToken = default) { await _testHost.StopAsync(cancellationToken); diff --git a/test/Blog.Tests/EndToEnd/PostTests.cs b/test/Blog.Tests/EndToEnd/PostTests.cs index f872f81..b866bc4 100644 --- a/test/Blog.Tests/EndToEnd/PostTests.cs +++ b/test/Blog.Tests/EndToEnd/PostTests.cs @@ -1,7 +1,7 @@ namespace Blog.Tests.EndToEnd; [TestFixture] -public class PostTests : BlogTest +public class PostTests : BlogTest { private const string TestPostSlug = "test-blog"; private const string TestPostTitle = "Test Blog"; diff --git a/test/Blog.Tests/EndToEnd/RssTests.cs b/test/Blog.Tests/EndToEnd/RssTests.cs index 8493f4d..8119a63 100644 --- a/test/Blog.Tests/EndToEnd/RssTests.cs +++ b/test/Blog.Tests/EndToEnd/RssTests.cs @@ -1,8 +1,3 @@ -using System.ServiceModel.Syndication; -using System.Text; -using System.Xml; -using System.Xml.Linq; - namespace Blog.Tests.EndToEnd; [TestFixture] @@ -15,9 +10,9 @@ public class RssTests : BlogTest response.Status.Should().Be((int)HttpStatusCode.OK); var xml = await response.BodyAsync(); - + var streamReader = new StreamReader( - new MemoryStream(xml), + new MemoryStream(xml), Encoding.UTF8 ); diff --git a/test/Blog.Tests/GlobalUsings.cs b/test/Blog.Tests/GlobalUsings.cs index 49e8e35..9d04d92 100644 --- a/test/Blog.Tests/GlobalUsings.cs +++ b/test/Blog.Tests/GlobalUsings.cs @@ -1,5 +1,8 @@ global using System.IO.Abstractions; global using System.Net; +global using System.ServiceModel.Syndication; +global using System.Text; +global using System.Xml; global using Blog.Posts; @@ -10,10 +13,10 @@ global using Microsoft.AspNetCore.Hosting.Server; global using Microsoft.AspNetCore.Mvc.Testing; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; global using Microsoft.Playwright; global using Microsoft.Playwright.NUnit; -global using Microsoft.Extensions.Logging; global using Moq; diff --git a/test/Blog.Tests/Unit/FilePostServiceTests.cs b/test/Blog.Tests/Unit/FilePostServiceTests.cs index cff7740..85deb14 100644 --- a/test/Blog.Tests/Unit/FilePostServiceTests.cs +++ b/test/Blog.Tests/Unit/FilePostServiceTests.cs @@ -15,7 +15,7 @@ class FilePostServiceTests .Returns(new FilePostServiceOptions { PostsDirectory = "posts" }); _sut = new FilePostService( - _mockOptions.Object, + _mockOptions.Object, _mockFileSystem.Object, _mockLogger.Object ); @@ -326,8 +326,8 @@ class FilePostServiceTests _mockFileSystem .Setup(x => x.Directory.GetDirectories(It.IsAny())) .Returns([ - "valid-blog-post", - "another-valid-blog-post", + "valid-blog-post", + "another-valid-blog-post", "invalid-blog-post" ]);