fix(guild): subleaders can accept/reject join requests (matches GuildManager.HasAuthorityInviteAndApproval)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-27 14:06:30 -04:00
parent dd955fe4be
commit 901c3d7568
2 changed files with 153 additions and 3 deletions

View File

@@ -331,7 +331,7 @@ public sealed class GuildService : IGuildService
{
var m = await _members.GetMembershipAsync(callerViewerId, ct);
if (m is null) return new(GuildOpResultCode.NotInGuild);
if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied);
if (m.Role is not (GuildRole.Leader or GuildRole.SubLeader)) return new(GuildOpResultCode.PermissionDenied);
var req = await _joinRequests.GetAsync(m.GuildId, applicantViewerId, ct);
if (req is null) return new(GuildOpResultCode.JoinRequestNotFound);
@@ -351,7 +351,7 @@ public sealed class GuildService : IGuildService
{
var m = await _members.GetMembershipAsync(callerViewerId, ct);
if (m is null) return new(GuildOpResultCode.NotInGuild);
if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied);
if (m.Role is not (GuildRole.Leader or GuildRole.SubLeader)) return new(GuildOpResultCode.PermissionDenied);
var req = await _joinRequests.GetAsync(m.GuildId, applicantViewerId, ct);
if (req is null) return new(GuildOpResultCode.JoinRequestNotFound);

View File

@@ -1,6 +1,8 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using SVSim.Database;
using SVSim.Database.Entities.Guild;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Integration.Guild;
@@ -550,7 +552,7 @@ public class GuildJoinFlowTests
}
[Test]
public async Task Accept_non_leader_returns_error()
public async Task Accept_caller_not_in_any_guild_returns_error()
{
using var factory = new SVSimTestFactory();
@@ -593,6 +595,154 @@ public class GuildJoinFlowTests
Assert.That(rc.GetInt32(), Is.EqualTo(2), $"Non-guild-member accept must return error, got: {acceptJson}");
}
[Test]
public async Task SubLeader_can_accept_join_request()
{
using var factory = new SVSimTestFactory();
// A creates an APPROVAL guild (becomes Leader).
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_001UL, displayName: "SubLeaderAcceptLeaderA");
using var clientA = factory.CreateAuthenticatedClient(viewerA);
int guildId = await CreateGuildAndGetIdAsync(clientA, "SubLeaderAcceptGuild", joinCondition: 2);
// B joins A's guild as Regular, then we promote to SubLeader directly in the DB.
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_002UL, displayName: "SubLeaderAcceptB");
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 }));
// A accepts B so B is a full member.
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 }));
// Promote B to SubLeader via direct DB mutation.
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var bMember = await db.GuildMembers.FindAsync(guildId, viewerB);
bMember!.Role = GuildRole.SubLeader;
await db.SaveChangesAsync();
}
// C applies to A's guild.
long viewerC = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_003UL, displayName: "SubLeaderAcceptApplicantC");
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 (SubLeader) accepts C's request.
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, $"SubLeader 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), $"SubLeader accept returned error: {acceptJson}");
// C must now be a member (guild_status=2).
var cInfoResp = await clientC.PostAsync("/guild/info",
JsonContent.Create(new { viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
var cInfoJson = await cInfoResp.Content.ReadAsStringAsync();
using var cInfoDoc = JsonDocument.Parse(cInfoJson);
Assert.That(GetStringifiedInt(cInfoDoc.RootElement, "guild_status"), Is.EqualTo(2),
$"C must be a member (guild_status=2) after SubLeader accept, got: {cInfoJson}");
}
[Test]
public async Task SubLeader_can_reject_join_request()
{
using var factory = new SVSimTestFactory();
// A creates an APPROVAL guild (becomes Leader).
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_011UL, displayName: "SubLeaderRejectLeaderA");
using var clientA = factory.CreateAuthenticatedClient(viewerA);
int guildId = await CreateGuildAndGetIdAsync(clientA, "SubLeaderRejectGuild", joinCondition: 2);
// B joins and gets accepted, then promoted to SubLeader.
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_012UL, displayName: "SubLeaderRejectB");
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 }));
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var bMember = await db.GuildMembers.FindAsync(guildId, viewerB);
bMember!.Role = GuildRole.SubLeader;
await db.SaveChangesAsync();
}
// C applies.
long viewerC = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_013UL, displayName: "SubLeaderRejectApplicantC");
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 (SubLeader) rejects C's request.
var rejectResp = await clientB.PostAsync("/guild/reject_join_request",
JsonContent.Create(new { request_viewer_id = (int)viewerC,
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
var rejectJson = await rejectResp.Content.ReadAsStringAsync();
Assert.That(rejectResp.IsSuccessStatusCode, Is.True, $"SubLeader 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), $"SubLeader reject returned error: {rejectJson}");
// C must NOT be a member (guild_status=0).
var cInfoResp = await clientC.PostAsync("/guild/info",
JsonContent.Create(new { viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
var cInfoJson = await cInfoResp.Content.ReadAsStringAsync();
using var cInfoDoc = JsonDocument.Parse(cInfoJson);
Assert.That(GetStringifiedInt(cInfoDoc.RootElement, "guild_status"), Is.EqualTo(0),
$"C must NOT be a member (guild_status=0) after SubLeader reject, got: {cInfoJson}");
}
[Test]
public async Task Regular_member_cannot_accept_join_request()
{
using var factory = new SVSimTestFactory();
// A creates an APPROVAL guild (becomes Leader).
long viewerA = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_021UL, displayName: "RegularPermLeaderA");
using var clientA = factory.CreateAuthenticatedClient(viewerA);
int guildId = await CreateGuildAndGetIdAsync(clientA, "RegularPermGuild", joinCondition: 2);
// B joins and gets accepted — remains Regular.
long viewerB = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_022UL, displayName: "RegularPermMemberB");
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 }));
// B's role is Regular (no promotion).
// C applies.
long viewerC = await factory.SeedViewerAsync(steamId: 76_561_198_400_001_023UL, displayName: "RegularPermApplicantC");
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) tries to accept C's request → PermissionDenied.
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 Regular-member accept, got: {acceptJson}");
Assert.That(rc.GetInt32(), Is.EqualTo(2), $"Regular member accept must return error (result_code=2), got: {acceptJson}");
}
[Test]
public async Task Accept_full_guild_returns_error()
{