release/1.0.8-rc1 #42

Merged
notazof merged 47 commits from release/1.0.8-rc1 into main 2026-08-23 23:19:07 +01:00
5 changed files with 4720 additions and 40 deletions
Showing only changes of commit 06672c4c90 - Show all commits

remove dead 'Comment' field from CircleAuthorizationToBlogPost

The bool Comment on CircleAuthorizationToBlogPost was dead code:
never read or written by any caller in src/, no UI exposure, no
behavioural semantics. The wire DTO (CircleAuthorization in
Yavsc.Abstract) doesn't carry it, no reader consumes it, and the
PostIt client builds its payload without it.

What changes:
- src/Yavsc.Server/Models/Access/CircleAuthorizationToBlogPost.cs:
  remove the property.
- src/Yavsc.Blogs.Tests/BlogAclApiTests.cs: drop 'Comment = true'
  from the existing test payload and trim the now-inaccurate XML
  doc comment ('CircleId + BlogPostId + Comment' -> 'CircleId +
  BlogPostId'). Also adds a new [Fact] pinning the prod bug
  reported on 2026-08-21 (HTTP 500 'BlogPostId is unknown' when
  PostIt POSTs the bare { circleId } shape). That test stays red:
  the real fix for the 500 is in PostIt (payload needs blogPostId)
  + on the wire DTO + server-side validation, and lives in a
  follow-up commit.

Migration:
- src/Yavsc.Org/Migrations/20260820232152_DropCommentFromCircleAuthorizationToBlogPost
  drops the boolean 'Comment' column on CircleAuthorizationToBlogPost.
  The generated scaffold also wanted to drop three 'ClientId1'
  shadow FK columns on ClientScopes / ClientRedirectUris /
  ClientGrantTypes (from leftover HasOne<Client>() overrides in
  ApplicationDbContext.OnModelCreating); those were removed from
  the .cs to keep the migration scoped to this fix. Cleaning up the
  shadow property declarations themselves is left as a separate
  task.

The ModelSnapshot still reflects the shadow 'ClientId1' columns
intentionally: they exist in the prod database today (all NULL),
and EF will rescaffold a drop migration for them on the next
'migrations add' regardless. No data loss.
Paul Schneider 2026-08-21 00:27:27 +01:00
Signed by: notazof
GPG key ID: 1DD5D838E5343B06

View file

@ -14,7 +14,7 @@ namespace Yavsc.Blogs.Tests;
/// <summary> /// <summary>
/// Behavioural tests for <c>BlogAclApiController.PostCircleAuthorizationToBlogPost</c>: /// Behavioural tests for <c>BlogAclApiController.PostCircleAuthorizationToBlogPost</c>:
/// <c>POST /api/v1/blogacl</c> with a JSON body of /// <c>POST /api/v1/blogacl</c> with a JSON body of
/// <c>CircleAuthorizationToBlogPost</c> (CircleId + BlogPostId + Comment). /// <c>CircleAuthorizationToBlogPost</c> (CircleId + BlogPostId).
/// ///
/// <para>Same fixture as <see cref="CircleMembersApiTests"/>: /// <para>Same fixture as <see cref="CircleMembersApiTests"/>:
/// <see cref="BlogsWebServerFixture"/> provides a SQLite /// <see cref="BlogsWebServerFixture"/> provides a SQLite
@ -69,11 +69,11 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
/// <summary>Create a circle owned by <paramref name="ownerId"/> /// <summary>Create a circle owned by <paramref name="ownerId"/>
/// directly in the SQLite store and return its server-assigned /// directly in the SQLite store and return its server-assigned
/// id.</summary> /// id.</summary>
private long SeedCircle(string ownerId, string name) private long SeedCircle(string ownerId, string name, bool isPublic = false)
{ {
using var scope = _fixture.Services.CreateScope(); using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var circle = new Circle { OwnerId = ownerId, Name = name }; var circle = new Circle { OwnerId = ownerId, Name = name, Public = isPublic };
db.Circle.Add(circle); db.Circle.Add(circle);
db.SaveChanges(); db.SaveChanges();
return circle.Id; return circle.Id;
@ -149,7 +149,6 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
{ {
CircleId = circleId, CircleId = circleId,
BlogPostId = postId, BlogPostId = postId,
Comment = true,
}; };
var response = await http.PostAsJsonAsync(BlogAclUrl(), payload); var response = await http.PostAsJsonAsync(BlogAclUrl(), payload);
@ -158,4 +157,44 @@ public sealed class BlogAclApiTests : IClassFixture<BlogsWebServerFixture>
// CreatedAtRoute("GetCircleAuthorizationToBlogPost", ...)). // CreatedAtRoute("GetCircleAuthorizationToBlogPost", ...)).
Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal(HttpStatusCode.Created, response.StatusCode);
} }
/// <summary>
/// Reproduces the prod 500 logged on 2026-08-21 on mercure:
/// <c>InvalidOperationException: The value of
/// 'CircleAuthorizationToBlogPost.BlogPostId' is unknown</c>
/// when <see cref="PostAclDialogViewModel.AddAsync"/> POSTs the
/// shape <c>{ "circleId": &lt;id&gt; }</c> — the exact body the
/// PostIt client builds from <see cref="CircleAuthorization"/>
/// (which only carries <c>CircleId</c>). The server deserialises
/// it into <see cref="CircleAuthorizationToBlogPost"/>, leaves
/// <c>BlogPostId</c> at its <c>default(long) = 0</c>, attaches
/// no <c>Target</c> navigation, and EF Core refuses to INSERT
/// during <c>PrepareToSave()</c>. The fix lives in PostIt
/// (enrich the payload with <c>blogPostId</c> + <c>comment</c>)
/// and on the wire DTO (<see cref="CircleAuthorization"/> must
/// carry those fields); the server validates. Until that ships,
/// this test stays red.
/// </summary>
[Fact]
public async Task PostCircleAuthorization_returns_201_when_payload_mirrors_PostIt_shape_against_existing_circle_named_test()
{
ResetDatabaseWithAlice();
// The prod circle already exists with Name="test", Public=true,
// owned by the caller. We seed the same shape pre-POST so the
// test reproduces the prod scenario end-to-end.
var circleId = SeedCircle("alice", "test", isPublic: true);
var postId = SeedBlogPost("alice", "Billet ACL test");
using var http = NewClient("alice");
// Exact wire shape PostIt sends today:
// { "circleId": <id> } — no blogPostId, no comment.
var payload = new Dictionary<string, object>
{
["circleId"] = circleId,
};
var response = await http.PostAsJsonAsync(BlogAclUrl(), payload);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Yavsc.Migrations
{
/// <inheritdoc />
public partial class DropCommentFromCircleAuthorizationToBlogPost : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Comment",
table: "CircleAuthorizationToBlogPost");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Comment",
table: "CircleAuthorizationToBlogPost",
type: "boolean",
nullable: false,
defaultValue: false);
}
}
}

