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"
]);