diff --git a/tests/GmRelay.Bot.Tests/Rendering/DiscordSessionBatchRendererTests.cs b/tests/GmRelay.Bot.Tests/Rendering/DiscordSessionBatchRendererTests.cs new file mode 100644 index 0000000..febf077 --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Rendering/DiscordSessionBatchRendererTests.cs @@ -0,0 +1,139 @@ +using GmRelay.DiscordBot.Rendering; +using GmRelay.Shared.Domain; +using GmRelay.Shared.Rendering; +using NetCord; +using NetCord.Rest; + +namespace GmRelay.Bot.Tests.Rendering; + +public sealed class DiscordSessionBatchRendererTests +{ + [Fact] + public void Render_ShouldProduceEmbedsAndButtonsForMultipleSessions() + { + var firstSessionId = Guid.NewGuid(); + var secondSessionId = Guid.NewGuid(); + var cancelledSessionId = Guid.NewGuid(); + + var sessions = new[] + { + new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned, 4, "https://example.com/game2"), + 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, "https://example.com/game1") + }; + var participants = new[] + { + new ParticipantBatchDto(secondSessionId, "Alice", "alice", ParticipantRegistrationStatus.Active), + new ParticipantBatchDto(secondSessionId, "Charlie", null, ParticipantRegistrationStatus.Waitlisted), + new ParticipantBatchDto(cancelledSessionId, "Bob", null, ParticipantRegistrationStatus.Active) + }; + + var view = SessionBatchViewBuilder.Build("Campaign", sessions, participants); + var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(view); + + Assert.Equal(3, embeds.Count); + Assert.Equal(2, actionRows.Count); // cancelled skipped + + // Embed titles contain game title and Moscow date + Assert.Contains(embeds, e => e.Title.Contains("Campaign") && e.Title.Contains("26")); + Assert.Contains(embeds, e => e.Title.Contains("Campaign") && e.Title.Contains("27")); + Assert.Contains(embeds, e => e.Title.Contains("Campaign") && e.Title.Contains("28")); + + // Cancelled session embed description indicates cancellation + var cancelledEmbed = embeds.First(e => e.Description.Contains("отменена") || e.Description.Contains("Отменена")); + Assert.NotNull(cancelledEmbed); + + // Active session embeds contain player names + Assert.Contains(embeds, e => e.Description != null && e.Description.Contains("Alice")); + Assert.Contains(embeds, e => e.Description != null && e.Description.Contains("Charlie")); + + // Buttons for active sessions + var allButtons = actionRows.SelectMany(r => r).OfType().ToList(); + Assert.Contains(allButtons, b => b.CustomId == $"join_session:{firstSessionId}"); + Assert.Contains(allButtons, b => b.CustomId == $"leave_session:{firstSessionId}"); + Assert.Contains(allButtons, b => b.CustomId == $"join_session:{secondSessionId}"); + Assert.Contains(allButtons, b => b.CustomId == $"leave_session:{secondSessionId}"); + } + + [Fact] + public void Render_ShouldSkipActionRowsForCancelledSessions() + { + var cancelledSessionId = Guid.NewGuid(); + var sessions = new[] { new SessionBatchDto(cancelledSessionId, DateTime.UtcNow, SessionStatus.Cancelled, null, "") }; + var participants = Array.Empty(); + + var view = SessionBatchViewBuilder.Build("Test", sessions, participants); + var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(view); + + Assert.Single(embeds); + Assert.Empty(actionRows); + } + + [Fact] + public void Render_ShouldShowWaitlistButtonWhenFull() + { + var sessionId = Guid.NewGuid(); + var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 1, "https://example.com/game") }; + var participants = new[] { new ParticipantBatchDto(sessionId, "Alice", "alice", ParticipantRegistrationStatus.Active) }; + + var view = SessionBatchViewBuilder.Build("Test", sessions, participants); + var (_, actionRows) = DiscordSessionBatchRenderer.Render(view); + var buttons = actionRows.SelectMany(r => r).OfType().ToList(); + var joinButton = buttons.First(b => b.CustomId == $"join_session:{sessionId}"); + + Assert.Contains("ожидания", joinButton.Label); + } + + [Fact] + public void Render_ShouldUseRedColorForCancelledSessions() + { + var sessionId = Guid.NewGuid(); + var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Cancelled, null, "") }; + var participants = Array.Empty(); + + var view = SessionBatchViewBuilder.Build("Test", sessions, participants); + var (embeds, _) = DiscordSessionBatchRenderer.Render(view); + + Assert.Equal(0xED4245, embeds[0].Color!.Value.RawValue); + } + + [Fact] + public void Render_ShouldUseGreenColorForOpenSessions() + { + var sessionId = Guid.NewGuid(); + var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "https://example.com/game") }; + var participants = Array.Empty(); + + var view = SessionBatchViewBuilder.Build("Test", sessions, participants); + var (embeds, _) = DiscordSessionBatchRenderer.Render(view); + + Assert.Equal(0x57F287, embeds[0].Color!.Value.RawValue); + } + + [Fact] + public void Render_ShouldUseYellowColorForFullSessions() + { + var sessionId = Guid.NewGuid(); + var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 1, "https://example.com/game") }; + var participants = new[] { new ParticipantBatchDto(sessionId, "Alice", "alice", ParticipantRegistrationStatus.Active) }; + + var view = SessionBatchViewBuilder.Build("Test", sessions, participants); + var (embeds, _) = DiscordSessionBatchRenderer.Render(view); + + Assert.Equal(0xFEE75C, embeds[0].Color!.Value.RawValue); + } + + [Fact] + public void Render_ShouldHandleRescheduleStatus() + { + var sessionId = Guid.NewGuid(); + var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, "Rescheduled", 4, "") }; + var participants = Array.Empty(); + + var view = SessionBatchViewBuilder.Build("Test", sessions, participants); + var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(view); + + Assert.Single(embeds); + Assert.Single(actionRows); // not cancelled → actions present + } +}