diff --git a/SVSim.Database/Services/Guild/GuildService.cs b/SVSim.Database/Services/Guild/GuildService.cs index 63e1399d..4b284941 100644 --- a/SVSim.Database/Services/Guild/GuildService.cs +++ b/SVSim.Database/Services/Guild/GuildService.cs @@ -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); diff --git a/SVSim.UnitTests/Integration/Guild/GuildJoinFlowTests.cs b/SVSim.UnitTests/Integration/Guild/GuildJoinFlowTests.cs index f5e6d5d1..94325c11 100644 --- a/SVSim.UnitTests/Integration/Guild/GuildJoinFlowTests.cs +++ b/SVSim.UnitTests/Integration/Guild/GuildJoinFlowTests.cs @@ -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(); + 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(); + 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() {