diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index c3cbeb0..4ddf91e 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,7 +6,7 @@ on: - main env: - VERSION: 3.9.4 + VERSION: 3.9.5 jobs: # ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами) diff --git a/Directory.Build.props b/Directory.Build.props index 3d01b11..2139fad 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 3.9.4 + 3.9.5 net10.0 preview enable diff --git a/compose.yaml b/compose.yaml index e98ed76..465b800 100644 --- a/compose.yaml +++ b/compose.yaml @@ -49,7 +49,7 @@ services: crond -f bot: - image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.4 + image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.5 restart: always depends_on: db: @@ -67,7 +67,7 @@ services: retries: 3 discord: - image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.4 + image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.5 restart: always depends_on: db: @@ -86,7 +86,7 @@ services: retries: 3 web: - image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.4 + image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.5 restart: always depends_on: db: diff --git a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs index 2b523de..cea0e52 100644 --- a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs +++ b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs @@ -298,13 +298,25 @@ public sealed class GameCreationWizard : (null, "Неверная длительность"), }; - private static (string?, string?) ApplyCapacityChoice(WizardPayload p, string choice) => choice switch + private static (string?, string?) ApplyCapacityChoice(WizardPayload p, string choice) { - "waitlist:on" => (WizardStepNames.Visibility, SetWaitlist(p, true)), - "waitlist:off" => (WizardStepNames.Visibility, SetWaitlist(p, false)), - "no_limit" => (WizardStepNames.Visibility, SetMaxPlayers(p, null)), - _ => (null, "Неизвестный выбор"), - }; + if (choice is "no_limit") + { + return (WizardStepNames.Visibility, SetMaxPlayers(p, null)); + } + + if (choice is "waitlist:on" or "waitlist:off" && p.Single?.MaxPlayers is null) + { + return (null, "Сначала введите лимит мест или нажмите «♾ Без лимита»"); + } + + return choice switch + { + "waitlist:on" => (WizardStepNames.Visibility, SetWaitlist(p, true)), + "waitlist:off" => (WizardStepNames.Visibility, SetWaitlist(p, false)), + _ => (null, "Неизвестный выбор"), + }; + } private static (string?, string?) ApplyVisibilityChoice(WizardPayload p, string choice) => choice switch { @@ -316,9 +328,15 @@ public sealed class GameCreationWizard }; private static (string?, string?) ApplyPickClubChoice(WizardPayload p, string choice) - => Guid.TryParse(choice, out var id) - ? (NextAfterVisibility(p), SetClubId(p, id)) - : (null, "Неверный идентификатор клуба"); + { + if (!Guid.TryParse(choice, out var id)) + { + return (null, "Неверный идентификатор клуба"); + } + + var error = SetClubId(p, id); + return (NextAfterVisibility(p), error); + } private static (string?, string?) ApplyPublishChoice(WizardPayload p, string choice) => choice switch { diff --git a/src/GmRelay.Web/Components/Layout/NavMenu.razor b/src/GmRelay.Web/Components/Layout/NavMenu.razor index bb8c9b0..776b179 100644 --- a/src/GmRelay.Web/Components/Layout/NavMenu.razor +++ b/src/GmRelay.Web/Components/Layout/NavMenu.razor @@ -82,7 +82,7 @@ - + diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs index 6064da5..bf9b549 100644 --- a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs @@ -20,9 +20,7 @@ public sealed class GameCreationWizardStepTransitionsTests [InlineData(WizardStepNames.System, "Dnd5e", WizardStepNames.Duration)] // Duration → DateTime (single, no maxPlayers yet) [InlineData(WizardStepNames.Duration, "240", WizardStepNames.DateTime)] - // Capacity → Visibility - [InlineData(WizardStepNames.Capacity, "waitlist:on", WizardStepNames.Visibility)] - [InlineData(WizardStepNames.Capacity, "waitlist:off", WizardStepNames.Visibility)] + // Capacity → Visibility (only explicit no-limit can skip numeric capacity) [InlineData(WizardStepNames.Capacity, "no_limit", WizardStepNames.Visibility)] // Visibility → Publish (public, no club) [InlineData(WizardStepNames.Visibility, "public", WizardStepNames.Publish)] @@ -98,13 +96,11 @@ public sealed class GameCreationWizardStepTransitionsTests } [Fact] - public async Task ChoiceCallback_FromMismatchedStep_AdvancesBasedOnCallbackStep() + public async Task StaleCapacityWaitlistCallback_WithoutCapacity_StaysOnCurrentStep() { - // The wizard's callback parser uses the step encoded in the callback - // (not the draft's current step) to drive transitions. So a stale - // "Capacity" button pressed while the user is on System will in fact - // move the draft forward as if they had pressed it on Capacity. We - // lock that behaviour in. + // A stale waitlist button from Capacity must not move a draft forward + // unless MaxPlayers is already set. Otherwise users can reach Confirm + // with a missing capacity and get "Не заполнены поля: лимит мест". var wizard = BuildWizard(out var drafts, out _); var draft = NewDraft(WizardStepNames.System); drafts.Seed(draft); @@ -112,17 +108,13 @@ public sealed class GameCreationWizardStepTransitionsTests var data = WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:on"); await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None); - Assert.Equal(WizardStepNames.Visibility, draft.Step); + Assert.Equal(WizardStepNames.System, draft.Step); } [Fact] - public async Task PickClub_ValidGuid_ReachesStableStep() + public async Task PickClub_ValidGuid_AdvancesToPublishOnFirstClick() { - // The wizard has a quirk: NextAfterVisibility is evaluated before - // SetClubId, so a single click leaves the draft still on PickClub. - // We assert that the wizard does NOT throw and the messenger is asked - // to re-render (i.e. the handler ran end-to-end). - var wizard = BuildWizard(out var drafts, out var messenger); + var wizard = BuildWizard(out var drafts, out _); var clubId = Guid.NewGuid(); var payload = new WizardPayload { @@ -138,8 +130,11 @@ public sealed class GameCreationWizardStepTransitionsTests var data = WizardCallbackData.Choice(WizardStepNames.PickClub, clubId.ToString()); await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None); - // Wizard acknowledged the callback and re-rendered the (still PickClub) step. - Assert.NotEmpty(messenger.Edits); + Assert.Equal(WizardStepNames.Publish, draft.Step); + using var doc = JsonDocument.Parse(draft.PayloadJson); + var root = doc.RootElement; + Assert.True(root.TryGetProperty("clubId", out var clubIdJson)); + Assert.Equal(clubId, clubIdJson.GetGuid()); } [Fact] diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardValidationTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardValidationTests.cs index d33ec9f..0a55d25 100644 --- a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardValidationTests.cs +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardValidationTests.cs @@ -136,6 +136,29 @@ public sealed class GameCreationWizardValidationTests Assert.Equal(WizardStepNames.Capacity, draft.Step); } + [Theory] + [InlineData("waitlist:on")] + [InlineData("waitlist:off")] + public async Task WaitlistChoiceWithoutCapacity_StaysOnCapacityStep(string choice) + { + 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); + + var data = WizardCallbackData.Choice(WizardStepNames.Capacity, choice); + await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None); + + Assert.Equal(WizardStepNames.Capacity, draft.Step); + } + [Theory] [InlineData("0")] [InlineData("13")]