320ec18ab0
PR Checks / test-and-build (pull_request) Successful in 12m33s
After 3.9.6 fixed long-polling, the bot finally reaches the final '✅ Создать' step. Users pressing '♾ Без лимита' on the Capacity step get a valid payload where Single.MaxPlayers = null (the legitimate no-limit choice from GameCreationWizard.ApplyCapacityChoice 'no_limit'), but CreateSessionHandler.IsComplete then reports 'лимит мест' as missing, blocking session creation. This regression existed since 3.9.3 (when 'no_limit' was added) but stayed invisible because 3.9.4 and 3.9.5 never reached SubmitDraft (libgssapi-krb5 missing → long-polling hung). Once 3.9.6 restored polling, the bug surfaced immediately. Fix: drop the null-MaxPlayers check from IsComplete for Single type. Null is a valid 'no limit' state and must pass through to BuildCommands → shared handler, which already accepts null MaxPlayers correctly. Closes #131. Bump version 3.9.6 -> 3.9.7
193 lines
6.7 KiB
C#
193 lines
6.7 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using GmRelay.Bot.Features.Sessions.CreateSession;
|
|
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
|
|
|
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
|
|
|
/// <summary>
|
|
/// Verifies the validation gates inside
|
|
/// <see cref="CreateSessionHandler.SubmitDraftAsync"/>. We never reach the
|
|
/// shared handler in any of these tests, so the shared dependency is
|
|
/// passed as <c>null!</c> — a NRE on that branch would itself prove the
|
|
/// validation did not fire.
|
|
/// </summary>
|
|
public sealed class CreateSessionHandlerSubmitValidationTests
|
|
{
|
|
[Fact]
|
|
public async Task SubmitDraftAsync_MissingVisibility_EditsMessageNamingVisibility()
|
|
{
|
|
var drafts = new FakeWizardDraftRepository();
|
|
var messenger = new FakeWizardMessenger();
|
|
|
|
var sut = new CreateSessionHandler(
|
|
drafts,
|
|
shared: null!,
|
|
messenger,
|
|
NullLogger<CreateSessionHandler>.Instance);
|
|
|
|
// All required fields set except Visibility.
|
|
var payload = new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Single,
|
|
Title = "T",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
Single = new WizardSingleInput
|
|
{
|
|
ScheduledAt = DateTimeOffset.UtcNow.AddDays(7),
|
|
MaxPlayers = 4,
|
|
},
|
|
};
|
|
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
|
drafts.Seed(draft);
|
|
|
|
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
|
|
|
Assert.Single(messenger.Edits);
|
|
Assert.Contains("видимость", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitDraftAsync_MissingSystem_EditsMessageNamingSystem()
|
|
{
|
|
var drafts = new FakeWizardDraftRepository();
|
|
var messenger = new FakeWizardMessenger();
|
|
|
|
var sut = new CreateSessionHandler(
|
|
drafts,
|
|
shared: null!,
|
|
messenger,
|
|
NullLogger<CreateSessionHandler>.Instance);
|
|
|
|
// All required fields set except System.
|
|
var payload = new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Single,
|
|
Title = "T",
|
|
DurationMinutes = 240,
|
|
Visibility = WizardVisibility.Public,
|
|
Single = new WizardSingleInput
|
|
{
|
|
ScheduledAt = DateTimeOffset.UtcNow.AddDays(7),
|
|
MaxPlayers = 4,
|
|
},
|
|
};
|
|
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
|
drafts.Seed(draft);
|
|
|
|
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
|
|
|
Assert.Single(messenger.Edits);
|
|
Assert.Contains("система", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitDraftAsync_MissingDateTimeForSingleType_EditsMessageNamingDateTime()
|
|
{
|
|
var drafts = new FakeWizardDraftRepository();
|
|
var messenger = new FakeWizardMessenger();
|
|
|
|
var sut = new CreateSessionHandler(
|
|
drafts,
|
|
shared: null!,
|
|
messenger,
|
|
NullLogger<CreateSessionHandler>.Instance);
|
|
|
|
// All required fields set except ScheduledAt for Single type.
|
|
var payload = new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Single,
|
|
Title = "T",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
Visibility = WizardVisibility.Public,
|
|
Single = new WizardSingleInput { MaxPlayers = 4 },
|
|
};
|
|
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
|
drafts.Seed(draft);
|
|
|
|
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
|
|
|
Assert.Single(messenger.Edits);
|
|
Assert.Contains("дата/время", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitDraftAsync_EmptyPool_EditsMessageNamingSlots()
|
|
{
|
|
var drafts = new FakeWizardDraftRepository();
|
|
var messenger = new FakeWizardMessenger();
|
|
|
|
var sut = new CreateSessionHandler(
|
|
drafts,
|
|
shared: null!,
|
|
messenger,
|
|
NullLogger<CreateSessionHandler>.Instance);
|
|
|
|
// Pool type with no slots at all.
|
|
var payload = new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Pool,
|
|
Title = "P",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
Visibility = WizardVisibility.Public,
|
|
Pool = new WizardPoolInput(),
|
|
};
|
|
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
|
drafts.Seed(draft);
|
|
|
|
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
|
|
|
Assert.Single(messenger.Edits);
|
|
Assert.Contains("слоты", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubmitDraftAsync_SingleWithNoLimit_DoesNotReportMaxPlayersAsMissing()
|
|
{
|
|
// Regression for #131: pressing "♾ Без лимита" sets MaxPlayers = null.
|
|
// IsComplete must NOT flag that as a missing field; null means
|
|
// "no player limit" and is a valid final state.
|
|
var drafts = new FakeWizardDraftRepository();
|
|
var messenger = new FakeWizardMessenger();
|
|
|
|
var sut = new CreateSessionHandler(
|
|
drafts,
|
|
shared: null!,
|
|
messenger,
|
|
NullLogger<CreateSessionHandler>.Instance);
|
|
|
|
var payload = new WizardPayload
|
|
{
|
|
Type = WizardCreationType.Single,
|
|
Title = "T",
|
|
System = "Dnd5e",
|
|
DurationMinutes = 240,
|
|
Visibility = WizardVisibility.Public,
|
|
Single = new WizardSingleInput
|
|
{
|
|
ScheduledAt = DateTimeOffset.UtcNow.AddDays(7),
|
|
MaxPlayers = null,
|
|
},
|
|
};
|
|
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
|
drafts.Seed(draft);
|
|
|
|
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
|
|
|
// Validation must let the no-limit payload through. The shared
|
|
// handler is null, so anything that reached the database call would
|
|
// throw a NullReferenceException — that is caught by the retry
|
|
// path and reported as a "💥 Ошибка:" edit, not a missing-fields
|
|
// edit. Therefore we assert that NO edit mentions a missing field.
|
|
Assert.NotEmpty(messenger.Edits);
|
|
var lastEdit = messenger.Edits[^1].Text;
|
|
Assert.DoesNotContain("Не заполнены", lastEdit, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|