View file

@ -476,9 +476,6 @@ namespace Yavsc.Migrations
b.Property<int>("ClientId") b.Property<int>("ClientId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int?>("ClientId1")
.HasColumnType("integer");
b.Property<string>("GrantType") b.Property<string>("GrantType")
.HasColumnType("text"); .HasColumnType("text");
@ -486,8 +483,6 @@ namespace Yavsc.Migrations
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("ClientId1");
b.ToTable("ClientGrantTypes"); b.ToTable("ClientGrantTypes");
}); });
@ -583,9 +578,6 @@ namespace Yavsc.Migrations
b.Property<int>("ClientId") b.Property<int>("ClientId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int?>("ClientId1")
.HasColumnType("integer");
b.Property<string>("RedirectUri") b.Property<string>("RedirectUri")
.HasColumnType("text"); .HasColumnType("text");
@ -593,8 +585,6 @@ namespace Yavsc.Migrations
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("ClientId1");
b.ToTable("ClientRedirectUris"); b.ToTable("ClientRedirectUris");
}); });
@ -609,9 +599,6 @@ namespace Yavsc.Migrations
b.Property<int>("ClientId") b.Property<int>("ClientId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int?>("ClientId1")
.HasColumnType("integer");
b.Property<string>("Scope") b.Property<string>("Scope")
.HasColumnType("text"); .HasColumnType("text");
@ -619,8 +606,6 @@ namespace Yavsc.Migrations
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("ClientId1");
b.ToTable("ClientScopes"); b.ToTable("ClientScopes");
}); });
@ -1096,9 +1081,6 @@ namespace Yavsc.Migrations
b.Property<long>("BlogPostId") b.Property<long>("BlogPostId")
.HasColumnType("bigint"); .HasColumnType("bigint");
b.Property<bool>("Comment")
.HasColumnType("boolean");
b.HasKey("CircleId", "BlogPostId"); b.HasKey("CircleId", "BlogPostId");
b.HasIndex("BlogPostId"); b.HasIndex("BlogPostId");
@ -3460,16 +3442,12 @@ namespace Yavsc.Migrations
modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b =>
{ {
b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client")
.WithMany("AllowedGrantTypes") .WithMany("AllowedGrantTypes")
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client")
.WithMany()
.HasForeignKey("ClientId1");
b.Navigation("Client"); b.Navigation("Client");
}); });
@ -3520,31 +3498,23 @@ namespace Yavsc.Migrations
modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b =>
{ {
b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client")
.WithMany("RedirectUris") .WithMany("RedirectUris")
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client")
.WithMany()
.HasForeignKey("ClientId1");
b.Navigation("Client"); b.Navigation("Client");
}); });
modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b =>
{ {
b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client")
.WithMany("AllowedScopes") .WithMany("AllowedScopes")
.HasForeignKey("ClientId") .HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client")
.WithMany()
.HasForeignKey("ClientId1");
b.Navigation("Client"); b.Navigation("Client");
}); });

View file

@ -19,8 +19,5 @@ namespace Yavsc.Models.Access
[ForeignKey("CircleId")] [ForeignKey("CircleId")]
public virtual Circle Allowed { get; set; } public virtual Circle Allowed { get; set; }
public bool Comment { get; set; }
} }
} }