using System; using System.Threading; using System.Threading.Tasks; using GmRelay.Bot.Features.Sessions.CreateSession; using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; using Microsoft.Extensions.Logging.Abstractions; using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes; namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; /// /// Verifies the validation gates inside /// . We never reach the /// shared handler in any of these tests, so the shared dependency is /// passed as null! — a NRE on that branch would itself prove the /// validation did not fire. /// public sealed class CreateSessionHandlerSubmitValidationTests { [Fact] public async Task SubmitDraftAsync_MissingVisibility_EditsMessageNamingVisibility() { var drafts = new FakeWizardDraftRepository(); var messenger = new FakeWizardMessenger(); var sut = new CreateSessionHandler( drafts, shared: null!, messenger, NullLogger.Instance); // All required fields set except Visibility. var payload = new WizardPayload { Type = WizardCreationType.Single, Title = "T", System = "Dnd5e", DurationMinutes = 240, Format = WizardSessionFormat.Online, JoinLink = "https://vtt.example/game", Single = new WizardSingleInput { ScheduledAt = DateTimeOffset.UtcNow.AddDays(7), MaxPlayers = 4, }, }; var draft = NewDraft(WizardStepNames.Confirm, payload); drafts.Seed(draft); await sut.SubmitDraftAsync(draft, CancellationToken.None); Assert.Single(messenger.Edits); Assert.Contains("видимость", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task SubmitDraftAsync_MissingSystem_EditsMessageNamingSystem() { var drafts = new FakeWizardDraftRepository(); var messenger = new FakeWizardMessenger(); var sut = new CreateSessionHandler( drafts, shared: null!, messenger, NullLogger.Instance); // All required fields set except System. var payload = new WizardPayload { Type = WizardCreationType.Single, Title = "T", DurationMinutes = 240, Format = WizardSessionFormat.Online, JoinLink = "https://vtt.example/game", Visibility = WizardVisibility.Public, Single = new WizardSingleInput { ScheduledAt = DateTimeOffset.UtcNow.AddDays(7), MaxPlayers = 4, }, }; var draft = NewDraft(WizardStepNames.Confirm, payload); drafts.Seed(draft); await sut.SubmitDraftAsync(draft, CancellationToken.None); Assert.Single(messenger.Edits); Assert.Contains("система", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task SubmitDraftAsync_MissingDateTimeForSingleType_EditsMessageNamingDateTime() { var drafts = new FakeWizardDraftRepository(); var messenger = new FakeWizardMessenger(); var sut = new CreateSessionHandler( drafts, shared: null!, messenger, NullLogger.Instance); // All required fields set except ScheduledAt for Single type. var payload = new WizardPayload { Type = WizardCreationType.Single, Title = "T", System = "Dnd5e", DurationMinutes = 240, Format = WizardSessionFormat.Online, JoinLink = "https://vtt.example/game", Visibility = WizardVisibility.Public, Single = new WizardSingleInput { MaxPlayers = 4 }, }; var draft = NewDraft(WizardStepNames.Confirm, payload); drafts.Seed(draft); await sut.SubmitDraftAsync(draft, CancellationToken.None); Assert.Single(messenger.Edits); Assert.Contains("дата/время", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task SubmitDraftAsync_EmptyPool_EditsMessageNamingSlots() { var drafts = new FakeWizardDraftRepository(); var messenger = new FakeWizardMessenger(); var sut = new CreateSessionHandler( drafts, shared: null!, messenger, NullLogger.Instance); // Pool type with no slots at all. var payload = new WizardPayload { Type = WizardCreationType.Pool, Title = "P", System = "Dnd5e", DurationMinutes = 240, Format = WizardSessionFormat.Online, JoinLink = "https://vtt.example/game", Visibility = WizardVisibility.Public, Pool = new WizardPoolInput(), }; var draft = NewDraft(WizardStepNames.Confirm, payload); drafts.Seed(draft); await sut.SubmitDraftAsync(draft, CancellationToken.None); Assert.Single(messenger.Edits); Assert.Contains("слоты", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task SubmitDraftAsync_SingleWithNoLimit_DoesNotReportMaxPlayersAsMissing() { // Regression for #131: pressing "♾ Без лимита" sets MaxPlayers = null. // IsComplete must NOT flag that as a missing field; null means // "no player limit" and is a valid final state. var drafts = new FakeWizardDraftRepository(); var messenger = new FakeWizardMessenger(); var sut = new CreateSessionHandler( drafts, shared: null!, messenger, NullLogger.Instance); var payload = new WizardPayload { Type = WizardCreationType.Single, Title = "T", System = "Dnd5e", DurationMinutes = 240, Format = WizardSessionFormat.Online, JoinLink = "https://vtt.example/game", Visibility = WizardVisibility.Public, Single = new WizardSingleInput { ScheduledAt = DateTimeOffset.UtcNow.AddDays(7), MaxPlayers = null, }, }; var draft = NewDraft(WizardStepNames.Confirm, payload); drafts.Seed(draft); await sut.SubmitDraftAsync(draft, CancellationToken.None); // Validation must let the no-limit payload through. The shared // handler is null, so anything that reached the database call would // throw a NullReferenceException — that is caught by the retry // path and reported as a "💥 Ошибка:" edit, not a missing-fields // edit. Therefore we assert that NO edit mentions a missing field. Assert.NotEmpty(messenger.Edits); var lastEdit = messenger.Edits[^1].Text; Assert.DoesNotContain("Не заполнены", lastEdit, StringComparison.OrdinalIgnoreCase); } }