From c0e1c10192cb99f43c480fbeb5352d92ec88cb3e Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 27 Jun 2026 12:47:00 -0400 Subject: [PATCH] feat(guild): update + update_description + update_emblem --- .../Repositories/Guild/GuildRepository.cs | 25 ++ .../Repositories/Guild/IGuildRepository.cs | 9 + SVSim.Database/Services/Guild/GuildService.cs | 40 +- .../Controllers/GuildController.cs | 53 ++- .../Services/Guild/GuildServiceUpdateTests.cs | 366 ++++++++++++++++++ 5 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 SVSim.UnitTests/Services/Guild/GuildServiceUpdateTests.cs diff --git a/SVSim.Database/Repositories/Guild/GuildRepository.cs b/SVSim.Database/Repositories/Guild/GuildRepository.cs index d4f7ee8f..28a9974e 100644 --- a/SVSim.Database/Repositories/Guild/GuildRepository.cs +++ b/SVSim.Database/Repositories/Guild/GuildRepository.cs @@ -69,4 +69,29 @@ public sealed class GuildRepository : IGuildRepository return await q.Take(resultCap).ToListAsync(ct); } + + public async Task UpdateActivityAndJoinConditionAsync(int guildId, int? activity, int? joinCondition, CancellationToken ct = default) + { + var guild = await _db.Guilds.FirstOrDefaultAsync(g => g.GuildId == guildId, ct); + if (guild is null) return; + if (activity.HasValue) guild.Activity = (Entities.Guild.GuildActivity)activity.Value; + if (joinCondition.HasValue) guild.JoinCondition = (Entities.Guild.GuildJoinCondition)joinCondition.Value; + await _db.SaveChangesAsync(ct); + } + + public async Task UpdateDescriptionAsync(int guildId, string description, CancellationToken ct = default) + { + var guild = await _db.Guilds.FirstOrDefaultAsync(g => g.GuildId == guildId, ct); + if (guild is null) return; + guild.Description = description; + await _db.SaveChangesAsync(ct); + } + + public async Task UpdateEmblemAsync(int guildId, long emblemId, CancellationToken ct = default) + { + var guild = await _db.Guilds.FirstOrDefaultAsync(g => g.GuildId == guildId, ct); + if (guild is null) return; + guild.EmblemId = emblemId; + await _db.SaveChangesAsync(ct); + } } diff --git a/SVSim.Database/Repositories/Guild/IGuildRepository.cs b/SVSim.Database/Repositories/Guild/IGuildRepository.cs index 6ee67938..e5b41b91 100644 --- a/SVSim.Database/Repositories/Guild/IGuildRepository.cs +++ b/SVSim.Database/Repositories/Guild/IGuildRepository.cs @@ -24,4 +24,13 @@ public interface IGuildRepository Task> SearchAsync( string name, int activity, int joinCondition, int memberBucket, int maxMemberCap, int resultCap, CancellationToken ct = default); + + /// Updates activity and/or join_condition. Only non-null fields are written. + Task UpdateActivityAndJoinConditionAsync(int guildId, int? activity, int? joinCondition, CancellationToken ct = default); + + /// Overwrites the description field. + Task UpdateDescriptionAsync(int guildId, string description, CancellationToken ct = default); + + /// Overwrites the emblem_id field. + Task UpdateEmblemAsync(int guildId, long emblemId, CancellationToken ct = default); } diff --git a/SVSim.Database/Services/Guild/GuildService.cs b/SVSim.Database/Services/Guild/GuildService.cs index 52a7f742..af0d0ec2 100644 --- a/SVSim.Database/Services/Guild/GuildService.cs +++ b/SVSim.Database/Services/Guild/GuildService.cs @@ -123,14 +123,42 @@ public sealed class GuildService : IGuildService return emblemId is > 0 ? emblemId.Value : 100_000_000L; } - public Task UpdateAsync(long viewerId, UpdateGuildRequest req, CancellationToken ct = default) - => throw new NotImplementedException(); + public async Task UpdateAsync(long viewerId, UpdateGuildRequest req, CancellationToken ct = default) + { + var m = await _members.GetMembershipAsync(viewerId, ct); + if (m is null) return new(GuildOpResultCode.NotInGuild); + if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied); - public Task UpdateDescriptionAsync(long viewerId, string description, CancellationToken ct = default) - => throw new NotImplementedException(); + if (req.Activity.HasValue && req.Activity.Value is < 1 or > 16) + return new(GuildOpResultCode.NameInvalid); + if (req.JoinCondition.HasValue && req.JoinCondition.Value is < 1 or > 3) + return new(GuildOpResultCode.NameInvalid); - public Task UpdateEmblemAsync(long viewerId, long emblemId, CancellationToken ct = default) - => throw new NotImplementedException(); + await _guilds.UpdateActivityAndJoinConditionAsync(m.GuildId, req.Activity, req.JoinCondition, ct); + return GuildOpResult.Ok; + } + + public async Task UpdateDescriptionAsync(long viewerId, string description, CancellationToken ct = default) + { + var m = await _members.GetMembershipAsync(viewerId, ct); + if (m is null) return new(GuildOpResultCode.NotInGuild); + if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied); + if (description.Length > 512) return new(GuildOpResultCode.NameInvalid); + + await _guilds.UpdateDescriptionAsync(m.GuildId, description, ct); + await _chat.EmitSystemEventAsync(m.GuildId, viewerId, GuildChatMessageType.Description, body: null, ct); + return GuildOpResult.Ok; + } + + public async Task UpdateEmblemAsync(long viewerId, long emblemId, CancellationToken ct = default) + { + var m = await _members.GetMembershipAsync(viewerId, ct); + if (m is null) return new(GuildOpResultCode.NotInGuild); + if (m.Role != GuildRole.Leader) return new(GuildOpResultCode.PermissionDenied); + + await _guilds.UpdateEmblemAsync(m.GuildId, emblemId, ct); + return GuildOpResult.Ok; + } public async Task BreakupAsync(long viewerId, CancellationToken ct = default) { diff --git a/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs b/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs index 1eea1007..9cc1aebf 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs @@ -83,15 +83,52 @@ public sealed class GuildController : SVSimController } [HttpPost("update")] - public Task> Update([FromBody] GuildUpdateRequest req, CancellationToken ct) - => Task.FromResult>(new GuildUpdateResponse()); + public async Task> Update([FromBody] GuildUpdateRequest req, CancellationToken ct) + { + if (!TryGetViewerId(out var viewerId)) return Unauthorized(); + int? activity = req.Activity != 0 ? req.Activity : null; + int? joinCondition = req.JoinCondition != 0 ? req.JoinCondition : null; + var r = await _guild.UpdateAsync(viewerId, new(activity, joinCondition), ct); + if (!r.IsOk) return WireError(); + + var m = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct); + if (m is null) return WireError(); + var guild = await _guild.GetActiveAsync(m.GuildId, ct); + if (guild is null) return WireError(); + var memberCount = await _db.GuildMembers.CountAsync(x => x.GuildId == guild.GuildId, ct); + var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct); + return new GuildUpdateResponse + { + Guild = new GuildDetailSubTree { Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") } + }; + } [HttpPost("update_description")] - public Task> UpdateDescription([FromBody] GuildUpdateDescriptionRequest req, CancellationToken ct) => Stub(); + public async Task> UpdateDescription([FromBody] GuildUpdateDescriptionRequest req, CancellationToken ct) + { + if (!TryGetViewerId(out var viewerId)) return Unauthorized(); + var r = await _guild.UpdateDescriptionAsync(viewerId, req.Description, ct); + return r.IsOk ? new EmptyResponse() : MapErrorToWire(r); + } [HttpPost("update_emblem")] - public Task> UpdateEmblem([FromBody] GuildUpdateEmblemRequest req, CancellationToken ct) - => Task.FromResult>(new GuildUpdateResponse()); + public async Task> UpdateEmblem([FromBody] GuildUpdateEmblemRequest req, CancellationToken ct) + { + if (!TryGetViewerId(out var viewerId)) return Unauthorized(); + var r = await _guild.UpdateEmblemAsync(viewerId, req.EmblemId, ct); + if (!r.IsOk) return WireError(); + + var m = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct); + if (m is null) return WireError(); + var guild = await _guild.GetActiveAsync(m.GuildId, ct); + if (guild is null) return WireError(); + var memberCount = await _db.GuildMembers.CountAsync(x => x.GuildId == guild.GuildId, ct); + var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct); + return new GuildUpdateResponse + { + Guild = new GuildDetailSubTree { Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") } + }; + } [HttpPost("search_guild")] public Task> SearchGuild([FromBody] GuildSearchGuildRequest req, CancellationToken ct) @@ -238,6 +275,12 @@ public sealed class GuildController : SVSimController private ActionResult MapErrorToWire(GuildOpResult res) => Ok(new { result_code = GuildErrorResultCode }); + /// + /// Returns a wire-error for endpoints that return a non-EmptyResponse type. + /// Identical result_code=2 body, but typed as ObjectResult so it fits any TResult. + /// + private ObjectResult WireError() => Ok(new { result_code = GuildErrorResultCode }); + private static Task> Stub() => Task.FromResult>(new EmptyResponse()); } diff --git a/SVSim.UnitTests/Services/Guild/GuildServiceUpdateTests.cs b/SVSim.UnitTests/Services/Guild/GuildServiceUpdateTests.cs new file mode 100644 index 00000000..6b6b48ea --- /dev/null +++ b/SVSim.UnitTests/Services/Guild/GuildServiceUpdateTests.cs @@ -0,0 +1,366 @@ +using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Database; +using SVSim.Database.Entities.Guild; +using SVSim.Database.Services.Guild; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Services.Guild; + +// --------------------------------------------------------------------------- +// Spy IGuildChatService — counts EmitSystemEventAsync calls per GuildChatMessageType. +// --------------------------------------------------------------------------- +internal sealed class SpyGuildChatService : IGuildChatService +{ + public readonly List<(int GuildId, long ActorId, GuildChatMessageType Type)> Emissions = new(); + + public Task EmitSystemEventAsync(int guildId, long actorViewerId, GuildChatMessageType type, string? body = null, CancellationToken ct = default) + { + Emissions.Add((guildId, actorViewerId, type)); + return Task.CompletedTask; + } + + // ---- stub out the rest ----------------------------------------------- + public Task GetWindowAsync(long v, int s, int d, int w, CancellationToken ct = default) => throw new NotImplementedException(); + public Task PostTextOrStampAsync(long v, int t, string m, CancellationToken ct = default) => throw new NotImplementedException(); + public Task PostDeckAsync(long v, string j, CancellationToken ct = default) => throw new NotImplementedException(); + public Task DeleteDeckAsync(long v, int id, CancellationToken ct = default) => throw new NotImplementedException(); + public Task PostReplayAsync(long v, string j, CancellationToken ct = default) => throw new NotImplementedException(); + public Task GetReplayDetailAsync(long v, int id, CancellationToken ct = default) => throw new NotImplementedException(); + public Task GetDeckLogAsync(long v, int id, CancellationToken ct = default) => throw new NotImplementedException(); +} + +// --------------------------------------------------------------------------- +// Factory variant that replaces IGuildChatService with our spy singleton. +// --------------------------------------------------------------------------- +internal sealed class SpyGuildChatFactory : SVSimTestFactory +{ + public readonly SpyGuildChatService ChatSpy = new(); + + protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + builder.ConfigureTestServices(services => + { + // Remove all existing registrations for IGuildChatService. + var descriptors = services + .Where(d => d.ServiceType == typeof(IGuildChatService)) + .ToList(); + foreach (var d in descriptors) services.Remove(d); + + // Register our singleton spy so every scope gets the same instance. + services.AddSingleton(ChatSpy); + }); + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +public class GuildServiceUpdateTests +{ + // ----------------------------------------------------------------------- + // UpdateAsync — activity + join_condition + // ----------------------------------------------------------------------- + + [Test] + public async Task UpdateAsync_leader_can_change_activity_and_join_condition() + { + using var factory = new SVSimTestFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_001UL, "UpdateLeader1"); + + int guildId; + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var res = await svc.CreateAsync(leaderId, new("UpdateGuild1", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + Assert.That(res.IsOk, Is.True); + guildId = res.GuildId!.Value; + } + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var r = await svc.UpdateAsync(leaderId, new((int)GuildActivity.Rotation, (int)GuildJoinCondition.Approval)); + Assert.That(r.IsOk, Is.True); + } + + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var g = await db.Guilds.FirstAsync(x => x.GuildId == guildId); + Assert.That((int)g.Activity, Is.EqualTo((int)GuildActivity.Rotation)); + Assert.That((int)g.JoinCondition, Is.EqualTo((int)GuildJoinCondition.Approval)); + } + } + + [Test] + public async Task UpdateAsync_non_leader_returns_PermissionDenied() + { + using var factory = new SVSimTestFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_002UL, "UpdateLeader2"); + var memberId = await factory.SeedViewerAsync(76_561_198_200_000_003UL, "UpdateMember2"); + + int guildId; + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var res = await svc.CreateAsync(leaderId, new("UpdateGuild2", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + Assert.That(res.IsOk, Is.True); + guildId = res.GuildId!.Value; + } + + // Add a regular member. + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.GuildMembers.Add(new GuildMember { GuildId = guildId, ViewerId = memberId, Role = GuildRole.Regular, JoinedAt = DateTime.UtcNow }); + var v = await db.Viewers.FirstAsync(x => x.Id == memberId); + v.GuildId = guildId; + await db.SaveChangesAsync(); + } + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var r = await svc.UpdateAsync(memberId, new((int)GuildActivity.Stoic, null)); + Assert.That(r.Code, Is.EqualTo(GuildOpResultCode.PermissionDenied)); + } + } + + [Test] + public async Task UpdateAsync_invalid_activity_returns_NameInvalid() + { + using var factory = new SVSimTestFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_004UL, "UpdateLeader3"); + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + await svc.CreateAsync(leaderId, new("UpdateGuild3", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + } + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + // Activity 0 is out of range (valid: 1..16). + var r = await svc.UpdateAsync(leaderId, new(0, null)); + Assert.That(r.Code, Is.EqualTo(GuildOpResultCode.NameInvalid)); + } + } + + [Test] + public async Task UpdateAsync_invalid_join_condition_returns_NameInvalid() + { + using var factory = new SVSimTestFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_005UL, "UpdateLeader4"); + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + await svc.CreateAsync(leaderId, new("UpdateGuild4", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + } + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + // JoinCondition 99 is out of range (valid: 1..3). + var r = await svc.UpdateAsync(leaderId, new(null, 99)); + Assert.That(r.Code, Is.EqualTo(GuildOpResultCode.NameInvalid)); + } + } + + // ----------------------------------------------------------------------- + // UpdateDescriptionAsync + // ----------------------------------------------------------------------- + + [Test] + public async Task UpdateDescriptionAsync_leader_updates_description_and_emits_chat_event() + { + using var factory = new SpyGuildChatFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_010UL, "DescLeader1"); + + int guildId; + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var res = await svc.CreateAsync(leaderId, new("DescGuild1", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + Assert.That(res.IsOk, Is.True); + guildId = res.GuildId!.Value; + } + + // Drain any emissions from CreateAsync (CreateGuild event). + factory.ChatSpy.Emissions.Clear(); + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var r = await svc.UpdateDescriptionAsync(leaderId, "New description text"); + Assert.That(r.IsOk, Is.True); + } + + // Verify DB updated. + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var g = await db.Guilds.FirstAsync(x => x.GuildId == guildId); + Assert.That(g.Description, Is.EqualTo("New description text")); + } + + // Verify exactly one Description chat event was emitted. + Assert.That(factory.ChatSpy.Emissions.Count, Is.EqualTo(1)); + Assert.That(factory.ChatSpy.Emissions[0].Type, Is.EqualTo(GuildChatMessageType.Description)); + Assert.That(factory.ChatSpy.Emissions[0].GuildId, Is.EqualTo(guildId)); + Assert.That(factory.ChatSpy.Emissions[0].ActorId, Is.EqualTo(leaderId)); + } + + [Test] + public async Task UpdateDescriptionAsync_non_leader_returns_PermissionDenied() + { + using var factory = new SVSimTestFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_011UL, "DescLeader2"); + var memberId = await factory.SeedViewerAsync(76_561_198_200_000_012UL, "DescMember2"); + + int guildId; + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var res = await svc.CreateAsync(leaderId, new("DescGuild2", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + Assert.That(res.IsOk, Is.True); + guildId = res.GuildId!.Value; + } + + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.GuildMembers.Add(new GuildMember { GuildId = guildId, ViewerId = memberId, Role = GuildRole.Regular, JoinedAt = DateTime.UtcNow }); + var v = await db.Viewers.FirstAsync(x => x.Id == memberId); + v.GuildId = guildId; + await db.SaveChangesAsync(); + } + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var r = await svc.UpdateDescriptionAsync(memberId, "Hacked"); + Assert.That(r.Code, Is.EqualTo(GuildOpResultCode.PermissionDenied)); + } + } + + [Test] + public async Task UpdateDescriptionAsync_over_512_chars_returns_NameInvalid() + { + using var factory = new SVSimTestFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_013UL, "DescLeader3"); + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + await svc.CreateAsync(leaderId, new("DescGuild3", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + } + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var tooLong = new string('x', 513); + var r = await svc.UpdateDescriptionAsync(leaderId, tooLong); + Assert.That(r.Code, Is.EqualTo(GuildOpResultCode.NameInvalid)); + } + } + + // ----------------------------------------------------------------------- + // UpdateEmblemAsync + // ----------------------------------------------------------------------- + + [Test] + public async Task UpdateEmblemAsync_leader_updates_emblem_id() + { + using var factory = new SVSimTestFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_020UL, "EmblemLeader1"); + + int guildId; + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var res = await svc.CreateAsync(leaderId, new("EmblemGuild1", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + Assert.That(res.IsOk, Is.True); + guildId = res.GuildId!.Value; + } + + const long newEmblemId = 100_000_042L; + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var r = await svc.UpdateEmblemAsync(leaderId, newEmblemId); + Assert.That(r.IsOk, Is.True); + } + + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var g = await db.Guilds.FirstAsync(x => x.GuildId == guildId); + Assert.That(g.EmblemId, Is.EqualTo(newEmblemId)); + } + } + + [Test] + public async Task UpdateEmblemAsync_non_leader_returns_PermissionDenied() + { + using var factory = new SVSimTestFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_021UL, "EmblemLeader2"); + var memberId = await factory.SeedViewerAsync(76_561_198_200_000_022UL, "EmblemMember2"); + + int guildId; + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var res = await svc.CreateAsync(leaderId, new("EmblemGuild2", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + Assert.That(res.IsOk, Is.True); + guildId = res.GuildId!.Value; + } + + using (var scope = factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.GuildMembers.Add(new GuildMember { GuildId = guildId, ViewerId = memberId, Role = GuildRole.Regular, JoinedAt = DateTime.UtcNow }); + var v = await db.Viewers.FirstAsync(x => x.Id == memberId); + v.GuildId = guildId; + await db.SaveChangesAsync(); + } + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var r = await svc.UpdateEmblemAsync(memberId, 999L); + Assert.That(r.Code, Is.EqualTo(GuildOpResultCode.PermissionDenied)); + } + } + + [Test] + public async Task UpdateEmblemAsync_does_NOT_emit_chat_event() + { + using var factory = new SpyGuildChatFactory(); + var leaderId = await factory.SeedViewerAsync(76_561_198_200_000_023UL, "EmblemLeader3"); + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + await svc.CreateAsync(leaderId, new("EmblemGuild3", (int)GuildActivity.All, (int)GuildJoinCondition.Free)); + } + + // Drain CreateGuild system event. + factory.ChatSpy.Emissions.Clear(); + + using (var scope = factory.Services.CreateScope()) + { + var svc = scope.ServiceProvider.GetRequiredService(); + var r = await svc.UpdateEmblemAsync(leaderId, 100_000_099L); + Assert.That(r.IsOk, Is.True); + } + + Assert.That(factory.ChatSpy.Emissions.Count, Is.EqualTo(0), + "UpdateEmblemAsync must NOT emit a chat system event"); + } +}