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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user