From 3e7e76edf2c4a03d47c5beb88880f89905999ba4 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Thu, 17 Sep 2026 00:57:52 +0100 Subject: [PATCH] BlogAttachmentLinkTests --- src/PostIt/PostIt.Tests/BlogApiTestFakes.cs | 13 ++++ .../PostIt.Tests/BlogAttachmentLinkTests.cs | 75 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs diff --git a/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs b/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs index 4f102be20..112c11752 100644 --- a/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs +++ b/src/PostIt/PostIt.Tests/BlogApiTestFakes.cs @@ -62,4 +62,17 @@ internal sealed class RecordingYavscApiClient : YavscApiClient }); return Task.FromResult(default(T)!); } + + /// Multipart overload: BlogApiClient routes + /// Create/UpdatePostAsync with attachments through here. + /// Without this override the call would fall through to the + /// base and attempt a real HTTP + /// round-trip against the stub authority. The body is a + /// factory, so we record the delegate itself; tests that + /// care about the multipart shape invoke it. + public override Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + { + _recorder.Calls.Add((method, path, contentFactory)); + return Task.FromResult(default(T)!); + } } diff --git a/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs b/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs new file mode 100644 index 000000000..6572fdfbb --- /dev/null +++ b/src/PostIt/PostIt.Tests/BlogAttachmentLinkTests.cs @@ -0,0 +1,75 @@ +using Yavsc.Blogspot; +using Yavsc.Api.Client; +using PostIt.ViewModels; + +namespace PostIt.Tests; + +/// +/// Pins the markdown link that +/// appends to the article after a successful attachment upload. +/// +/// The link must be an absolute URL derived from the OIDC +/// authority via FileServerUrlHelpers.GetUserFilesUri +/// (Yavsc.Abstract): the /files file server is hosted by +/// Yavsc.Org — the IdP — not by the blogs API host, so a link +/// built from BlogsApiUrl (or left relative) 404s when the +/// reader opens the post. The regression this guards: a generated +/// link of the shape /files/{user}/blogs/{id}/{name} +/// (relative, authority-less) instead of +/// https://{authority}/files/{user}/blogs/{id}/{name}. +/// +public class BlogAttachmentLinkTests +{ + [Fact] + public async Task Save_with_attachment_appends_absolute_link_derived_from_the_oidc_authority() + { + const string authority = "https://idp.example.test"; + + var recorder = new CallRecorder(); + var api = new RecordingYavscApiClient(recorder); + var blog = new BlogApiClient(api, "https://blogs.example.test/api/v1/"); + var settings = new Settings + { + Authentication = new AuthenticationSettings + { + Authority = authority, + ClientId = "stub", + Scopes = new[] { "openid" }, + }, + }; + var vm = new BlogsViewModel(blog, settings); + + // Update branch: an existing post is selected, the user + // edits the buffers and attaches a file. + vm.SelectedPost = new BlogPostDto + { + Id = 42, + AuthorId = "tester", + Title = "Avant", + Article = "Contenu initial.", + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + }; + vm.DraftTitle = "Après"; + vm.DraftArticle = "Contenu modifié."; + vm.DraftAttachments.Add(new BlogUploadFile( + "note.txt", + System.Text.Encoding.UTF8.GetBytes("payload test"), + "text/plain")); + + await vm.SaveAsync(); + + // The update branch issues two PUTs: the multipart one + // carrying the file, then — because TryAppendAttachmentLinks + // appended a link — a JSON one carrying the final article. + var linkPut = recorder.Calls.LastOrDefault( + c => c.method == HttpMethod.Put && c.body is BlogPostDto); + Assert.NotEqual(default, linkPut); + var sent = Assert.IsType(linkPut.body); + + var expectedUrl = $"{authority}/files/tester/blogs/42/note.txt"; + Assert.Contains($"- [note.txt]({expectedUrl})", sent.Article); + // The regression shape: a relative, authority-less link. + Assert.DoesNotContain("](/files/", sent.Article); + } +}