using GmRelay.Web.Services; 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_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 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) : ISessionStore { private readonly Dictionary groupsById = groups?.ToDictionary(group => group.Id) ?? []; private readonly Dictionary sessionsById = sessions?.ToDictionary(session => session.Id) ?? []; public bool UpdateCalled { get; private set; } public bool PromoteCalled { get; private set; } public bool UpdateBatchDetailsCalled { get; private set; } public bool RescheduleBatchCalled { get; private set; } public bool CloneBatchCalled { 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? 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 Task> GetGroupsForGmAsync(long gmId) => Task.FromResult(groupsById.Values.Where(group => group.GmTelegramId == gmId).ToList()); public Task GetGroupAsync(Guid groupId) { groupsById.TryGetValue(groupId, out var group); return Task.FromResult(group); } 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 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)); } } }