feat(guild): join_request_accept + reject_join_request
This commit is contained in:
@@ -327,11 +327,40 @@ public sealed class GuildService : IGuildService
|
|||||||
return GuildOpResult.Ok;
|
return GuildOpResult.Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<GuildOpResult> AcceptJoinRequestAsync(long callerViewerId, long applicantViewerId, CancellationToken ct = default)
|
public async Task<GuildOpResult> AcceptJoinRequestAsync(long callerViewerId, long applicantViewerId, CancellationToken ct = default)
|
||||||
=> throw new NotImplementedException();
|
{
|
||||||
|
var m = await _members.GetMembershipAsync(callerViewerId, ct);
|
||||||
|
if (m is null) return new(GuildOpResultCode.NotInGuild);
|
||||||
|
if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied);
|
||||||
|
|
||||||
public Task<GuildOpResult> RejectJoinRequestAsync(long callerViewerId, long applicantViewerId, CancellationToken ct = default)
|
var req = await _joinRequests.GetAsync(m.GuildId, applicantViewerId, ct);
|
||||||
=> throw new NotImplementedException();
|
if (req is null) return new(GuildOpResultCode.JoinRequestNotFound);
|
||||||
|
if (req.Status != GuildJoinRequestStatus.Pending) return new(GuildOpResultCode.JoinRequestAlreadyResolved);
|
||||||
|
|
||||||
|
var cfg = _config.Get<GuildConfig>();
|
||||||
|
var memberCount = await _members.CountByGuildAsync(m.GuildId, ct);
|
||||||
|
if (memberCount >= cfg.MaxMemberNum) return new(GuildOpResultCode.MemberCapReached);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
await _joinRequests.UpdateStatusAsync(m.GuildId, applicantViewerId, GuildJoinRequestStatus.Accepted, now, ct);
|
||||||
|
await CommitJoinAsync(applicantViewerId, m.GuildId, now, ct);
|
||||||
|
return GuildOpResult.Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GuildOpResult> RejectJoinRequestAsync(long callerViewerId, long applicantViewerId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var m = await _members.GetMembershipAsync(callerViewerId, ct);
|
||||||
|
if (m is null) return new(GuildOpResultCode.NotInGuild);
|
||||||
|
if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied);
|
||||||
|
|
||||||
|
var req = await _joinRequests.GetAsync(m.GuildId, applicantViewerId, ct);
|
||||||
|
if (req is null) return new(GuildOpResultCode.JoinRequestNotFound);
|
||||||
|
if (req.Status != GuildJoinRequestStatus.Pending) return new(GuildOpResultCode.JoinRequestAlreadyResolved);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
await _joinRequests.UpdateStatusAsync(m.GuildId, applicantViewerId, GuildJoinRequestStatus.Rejected, now, ct);
|
||||||
|
return GuildOpResult.Ok;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<GuildOpResult> LeaveAsync(long viewerId, CancellationToken ct = default)
|
public Task<GuildOpResult> LeaveAsync(long viewerId, CancellationToken ct = default)
|
||||||
=> throw new NotImplementedException();
|
=> throw new NotImplementedException();
|
||||||
|
|||||||
@@ -283,10 +283,20 @@ public sealed class GuildController : SVSimController
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("join_request_accept")]
|
[HttpPost("join_request_accept")]
|
||||||
public Task<ActionResult<EmptyResponse>> JoinRequestAccept([FromBody] GuildJoinRequestAcceptRequest req, CancellationToken ct) => Stub();
|
public async Task<ActionResult<EmptyResponse>> JoinRequestAccept([FromBody] GuildJoinRequestAcceptRequest req, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
|
||||||
|
var r = await _guild.AcceptJoinRequestAsync(viewerId, req.RequestViewerId, ct);
|
||||||
|
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("reject_join_request")]
|
[HttpPost("reject_join_request")]
|
||||||
public Task<ActionResult<EmptyResponse>> RejectJoinRequest([FromBody] GuildRejectJoinRequestRequest req, CancellationToken ct) => Stub();
|
public async Task<ActionResult<EmptyResponse>> RejectJoinRequest([FromBody] GuildRejectJoinRequestRequest req, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
|
||||||
|
var r = await _guild.RejectJoinRequestAsync(viewerId, req.RequestViewerId, ct);
|
||||||
|
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("leave")]
|
[HttpPost("leave")]
|
||||||
public Task<ActionResult<EmptyResponse>> Leave([FromBody] BaseRequest _, CancellationToken ct) => Stub();
|
public Task<ActionResult<EmptyResponse>> Leave([FromBody] BaseRequest _, CancellationToken ct) => Stub();
|
||||||
|
|||||||
@@ -458,6 +458,324 @@ public class GuildJoinFlowTests
|
|||||||
"Regular member must see empty list from join_request_list");
|
"Regular member must see empty list from join_request_list");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── join_request_accept ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Accept_makes_applicant_a_member_and_emits_join_chat_event()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
|
||||||
|
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_111UL, displayName: "AcceptLeader");
|
||||||
|
using var clientA = factory.CreateAuthenticatedClient(viewerA);
|
||||||
|
int guildId = await CreateGuildAndGetIdAsync(clientA, "AcceptGuild", joinCondition: 2);
|
||||||
|
|
||||||
|
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_112UL, displayName: "AcceptApplicant");
|
||||||
|
using var clientB = factory.CreateAuthenticatedClient(viewerB);
|
||||||
|
|
||||||
|
// B applies.
|
||||||
|
await clientB.PostAsync("/guild/join",
|
||||||
|
JsonContent.Create(new { guild_id = guildId, from_invite = false,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// A accepts B.
|
||||||
|
var acceptResp = await clientA.PostAsync("/guild/join_request_accept",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerB,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var acceptJson = await acceptResp.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(acceptResp.IsSuccessStatusCode, Is.True, $"accept HTTP failed: {acceptJson}");
|
||||||
|
using var acceptDoc = JsonDocument.Parse(acceptJson);
|
||||||
|
if (acceptDoc.RootElement.TryGetProperty("result_code", out var rc))
|
||||||
|
Assert.That(rc.GetInt32(), Is.Not.EqualTo(2), $"accept returned error: {acceptJson}");
|
||||||
|
|
||||||
|
// B is now a member.
|
||||||
|
var bInfoResp = await clientB.PostAsync("/guild/info",
|
||||||
|
JsonContent.Create(new { viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var bInfoJson = await bInfoResp.Content.ReadAsStringAsync();
|
||||||
|
using var bInfoDoc = JsonDocument.Parse(bInfoJson);
|
||||||
|
Assert.That(GetStringifiedInt(bInfoDoc.RootElement, "guild_status"), Is.EqualTo(2),
|
||||||
|
$"B must be a member (guild_status=2) after accept, got: {bInfoJson}");
|
||||||
|
|
||||||
|
// A's join_request_list is now empty.
|
||||||
|
var jrlResp = await clientA.PostAsync("/guild/join_request_list",
|
||||||
|
JsonContent.Create(new { page = 0, oldest_time = 0,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
using var jrlDoc = JsonDocument.Parse(await jrlResp.Content.ReadAsStringAsync());
|
||||||
|
Assert.That(jrlDoc.RootElement.GetProperty("list").GetArrayLength(), Is.EqualTo(0),
|
||||||
|
"join_request_list must be empty after accept");
|
||||||
|
|
||||||
|
// GuildService.CommitJoinAsync calls IGuildChatService.EmitSystemEventAsync(Join).
|
||||||
|
// That service is a no-op stub (T15 will implement it), so we can't assert a DB row here.
|
||||||
|
// Confirmed the call path via code inspection; the DB assertion will land with T15.
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Accept_consumes_applicant_pending_invites()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
|
||||||
|
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_113UL, displayName: "AcceptInvLeader");
|
||||||
|
using var clientA = factory.CreateAuthenticatedClient(viewerA);
|
||||||
|
int guildId = await CreateGuildAndGetIdAsync(clientA, "AcceptInvGuild", joinCondition: 2);
|
||||||
|
|
||||||
|
// Separate guild that invites B (to ensure invites are cleared on accept).
|
||||||
|
long viewerC = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_114UL, displayName: "OtherLeader");
|
||||||
|
using var clientC = factory.CreateAuthenticatedClient(viewerC);
|
||||||
|
await CreateGuildAndGetIdAsync(clientC, "OtherGuild", joinCondition: 1);
|
||||||
|
|
||||||
|
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_115UL, displayName: "AcceptInvApplicant");
|
||||||
|
using var clientB = factory.CreateAuthenticatedClient(viewerB);
|
||||||
|
|
||||||
|
// C invites B.
|
||||||
|
await clientC.PostAsync("/guild/invite",
|
||||||
|
JsonContent.Create(new { invited_viewer_id = (int)viewerB, invite_message = "",
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// B applies to A's guild.
|
||||||
|
await clientB.PostAsync("/guild/join",
|
||||||
|
JsonContent.Create(new { guild_id = guildId, from_invite = false,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// A accepts B.
|
||||||
|
var acceptResp = await clientA.PostAsync("/guild/join_request_accept",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerB,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
Assert.That(acceptResp.IsSuccessStatusCode, Is.True);
|
||||||
|
|
||||||
|
// B's invited_guild_list must now be empty (invite consumed by CommitJoinAsync).
|
||||||
|
var invitedList = await clientB.PostAsync("/guild/invited_guild_list",
|
||||||
|
JsonContent.Create(new { page = 0, viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
using var ilDoc = JsonDocument.Parse(await invitedList.Content.ReadAsStringAsync());
|
||||||
|
Assert.That(ilDoc.RootElement.GetProperty("list").GetArrayLength(), Is.EqualTo(0),
|
||||||
|
"Pending invites must be consumed when accept commits join");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Accept_non_leader_returns_error()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
|
||||||
|
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_116UL, displayName: "AcceptPermLeader");
|
||||||
|
using var clientA = factory.CreateAuthenticatedClient(viewerA);
|
||||||
|
int guildId = await CreateGuildAndGetIdAsync(clientA, "AcceptPermGuild", joinCondition: 2);
|
||||||
|
|
||||||
|
// B is a regular member of A's guild (joined via FREE — repurpose guild as mixed joinCondition by seeding directly).
|
||||||
|
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_117UL, displayName: "RegularMemberAccept");
|
||||||
|
using var clientB = factory.CreateAuthenticatedClient(viewerB);
|
||||||
|
// B joins A's guild (change to joinCondition: 1 is needed; create separate FREE guild for B to join and then act as non-leader).
|
||||||
|
// Simplest: just have B NOT in any guild → NotInGuild → but we need PermissionDenied specifically.
|
||||||
|
// Use a separate FREE guild so B can join it, then try to accept on A's guild (not B's guild → different result).
|
||||||
|
// Better: A creates an approval guild, B applies to a 3rd guild, then B (not in A's guild) tries to accept → NotInGuild.
|
||||||
|
// For PermissionDenied specifically: need B in A's guild as Regular. Let's create a second client in A's guild.
|
||||||
|
long viewerD = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_118UL, displayName: "FreeGuildLeader");
|
||||||
|
using var clientD = factory.CreateAuthenticatedClient(viewerD);
|
||||||
|
// D creates a FREE guild, B joins it (B becomes Regular in D's guild, not A's approval guild).
|
||||||
|
int dGuildId = await CreateGuildAndGetIdAsync(clientD, "FreeGuildForPerm", joinCondition: 1);
|
||||||
|
await clientB.PostAsync("/guild/join",
|
||||||
|
JsonContent.Create(new { guild_id = dGuildId, from_invite = false,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// Now have C apply to A's guild.
|
||||||
|
long viewerC = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_119UL, displayName: "ApplicantForPerm");
|
||||||
|
using var clientC = factory.CreateAuthenticatedClient(viewerC);
|
||||||
|
await clientC.PostAsync("/guild/join",
|
||||||
|
JsonContent.Create(new { guild_id = guildId, from_invite = false,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// B (Regular member of D's guild, NOT in A's guild) tries to accept C's request on A's guild → NotInGuild error.
|
||||||
|
var acceptResp = await clientB.PostAsync("/guild/join_request_accept",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerC,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var acceptJson = await acceptResp.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(acceptResp.IsSuccessStatusCode, Is.True, "HTTP must be 200");
|
||||||
|
using var acceptDoc = JsonDocument.Parse(acceptJson);
|
||||||
|
Assert.That(acceptDoc.RootElement.TryGetProperty("result_code", out var rc), Is.True,
|
||||||
|
$"Expected error from non-guild-member accept, got: {acceptJson}");
|
||||||
|
Assert.That(rc.GetInt32(), Is.EqualTo(2), $"Non-guild-member accept must return error, got: {acceptJson}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Accept_full_guild_returns_error()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
|
||||||
|
// Cap at 2 members so we can fill the guild (leader + 1 member) then try to accept a 3rd.
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||||
|
const string json = "{\"MaxMemberNum\":2,\"MaxSubLeaderNum\":3,\"SearchResultCap\":20,\"UsableStampList\":[1,2,3,4,5]}";
|
||||||
|
var existing = db.GameConfigs.FirstOrDefault(s => s.SectionName == "Guild");
|
||||||
|
if (existing is null)
|
||||||
|
db.GameConfigs.Add(new SVSim.Database.Models.GameConfigSection { SectionName = "Guild", ValueJson = json });
|
||||||
|
else
|
||||||
|
existing.ValueJson = json;
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_121UL, displayName: "CapAcceptLeader");
|
||||||
|
using var clientA = factory.CreateAuthenticatedClient(viewerA);
|
||||||
|
int guildId = await CreateGuildAndGetIdAsync(clientA, "CapAcceptGuild", joinCondition: 2);
|
||||||
|
|
||||||
|
// B applies and gets accepted (fills slot 2/2).
|
||||||
|
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_122UL, displayName: "CapAcceptMember");
|
||||||
|
using var clientB = factory.CreateAuthenticatedClient(viewerB);
|
||||||
|
await clientB.PostAsync("/guild/join",
|
||||||
|
JsonContent.Create(new { guild_id = guildId, from_invite = false,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
await clientA.PostAsync("/guild/join_request_accept",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerB,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// C also applies.
|
||||||
|
long viewerC = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_123UL, displayName: "CapAcceptApplicant");
|
||||||
|
using var clientC = factory.CreateAuthenticatedClient(viewerC);
|
||||||
|
await clientC.PostAsync("/guild/join",
|
||||||
|
JsonContent.Create(new { guild_id = guildId, from_invite = false,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// A tries to accept C, but guild is now full.
|
||||||
|
var acceptResp = await clientA.PostAsync("/guild/join_request_accept",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerC,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var acceptJson = await acceptResp.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(acceptResp.IsSuccessStatusCode, Is.True, "HTTP must be 200");
|
||||||
|
using var acceptDoc = JsonDocument.Parse(acceptJson);
|
||||||
|
Assert.That(acceptDoc.RootElement.TryGetProperty("result_code", out var rc), Is.True,
|
||||||
|
$"Expected error when guild is full, got: {acceptJson}");
|
||||||
|
Assert.That(rc.GetInt32(), Is.EqualTo(2), $"Full guild accept must return error, got: {acceptJson}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Accept_nonexistent_request_returns_error()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
|
||||||
|
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_131UL, displayName: "NotFoundLeader");
|
||||||
|
using var clientA = factory.CreateAuthenticatedClient(viewerA);
|
||||||
|
await CreateGuildAndGetIdAsync(clientA, "NotFoundGuild", joinCondition: 2);
|
||||||
|
|
||||||
|
// No applicant — accept a viewer_id that never applied.
|
||||||
|
var acceptResp = await clientA.PostAsync("/guild/join_request_accept",
|
||||||
|
JsonContent.Create(new { request_viewer_id = 999_999,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var acceptJson = await acceptResp.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(acceptResp.IsSuccessStatusCode, Is.True, "HTTP must be 200");
|
||||||
|
using var acceptDoc = JsonDocument.Parse(acceptJson);
|
||||||
|
Assert.That(acceptDoc.RootElement.TryGetProperty("result_code", out var rc), Is.True,
|
||||||
|
$"Expected error for non-existent request, got: {acceptJson}");
|
||||||
|
Assert.That(rc.GetInt32(), Is.EqualTo(2), $"Non-existent accept must return error, got: {acceptJson}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Accept_already_resolved_request_returns_error()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
|
||||||
|
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_141UL, displayName: "AlreadyResolvedLeader");
|
||||||
|
using var clientA = factory.CreateAuthenticatedClient(viewerA);
|
||||||
|
int guildId = await CreateGuildAndGetIdAsync(clientA, "AlreadyResolvedGuild", joinCondition: 2);
|
||||||
|
|
||||||
|
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_142UL, displayName: "AlreadyResolvedApplicant");
|
||||||
|
using var clientB = factory.CreateAuthenticatedClient(viewerB);
|
||||||
|
|
||||||
|
// B applies and A accepts first time — request is now Accepted.
|
||||||
|
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 firstAccept = await clientA.PostAsync("/guild/join_request_accept",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerB,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
Assert.That(firstAccept.IsSuccessStatusCode, Is.True, "First accept must succeed");
|
||||||
|
|
||||||
|
// A tries to accept again — request already resolved.
|
||||||
|
var secondAccept = await clientA.PostAsync("/guild/join_request_accept",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerB,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var secondAcceptJson = await secondAccept.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(secondAccept.IsSuccessStatusCode, Is.True, "HTTP must be 200");
|
||||||
|
using var secondDoc = JsonDocument.Parse(secondAcceptJson);
|
||||||
|
Assert.That(secondDoc.RootElement.TryGetProperty("result_code", out var rc), Is.True,
|
||||||
|
$"Expected error on double-accept, got: {secondAcceptJson}");
|
||||||
|
Assert.That(rc.GetInt32(), Is.EqualTo(2), $"Double-accept must return error, got: {secondAcceptJson}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── reject_join_request ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Reject_flips_status_no_membership_no_chat_event()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
|
||||||
|
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_151UL, displayName: "RejectLeader");
|
||||||
|
using var clientA = factory.CreateAuthenticatedClient(viewerA);
|
||||||
|
int guildId = await CreateGuildAndGetIdAsync(clientA, "RejectGuild", joinCondition: 2);
|
||||||
|
|
||||||
|
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_152UL, displayName: "RejectApplicant");
|
||||||
|
using var clientB = factory.CreateAuthenticatedClient(viewerB);
|
||||||
|
|
||||||
|
// B applies.
|
||||||
|
await clientB.PostAsync("/guild/join",
|
||||||
|
JsonContent.Create(new { guild_id = guildId, from_invite = false,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// A rejects B.
|
||||||
|
var rejectResp = await clientA.PostAsync("/guild/reject_join_request",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerB,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var rejectJson = await rejectResp.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(rejectResp.IsSuccessStatusCode, Is.True, $"reject HTTP failed: {rejectJson}");
|
||||||
|
using var rejectDoc = JsonDocument.Parse(rejectJson);
|
||||||
|
if (rejectDoc.RootElement.TryGetProperty("result_code", out var rc))
|
||||||
|
Assert.That(rc.GetInt32(), Is.Not.EqualTo(2), $"reject returned error: {rejectJson}");
|
||||||
|
|
||||||
|
// B must NOT be a member (guild_status=0).
|
||||||
|
var bInfoResp = await clientB.PostAsync("/guild/info",
|
||||||
|
JsonContent.Create(new { viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var bInfoJson = await bInfoResp.Content.ReadAsStringAsync();
|
||||||
|
using var bInfoDoc = JsonDocument.Parse(bInfoJson);
|
||||||
|
Assert.That(GetStringifiedInt(bInfoDoc.RootElement, "guild_status"), Is.EqualTo(0),
|
||||||
|
$"B must NOT be a member after reject (guild_status=0), got: {bInfoJson}");
|
||||||
|
|
||||||
|
// No Join chat event (IGuildChatService.EmitSystemEventAsync is a no-op stub until T15 — no DB row expected).
|
||||||
|
using var scope = factory.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<SVSim.Database.SVSimDbContext>();
|
||||||
|
var hasJoinEvent = db.GuildChatMessages
|
||||||
|
.Any(m => m.GuildId == guildId && m.AuthorViewerId == viewerB
|
||||||
|
&& m.MessageType == SVSim.Database.Entities.Guild.GuildChatMessageType.Join);
|
||||||
|
Assert.That(hasJoinEvent, Is.False, "No Join chat event should exist after reject");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Reject_already_resolved_request_returns_error()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
|
||||||
|
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_161UL, displayName: "RejectDoubleLeader");
|
||||||
|
using var clientA = factory.CreateAuthenticatedClient(viewerA);
|
||||||
|
int guildId = await CreateGuildAndGetIdAsync(clientA, "RejectDoubleGuild", joinCondition: 2);
|
||||||
|
|
||||||
|
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_000_162UL, displayName: "RejectDoubleApplicant");
|
||||||
|
using var clientB = factory.CreateAuthenticatedClient(viewerB);
|
||||||
|
|
||||||
|
// B applies, A rejects once.
|
||||||
|
await clientB.PostAsync("/guild/join",
|
||||||
|
JsonContent.Create(new { guild_id = guildId, from_invite = false,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
await clientA.PostAsync("/guild/reject_join_request",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerB,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
|
||||||
|
// A tries to reject again.
|
||||||
|
var secondReject = await clientA.PostAsync("/guild/reject_join_request",
|
||||||
|
JsonContent.Create(new { request_viewer_id = (int)viewerB,
|
||||||
|
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||||
|
var secondRejectJson = await secondReject.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(secondReject.IsSuccessStatusCode, Is.True, "HTTP must be 200");
|
||||||
|
using var secondDoc = JsonDocument.Parse(secondRejectJson);
|
||||||
|
Assert.That(secondDoc.RootElement.TryGetProperty("result_code", out var rc), Is.True,
|
||||||
|
$"Expected error on double-reject, got: {secondRejectJson}");
|
||||||
|
Assert.That(rc.GetInt32(), Is.EqualTo(2), $"Double-reject must return error, got: {secondRejectJson}");
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Wire-shape test for join_request_list ───────────────────────────────────
|
// ─── Wire-shape test for join_request_list ───────────────────────────────────
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
Reference in New Issue
Block a user