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);
+ }
+}