baa25f2e1e
PR Checks / test-and-build (pull_request) Successful in 7m6s
- Add V020 migration: player_links + identity_audit_log tables - Add ISessionStore methods: ResolveEffectivePlayerId, LinkIdentity, UnlinkIdentity, GetLinkedIdentities - Update SessionService to resolve effective player id for all permission checks - Add /auth/discord/callback linking flow when already authenticated - Add /api/me/identities GET/DELETE endpoints - Add Profile.razor page for managing linked accounts - Update NavMenu with profile link and v3.0.0 badge - Bump version to 3.0.0 across all files Bump version → 3.0.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1156 lines
46 KiB
C#
1156 lines
46 KiB
C#
using GmRelay.Web.Services;
|
|
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Http;
|
|
using GmRelay.Shared.Domain;
|
|
|
|
namespace GmRelay.Bot.Tests.Web;
|
|
|
|
public sealed class AuthorizedSessionServiceTests
|
|
{
|
|
private static IHttpContextAccessor CreateAccessor(string externalUserId, string? name = null)
|
|
{
|
|
var claims = new List<Claim>
|
|
{
|
|
new Claim(ClaimTypes.NameIdentifier, externalUserId),
|
|
new Claim("TelegramId", externalUserId),
|
|
new Claim("Platform", "Telegram")
|
|
};
|
|
if (name is not null)
|
|
claims.Add(new Claim(ClaimTypes.Name, name));
|
|
|
|
var identity = new ClaimsIdentity(claims, "Test");
|
|
var principal = new ClaimsPrincipal(identity);
|
|
var httpContext = new DefaultHttpContext { User = principal };
|
|
return new HttpContextAccessor { HttpContext = httpContext };
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetUpcomingSessionsForGmAsync_ReturnsSessions_WhenGroupBelongsToGm()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var sessions = await service.GetUpcomingSessionsForCurrentUserAsync(groupId);
|
|
|
|
Assert.NotNull(sessions);
|
|
Assert.Single(sessions);
|
|
Assert.Equal("Session A", sessions[0].Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetUpcomingSessionsForGmAsync_ReturnsSessions_WhenUserIsCoGm()
|
|
{
|
|
var ownerId = 1001L;
|
|
var coGmId = 2002L;
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", ownerId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
],
|
|
managers:
|
|
[
|
|
new(groupId, coGmId, GroupManagerRole.CoGm)
|
|
]);
|
|
var accessor = CreateAccessor(coGmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var sessions = await service.GetUpcomingSessionsForCurrentUserAsync(groupId);
|
|
|
|
Assert.NotNull(sessions);
|
|
Assert.Single(sessions);
|
|
Assert.Equal("Session A", sessions[0].Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetUpcomingSessionsForGmAsync_ReturnsNull_WhenGroupBelongsToAnotherGm()
|
|
{
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", 2002L)
|
|
]);
|
|
var accessor = CreateAccessor("1001");
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var sessions = await service.GetUpcomingSessionsForCurrentUserAsync(groupId);
|
|
|
|
Assert.Null(sessions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetSessionForGmAsync_ReturnsSession_WhenSessionBelongsToOwnedGroup()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var session = await service.GetSessionForCurrentUserAsync(sessionId);
|
|
|
|
Assert.NotNull(session);
|
|
Assert.Equal(sessionId, session.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetSessionForGmAsync_ReturnsNull_WhenSessionBelongsToAnotherGm()
|
|
{
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", 2002L)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor("1001");
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var session = await service.GetSessionForCurrentUserAsync(sessionId);
|
|
|
|
Assert.Null(session);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateSessionForGmAsync_Throws_WhenSessionBelongsToAnotherGm()
|
|
{
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", 2002L)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor("1001");
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var action = () => service.UpdateSessionForCurrentUserAsync(sessionId, "Updated", DateTime.UtcNow.AddDays(1), "https://example.test/b", 5);
|
|
|
|
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
|
Assert.False(store.UpdateCalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateSessionForGmAsync_UpdatesOwnedSession()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var scheduledAt = DateTime.UtcNow.AddDays(1);
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.UpdateSessionForCurrentUserAsync(sessionId, "Updated", scheduledAt, "https://example.test/b", 5);
|
|
|
|
Assert.True(store.UpdateCalled);
|
|
Assert.Equal(groupId, store.LastUpdatedGroupId);
|
|
Assert.Equal(sessionId, store.LastUpdatedSessionId);
|
|
Assert.Equal("Updated", store.LastUpdatedTitle);
|
|
Assert.Equal(scheduledAt, store.LastUpdatedScheduledAt);
|
|
Assert.Equal("https://example.test/b", store.LastUpdatedJoinLink);
|
|
Assert.Equal(5, store.LastUpdatedMaxPlayers);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateSessionForGmAsync_LogsAudit_WhenTitleChanges()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var originalTime = DateTime.UtcNow;
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", originalTime, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.UpdateSessionForCurrentUserAsync(sessionId, "Updated Title", originalTime, "https://example.test/a", 4);
|
|
|
|
Assert.Single(store.LogEntries);
|
|
Assert.Equal("Title", store.LogEntries[0].ChangeType);
|
|
Assert.Equal("Session A", store.LogEntries[0].OldValue);
|
|
Assert.Equal("Updated Title", store.LogEntries[0].NewValue);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateSessionForGmAsync_LogsMultipleAudits_WhenMultipleFieldsChange()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var originalTime = DateTime.UtcNow;
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", originalTime, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var newTime = originalTime.AddDays(1);
|
|
await service.UpdateSessionForCurrentUserAsync(sessionId, "Updated Title", newTime, "https://example.test/b", 5);
|
|
|
|
Assert.Equal(4, store.LogEntries.Count);
|
|
Assert.Contains(store.LogEntries, e => e.ChangeType == "Title");
|
|
Assert.Contains(store.LogEntries, e => e.ChangeType == "Time");
|
|
Assert.Contains(store.LogEntries, e => e.ChangeType == "Link");
|
|
Assert.Contains(store.LogEntries, e => e.ChangeType == "MaxPlayers");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateSessionForGmAsync_DoesNotLogAudit_WhenNothingChanges()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var originalTime = DateTime.UtcNow;
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", originalTime, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.UpdateSessionForCurrentUserAsync(sessionId, "Session A", originalTime, "https://example.test/a", 4);
|
|
|
|
Assert.Empty(store.LogEntries);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetSessionHistoryForGmAsync_ReturnsHistory_WhenSessionBelongsToOwnedGroup()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var history = await service.GetSessionHistoryForCurrentUserAsync(sessionId);
|
|
|
|
Assert.NotNull(history);
|
|
Assert.Empty(history);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetSessionHistoryForGmAsync_ReturnsNull_WhenSessionBelongsToAnotherGm()
|
|
{
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", 2002L)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor("1001");
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var history = await service.GetSessionHistoryForCurrentUserAsync(sessionId);
|
|
|
|
Assert.Null(history);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PromoteWaitlistedPlayerForGmAsync_PromotesOwnedSession()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 3, 1)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.PromoteWaitlistedPlayerForCurrentUserAsync(sessionId);
|
|
|
|
Assert.True(store.PromoteCalled);
|
|
Assert.Equal(groupId, store.LastPromotedGroupId);
|
|
Assert.Equal(sessionId, store.LastPromotedSessionId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateBatchDetailsForGmAsync_Throws_WhenBatchBelongsToAnotherGm()
|
|
{
|
|
var batchId = Guid.NewGuid();
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", 2002L)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor("1001");
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var action = () => service.UpdateBatchDetailsForCurrentUserAsync(batchId, "Updated", "https://example.test/b");
|
|
|
|
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
|
Assert.False(store.UpdateBatchDetailsCalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateBatchDetailsForGmAsync_UpdatesOwnedBatch()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var batchId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.UpdateBatchDetailsForCurrentUserAsync(batchId, "Updated", "https://example.test/b");
|
|
|
|
Assert.True(store.UpdateBatchDetailsCalled);
|
|
Assert.Equal(batchId, store.LastUpdatedBatchId);
|
|
Assert.Equal(groupId, store.LastUpdatedBatchGroupId);
|
|
Assert.Equal("Updated", store.LastUpdatedBatchTitle);
|
|
Assert.Equal("https://example.test/b", store.LastUpdatedBatchJoinLink);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateBatchDetailsForGmAsync_UpdatesBatch_WhenUserIsCoGm()
|
|
{
|
|
var ownerId = 1001L;
|
|
var coGmId = 2002L;
|
|
var groupId = Guid.NewGuid();
|
|
var batchId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", ownerId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
|
],
|
|
managers:
|
|
[
|
|
new(groupId, coGmId, GroupManagerRole.CoGm)
|
|
]);
|
|
var accessor = CreateAccessor(coGmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.UpdateBatchDetailsForCurrentUserAsync(batchId, "Updated", "https://example.test/b");
|
|
|
|
Assert.True(store.UpdateBatchDetailsCalled);
|
|
Assert.Equal(batchId, store.LastUpdatedBatchId);
|
|
Assert.Equal(groupId, store.LastUpdatedBatchGroupId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddCoGmForOwnerAsync_AddsCoGm_WhenUserIsOwner()
|
|
{
|
|
var ownerId = 1001L;
|
|
var coGmId = 2002L;
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", ownerId)
|
|
]);
|
|
var accessor = CreateAccessor(ownerId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.AddCoGmForOwnerAsync(groupId, "Telegram", coGmId.ToString(), "Assistant GM", "assistant");
|
|
|
|
Assert.True(store.AddCoGmCalled);
|
|
Assert.Equal(groupId, store.LastAddedCoGmGroupId);
|
|
Assert.Equal(coGmId, store.LastAddedCoGmTelegramId);
|
|
Assert.Equal("Assistant GM", store.LastAddedCoGmDisplayName);
|
|
Assert.Equal("assistant", store.LastAddedCoGmUsername);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddCoGmForOwnerAsync_Throws_WhenUserIsCoGm()
|
|
{
|
|
var ownerId = 1001L;
|
|
var coGmId = 2002L;
|
|
var newCoGmId = 3003L;
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", ownerId)
|
|
],
|
|
managers:
|
|
[
|
|
new(groupId, coGmId, GroupManagerRole.CoGm)
|
|
]);
|
|
var accessor = CreateAccessor(coGmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var action = () => service.AddCoGmForOwnerAsync(groupId, "Telegram", newCoGmId.ToString(), "Second Assistant", null);
|
|
|
|
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
|
Assert.False(store.AddCoGmCalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemoveCoGmForOwnerAsync_RemovesCoGm_WhenUserIsOwner()
|
|
{
|
|
var ownerId = 1001L;
|
|
var coGmId = 2002L;
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", ownerId)
|
|
],
|
|
managers:
|
|
[
|
|
new(groupId, coGmId, GroupManagerRole.CoGm)
|
|
]);
|
|
var accessor = CreateAccessor(ownerId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.RemoveCoGmForOwnerAsync(groupId, "Telegram", coGmId.ToString());
|
|
|
|
Assert.True(store.RemoveCoGmCalled);
|
|
Assert.Equal(groupId, store.LastRemovedCoGmGroupId);
|
|
Assert.Equal(coGmId, store.LastRemovedCoGmTelegramId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateBatchNotificationModeForGmAsync_Throws_WhenBatchBelongsToAnotherGm()
|
|
{
|
|
var batchId = Guid.NewGuid();
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", 2002L)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor("1001");
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var action = () => service.UpdateBatchNotificationModeForCurrentUserAsync(batchId, SessionNotificationMode.GroupOnly);
|
|
|
|
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
|
Assert.False(store.UpdateBatchNotificationModeCalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateBatchNotificationModeForGmAsync_UpdatesOwnedBatch()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var batchId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.UpdateBatchNotificationModeForCurrentUserAsync(batchId, SessionNotificationMode.GroupOnly);
|
|
|
|
Assert.True(store.UpdateBatchNotificationModeCalled);
|
|
Assert.Equal(batchId, store.LastUpdatedNotificationBatchId);
|
|
Assert.Equal(groupId, store.LastUpdatedNotificationGroupId);
|
|
Assert.Equal(SessionNotificationMode.GroupOnly, store.LastUpdatedNotificationMode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RescheduleBatchForGmAsync_RejectsNonPositiveInterval()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var batchId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var action = () => service.RescheduleBatchForCurrentUserAsync(batchId, DateTime.UtcNow.AddDays(7), intervalDays: 0);
|
|
|
|
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(action);
|
|
Assert.False(store.RescheduleBatchCalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RescheduleBatchForGmAsync_ReschedulesOwnedBatch()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var batchId = Guid.NewGuid();
|
|
var firstScheduledAt = DateTime.UtcNow.AddDays(7);
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.RescheduleBatchForCurrentUserAsync(batchId, firstScheduledAt, intervalDays: 14);
|
|
|
|
Assert.True(store.RescheduleBatchCalled);
|
|
Assert.Equal(batchId, store.LastRescheduledBatchId);
|
|
Assert.Equal(groupId, store.LastRescheduledBatchGroupId);
|
|
Assert.Equal(firstScheduledAt, store.LastRescheduledFirstScheduledAt);
|
|
Assert.Equal(14, store.LastRescheduledIntervalDays);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CloneBatchForGmAsync_ClonesOwnedBatch()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var batchId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
sessions:
|
|
[
|
|
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", batchId, 10, 42, 4, 1, 0)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.CloneBatchForCurrentUserAsync(batchId, BatchCloneInterval.NextWeek);
|
|
|
|
Assert.True(store.CloneBatchCalled);
|
|
Assert.Equal(batchId, store.LastClonedBatchId);
|
|
Assert.Equal(groupId, store.LastClonedBatchGroupId);
|
|
Assert.Equal(BatchCloneInterval.NextWeek, store.LastCloneInterval);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateCampaignTemplateForGmAsync_CreatesTemplate_WhenGroupBelongsToGm()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.CreateCampaignTemplateForCurrentUserAsync(
|
|
groupId,
|
|
new CreateCampaignTemplateRequest(
|
|
" Weekly arc ",
|
|
" Kingmaker ",
|
|
" https://example.test/kingmaker ",
|
|
SessionCount: 6,
|
|
IntervalDays: 7,
|
|
MaxPlayers: 5,
|
|
SessionNotificationMode.GroupOnly));
|
|
|
|
Assert.True(store.CreateCampaignTemplateCalled);
|
|
Assert.Equal(groupId, store.LastCreatedCampaignTemplateGroupId);
|
|
Assert.Equal("Weekly arc", store.LastCreatedCampaignTemplateRequest?.Name);
|
|
Assert.Equal("Kingmaker", store.LastCreatedCampaignTemplateRequest?.Title);
|
|
Assert.Equal("https://example.test/kingmaker", store.LastCreatedCampaignTemplateRequest?.JoinLink);
|
|
Assert.Equal(6, store.LastCreatedCampaignTemplateRequest?.SessionCount);
|
|
Assert.Equal(7, store.LastCreatedCampaignTemplateRequest?.IntervalDays);
|
|
Assert.Equal(5, store.LastCreatedCampaignTemplateRequest?.MaxPlayers);
|
|
Assert.Equal(SessionNotificationMode.GroupOnly, store.LastCreatedCampaignTemplateRequest?.NotificationMode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateCampaignTemplateForGmAsync_AllowsNoSeatLimit()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.CreateCampaignTemplateForCurrentUserAsync(
|
|
groupId,
|
|
new CreateCampaignTemplateRequest(
|
|
"Open table",
|
|
"West Marches",
|
|
"https://example.test/west",
|
|
8,
|
|
7,
|
|
null,
|
|
SessionNotificationMode.GroupAndDirect));
|
|
|
|
Assert.True(store.CreateCampaignTemplateCalled);
|
|
Assert.Null(store.LastCreatedCampaignTemplateRequest?.MaxPlayers);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateCampaignTemplateForGmAsync_Throws_WhenGroupBelongsToAnotherGm()
|
|
{
|
|
var groupId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", 2002L)
|
|
]);
|
|
var accessor = CreateAccessor("1001");
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var action = () => service.CreateCampaignTemplateForCurrentUserAsync(
|
|
groupId,
|
|
new CreateCampaignTemplateRequest(
|
|
"Weekly arc",
|
|
"Kingmaker",
|
|
"https://example.test/kingmaker",
|
|
SessionCount: 6,
|
|
IntervalDays: 7,
|
|
MaxPlayers: 5,
|
|
SessionNotificationMode.GroupOnly));
|
|
|
|
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
|
Assert.False(store.CreateCampaignTemplateCalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateBatchFromCampaignTemplateForGmAsync_CreatesBatch_WhenTemplateGroupBelongsToGm()
|
|
{
|
|
var gmId = 1001L;
|
|
var groupId = Guid.NewGuid();
|
|
var templateId = Guid.NewGuid();
|
|
var firstScheduledAt = DateTime.UtcNow.AddDays(3);
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", gmId)
|
|
],
|
|
templates:
|
|
[
|
|
new(templateId, groupId, "Weekly arc", "Kingmaker", "https://example.test/kingmaker", 6, 7, 5, SessionNotificationModeExtensions.GroupOnlyValue, DateTime.UtcNow, DateTime.UtcNow)
|
|
]);
|
|
var accessor = CreateAccessor(gmId.ToString());
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
await service.CreateBatchFromCampaignTemplateForCurrentUserAsync(templateId, firstScheduledAt);
|
|
|
|
Assert.True(store.CreateBatchFromTemplateCalled);
|
|
Assert.Equal(templateId, store.LastCreatedBatchTemplateId);
|
|
Assert.Equal(groupId, store.LastCreatedBatchTemplateGroupId);
|
|
Assert.Equal(firstScheduledAt, store.LastCreatedBatchFirstScheduledAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateBatchFromCampaignTemplateForGmAsync_Throws_WhenTemplateGroupBelongsToAnotherGm()
|
|
{
|
|
var groupId = Guid.NewGuid();
|
|
var templateId = Guid.NewGuid();
|
|
var store = new FakeSessionStore(
|
|
groups:
|
|
[
|
|
new(groupId, 42, "Alpha", 2002L)
|
|
],
|
|
templates:
|
|
[
|
|
new(templateId, groupId, "Weekly arc", "Kingmaker", "https://example.test/kingmaker", 6, 7, 5, SessionNotificationModeExtensions.GroupOnlyValue, DateTime.UtcNow, DateTime.UtcNow)
|
|
]);
|
|
var accessor = CreateAccessor("1001");
|
|
var service = new AuthorizedSessionService(store, accessor);
|
|
|
|
var action = () => service.CreateBatchFromCampaignTemplateForCurrentUserAsync(templateId, DateTime.UtcNow.AddDays(3));
|
|
|
|
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
|
Assert.False(store.CreateBatchFromTemplateCalled);
|
|
}
|
|
|
|
private sealed class FakeSessionStore(
|
|
IEnumerable<WebGameGroup>? groups = null,
|
|
IEnumerable<WebSession>? sessions = null,
|
|
IEnumerable<FakeGroupManager>? managers = null,
|
|
IEnumerable<WebCampaignTemplate>? templates = null) : ISessionStore
|
|
{
|
|
private readonly Dictionary<Guid, WebGameGroup> groupsById = groups?.ToDictionary(group => group.Id) ?? [];
|
|
private readonly Dictionary<Guid, WebSession> sessionsById = sessions?.ToDictionary(session => session.Id) ?? [];
|
|
private readonly Dictionary<Guid, WebCampaignTemplate> templatesById = templates?.ToDictionary(template => template.Id) ?? [];
|
|
private readonly List<FakeGroupManager> managers = managers?.ToList() ?? [];
|
|
|
|
public bool UpdateCalled { get; private set; }
|
|
public bool PromoteCalled { get; private set; }
|
|
public bool UpdateBatchDetailsCalled { get; private set; }
|
|
public bool UpdateBatchNotificationModeCalled { get; private set; }
|
|
public bool RescheduleBatchCalled { get; private set; }
|
|
public bool CloneBatchCalled { get; private set; }
|
|
public bool CreateCampaignTemplateCalled { get; private set; }
|
|
public bool DeleteCampaignTemplateCalled { get; private set; }
|
|
public bool CreateBatchFromTemplateCalled { get; private set; }
|
|
public bool AddCoGmCalled { get; private set; }
|
|
public bool RemoveCoGmCalled { get; private set; }
|
|
public Guid? LastUpdatedSessionId { get; private set; }
|
|
public Guid? LastUpdatedGroupId { get; private set; }
|
|
public string? LastUpdatedTitle { get; private set; }
|
|
public DateTime? LastUpdatedScheduledAt { get; private set; }
|
|
public string? LastUpdatedJoinLink { get; private set; }
|
|
public int? LastUpdatedMaxPlayers { get; private set; }
|
|
public Guid? LastPromotedSessionId { get; private set; }
|
|
public Guid? LastPromotedGroupId { get; private set; }
|
|
public Guid? LastUpdatedBatchId { get; private set; }
|
|
public Guid? LastUpdatedBatchGroupId { get; private set; }
|
|
public string? LastUpdatedBatchTitle { get; private set; }
|
|
public string? LastUpdatedBatchJoinLink { get; private set; }
|
|
public Guid? LastUpdatedNotificationBatchId { get; private set; }
|
|
public Guid? LastUpdatedNotificationGroupId { get; private set; }
|
|
public SessionNotificationMode? LastUpdatedNotificationMode { get; private set; }
|
|
public Guid? LastRescheduledBatchId { get; private set; }
|
|
public Guid? LastRescheduledBatchGroupId { get; private set; }
|
|
public DateTime? LastRescheduledFirstScheduledAt { get; private set; }
|
|
public int? LastRescheduledIntervalDays { get; private set; }
|
|
public Guid? LastClonedBatchId { get; private set; }
|
|
public Guid? LastClonedBatchGroupId { get; private set; }
|
|
public BatchCloneInterval? LastCloneInterval { get; private set; }
|
|
public Guid? LastCreatedCampaignTemplateGroupId { get; private set; }
|
|
public CreateCampaignTemplateRequest? LastCreatedCampaignTemplateRequest { get; private set; }
|
|
public Guid? LastDeletedCampaignTemplateId { get; private set; }
|
|
public Guid? LastDeletedCampaignTemplateGroupId { get; private set; }
|
|
public Guid? LastCreatedBatchTemplateId { get; private set; }
|
|
public Guid? LastCreatedBatchTemplateGroupId { get; private set; }
|
|
public DateTime? LastCreatedBatchFirstScheduledAt { get; private set; }
|
|
public Guid? LastAddedCoGmGroupId { get; private set; }
|
|
public long? LastAddedCoGmTelegramId { get; private set; }
|
|
public string? LastAddedCoGmDisplayName { get; private set; }
|
|
public string? LastAddedCoGmUsername { get; private set; }
|
|
public Guid? LastRemovedCoGmGroupId { get; private set; }
|
|
public long? LastRemovedCoGmTelegramId { get; private set; }
|
|
public bool RemovePlayerCalled { get; private set; }
|
|
public Guid? LastRemovedPlayerSessionId { get; private set; }
|
|
public Guid? LastRemovedPlayerGroupId { get; private set; }
|
|
public Guid? LastRemovedPlayerParticipantId { get; private set; }
|
|
public List<SessionAuditLogEntry> LogEntries { get; private set; } = new();
|
|
public Guid? LastLogSessionId { get; private set; }
|
|
public long? LastLogActorTelegramId { get; private set; }
|
|
public string? LastLogActorName { get; private set; }
|
|
public string? LastLogChangeType { get; private set; }
|
|
public string? LastLogOldValue { get; private set; }
|
|
public string? LastLogNewValue { get; private set; }
|
|
|
|
public Task<List<WebGameGroup>> GetGroupsForGmAsync(long gmId) =>
|
|
Task.FromResult(groupsById.Values.Where(group => IsManager(group.Id, gmId)).ToList());
|
|
|
|
public Task<WebGameGroup?> GetGroupAsync(Guid groupId)
|
|
{
|
|
groupsById.TryGetValue(groupId, out var group);
|
|
return Task.FromResult(group);
|
|
}
|
|
|
|
public Task<bool> IsGroupManagerAsync(Guid groupId, long telegramId) =>
|
|
Task.FromResult(IsManager(groupId, telegramId));
|
|
|
|
public Task<bool> IsGroupOwnerAsync(Guid groupId, long telegramId) =>
|
|
Task.FromResult(IsOwner(groupId, telegramId));
|
|
|
|
public Task<List<WebGroupManager>> GetGroupManagersAsync(Guid groupId)
|
|
{
|
|
if (!groupsById.TryGetValue(groupId, out var group))
|
|
{
|
|
return Task.FromResult(new List<WebGroupManager>());
|
|
}
|
|
|
|
var result = new List<WebGroupManager>
|
|
{
|
|
new(group.GmTelegramId, "Owner GM", null, GroupManagerRoleExtensions.OwnerValue, DateTime.UtcNow)
|
|
};
|
|
|
|
result.AddRange(managers
|
|
.Where(manager => manager.GroupId == groupId)
|
|
.Select(manager => new WebGroupManager(
|
|
manager.TelegramId,
|
|
$"Co-GM {manager.TelegramId}",
|
|
null,
|
|
manager.Role.ToDatabaseValue(),
|
|
DateTime.UtcNow)));
|
|
|
|
return Task.FromResult(result);
|
|
}
|
|
|
|
public Task<List<WebSession>> GetUpcomingSessionsAsync(Guid groupId) =>
|
|
Task.FromResult(sessionsById.Values.Where(session => session.GroupId == groupId).ToList());
|
|
|
|
public Task<WebSession?> GetSessionAsync(Guid sessionId)
|
|
{
|
|
sessionsById.TryGetValue(sessionId, out var session);
|
|
return Task.FromResult(session);
|
|
}
|
|
|
|
public Task<WebSessionBatch?> GetBatchAsync(Guid batchId)
|
|
{
|
|
var batchSessions = sessionsById.Values
|
|
.Where(session => session.BatchId == batchId)
|
|
.OrderBy(session => session.ScheduledAt)
|
|
.ToList();
|
|
|
|
if (batchSessions.Count == 0)
|
|
{
|
|
return Task.FromResult<WebSessionBatch?>(null);
|
|
}
|
|
|
|
var firstSession = batchSessions[0];
|
|
return Task.FromResult<WebSessionBatch?>(new(
|
|
batchId,
|
|
firstSession.GroupId,
|
|
firstSession.Title,
|
|
firstSession.JoinLink,
|
|
firstSession.ScheduledAt,
|
|
batchSessions[^1].ScheduledAt,
|
|
batchSessions.Count));
|
|
}
|
|
|
|
public Task UpdateSessionAsync(Guid sessionId, Guid groupId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers)
|
|
{
|
|
UpdateCalled = true;
|
|
LastUpdatedSessionId = sessionId;
|
|
LastUpdatedGroupId = groupId;
|
|
LastUpdatedTitle = title;
|
|
LastUpdatedScheduledAt = scheduledAt;
|
|
LastUpdatedJoinLink = joinLink;
|
|
LastUpdatedMaxPlayers = maxPlayers;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task PromoteWaitlistedPlayerAsync(Guid sessionId, Guid groupId)
|
|
{
|
|
PromoteCalled = true;
|
|
LastPromotedSessionId = sessionId;
|
|
LastPromotedGroupId = groupId;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task UpdateBatchDetailsAsync(Guid batchId, Guid groupId, string title, string joinLink)
|
|
{
|
|
UpdateBatchDetailsCalled = true;
|
|
LastUpdatedBatchId = batchId;
|
|
LastUpdatedBatchGroupId = groupId;
|
|
LastUpdatedBatchTitle = title;
|
|
LastUpdatedBatchJoinLink = joinLink;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task UpdateBatchNotificationModeAsync(Guid batchId, Guid groupId, SessionNotificationMode notificationMode)
|
|
{
|
|
UpdateBatchNotificationModeCalled = true;
|
|
LastUpdatedNotificationBatchId = batchId;
|
|
LastUpdatedNotificationGroupId = groupId;
|
|
LastUpdatedNotificationMode = notificationMode;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task RescheduleBatchAsync(Guid batchId, Guid groupId, DateTime firstScheduledAt, int intervalDays)
|
|
{
|
|
RescheduleBatchCalled = true;
|
|
LastRescheduledBatchId = batchId;
|
|
LastRescheduledBatchGroupId = groupId;
|
|
LastRescheduledFirstScheduledAt = firstScheduledAt;
|
|
LastRescheduledIntervalDays = intervalDays;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<WebSessionBatch> CloneBatchAsync(Guid batchId, Guid groupId, BatchCloneInterval interval)
|
|
{
|
|
CloneBatchCalled = true;
|
|
LastClonedBatchId = batchId;
|
|
LastClonedBatchGroupId = groupId;
|
|
LastCloneInterval = interval;
|
|
return Task.FromResult(new WebSessionBatch(
|
|
Guid.NewGuid(),
|
|
groupId,
|
|
"Session A",
|
|
"https://example.test/a",
|
|
DateTime.UtcNow.AddDays(7),
|
|
DateTime.UtcNow.AddDays(7),
|
|
1));
|
|
}
|
|
|
|
public Task<List<WebCampaignTemplate>> GetCampaignTemplatesAsync(Guid groupId) =>
|
|
Task.FromResult(templatesById.Values.Where(template => template.GroupId == groupId).ToList());
|
|
|
|
public Task<WebCampaignTemplate?> GetCampaignTemplateAsync(Guid templateId)
|
|
{
|
|
templatesById.TryGetValue(templateId, out var template);
|
|
return Task.FromResult(template);
|
|
}
|
|
|
|
public Task<WebCampaignTemplate> CreateCampaignTemplateAsync(Guid groupId, CreateCampaignTemplateRequest request)
|
|
{
|
|
CreateCampaignTemplateCalled = true;
|
|
LastCreatedCampaignTemplateGroupId = groupId;
|
|
LastCreatedCampaignTemplateRequest = request;
|
|
|
|
var template = new WebCampaignTemplate(
|
|
Guid.NewGuid(),
|
|
groupId,
|
|
request.Name,
|
|
request.Title,
|
|
request.JoinLink,
|
|
request.SessionCount,
|
|
request.IntervalDays,
|
|
request.MaxPlayers,
|
|
request.NotificationMode.ToDatabaseValue(),
|
|
DateTime.UtcNow,
|
|
DateTime.UtcNow);
|
|
|
|
templatesById[template.Id] = template;
|
|
return Task.FromResult(template);
|
|
}
|
|
|
|
public Task DeleteCampaignTemplateAsync(Guid templateId, Guid groupId)
|
|
{
|
|
DeleteCampaignTemplateCalled = true;
|
|
LastDeletedCampaignTemplateId = templateId;
|
|
LastDeletedCampaignTemplateGroupId = groupId;
|
|
templatesById.Remove(templateId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<WebSessionBatch> CreateBatchFromTemplateAsync(Guid templateId, Guid groupId, DateTime firstScheduledAt)
|
|
{
|
|
CreateBatchFromTemplateCalled = true;
|
|
LastCreatedBatchTemplateId = templateId;
|
|
LastCreatedBatchTemplateGroupId = groupId;
|
|
LastCreatedBatchFirstScheduledAt = firstScheduledAt;
|
|
|
|
var template = templatesById[templateId];
|
|
return Task.FromResult(new WebSessionBatch(
|
|
Guid.NewGuid(),
|
|
groupId,
|
|
template.Title,
|
|
template.JoinLink,
|
|
firstScheduledAt,
|
|
firstScheduledAt.AddDays(template.IntervalDays * (template.SessionCount - 1)),
|
|
template.SessionCount,
|
|
template.NotificationMode));
|
|
}
|
|
|
|
public Task AddGroupCoGmAsync(Guid groupId, long ownerTelegramId, long coGmTelegramId, string displayName, string? telegramUsername)
|
|
{
|
|
AddCoGmCalled = true;
|
|
LastAddedCoGmGroupId = groupId;
|
|
LastAddedCoGmTelegramId = coGmTelegramId;
|
|
LastAddedCoGmDisplayName = displayName;
|
|
LastAddedCoGmUsername = telegramUsername;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task RemoveGroupCoGmAsync(Guid groupId, long coGmTelegramId)
|
|
{
|
|
RemoveCoGmCalled = true;
|
|
LastRemovedCoGmGroupId = groupId;
|
|
LastRemovedCoGmTelegramId = coGmTelegramId;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<List<WebParticipant>> GetSessionParticipantsAsync(Guid sessionId) =>
|
|
Task.FromResult(new List<WebParticipant>());
|
|
|
|
public Task RemovePlayerFromSessionAsync(Guid sessionId, Guid groupId, Guid participantId)
|
|
{
|
|
RemovePlayerCalled = true;
|
|
LastRemovedPlayerSessionId = sessionId;
|
|
LastRemovedPlayerGroupId = groupId;
|
|
LastRemovedPlayerParticipantId = participantId;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<List<PlayerAttendanceStats>> GetGroupAttendanceStatsAsync(Guid groupId) =>
|
|
Task.FromResult(new List<PlayerAttendanceStats>());
|
|
|
|
public Task LogSessionChangeAsync(Guid sessionId, long actorTelegramId, string actorName, string changeType, string? oldValue, string? newValue)
|
|
{
|
|
var entry = new SessionAuditLogEntry(Guid.NewGuid(), sessionId, actorTelegramId.ToString(), actorName, changeType, oldValue, newValue, DateTime.UtcNow);
|
|
LogEntries.Add(entry);
|
|
LastLogSessionId = sessionId;
|
|
LastLogActorTelegramId = actorTelegramId;
|
|
LastLogActorName = actorName;
|
|
LastLogChangeType = changeType;
|
|
LastLogOldValue = oldValue;
|
|
LastLogNewValue = newValue;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<List<SessionAuditLogEntry>> GetSessionHistoryAsync(Guid sessionId) =>
|
|
Task.FromResult(LogEntries.Where(e => e.SessionId == sessionId).OrderByDescending(e => e.ChangedAt).ToList());
|
|
|
|
public Task<List<WebGameGroup>> GetGroupsForUserAsync(string platform, string externalUserId) =>
|
|
Task.FromResult(groupsById.Values.Where(group => IsManager(group.Id, externalUserId)).ToList());
|
|
|
|
public Task<bool> IsGroupManagerAsync(Guid groupId, string platform, string externalUserId) =>
|
|
Task.FromResult(IsManager(groupId, externalUserId));
|
|
|
|
public Task<bool> IsGroupOwnerAsync(Guid groupId, string platform, string externalUserId) =>
|
|
Task.FromResult(IsOwner(groupId, externalUserId));
|
|
|
|
public Task AddGroupCoGmAsync(Guid groupId, string ownerPlatform, string ownerExternalUserId, string coGmPlatform, string coGmExternalUserId, string displayName, string? externalUsername)
|
|
{
|
|
AddCoGmCalled = true;
|
|
LastAddedCoGmGroupId = groupId;
|
|
LastAddedCoGmTelegramId = long.TryParse(coGmExternalUserId, out var id) ? id : null;
|
|
LastAddedCoGmDisplayName = displayName;
|
|
LastAddedCoGmUsername = externalUsername;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task RemoveGroupCoGmAsync(Guid groupId, string coGmPlatform, string coGmExternalUserId)
|
|
{
|
|
RemoveCoGmCalled = true;
|
|
LastRemovedCoGmGroupId = groupId;
|
|
LastRemovedCoGmTelegramId = long.TryParse(coGmExternalUserId, out var id) ? id : null;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task LogSessionChangeAsync(Guid sessionId, string actorExternalUserId, string actorName, string changeType, string? oldValue, string? newValue)
|
|
{
|
|
var entry = new SessionAuditLogEntry(Guid.NewGuid(), sessionId, actorExternalUserId, actorName, changeType, oldValue, newValue, DateTime.UtcNow);
|
|
LogEntries.Add(entry);
|
|
LastLogSessionId = sessionId;
|
|
LastLogActorTelegramId = long.TryParse(actorExternalUserId, out var id) ? (long?)id : null;
|
|
LastLogActorName = actorName;
|
|
LastLogChangeType = changeType;
|
|
LastLogOldValue = oldValue;
|
|
LastLogNewValue = newValue;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task UpsertDiscordUserAsync(string discordId, string displayName, string? avatarUrl) =>
|
|
Task.CompletedTask;
|
|
|
|
public Task<Guid?> ResolveEffectivePlayerIdAsync(string platform, string externalUserId) =>
|
|
Task.FromResult<Guid?>(Guid.NewGuid());
|
|
|
|
public Task<List<LinkedIdentity>> GetLinkedIdentitiesAsync(string platform, string externalUserId) =>
|
|
Task.FromResult(new List<LinkedIdentity>());
|
|
|
|
public Task LinkIdentityAsync(string currentPlatform, string currentExternalUserId, string targetPlatform, string targetExternalUserId, string? currentName) =>
|
|
Task.CompletedTask;
|
|
|
|
public Task UnlinkIdentityAsync(string currentPlatform, string currentExternalUserId, string targetPlatform, string targetExternalUserId) =>
|
|
Task.CompletedTask;
|
|
|
|
public Task UpsertPlayerAsync(string platform, string externalUserId, string displayName, string? avatarUrl) =>
|
|
Task.CompletedTask;
|
|
|
|
private bool IsManager(Guid groupId, long telegramId) =>
|
|
IsOwner(groupId, telegramId) ||
|
|
managers.Any(manager => manager.GroupId == groupId && manager.TelegramId == telegramId);
|
|
|
|
private bool IsOwner(Guid groupId, long telegramId) =>
|
|
groupsById.TryGetValue(groupId, out var group) && group.GmTelegramId == telegramId;
|
|
|
|
private bool IsManager(Guid groupId, string externalUserId) =>
|
|
IsOwner(groupId, externalUserId) ||
|
|
managers.Any(manager => manager.GroupId == groupId && manager.TelegramId.ToString() == externalUserId);
|
|
|
|
private bool IsOwner(Guid groupId, string externalUserId) =>
|
|
groupsById.TryGetValue(groupId, out var group) && group.GmTelegramId.ToString() == externalUserId;
|
|
}
|
|
|
|
private sealed record FakeGroupManager(Guid GroupId, long TelegramId, GroupManagerRole Role);
|
|
}
|