IBlogPost porte IsPublished : le endpoint liste ne le lâche plus sur le wire
Le blog API (Yavsc.Blogs/Program.cs) sérialise en System.Text.Json (.AddControllers() par défaut), et BlogApiController.GetBlogspot retourne IEnumerable<IBlogPost>. STJ n'émet que les membres du type déclaré (l'interface), donc IsPublished — qui n'était que sur les classes concrètes BlogPost / BlogPostDto — était dropped du payload liste. PostIt lisait alors IsPublished=false pour tout billet, y compris publiés (cas prod /BlogSpot/Details/13). Fix : déclarer IsPublished sur IBlogPost. BlogPost et BlogPostDto l'implémentent déjà. Tests de wire-shape (IBlogPostWireShapeTests) ajoutés pour verrouiller le contrat au niveau du boundary. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
a17a9cef9a
commit
384f6bc300
2 changed files with 105 additions and 0 deletions
|
|
@ -15,5 +15,23 @@ namespace Yavsc.Blogspot
|
||||||
// BlogPost entity implements this getter by mapping its
|
// BlogPost entity implements this getter by mapping its
|
||||||
// ApplicationUser navigation to a BlogPostAuthorDto.
|
// ApplicationUser navigation to a BlogPostAuthorDto.
|
||||||
BlogPostAuthorDto? Author { get; }
|
BlogPostAuthorDto? Author { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this post is published. The server derives this
|
||||||
|
/// from the existence of a row in <c>BlogSpotPublication</c>
|
||||||
|
/// and serves it on the wire so clients (PostIt) can render
|
||||||
|
/// the current publication state without a follow-up request.
|
||||||
|
///
|
||||||
|
/// <para>This MUST be declared on the interface — not just on
|
||||||
|
/// the concrete <c>BlogPost</c> entity / <c>BlogPostDto</c> —
|
||||||
|
/// because the blog API's list endpoint returns
|
||||||
|
/// <c>IEnumerable<IBlogPost></c>, and the blog API host
|
||||||
|
/// serialises with System.Text.Json, which emits only the
|
||||||
|
/// members of the declared (interface) type. When
|
||||||
|
/// <c>IsPublished</c> lived only on the concrete classes, STJ
|
||||||
|
/// dropped it from the list payload and PostIt read
|
||||||
|
/// <c>false</c> for every post, including published ones.</para>
|
||||||
|
/// </summary>
|
||||||
|
bool IsPublished { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
src/Yavsc.Blogs.Tests/IBlogPostWireShapeTests.cs
Normal file
87
src/Yavsc.Blogs.Tests/IBlogPostWireShapeTests.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Yavsc.Blogspot;
|
||||||
|
using Yavsc.Models.Blog;
|
||||||
|
|
||||||
|
namespace Yavsc.Blogs.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wire-shape tests pinning that <see cref="IBlogPost.IsPublished"/>
|
||||||
|
/// is carried on the JSON the blog API list endpoint emits.
|
||||||
|
///
|
||||||
|
/// <para>The blog API host (<c>Yavsc.Blogs/Program.cs</c>) serialises
|
||||||
|
/// with System.Text.Json (the <c>.AddControllers()</c> default — no
|
||||||
|
/// <c>AddNewtonsoftJson</c>), and <c>BlogApiController.GetBlogspot</c>
|
||||||
|
/// returns <c>IEnumerable<IBlogPost></c>. System.Text.Json emits
|
||||||
|
/// only the members of the <b>declared</b> type, so a property that
|
||||||
|
/// exists solely on the concrete <c>BlogPost</c> entity is dropped from
|
||||||
|
/// the payload. <c>IsPublished</c> used to be one such property: it was
|
||||||
|
/// on <c>BlogPost</c> and <c>BlogPostDto</c> but not on
|
||||||
|
/// <see cref="IBlogPost"/>, so the list response omitted it and PostIt
|
||||||
|
/// read <c>IsPublished == false</c> for every post — including
|
||||||
|
/// published ones.</para>
|
||||||
|
///
|
||||||
|
/// <para>These tests reproduce that serialisation path directly
|
||||||
|
/// (declared type <c>IEnumerable<IBlogPost></c> through
|
||||||
|
/// <see cref="JsonSerializer.Serialize(object, Type)"/>) and assert
|
||||||
|
/// <c>isPublished</c> survives the round trip. They live in
|
||||||
|
/// <c>Yavsc.Blogs.Tests</c> — the assembly that owns the blog API
|
||||||
|
/// contract — so a regression at the wire boundary is caught here,
|
||||||
|
/// where it manifested in production (PostIt showing a published
|
||||||
|
/// post's status as <c>false</c>).</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class IBlogPostWireShapeTests
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions WebDefaults
|
||||||
|
= new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsPublished_is_emitted_when_list_is_serialised_as_IEnumerable_IBlogPost()
|
||||||
|
{
|
||||||
|
var post = new BlogPost
|
||||||
|
{
|
||||||
|
Id = 13,
|
||||||
|
Title = "Published",
|
||||||
|
AuthorId = "alice",
|
||||||
|
Article = "x",
|
||||||
|
};
|
||||||
|
post.IsPublished = true;
|
||||||
|
|
||||||
|
// Declared type mirrors GetBlogspot's return: IEnumerable<IBlogPost>.
|
||||||
|
IEnumerable<IBlogPost> list = new List<BlogPost> { post };
|
||||||
|
|
||||||
|
// Serialize with the DECLARED type, the way ASP.NET Core's
|
||||||
|
// SystemTextJsonOutputFormatter hands context.ObjectType (the
|
||||||
|
// action return type) to JsonSerializer.Serialize.
|
||||||
|
var json = JsonSerializer.Serialize(list, typeof(IEnumerable<IBlogPost>),
|
||||||
|
WebDefaults);
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var element = doc.RootElement[0];
|
||||||
|
// Web defaults use camelCase, so the property is "isPublished".
|
||||||
|
Assert.True(element.TryGetProperty("isPublished", out var isPublished),
|
||||||
|
$"isPublished was dropped from the list payload. JSON: {json}");
|
||||||
|
Assert.True(isPublished.GetBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsPublished_round_trips_through_BlogPostDto_on_the_client()
|
||||||
|
{
|
||||||
|
// The shape the client (PostIt) receives and deserialises with
|
||||||
|
// PropertyNameCaseInsensitive = true (cf. YavscApiClient).
|
||||||
|
var json = """
|
||||||
|
[{
|
||||||
|
"id": 13,
|
||||||
|
"title": "Published",
|
||||||
|
"article": "x",
|
||||||
|
"authorId": "alice",
|
||||||
|
"isPublished": true
|
||||||
|
}]
|
||||||
|
""";
|
||||||
|
|
||||||
|
var posts = JsonSerializer.Deserialize<List<BlogPostDto>>(json,
|
||||||
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||||
|
|
||||||
|
var post = Assert.Single(posts);
|
||||||
|
Assert.True(post.IsPublished);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue