feat(guild): /guild/create + populated /guild/info
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -27,4 +27,10 @@ public interface IViewerRepository
|
||||
/// doesn't exist.
|
||||
/// </summary>
|
||||
Task<Models.Viewer?> LoadForMatchContextAsync(long viewerId);
|
||||
|
||||
/// <summary>Sets Viewer.GuildId to <paramref name="guildId"/>. No-op if the viewer does not exist.</summary>
|
||||
Task SetGuildIdAsync(long viewerId, int guildId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Clears Viewer.GuildId to null. No-op if the viewer does not exist.</summary>
|
||||
Task ClearGuildIdAsync(long viewerId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -268,6 +268,24 @@ public class ViewerRepository : IViewerRepository
|
||||
.FirstOrDefaultAsync(v => v.Id == viewerId);
|
||||
}
|
||||
|
||||
public async Task SetGuildIdAsync(long viewerId, int guildId, CancellationToken ct = default)
|
||||
{
|
||||
var viewer = await _dbContext.Set<Models.Viewer>()
|
||||
.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
|
||||
if (viewer is null) return;
|
||||
viewer.GuildId = guildId;
|
||||
await _dbContext.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task ClearGuildIdAsync(long viewerId, CancellationToken ct = default)
|
||||
{
|
||||
var viewer = await _dbContext.Set<Models.Viewer>()
|
||||
.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
|
||||
if (viewer is null) return;
|
||||
viewer.GuildId = null;
|
||||
await _dbContext.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
private async Task<Models.Viewer> BuildDefaultViewer(string displayName, int initialTutorialState = 1)
|
||||
{
|
||||
Models.Viewer viewer = new Models.Viewer
|
||||
|
||||
26
SVSim.Database/Services/Guild/GuildIdGenerator.cs
Normal file
26
SVSim.Database/Services/Guild/GuildIdGenerator.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace SVSim.Database.Services.Guild;
|
||||
|
||||
public interface IGuildIdGenerator
|
||||
{
|
||||
Task<int> NextAsync(Func<int, CancellationToken, Task<bool>> existsAsync, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class GuildIdGenerator : IGuildIdGenerator
|
||||
{
|
||||
private static readonly System.Security.Cryptography.RandomNumberGenerator _rng =
|
||||
System.Security.Cryptography.RandomNumberGenerator.Create();
|
||||
|
||||
public async Task<int> NextAsync(Func<int, CancellationToken, Task<bool>> existsAsync, CancellationToken ct)
|
||||
{
|
||||
const int min = 100_000_000, max = 999_999_999;
|
||||
const int attempts = 16;
|
||||
for (int i = 0; i < attempts; i++)
|
||||
{
|
||||
var bytes = new byte[4]; _rng.GetBytes(bytes);
|
||||
var raw = BitConverter.ToUInt32(bytes, 0);
|
||||
int id = min + (int)(raw % (uint)(max - min + 1));
|
||||
if (!await existsAsync(id, ct)) return id;
|
||||
}
|
||||
throw new InvalidOperationException("Exhausted guild_id collision retries — id space too saturated.");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database.Entities.Guild;
|
||||
using SVSim.Database.Repositories.Guild;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
using SVSim.Database.Services;
|
||||
|
||||
namespace SVSim.Database.Services.Guild;
|
||||
@@ -11,6 +14,10 @@ public sealed class GuildService : IGuildService
|
||||
private readonly IGuildJoinRequestRepository _joinRequests;
|
||||
private readonly IGuildChatMessageRepository _chatMessages;
|
||||
private readonly IGameConfigService _config;
|
||||
private readonly IGuildIdGenerator _idGen;
|
||||
private readonly IGuildChatService _chat;
|
||||
private readonly IViewerRepository _viewers;
|
||||
private readonly SVSimDbContext _db;
|
||||
|
||||
public GuildService(
|
||||
IGuildRepository guilds,
|
||||
@@ -18,7 +25,11 @@ public sealed class GuildService : IGuildService
|
||||
IGuildInviteRepository invites,
|
||||
IGuildJoinRequestRepository joinRequests,
|
||||
IGuildChatMessageRepository chatMessages,
|
||||
IGameConfigService config)
|
||||
IGameConfigService config,
|
||||
IGuildIdGenerator idGen,
|
||||
IGuildChatService chat,
|
||||
IViewerRepository viewers,
|
||||
SVSimDbContext db)
|
||||
{
|
||||
_guilds = guilds;
|
||||
_members = members;
|
||||
@@ -26,20 +37,91 @@ public sealed class GuildService : IGuildService
|
||||
_joinRequests = joinRequests;
|
||||
_chatMessages = chatMessages;
|
||||
_config = config;
|
||||
_idGen = idGen;
|
||||
_chat = chat;
|
||||
_viewers = viewers;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
// Returns null so /guild/info can short-circuit to a non-joined response in Phase 1.
|
||||
public Task<GuildFullView?> GetMyGuildAsync(long viewerId, CancellationToken ct = default)
|
||||
=> Task.FromResult<GuildFullView?>(null);
|
||||
public async Task<GuildFullView?> GetMyGuildAsync(long viewerId, CancellationToken ct = default)
|
||||
{
|
||||
var membership = await _members.GetMembershipAsync(viewerId, ct);
|
||||
if (membership is null) return null;
|
||||
|
||||
var guild = await _guilds.GetWithMembersAsync(membership.GuildId, ct);
|
||||
if (guild is null || guild.BreakupAt is not null) return null;
|
||||
|
||||
int joinReqCount = await _joinRequests.CountPendingForGuildAsync(guild.GuildId, ct);
|
||||
int inviteCount = await _invites.CountPendingForInviteeAsync(viewerId, ct);
|
||||
|
||||
return new GuildFullView(guild, guild.Members, joinReqCount, inviteCount);
|
||||
}
|
||||
|
||||
public Task<Entities.Guild.Guild?> GetActiveAsync(int guildId, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
=> _guilds.GetActiveByIdAsync(guildId, ct);
|
||||
|
||||
public Task<IReadOnlyList<GuildSearchEntry>> SearchAsync(string name, int activity, int joinCondition, int memberBucket, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<GuildOpResult> CreateAsync(long viewerId, CreateGuildRequest req, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
public async Task<GuildOpResult> CreateAsync(long viewerId, CreateGuildRequest req, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.Name) || req.Name.Length > 64)
|
||||
return new(GuildOpResultCode.NameInvalid);
|
||||
if (req.Activity is < 1 or > 16)
|
||||
return new(GuildOpResultCode.NameInvalid);
|
||||
if (req.JoinCondition is < 1 or > 3)
|
||||
return new(GuildOpResultCode.NameInvalid);
|
||||
|
||||
if (await _members.GetMembershipAsync(viewerId, ct) is not null)
|
||||
return new(GuildOpResultCode.AlreadyInGuild);
|
||||
if (await _guilds.NameExistsAsync(req.Name, ct))
|
||||
return new(GuildOpResultCode.NameTaken);
|
||||
|
||||
var viewer = await _db.Viewers
|
||||
.Include(v => v.Info.SelectedEmblem)
|
||||
.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
|
||||
if (viewer is null) return new(GuildOpResultCode.PermissionDenied);
|
||||
|
||||
var id = await _idGen.NextAsync(
|
||||
(cand, c) => _guilds.GetByIdAsync(cand, c).ContinueWith(t => t.Result is not null, c),
|
||||
ct);
|
||||
var now = DateTime.UtcNow;
|
||||
var g = new Entities.Guild.Guild
|
||||
{
|
||||
GuildId = id,
|
||||
Name = req.Name,
|
||||
Description = "",
|
||||
LeaderViewerId = viewerId,
|
||||
EmblemId = DefaultEmblemId(viewer),
|
||||
Activity = (GuildActivity)req.Activity,
|
||||
JoinCondition = (GuildJoinCondition)req.JoinCondition,
|
||||
CreatedAt = now,
|
||||
};
|
||||
await _guilds.AddAsync(g, ct);
|
||||
await _members.AddAsync(
|
||||
new GuildMember { GuildId = id, ViewerId = viewerId, Role = GuildRole.Leader, JoinedAt = now },
|
||||
ct);
|
||||
|
||||
// Side-effects: clear any pending invites + cancel pending join requests for this viewer.
|
||||
await _invites.ConsumePendingForViewerAsync(viewerId, now, ct);
|
||||
await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct);
|
||||
|
||||
// Update Viewer.GuildId pointer for quick lookups.
|
||||
viewer.GuildId = id;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
|
||||
await _chat.EmitSystemEventAsync(id, viewerId, GuildChatMessageType.CreateGuild, body: null, ct);
|
||||
return new(GuildOpResultCode.Ok, GuildId: id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the viewer's currently-equipped emblem id, or 100000000 (default) if none.
|
||||
/// </summary>
|
||||
private static long DefaultEmblemId(Models.Viewer viewer)
|
||||
{
|
||||
var emblemId = viewer.Info?.SelectedEmblem?.Id;
|
||||
return emblemId is > 0 ? emblemId.Value : 100_000_000L;
|
||||
}
|
||||
|
||||
public Task<GuildOpResult> UpdateAsync(long viewerId, UpdateGuildRequest req, CancellationToken ct = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Entities.Guild;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.Services;
|
||||
using SVSim.Database.Services.Guild;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Guild;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
|
||||
@@ -14,11 +19,16 @@ public sealed class GuildController : SVSimController
|
||||
{
|
||||
private readonly IGuildService _guild;
|
||||
private readonly IGameConfigService _configs;
|
||||
private readonly SVSimDbContext _db;
|
||||
|
||||
public GuildController(IGuildService guild, IGameConfigService configs)
|
||||
// Wire error code returned when guild operations fail (non-1 result_code).
|
||||
private const int GuildErrorResultCode = 2;
|
||||
|
||||
public GuildController(IGuildService guild, IGameConfigService configs, SVSimDbContext db)
|
||||
{
|
||||
_guild = guild;
|
||||
_configs = configs;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpPost("info")]
|
||||
@@ -39,8 +49,14 @@ public sealed class GuildController : SVSimController
|
||||
}
|
||||
else
|
||||
{
|
||||
resp.GuildStatus = 2; // JOINING — fully populated path lands in Task 7
|
||||
// Task 7 populates `Guild`, JoinRequestCount, InviteCount.
|
||||
resp.GuildStatus = 2; // JOINING
|
||||
resp.JoinRequestCount = view.JoinRequestCount;
|
||||
resp.InviteCount = view.InviteCount;
|
||||
resp.Guild = new GuildBundle
|
||||
{
|
||||
Detail = ToDetailDto(view.Guild, view.Members.Count),
|
||||
Members = await ToMemberDtoListAsync(view.Members, viewerId, ct),
|
||||
};
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
@@ -48,7 +64,13 @@ public sealed class GuildController : SVSimController
|
||||
// ===== 21 remaining stubs — each returns the response DTO with defaults =====
|
||||
|
||||
[HttpPost("create")]
|
||||
public Task<ActionResult<EmptyResponse>> Create([FromBody] GuildCreateRequest req, CancellationToken ct) => Stub();
|
||||
public async Task<ActionResult<EmptyResponse>> Create([FromBody] GuildCreateRequest req, CancellationToken ct)
|
||||
{
|
||||
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
|
||||
var res = await _guild.CreateAsync(viewerId, new(req.GuildName, req.Activity, req.JoinCondition), ct);
|
||||
if (res.IsOk) return new EmptyResponse();
|
||||
return MapErrorToWire(res);
|
||||
}
|
||||
|
||||
[HttpPost("breakup")]
|
||||
public Task<ActionResult<EmptyResponse>> Breakup([FromBody] BaseRequest _, CancellationToken ct) => Stub();
|
||||
@@ -124,6 +146,68 @@ public sealed class GuildController : SVSimController
|
||||
public Task<ActionResult<GuildChangeRoleResponse>> ChangeRole([FromBody] GuildChangeRoleRequest req, CancellationToken ct)
|
||||
=> Task.FromResult<ActionResult<GuildChangeRoleResponse>>(new GuildChangeRoleResponse { Members = new() });
|
||||
|
||||
// ===== Private helpers =====
|
||||
|
||||
private static GuildDetailDto ToDetailDto(SVSim.Database.Entities.Guild.Guild guild, int memberCount) => new()
|
||||
{
|
||||
GuildId = guild.GuildId,
|
||||
GuildName = guild.Name,
|
||||
Description = guild.Description,
|
||||
GuildEmblemId = guild.EmblemId,
|
||||
JoinCondition = (int)guild.JoinCondition,
|
||||
Activity = (int)guild.Activity,
|
||||
MemberNum = memberCount,
|
||||
LeaderViewerId = guild.LeaderViewerId,
|
||||
// LeaderName will be filled via members list join — leave "" here; caller populates it from member data if needed.
|
||||
LeaderName = "",
|
||||
};
|
||||
|
||||
private async Task<List<GuildMemberInfoDto>> ToMemberDtoListAsync(
|
||||
IReadOnlyList<GuildMember> members,
|
||||
long callerViewerId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (members.Count == 0) return new();
|
||||
|
||||
var viewerIds = members.Select(m => m.ViewerId).ToList();
|
||||
|
||||
// Batch-load viewer rows with Info + SelectedEmblem + SelectedDegree.
|
||||
var viewers = await _db.Viewers
|
||||
.AsNoTracking()
|
||||
.Include(v => v.Info.SelectedEmblem)
|
||||
.Include(v => v.Info.SelectedDegree)
|
||||
.Where(v => viewerIds.Contains(v.Id))
|
||||
.ToDictionaryAsync(v => v.Id, ct);
|
||||
|
||||
var result = new List<GuildMemberInfoDto>(members.Count);
|
||||
foreach (var m in members)
|
||||
{
|
||||
viewers.TryGetValue(m.ViewerId, out var v);
|
||||
result.Add(ToMemberDto(m, v));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static GuildMemberInfoDto ToMemberDto(GuildMember member, Viewer? viewer) => new()
|
||||
{
|
||||
ViewerId = member.ViewerId,
|
||||
Name = viewer?.DisplayName ?? "",
|
||||
EmblemId = viewer?.Info?.SelectedEmblem?.Id is > 0 ? viewer.Info.SelectedEmblem.Id : 100_000_000L,
|
||||
CountryCode = viewer?.Info?.CountryCode ?? "",
|
||||
Rank = 1, // TODO: populate from actual rank data when rank tracking lands
|
||||
DegreeId = viewer?.Info?.SelectedDegree?.Id ?? 0,
|
||||
IsOfficialMarkDisplayed = viewer?.Info?.IsOfficialMarkDisplayed == true ? 1 : 0,
|
||||
Role = (int)member.Role,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps a failed GuildOpResult to the wire error envelope convention used throughout the
|
||||
/// codebase (see CampaignController.Fail(), AchievementController, MissionController).
|
||||
/// Returns HTTP 200 with a JSON body carrying <c>result_code: 2</c>.
|
||||
/// </summary>
|
||||
private ActionResult<EmptyResponse> MapErrorToWire(GuildOpResult res)
|
||||
=> Ok(new { result_code = GuildErrorResultCode });
|
||||
|
||||
private static Task<ActionResult<EmptyResponse>> Stub() =>
|
||||
Task.FromResult<ActionResult<EmptyResponse>>(new EmptyResponse());
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ public class Program
|
||||
builder.Services.AddScoped<SVSim.Database.Repositories.Guild.IGuildInviteRepository, SVSim.Database.Repositories.Guild.GuildInviteRepository>();
|
||||
builder.Services.AddScoped<SVSim.Database.Repositories.Guild.IGuildJoinRequestRepository, SVSim.Database.Repositories.Guild.GuildJoinRequestRepository>();
|
||||
builder.Services.AddScoped<SVSim.Database.Repositories.Guild.IGuildChatMessageRepository, SVSim.Database.Repositories.Guild.GuildChatMessageRepository>();
|
||||
builder.Services.AddSingleton<SVSim.Database.Services.Guild.IGuildIdGenerator, SVSim.Database.Services.Guild.GuildIdGenerator>();
|
||||
builder.Services.AddScoped<SVSim.Database.Services.Guild.IGuildService, SVSim.Database.Services.Guild.GuildService>();
|
||||
builder.Services.AddScoped<SVSim.Database.Services.Guild.IGuildChatService, SVSim.Database.Services.Guild.GuildChatService>();
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Integration.Guild;
|
||||
|
||||
public class GuildCreateInfoFlowTests
|
||||
{
|
||||
[Test]
|
||||
public async Task PostingCreate_then_Info_returns_populated_guild()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync(
|
||||
steamId: 76_561_198_200_000_001UL,
|
||||
displayName: "AlphaIntPlayer");
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
// POST /guild/create — mirror the arena integration test style: include the base-request fields
|
||||
const string Vid = "0"; const int Sid = 0; const string Stk = "";
|
||||
var create = await client.PostAsync("/guild/create",
|
||||
System.Net.Http.Json.JsonContent.Create(new { guild_name = "AlphaInt", activity = 1, join_condition = 1,
|
||||
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||
Assert.That(create.IsSuccessStatusCode, Is.True, $"create failed ({create.StatusCode}): {await create.Content.ReadAsStringAsync()}");
|
||||
|
||||
// POST /guild/info — expect JOINING state with guild populated
|
||||
var info = await client.PostAsync("/guild/info",
|
||||
System.Net.Http.Json.JsonContent.Create(new { viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk }));
|
||||
var json = await info.Content.ReadAsStringAsync();
|
||||
Assert.That(info.IsSuccessStatusCode, Is.True, $"info failed: {json}");
|
||||
|
||||
// In test mode (no UnityPlayer user-agent) the translation middleware is a no-op;
|
||||
// the controller's raw DTO is returned directly (no {data_headers, data} envelope).
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
Assert.That(root.GetProperty("guild_status").GetString(), Is.EqualTo("2"),
|
||||
"guild_status should be JOINING (2)");
|
||||
Assert.That(root.GetProperty("guild").GetProperty("detail").GetProperty("guild_name").GetString(),
|
||||
Is.EqualTo("AlphaInt"));
|
||||
Assert.That(root.GetProperty("guild").GetProperty("members").GetArrayLength(),
|
||||
Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostingCreate_twice_returns_error_second_time()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync(
|
||||
steamId: 76_561_198_200_000_002UL,
|
||||
displayName: "DoubleCreatePlayer");
|
||||
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
const string Vid = "0"; const int Sid = 0; const string Stk = "";
|
||||
var createBody = new { guild_name = "OnlyOnce", activity = 1, join_condition = 1,
|
||||
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk };
|
||||
|
||||
// First create should succeed
|
||||
var first = await client.PostAsync("/guild/create",
|
||||
System.Net.Http.Json.JsonContent.Create(createBody));
|
||||
Assert.That(first.IsSuccessStatusCode, Is.True, $"First create failed: {await first.Content.ReadAsStringAsync()}");
|
||||
var firstJson = await first.Content.ReadAsStringAsync();
|
||||
using var firstDoc = JsonDocument.Parse(firstJson);
|
||||
// In test mode (no envelope), result_code may be present (failure) or absent (success EmptyResponse).
|
||||
// Success path returns EmptyResponse ({}), so result_code will not be present.
|
||||
var firstRoot = firstDoc.RootElement;
|
||||
if (firstRoot.TryGetProperty("result_code", out var rc1))
|
||||
{
|
||||
// If present it must not be 2 (error code).
|
||||
Assert.That(rc1.GetInt32(), Is.Not.EqualTo(2), "First create must not error");
|
||||
}
|
||||
|
||||
// Second create for the same viewer should return an error envelope (result_code = 2).
|
||||
var secondBody = new { guild_name = "OnlyOnceAgain", activity = 1, join_condition = 1,
|
||||
viewer_id = Vid, steam_id = Sid, steam_session_ticket = Stk };
|
||||
var second = await client.PostAsync("/guild/create",
|
||||
System.Net.Http.Json.JsonContent.Create(secondBody));
|
||||
Assert.That(second.IsSuccessStatusCode, Is.True, "HTTP level should still be 200");
|
||||
var secondJson = await second.Content.ReadAsStringAsync();
|
||||
using var secondDoc = JsonDocument.Parse(secondJson);
|
||||
// Error path: the MapErrorToWire helper returns { result_code = 2 }
|
||||
Assert.That(secondDoc.RootElement.TryGetProperty("result_code", out var rc2), Is.True,
|
||||
$"Second create should return result_code field, got: {secondJson}");
|
||||
Assert.That(rc2.GetInt32(), Is.EqualTo(2), "Second create should fail with result_code=2");
|
||||
}
|
||||
}
|
||||
115
SVSim.UnitTests/Services/Guild/GuildServiceCreateTests.cs
Normal file
115
SVSim.UnitTests/Services/Guild/GuildServiceCreateTests.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database.Entities.Guild;
|
||||
using SVSim.Database.Services.Guild;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
using GuildEntity = SVSim.Database.Entities.Guild.Guild;
|
||||
|
||||
namespace SVSim.UnitTests.Services.Guild;
|
||||
|
||||
public class GuildServiceCreateTests
|
||||
{
|
||||
private static async Task<long> CreateViewerAsync(SVSimTestFactory factory, ulong steamId = 76_561_198_100_000_001UL, string name = "GuildTestViewer")
|
||||
=> await factory.SeedViewerAsync(steamId: steamId, displayName: name);
|
||||
|
||||
private static IGuildService GuildService(SVSimTestFactory factory, out IServiceScope scope)
|
||||
{
|
||||
scope = factory.Services.CreateScope();
|
||||
return scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_makes_a_guild_with_caller_as_leader()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await CreateViewerAsync(factory, 76_561_198_100_000_001UL, "Alpha Leader");
|
||||
|
||||
GuildOpResult res;
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
res = await svc.CreateAsync(viewerId, new("Alpha", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
|
||||
}
|
||||
|
||||
Assert.That(res.IsOk, Is.True);
|
||||
Assert.That(res.GuildId, Is.Not.Null);
|
||||
|
||||
GuildFullView? view;
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
view = await svc.GetMyGuildAsync(viewerId);
|
||||
}
|
||||
|
||||
Assert.That(view, Is.Not.Null);
|
||||
Assert.That(view!.Guild.Name, Is.EqualTo("Alpha"));
|
||||
Assert.That(view.Guild.LeaderViewerId, Is.EqualTo(viewerId));
|
||||
Assert.That(view.Members.Single().Role, Is.EqualTo(GuildRole.Leader));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_returns_AlreadyInGuild_when_viewer_already_in_a_guild()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await CreateViewerAsync(factory, 76_561_198_100_000_002UL, "Already In");
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
var first = await svc.CreateAsync(viewerId, new("FirstGuild", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
|
||||
Assert.That(first.IsOk, Is.True);
|
||||
}
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
var second = await svc.CreateAsync(viewerId, new("SecondGuild", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
|
||||
Assert.That(second.Code, Is.EqualTo(GuildOpResultCode.AlreadyInGuild));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_returns_NameTaken_when_guild_name_already_exists()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var v1 = await CreateViewerAsync(factory, 76_561_198_100_000_003UL, "Viewer1");
|
||||
var v2 = await CreateViewerAsync(factory, 76_561_198_100_000_004UL, "Viewer2");
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
var first = await svc.CreateAsync(v1, new("SameName", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
|
||||
Assert.That(first.IsOk, Is.True);
|
||||
}
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
var second = await svc.CreateAsync(v2, new("SameName", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
|
||||
Assert.That(second.Code, Is.EqualTo(GuildOpResultCode.NameTaken));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_returns_NameInvalid_for_empty_name()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await CreateViewerAsync(factory, 76_561_198_100_000_005UL);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
var res = await svc.CreateAsync(viewerId, new("", (int)GuildActivity.All, (int)GuildJoinCondition.Free));
|
||||
Assert.That(res.Code, Is.EqualTo(GuildOpResultCode.NameInvalid));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetMyGuildAsync_returns_null_for_viewer_not_in_any_guild()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var viewerId = await CreateViewerAsync(factory, 76_561_198_100_000_006UL);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var svc = scope.ServiceProvider.GetRequiredService<IGuildService>();
|
||||
var view = await svc.GetMyGuildAsync(viewerId);
|
||||
Assert.That(view, Is.Null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user