feat: add session capacity waitlist
This commit is contained in:
+20
@@ -13,6 +13,7 @@ public sealed class NewSessionCommandParserTests
|
||||
Название: Curse of Strahd
|
||||
Время: 24.04.2026 19:30
|
||||
Время: 01.05.2026 20:00
|
||||
Мест: 4
|
||||
Ссылка: https://example.test/room
|
||||
""";
|
||||
|
||||
@@ -21,6 +22,7 @@ public sealed class NewSessionCommandParserTests
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("Curse of Strahd", result.Title);
|
||||
Assert.Equal("https://example.test/room", result.Link);
|
||||
Assert.Equal(4, result.MaxPlayers);
|
||||
Assert.Equal(
|
||||
[
|
||||
new DateTimeOffset(2026, 4, 24, 16, 30, 0, TimeSpan.Zero),
|
||||
@@ -65,4 +67,22 @@ public sealed class NewSessionCommandParserTests
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Null(result.Link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldCollectInvalidSeatLimit()
|
||||
{
|
||||
var text = """
|
||||
/newsession
|
||||
Название: Blades in the Dark
|
||||
Время: 25.04.2026 19:30
|
||||
Мест: 0
|
||||
Ссылка: https://example.test/blades
|
||||
""";
|
||||
|
||||
var result = NewSessionCommandParser.Parse(text, DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Null(result.MaxPlayers);
|
||||
Assert.Equal(["0"], result.InvalidSeatLimitInputs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using GmRelay.Shared.Domain;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession;
|
||||
|
||||
public sealed class SessionCapacityRulesTests
|
||||
{
|
||||
[Fact]
|
||||
public void DecideJoinStatus_ShouldReturnActive_WhenSessionHasFreeSeats()
|
||||
{
|
||||
var status = SessionCapacityRules.DecideJoinStatus(maxPlayers: 3, activeParticipants: 2);
|
||||
|
||||
Assert.Equal(ParticipantRegistrationStatus.Active, status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecideJoinStatus_ShouldReturnWaitlisted_WhenSessionReachedLimit()
|
||||
{
|
||||
var status = SessionCapacityRules.DecideJoinStatus(maxPlayers: 2, activeParticipants: 2);
|
||||
|
||||
Assert.Equal(ParticipantRegistrationStatus.Waitlisted, status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPromoteWaitlistedPlayer_ShouldRequireWaitlistAndFreeSeat()
|
||||
{
|
||||
Assert.True(SessionCapacityRules.CanPromoteWaitlistedPlayer(maxPlayers: 3, activeParticipants: 2, waitlistedParticipants: 1));
|
||||
Assert.False(SessionCapacityRules.CanPromoteWaitlistedPlayer(maxPlayers: 3, activeParticipants: 3, waitlistedParticipants: 1));
|
||||
Assert.False(SessionCapacityRules.CanPromoteWaitlistedPlayer(maxPlayers: 3, activeParticipants: 2, waitlistedParticipants: 0));
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,15 @@ public sealed class SessionBatchRendererTests
|
||||
|
||||
var sessions = new[]
|
||||
{
|
||||
new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned),
|
||||
new SessionBatchDto(cancelledSessionId, new DateTime(2026, 4, 28, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Cancelled),
|
||||
new SessionBatchDto(firstSessionId, new DateTime(2026, 4, 26, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned)
|
||||
new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned, 4),
|
||||
new SessionBatchDto(cancelledSessionId, new DateTime(2026, 4, 28, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Cancelled, null),
|
||||
new SessionBatchDto(firstSessionId, new DateTime(2026, 4, 26, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned, 2)
|
||||
};
|
||||
var participants = new[]
|
||||
{
|
||||
new ParticipantBatchDto(secondSessionId, "Alice", "alice"),
|
||||
new ParticipantBatchDto(cancelledSessionId, "Bob", null)
|
||||
new ParticipantBatchDto(secondSessionId, "Alice", "alice", ParticipantRegistrationStatus.Active),
|
||||
new ParticipantBatchDto(secondSessionId, "Charlie", null, ParticipantRegistrationStatus.Waitlisted),
|
||||
new ParticipantBatchDto(cancelledSessionId, "Bob", null, ParticipantRegistrationStatus.Active)
|
||||
};
|
||||
|
||||
var result = SessionBatchRenderer.Render("Campaign", sessions, participants);
|
||||
@@ -35,7 +36,11 @@ public sealed class SessionBatchRendererTests
|
||||
Assert.Contains("Campaign", text);
|
||||
Assert.True(firstIndex < secondIndex);
|
||||
Assert.True(secondIndex < thirdIndex);
|
||||
Assert.Contains("Места: 0/2", text);
|
||||
Assert.Contains("Места: 1/4", text);
|
||||
Assert.Contains("@alice", text);
|
||||
Assert.Contains("Лист ожидания (1)", text);
|
||||
Assert.Contains("Charlie", text);
|
||||
Assert.Contains("Bob", text);
|
||||
Assert.Equal(2, result.Markup.InlineKeyboard.Count());
|
||||
Assert.Collection(
|
||||
@@ -45,6 +50,7 @@ public sealed class SessionBatchRendererTests
|
||||
callbackData => Assert.Equal($"reschedule_session:{firstSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"join_session:{secondSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"cancel_session:{secondSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"reschedule_session:{secondSessionId}", callbackData));
|
||||
callbackData => Assert.Equal($"reschedule_session:{secondSessionId}", callbackData),
|
||||
callbackData => Assert.Equal($"promote_waitlist:{secondSessionId}", callbackData));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class AuthorizedSessionServiceTests
|
||||
],
|
||||
sessions:
|
||||
[
|
||||
new(Guid.NewGuid(), groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42)
|
||||
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);
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed class AuthorizedSessionServiceTests
|
||||
],
|
||||
sessions:
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42)
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
|
||||
@@ -78,7 +78,7 @@ public sealed class AuthorizedSessionServiceTests
|
||||
],
|
||||
sessions:
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42)
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42, 4, 1, 0)
|
||||
]);
|
||||
var service = new AuthorizedSessionService(store);
|
||||
|
||||
@@ -99,11 +99,11 @@ public sealed class AuthorizedSessionServiceTests
|
||||
],
|
||||
sessions:
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42)
|
||||
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");
|
||||
var action = () => service.UpdateSessionForGmAsync(sessionId, 1001L, "Updated", DateTime.UtcNow.AddDays(1), "https://example.test/b", 5);
|
||||
|
||||
await Assert.ThrowsAsync<SessionAccessDeniedException>(action);
|
||||
Assert.False(store.UpdateCalled);
|
||||
@@ -123,11 +123,11 @@ public sealed class AuthorizedSessionServiceTests
|
||||
],
|
||||
sessions:
|
||||
[
|
||||
new(sessionId, groupId, "Session A", DateTime.UtcNow, "Planned", "https://example.test/a", Guid.NewGuid(), 10, 42)
|
||||
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");
|
||||
await service.UpdateSessionForGmAsync(sessionId, gmId, "Updated", scheduledAt, "https://example.test/b", 5);
|
||||
|
||||
Assert.True(store.UpdateCalled);
|
||||
Assert.Equal(groupId, store.LastUpdatedGroupId);
|
||||
@@ -135,6 +135,31 @@ public sealed class AuthorizedSessionServiceTests
|
||||
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);
|
||||
}
|
||||
|
||||
private sealed class FakeSessionStore(
|
||||
@@ -145,11 +170,15 @@ public sealed class AuthorizedSessionServiceTests
|
||||
private readonly Dictionary<Guid, WebSession> sessionsById = sessions?.ToDictionary(session => session.Id) ?? [];
|
||||
|
||||
public bool UpdateCalled { get; private set; }
|
||||
public bool PromoteCalled { 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 Task<List<WebGameGroup>> GetGroupsForGmAsync(long gmId) =>
|
||||
Task.FromResult(groupsById.Values.Where(group => group.GmTelegramId == gmId).ToList());
|
||||
@@ -169,7 +198,7 @@ public sealed class AuthorizedSessionServiceTests
|
||||
return Task.FromResult(session);
|
||||
}
|
||||
|
||||
public Task UpdateSessionAsync(Guid sessionId, Guid groupId, string title, DateTime scheduledAt, string joinLink)
|
||||
public Task UpdateSessionAsync(Guid sessionId, Guid groupId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers)
|
||||
{
|
||||
UpdateCalled = true;
|
||||
LastUpdatedSessionId = sessionId;
|
||||
@@ -177,6 +206,15 @@ public sealed class AuthorizedSessionServiceTests
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user