using GmRelay.Web.Services; using GmRelay.Shared.Domain; namespace GmRelay.Bot.Tests.Web; public sealed class AuthorizedSessionServiceTests { [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 service = new AuthorizedSessionService(store); var sessions = await service.GetUpcomingSessionsForGmAsync(groupId, gmId); 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 service = new AuthorizedSessionService(store); var sessions = await service.GetUpcomingSessionsForGmAsync(groupId, coGmId); 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 service = new AuthorizedSessionService(store); var sessions = await service.GetUpcomingSessionsForGmAsync(groupId, 1001L); 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 service = new AuthorizedSessionService(store); var session = await service.GetSessionForGmAsync(sessionId, gmId); 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 service = new AuthorizedSessionService(store); var session = await service.GetSessionForGmAsync(sessionId, 1001L); 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 service = new AuthorizedSessionService(store); var action = () => service.UpdateSessionForGmAsync(sessionId, 1001L, "Updated", DateTime.UtcNow.AddDays(1), "https://example.test/b", 5); await Assert.ThrowsAsync(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 service = new AuthorizedSessionService(store); await service.UpdateSessionForGmAsync(sessionId, gmId, "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 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 service = new AuthorizedSessionService(store); await service.PromoteWaitlistedPlayerForGmAsync(sessionId, gmId); 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 service = new AuthorizedSessionService(store); var action = () => service.UpdateBatchDetailsForGmAsync(batchId, 1001L, "Updated", "https://example.test/b"); await Assert.ThrowsAsync(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 service = new AuthorizedSessionService(store); await service.UpdateBatchDetailsForGmAsync(batchId, gmId, "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 service = new AuthorizedSessionService(store); await service.UpdateBatchDetailsForGmAsync(batchId, coGmId, "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 service = new AuthorizedSessionService(store); await service.AddCoGmForOwnerAsync(groupId, ownerId, coGmId, "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 service = new AuthorizedSessionService(store); var action = () => service.AddCoGmForOwnerAsync(groupId, coGmId, newCoGmId, "Second Assistant", null); await Assert.ThrowsAsync(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 service = new AuthorizedSessionService(store); await service.RemoveCoGmForOwnerAsync(groupId, ownerId, coGmId); 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 service = new AuthorizedSessionService(store); var action = () => service.UpdateBatchNotificationModeForGmAsync(batchId, 1001L, SessionNotificationMode.GroupOnly); await Assert.ThrowsAsync(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 service = new AuthorizedSessionService(store); await service.UpdateBatchNotificationModeForGmAsync(batchId, gmId, 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 service = new AuthorizedSessionService(store); var action = () => service.RescheduleBatchForGmAsync(batchId, gmId, DateTime.UtcNow.AddDays(7), intervalDays: 0); await Assert.ThrowsAsync(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 service = new AuthorizedSessionService(store); await service.RescheduleBatchForGmAsync(batchId, gmId, 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 service = new AuthorizedSessionService(store); await service.CloneBatchForGmAsync(batchId, gmId, BatchCloneInterval.NextWeek); Assert.True(store.CloneBatchCalled); Assert.Equal(batchId, store.LastClonedBatchId); Assert.Equal(groupId, store.LastClonedBatchGroupId); Assert.Equal(BatchCloneInterval.NextWeek, store.LastCloneInterval); } private sealed class FakeSessionStore( IEnumerable? groups = null, IEnumerable? sessions = null, IEnumerable? managers = null) : ISessionStore { private readonly Dictionary groupsById = groups?.ToDictionary(group => group.Id) ?? []; private readonly Dictionary sessionsById = sessions?.ToDictionary(session => session.Id) ?? []; private readonly List 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 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? 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 Task> GetGroupsForGmAsync(long gmId) => Task.FromResult(groupsById.Values.Where(group => IsManager(group.Id, gmId)).ToList()); public Task GetGroupAsync(Guid groupId) { groupsById.TryGetValue(groupId, out var group); return Task.FromResult(group); } public Task IsGroupManagerAsync(Guid groupId, long telegramId) => Task.FromResult(IsManager(groupId, telegramId)); public Task IsGroupOwnerAsync(Guid groupId, long telegramId) => Task.FromResult(IsOwner(groupId, telegramId)); public Task> GetGroupManagersAsync(Guid groupId) { if (!groupsById.TryGetValue(groupId, out var group)) { return Task.FromResult(new List()); } var result = new List { 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> GetUpcomingSessionsAsync(Guid groupId) => Task.FromResult(sessionsById.Values.Where(session => session.GroupId == groupId).ToList()); public Task GetSessionAsync(Guid sessionId) { sessionsById.TryGetValue(sessionId, out var session); return Task.FromResult(session); } public Task GetBatchAsync(Guid batchId) { var batchSessions = sessionsById.Values .Where(session => session.BatchId == batchId) .OrderBy(session => session.ScheduledAt) .ToList(); if (batchSessions.Count == 0) { return Task.FromResult(null); } var firstSession = batchSessions[0]; return Task.FromResult(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 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 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; } 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 sealed record FakeGroupManager(Guid GroupId, long TelegramId, GroupManagerRole Role); }