15040eb954
In the session creation wizard (Telegram + Discord), the Capacity step only exposed waitlist on/off buttons. The 'no waitlist' button silently advanced to the next step without setting MaxPlayers, so users who tried to create a session with no player cap were blocked with 'Не заполнены поля: лимит мест'. The DB contract and CreateSessionCommand already supported null MaxPlayers (int?, ck_sessions_max_players check in V006), and the web form already exposes 'Без лимита' as an empty InputNumber — only the wizard flow was broken. Changes: - Add '♾ Без лимита' choice button to Capacity in shared WizardStepViewBuilder.BuildCapacity (Telegram) and to RenderCapacity / RenderPoolSlotCapacity in DiscordWizardStep (Discord). - Add 'no_limit' branch to GameCreationWizard.ApplyCapacityChoice that sets MaxPlayers to null and advances to Visibility. - Change GameCreationWizard.SetMaxPlayers signature from int to int? so the 'no limit' branch compiles. - Change CreateSessionCommand builder in both Telegram and Discord submitters to take int? maxPlayers and drop the '?? 0' that would have turned null into 0 (violating the DB CHECK and the 'no limit' contract). - In Discord BuildConfirmDescription, render '👥 Без лимита, waitlist вкл/выкл' when MaxPlayers is null (the previous code silently omitted the line). - Expose BuildCommand as internal in both submitters and add InternalsVisibleTo('GmRelay.Bot.Tests') to the DiscordBot assembly for unit-test access. Tests (9 new): - WizardStepRenderTests.CapacityStep_HasWaitlistButtons — asserts the 'Без лимита' button is present. - GameCreationWizardStepTransitionsTests.NoLimitCapacityButton_… — asserts the choice advances to Visibility and leaves MaxPlayers null in the JSON draft. - GameCreationWizardStepTransitionsTests.ChoiceCallback_AdvancesToExpectedStep — new Theory row for Capacity/no_limit. - CreateSessionHandlerBuildCommandTests (new) — null/value propagation through the Telegram submitter's BuildCommand. - DiscordWizardStepCapacityRenderTests (new) — 'Без лимита' button is rendered for both Capacity and PoolSlotCapacity, with the expected custom-id shape. - DiscordWizardSubmitterBuildCommandTests (new) — null/value propagation through the Discord submitter's BuildCommand. Closes #123
86 lines
2.6 KiB
C#
86 lines
2.6 KiB
C#
using GmRelay.Bot.Features.Sessions.CreateSession;
|
|
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
|
using Xunit;
|
|
|
|
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
|
|
|
/// <summary>
|
|
/// Regression coverage for <see cref="CreateSessionHandler.BuildCommand"/>:
|
|
/// when the wizard payload carries no player limit (e.g. user picked
|
|
/// «♾ Без лимита» on the Capacity step), the resulting
|
|
/// <c>CreateSessionCommand.MaxPlayers</c> must be <c>null</c> — never <c>0</c>.
|
|
/// <c>0</c> would violate the DB CHECK constraint
|
|
/// (<c>ck_sessions_max_players</c> in V006) by inserting <c>0</c> instead of
|
|
/// <c>NULL</c>, which is the wire-level representation of "no limit".
|
|
/// </summary>
|
|
public sealed class CreateSessionHandlerBuildCommandTests
|
|
{
|
|
[Fact]
|
|
public void BuildCommand_WhenSingleMaxPlayersIsNull_PropagatesNull()
|
|
{
|
|
var draft = new WizardDraft
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ChatId = "42",
|
|
OwnerId = "100",
|
|
Step = "confirm",
|
|
};
|
|
var payload = new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Single,
|
|
Title = "T",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
Visibility = WizardVisibility.Public,
|
|
Single = new WizardSingleInput
|
|
{
|
|
ScheduledAt = DateTimeOffset.UtcNow.AddDays(1),
|
|
MaxPlayers = null,
|
|
},
|
|
};
|
|
|
|
var cmd = CreateSessionHandler.BuildCommand(
|
|
draft,
|
|
payload,
|
|
new[] { payload.Single!.ScheduledAt!.Value },
|
|
payload.Single.MaxPlayers,
|
|
isOneShot: true);
|
|
|
|
Assert.Null(cmd.MaxPlayers);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildCommand_WhenSingleMaxPlayersIsSet_PropagatesValue()
|
|
{
|
|
var draft = new WizardDraft
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ChatId = "42",
|
|
OwnerId = "100",
|
|
Step = "confirm",
|
|
};
|
|
var payload = new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Single,
|
|
Title = "T",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
Visibility = WizardVisibility.Public,
|
|
Single = new WizardSingleInput
|
|
{
|
|
ScheduledAt = DateTimeOffset.UtcNow.AddDays(1),
|
|
MaxPlayers = 5,
|
|
},
|
|
};
|
|
|
|
var cmd = CreateSessionHandler.BuildCommand(
|
|
draft,
|
|
payload,
|
|
new[] { payload.Single!.ScheduledAt!.Value },
|
|
payload.Single.MaxPlayers,
|
|
isOneShot: true);
|
|
|
|
Assert.Equal(5, cmd.MaxPlayers);
|
|
}
|
|
}
|