feat(guild): update + update_description + update_emblem

This commit is contained in:
gamer147
2026-06-27 12:47:00 -04:00
parent f9361fa467
commit c0e1c10192
5 changed files with 482 additions and 11 deletions

View File

@@ -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<ChatWindow> GetWindowAsync(long v, int s, int d, int w, CancellationToken ct = default) => throw new NotImplementedException();
public Task<ChatPostResult> PostTextOrStampAsync(long v, int t, string m, CancellationToken ct = default) => throw new NotImplementedException();
public Task<ChatPostResult> PostDeckAsync(long v, string j, CancellationToken ct = default) => throw new NotImplementedException();
public Task<bool> DeleteDeckAsync(long v, int id, CancellationToken ct = default) => throw new NotImplementedException();
public Task<ChatPostResult> PostReplayAsync(long v, string j, CancellationToken ct = default) => throw new NotImplementedException();
public Task<string?> GetReplayDetailAsync(long v, int id, CancellationToken ct = default) => throw new NotImplementedException();
public Task<string?> 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<IGuildChatService>(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<IGuildService>();
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<IGuildService>();
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<SVSimDbContext>();
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<IGuildService>();
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<SVSimDbContext>();
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<IGuildService>();
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<IGuildService>();
await svc.CreateAsync(leaderId, new("UpdateGuild3", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
}
using (var scope = factory.Services.CreateScope())
{
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
// 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<IGuildService>();
await svc.CreateAsync(leaderId, new("UpdateGuild4", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
}
using (var scope = factory.Services.CreateScope())
{
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
// 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<IGuildService>();
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<IGuildService>();
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<SVSimDbContext>();
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<IGuildService>();
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<SVSimDbContext>();
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<IGuildService>();
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<IGuildService>();
await svc.CreateAsync(leaderId, new("DescGuild3", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
}
using (var scope = factory.Services.CreateScope())
{
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
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<IGuildService>();
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<IGuildService>();
var r = await svc.UpdateEmblemAsync(leaderId, newEmblemId);
Assert.That(r.IsOk, Is.True);
}
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
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<IGuildService>();
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<SVSimDbContext>();
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<IGuildService>();
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<IGuildService>();
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<IGuildService>();
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");
}
}