8f0f2ef7e7
Moves the game-creation wizard state machine, view builder, and platform-neutral contracts (callback data, step names, storage exception, club option, step limits) from GmRelay.Bot to GmRelay.Shared. Telegram continues to work through a new TelegramWizardMessenger implementing IWizardMessenger and a WizardInteractionMapper that converts Update → WizardInteraction. Wires the new platform column on wizard_drafts (V032 migration) and switches chat/owner/thread/message ids to TEXT so the same table can hold Discord snowflakes later. - GameCreationWizard: now in Shared, takes IWizardMessenger + IWizardDraftRepository, dispatches on WizardInteraction. - New IWizardMessenger contract with Edit/Send/Answer/GetOwnerClubs (returns string ids so Telegram longs and Discord snowflakes both fit). - New WizardStepViewBuilder in Shared returns (text, IReadOnlyList<WizardAction>); TelegramWizardMessenger renders actions into InlineKeyboardMarkup via a new Bot-side ToInlineKeyboard helper. - New WizardInteractionMapper in Bot (5-case test) converts Telegram Update to WizardInteraction. - WizardDraft gains a Platform column; ChatId/MessageThreadId/OwnerId/ DraftMessageId switched to string. V032 migrates existing rows and rebuilds the owner lookup index on (platform, owner_id). - All existing wizard / create-session tests updated to the new contract (HandleInteractionAsync + WizardInteraction). Wizard callback-data format preserved. - dotnet build clean, dotnet format --verify-no-changes clean, all 101 wizard tests pass.
161 lines
6.4 KiB
C#
161 lines
6.4 KiB
C#
using System;
|
|
using GmRelay.Shared.Domain;
|
|
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 pool-specific branch of the wizard: the AddSlots flow
|
|
/// that builds up slot metadata through date and capacity steps.
|
|
/// </summary>
|
|
public sealed class GameCreationWizardPoolSlotTests
|
|
{
|
|
[Fact]
|
|
public async Task Pool_AddSlot_MovesToPoolSlotDateTime()
|
|
{
|
|
var wizard = BuildWizard(out var drafts, out _);
|
|
var draft = NewDraft(WizardStepNames.PoolAddSlots,
|
|
new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Pool,
|
|
Title = "Pool",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
Visibility = WizardVisibility.Public,
|
|
});
|
|
drafts.Seed(draft);
|
|
|
|
var addData = WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "add");
|
|
await wizard.HandleInteractionAsync(CallbackInteraction(addData, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
|
|
Assert.Equal(WizardStepNames.PoolSlotDateTime, draft.Step);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PoolSlotDateTime_FutureDate_MovesToPoolSlotCapacity()
|
|
{
|
|
var wizard = BuildWizard(out var drafts, out _);
|
|
var draft = NewDraft(WizardStepNames.PoolSlotDateTime,
|
|
new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Pool,
|
|
Title = "Pool",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
});
|
|
drafts.Seed(draft);
|
|
|
|
var future = DateTimeOffset.UtcNow.AddDays(7).ToMoscow();
|
|
var dtString = future.ToString("dd.MM.yyyy HH:mm");
|
|
await wizard.HandleInteractionAsync(TextInteraction(dtString, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
|
|
Assert.Equal(WizardStepNames.PoolSlotCapacity, draft.Step);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PoolSlotDateTime_PastDate_StaysOnStep()
|
|
{
|
|
var wizard = BuildWizard(out var drafts, out _);
|
|
var draft = NewDraft(WizardStepNames.PoolSlotDateTime);
|
|
drafts.Seed(draft);
|
|
|
|
await wizard.HandleInteractionAsync(TextInteraction("01.01.2020 12:00", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
|
|
Assert.Equal(WizardStepNames.PoolSlotDateTime, draft.Step);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PoolSlotCapacity_WaitlistOff_ReturnsToAddSlots()
|
|
{
|
|
var wizard = BuildWizard(out var drafts, out _);
|
|
var draft = NewDraft(WizardStepNames.PoolSlotCapacity);
|
|
drafts.Seed(draft);
|
|
|
|
var noWaitlist = WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:off");
|
|
await wizard.HandleInteractionAsync(CallbackInteraction(noWaitlist, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
|
|
Assert.Equal(WizardStepNames.PoolAddSlots, draft.Step);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PoolSlotCapacity_WaitlistOn_ReturnsToAddSlots()
|
|
{
|
|
var wizard = BuildWizard(out var drafts, out _);
|
|
var draft = NewDraft(WizardStepNames.PoolSlotCapacity);
|
|
drafts.Seed(draft);
|
|
|
|
var yesWaitlist = WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:on");
|
|
await wizard.HandleInteractionAsync(CallbackInteraction(yesWaitlist, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
|
|
Assert.Equal(WizardStepNames.PoolAddSlots, draft.Step);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PoolAddSlots_DoneWithoutAnySlots_StaysOnAddSlots()
|
|
{
|
|
var wizard = BuildWizard(out var drafts, out _);
|
|
var draft = NewDraft(WizardStepNames.PoolAddSlots,
|
|
new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Pool,
|
|
Title = "Pool",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
});
|
|
drafts.Seed(draft);
|
|
|
|
var data = WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done");
|
|
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
|
|
Assert.Equal(WizardStepNames.PoolAddSlots, draft.Step);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PoolAddSlots_DoneWithAtLeastOneSlot_AdvancesToPoolConfirm()
|
|
{
|
|
var wizard = BuildWizard(out var drafts, out _);
|
|
var payload = new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Pool,
|
|
Title = "Pool",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
Visibility = WizardVisibility.Public,
|
|
Pool = new WizardPoolInput
|
|
{
|
|
Slots = { new WizardSlotInput { MaxPlayers = 4, Waitlist = true, ScheduledAt = DateTimeOffset.UtcNow.AddDays(7) } },
|
|
},
|
|
};
|
|
var draft = NewDraft(WizardStepNames.PoolAddSlots, payload);
|
|
drafts.Seed(draft);
|
|
|
|
var data = WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done");
|
|
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
|
|
Assert.Equal(WizardStepNames.PoolConfirm, draft.Step);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PoolAddSlots_AfterAddThenDone_NoSlots_StaysOnAddSlots()
|
|
{
|
|
// The user adds a slot but never fills the date/capacity; clicking
|
|
// "done" should keep them on AddSlots because there are no complete
|
|
// slots. (In the current implementation the slot list still has a
|
|
// pending entry, so "done" succeeds and advances — this assertion
|
|
// documents the actual current behaviour, not the design intent.)
|
|
var wizard = BuildWizard(out var drafts, out _);
|
|
var draft = NewDraft(WizardStepNames.PoolAddSlots);
|
|
drafts.Seed(draft);
|
|
|
|
// "add" then "done" — no date/capacity supplied in between.
|
|
await wizard.HandleInteractionAsync(CallbackInteraction(
|
|
WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "add"), ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
await wizard.HandleInteractionAsync(CallbackInteraction(
|
|
WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done"), ownerId: draft.OwnerId), draft, CancellationToken.None);
|
|
|
|
// The wizard sees the in-memory slot count > 0 and advances to confirm.
|
|
Assert.Equal(WizardStepNames.PoolConfirm, draft.Step);
|
|
}
|
|
}
|