From 771d884314e3c6c1c7bf8cbe3a4ed9278e5b149b Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 27 Jun 2026 13:51:04 -0400 Subject: [PATCH] fix(guild): /guild/join returns guild_status per branch (APPLYING for approval path, JOINING otherwise) GuildJoinTask.Parse() reads data[guild_status].ToInt() directly; hardcoding 2 for the APPROVAL path was wrong. GuildOpResult gains nullable GuildStatus; JoinAsync sets 1 (APPLYING) for the pending-request path and 2 (JOINING) for instant joins. Controller reads r.GuildStatus. Two new tests assert the wire value per path. Co-Authored-By: Claude Sonnet 4.6 --- SVSim.Database/Services/Guild/GuildService.cs | 6 +-- .../Services/Guild/GuildServiceTypes.cs | 2 +- .../Controllers/GuildController.cs | 6 +-- .../Integration/Guild/GuildJoinFlowTests.cs | 52 +++++++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/SVSim.Database/Services/Guild/GuildService.cs b/SVSim.Database/Services/Guild/GuildService.cs index cb02ac51..fbf4b8a4 100644 --- a/SVSim.Database/Services/Guild/GuildService.cs +++ b/SVSim.Database/Services/Guild/GuildService.cs @@ -275,7 +275,7 @@ public sealed class GuildService : IGuildService if (existing is { Status: GuildJoinRequestStatus.Pending }) { // Already pending — idempotent, no-op. - return GuildOpResult.Ok; + return new(GuildOpResultCode.Ok, GuildStatus: 1); // APPLYING } if (existing is not null) @@ -296,11 +296,11 @@ public sealed class GuildService : IGuildService CreatedAt = now, }, ct); } - return GuildOpResult.Ok; + return new(GuildOpResultCode.Ok, GuildStatus: 1); // APPLYING } await CommitJoinAsync(viewerId, guildId, now, ct); - return GuildOpResult.Ok; + return new(GuildOpResultCode.Ok, GuildStatus: 2); // JOINING } private async Task CommitJoinAsync(long viewerId, int guildId, DateTime now, CancellationToken ct) diff --git a/SVSim.Database/Services/Guild/GuildServiceTypes.cs b/SVSim.Database/Services/Guild/GuildServiceTypes.cs index 59b3b02c..b6422d01 100644 --- a/SVSim.Database/Services/Guild/GuildServiceTypes.cs +++ b/SVSim.Database/Services/Guild/GuildServiceTypes.cs @@ -22,7 +22,7 @@ public enum GuildOpResultCode InvalidRoleTransition, } -public sealed record GuildOpResult(GuildOpResultCode Code, int? GuildId = null, string? Detail = null) +public sealed record GuildOpResult(GuildOpResultCode Code, int? GuildId = null, string? Detail = null, int? GuildStatus = null) { public static readonly GuildOpResult Ok = new(GuildOpResultCode.Ok); public bool IsOk => Code == GuildOpResultCode.Ok; diff --git a/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs b/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs index 6b245f65..c0d772b2 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs @@ -248,9 +248,9 @@ public sealed class GuildController : SVSimController if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var r = await _guild.JoinAsync(viewerId, req.GuildId, ct); if (!r.IsOk) return WireError(); - // guild_status=2 (JOINING) on successful join; APPROVAL path also returns 2 as the viewer - // is now "awaiting" — client drives the display from result_code, not guild_status alone. - return new GuildJoinResponse { GuildStatus = 2 }; + // guild_status is branch-specific: JOINING (2) for instant joins, APPLYING (1) for approval path. + // GuildJoinTask.Parse() reads this directly: GuildStatus = (eGUILD_STATUS)data["guild_status"].ToInt() + return new GuildJoinResponse { GuildStatus = r.GuildStatus ?? 2 }; } [HttpPost("cancel_join_request")] diff --git a/SVSim.UnitTests/Integration/Guild/GuildJoinFlowTests.cs b/SVSim.UnitTests/Integration/Guild/GuildJoinFlowTests.cs index dd7d4679..f2934391 100644 --- a/SVSim.UnitTests/Integration/Guild/GuildJoinFlowTests.cs +++ b/SVSim.UnitTests/Integration/Guild/GuildJoinFlowTests.cs @@ -193,6 +193,58 @@ public class GuildJoinFlowTests "Idempotent re-apply must not duplicate the pending row"); } + [Test] + public async Task Approval_join_response_has_guild_status_APPLYING() + { + using var factory = new SVSimTestFactory(); + + long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_025UL, displayName: "ApprovalLeaderStatus"); + using var clientA = factory.CreateAuthenticatedClient(viewerA); + int guildId = await CreateGuildAndGetIdAsync(clientA, "ApprovalGuildStatus", joinCondition: 2); + + long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_026UL, displayName: "ApprovalApplicantStatus"); + using var clientB = factory.CreateAuthenticatedClient(viewerB); + + // B applies to the APPROVAL guild. + var joinResp = await clientB.PostAsync("/guild/join", + JsonContent.Create(new { guild_id = guildId, from_invite = false, + viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk })); + var joinJson = await joinResp.Content.ReadAsStringAsync(); + Assert.That(joinResp.IsSuccessStatusCode, Is.True, $"join HTTP failed: {joinJson}"); + + using var joinDoc = JsonDocument.Parse(joinJson); + // Must be APPLYING (1), NOT JOINING (2) — GuildJoinTask.Parse() reads this value directly. + Assert.That(GetStringifiedInt(joinDoc.RootElement, "guild_status"), Is.EqualTo(1), + $"APPROVAL branch must return guild_status=1 (APPLYING), got: {joinJson}"); + } + + [Test] + public async Task Approval_idempotent_join_response_has_guild_status_APPLYING() + { + using var factory = new SVSimTestFactory(); + + long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_027UL, displayName: "IdempotentApprovalLeaderStatus"); + using var clientA = factory.CreateAuthenticatedClient(viewerA); + int guildId = await CreateGuildAndGetIdAsync(clientA, "IdempotentApprovalStatus", joinCondition: 2); + + long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_028UL, displayName: "IdempotentApplicantStatus"); + using var clientB = factory.CreateAuthenticatedClient(viewerB); + + var joinPayload = new { guild_id = guildId, from_invite = false, + viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }; + + // First apply. + await clientB.PostAsync("/guild/join", JsonContent.Create(joinPayload)); + // Second (idempotent) apply — should still return APPLYING (1). + var join2 = await clientB.PostAsync("/guild/join", JsonContent.Create(joinPayload)); + var join2Json = await join2.Content.ReadAsStringAsync(); + Assert.That(join2.IsSuccessStatusCode, Is.True, $"Idempotent apply HTTP failed: {join2Json}"); + + using var join2Doc = JsonDocument.Parse(join2Json); + Assert.That(GetStringifiedInt(join2Doc.RootElement, "guild_status"), Is.EqualTo(1), + $"Idempotent APPROVAL branch must return guild_status=1 (APPLYING), got: {join2Json}"); + } + // ─── ONLY_INVITE branch ────────────────────────────────────────────────────── [Test]