using System.Text.Json; using Yavsc.Blogspot; namespace PostIt.Tests; /// /// Round-trip tests for the wire shape of a blog post as /// serialised by Yavsc.Blogs and consumed by PostIt. /// /// /// Background: in 1.0.7, BlogPostDto.Author was typed as /// the abstract interface IApplicationUser. System.Text.Json /// cannot materialise an interface without a polymorphic /// converter, so the "load posts" call from PostIt crashed when /// the server returned a post with a populated Author /// object. The fix replaced IApplicationUser with a thin /// concrete DTO, BlogPostAuthorDto, embedded directly in /// BlogPostDto.Author. /// /// /// /// These tests pin the wire shape: a JSON document with an /// Author object must deserialise without throwing and /// must round-trip the three fields PostIt exposes in the UI /// (Id, UserName, Avatar). They are intentionally placed in /// PostIt.Tests — the client-side assembly — so the /// regression is caught at the deserialisation boundary, where /// it actually manifested in production. /// /// public class BlogPostAuthorDtoTests { private static readonly JsonSerializerOptions CaseInsensitiveJson = new() { PropertyNameCaseInsensitive = true }; [Fact] public void BlogPostDto_deserialises_with_populated_author() { // A representative JSON shape the server would emit for // GET /api/BlogApi. The Author object is fully populated // — that's the shape that used to break deserialisation // when Author was typed as the abstract IApplicationUser // interface. var json = """ { "id": 42, "title": "Premier billet", "article": "Contenu", "photo": null, "dateCreated": "2026-08-01T12:00:00Z", "dateModified": "2026-08-02T12:00:00Z", "userCreated": "alice", "userModified": "alice", "authorId": "u-alice", "isPublished": true, "author": { "id": "u-alice", "userName": "alice", "avatar": "/avatars/alice.png" } } """; var post = JsonSerializer.Deserialize(json, CaseInsensitiveJson); Assert.NotNull(post); Assert.Equal(42, post!.Id); Assert.Equal("Premier billet", post.Title); Assert.Equal("u-alice", post.AuthorId); Assert.True(post.IsPublished); // The actual regression coverage: Author must // materialise as a concrete DTO, not be left null because // of a JsonException on IApplicationUser. Assert.NotNull(post.Author); Assert.Equal("u-alice", post.Author!.Id); Assert.Equal("alice", post.Author.UserName); Assert.Equal("/avatars/alice.png", post.Author.Avatar); } [Fact] public void BlogPostDto_deserialises_when_author_is_null() { // The server is allowed to omit Author (the field is // nullable on the wire — it maps to a navigation // property that may not have been Included). The client // must accept that shape without throwing. var json = """ { "id": 7, "title": "Sans auteur", "article": null, "photo": null, "dateCreated": "2026-08-01T12:00:00Z", "dateModified": "2026-08-01T12:00:00Z", "userCreated": "system", "userModified": "system", "authorId": "system", "isPublished": false, "author": null } """; var post = JsonSerializer.Deserialize(json, CaseInsensitiveJson); Assert.NotNull(post); Assert.Null(post!.Author); Assert.Equal("system", post.AuthorId); } [Fact] public void BlogPostDto_deserialises_when_author_field_is_missing() { // Forward-compatibility: an older server that doesn't // emit the Author field at all. Should not throw. var json = """ { "id": 9, "title": "Ancien format", "article": "Pas d'auteur dans la charge utile", "photo": null, "dateCreated": "2026-07-01T12:00:00Z", "dateModified": "2026-07-01T12:00:00Z", "userCreated": "bob", "userModified": "bob", "authorId": "u-bob", "isPublished": true } """; var post = JsonSerializer.Deserialize(json, CaseInsensitiveJson); Assert.NotNull(post); Assert.Null(post!.Author); } [Fact] public void BlogPostAuthorDto_serialises_back_to_expected_json_shape() { // Pin the wire shape on the way out too. The server // builds BlogPostAuthorDto from an ApplicationUser and // PostIt receives it as JSON; if the field names // change (e.g. case) the round-trip on the client side // is what would silently break. // // The server emits camelCase (ASP.NET Core's Web // defaults — PropertyNamingPolicy = CamelCase). We // mirror that here so the test reflects what the wire // actually looks like. PropertyNameCaseInsensitive on // the client deserialiser means we don't have to // hardcode the casing for the inbound assertions. var author = new BlogPostAuthorDto { Id = "u-alice", UserName = "alice", Avatar = "/avatars/alice.png" }; var json = JsonSerializer.Serialize(author, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; Assert.True(root.TryGetProperty("id", out _)); Assert.True(root.TryGetProperty("userName", out _)); Assert.True(root.TryGetProperty("avatar", out _)); } }