using System; using System.Text.Json; using GmRelay.Bot.Features.Sessions.CreateSession.Wizard; using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes; namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; /// /// Verifies the wizard's input validation: invalid input stays on the same /// step and re-renders with an error prefix. The repository is NOT called /// with a step change. /// public sealed class GameCreationWizardValidationTests { [Fact] public async Task EmptyTitle_StaysOnTitleStep() { var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.Title); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate(" "), draft, CancellationToken.None); Assert.Equal(WizardStepNames.Title, draft.Step); } [Fact] public async Task OverlongTitle_StaysOnTitleStep() { var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.Title); drafts.Seed(draft); var tooLong = new string('a', WizardStep.MaxTitleLength + 1); await wizard.HandleUpdateAsync(TextUpdate(tooLong), draft, CancellationToken.None); Assert.Equal(WizardStepNames.Title, draft.Step); } [Fact] public async Task PastDate_StaysOnDateTimeStep() { var wizard = BuildWizard(out var drafts, out _); var payload = new WizardPayload { Type = WizardCreationType.Single, Title = "T", System = "Dnd5e", DurationMinutes = 240, }; var draft = NewDraft(WizardStepNames.DateTime, payload); drafts.Seed(draft); // 2020-01-01 is firmly in the past await wizard.HandleUpdateAsync(TextUpdate("01.01.2020 12:00"), draft, CancellationToken.None); Assert.Equal(WizardStepNames.DateTime, draft.Step); } [Fact] public async Task UnparseableDate_StaysOnDateTimeStep() { var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.DateTime); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate("not a date"), draft, CancellationToken.None); Assert.Equal(WizardStepNames.DateTime, draft.Step); } [Fact] public async Task BadCoverUrl_StaysOnCoverStep() { var wizard = BuildWizard(out var drafts, out _); var payload = new WizardPayload { Type = WizardCreationType.Single, Title = "T", Description = "D", }; var draft = NewDraft(WizardStepNames.Cover, payload); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate("not a url"), draft, CancellationToken.None); Assert.Equal(WizardStepNames.Cover, draft.Step); } [Fact] public async Task ValidCoverUrl_AdvancesToSystem() { var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.Cover, new WizardPayload { Type = WizardCreationType.Single, Title = "T", Description = "D" }); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate("https://example.com/x.jpg"), draft, CancellationToken.None); Assert.Equal(WizardStepNames.System, draft.Step); } [Fact] public async Task SkipCover_Dash_AdvancesToSystem() { var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.Cover, new WizardPayload { Type = WizardCreationType.Single, Title = "T", Description = "D" }); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate("-"), draft, CancellationToken.None); Assert.Equal(WizardStepNames.System, draft.Step); } [Theory] [InlineData("0")] [InlineData("51")] [InlineData("not a number")] public async Task OutOfRangeCapacity_StaysOnCapacityStep(string input) { var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.Capacity, new WizardPayload { Type = WizardCreationType.Single, Title = "T", System = "Dnd5e", DurationMinutes = 240, Single = new WizardSingleInput { ScheduledAt = DateTimeOffset.UtcNow.AddDays(1) }, }); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate(input), draft, CancellationToken.None); Assert.Equal(WizardStepNames.Capacity, draft.Step); } [Theory] [InlineData("0")] [InlineData("13")] [InlineData("not-a-duration")] public async Task OutOfRangeDuration_StaysOnDurationStep(string input) { var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.Duration, new WizardPayload { Type = WizardCreationType.Single, Title = "T", System = "Dnd5e" }); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate(input), draft, CancellationToken.None); Assert.Equal(WizardStepNames.Duration, draft.Step); } [Fact] public async Task EmptyDescription_SkipDash_AdvancesToCover() { var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.Description, new WizardPayload { Type = WizardCreationType.Single, Title = "T" }); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate("-"), draft, CancellationToken.None); Assert.Equal(WizardStepNames.Cover, draft.Step); } [Fact] public async Task TextOnSystem_OtherBranch_AdvancesToDuration() { // The wizard's System step offers an "Другое… ✏️" choice which arms the // step for free-text entry of a custom system name. Once armed // (i.e. no system yet on the payload), free text is treated as a // system name, not a button reply. var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.System, new WizardPayload { Type = WizardCreationType.Single, Title = "T" }); drafts.Seed(draft); await wizard.HandleUpdateAsync(TextUpdate("CustomSystem"), draft, CancellationToken.None); Assert.Equal(WizardStepNames.Duration, draft.Step); } }