2819786f91
- Extract IWizardDraftRepository interface for testability (NSubstitute cannot mock sealed classes; the codebase uses fake-style doubles instead). - Add step-transition, pool-slot, validation, cancel/back, and render-shape tests using FakeWizardDraftRepository and FakeWizardMessenger. - Fix wizard payload persistence bug: HandleCallbackAsync and HandleTextAsync now call SavePayload after ApplyChoice/ApplyText mutations, so subsequent LoadPayload calls see the user's progress. Previously, local WizardPayload mutations were discarded and the wizard reset on every step. - CommitCurrentPoolSlot now auto-creates a slot via EnsureCurrentPoolSlot when one is missing, so the PoolSlotCapacity → waitlist click is recoverable even if the user lands on the step without a slot.
183 lines
6.3 KiB
C#
183 lines
6.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|