feat(discord): make /newsession identical to Telegram wizard
PR Checks / test-and-build (pull_request) Successful in 30m45s
PR Checks / test-and-build (pull_request) Successful in 30m45s
- Remove legacy DiscordNewSessionCommand/Handler and their tests. - Rename /newsession-wizard to /newsession. - Add shared pool capacity step before Format/Location. - Render Format and Location in Discord wizard; Location uses a modal. - Propagate Format, JoinLink and LocationAddress in BuildCommand. - Publish created sessions through existing IPlatformMessenger pipeline. - Update README, version bump to 3.11.1, sync compose/deploy/NavMenu.
This commit is contained in:
@@ -1,225 +0,0 @@
|
||||
using GmRelay.DiscordBot.Features.Sessions;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Discord;
|
||||
|
||||
public sealed class DiscordNewSessionHandlerTests
|
||||
{
|
||||
private static string GetRepoRoot()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (!string.IsNullOrEmpty(dir) && !File.Exists(Path.Combine(dir, "Directory.Build.props")))
|
||||
{
|
||||
dir = Directory.GetParent(dir)?.FullName;
|
||||
}
|
||||
|
||||
return dir ?? throw new InvalidOperationException("Could not find repo root");
|
||||
}
|
||||
|
||||
// --- Runtime tests for ParseTimeInput (static, no DB) ---
|
||||
|
||||
[Fact]
|
||||
public void ParseTimeInput_ShouldTreatInputAsMoscowTime()
|
||||
{
|
||||
var future = DateTimeOffset.UtcNow.AddDays(7);
|
||||
var result = DiscordNewSessionHandler.ParseTimeInput(
|
||||
future.ToString("yyyy-MM-dd '15:00'", System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
// 15:00 MSK = 12:00 UTC
|
||||
Assert.Equal(12, result.Value.Hour);
|
||||
Assert.Equal(0, result.Value.Minute);
|
||||
Assert.Equal(TimeSpan.Zero, result.Value.Offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTimeInput_ShouldParseDiscordDateFormat()
|
||||
{
|
||||
var expected = FutureDateAt1930();
|
||||
var result = DiscordNewSessionHandler.ParseTimeInput(
|
||||
expected.ToString("yyyy-MM-dd HH:mm", System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(expected.Year, result.Value.Year);
|
||||
Assert.Equal(expected.Month, result.Value.Month);
|
||||
Assert.Equal(expected.Day, result.Value.Day);
|
||||
// Input is treated as Moscow time; 19:30 MSK = 16:30 UTC
|
||||
Assert.Equal(16, result.Value.Hour);
|
||||
Assert.Equal(30, result.Value.Minute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTimeInput_ShouldRejectPastDate()
|
||||
{
|
||||
var result = DiscordNewSessionHandler.ParseTimeInput("2020-01-01 00:00");
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTimeInput_ShouldParseRussianDateFormat()
|
||||
{
|
||||
var expected = FutureDateAt1930();
|
||||
var result = DiscordNewSessionHandler.ParseTimeInput(
|
||||
expected.ToString("dd.MM.yyyy HH:mm", System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(expected.Year, result.Value.Year);
|
||||
Assert.Equal(expected.Month, result.Value.Month);
|
||||
Assert.Equal(expected.Day, result.Value.Day);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTimeInput_ShouldRejectInvalidFormat()
|
||||
{
|
||||
var result = DiscordNewSessionHandler.ParseTimeInput("not-a-date");
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.NotNull(result.Error);
|
||||
}
|
||||
|
||||
// --- Source-level structural tests ---
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldExist()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
Assert.True(File.Exists(handlerPath), "DiscordNewSessionHandler should exist.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldUseDapperForDatabaseAccess()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("QueryAsync", source, StringComparison.Ordinal);
|
||||
Assert.Contains("ExecuteAsync", source, StringComparison.Ordinal);
|
||||
Assert.Contains("ExecuteScalarAsync", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldUseNpgsqlDataSource()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("NpgsqlDataSource", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldCheckPermissionsViaPermissionChecker()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("CanManageSchedule", source, StringComparison.Ordinal);
|
||||
Assert.Contains("UnauthorizedAccessException", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldLoadCoGmPermissionsFromDiscordPlayers()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Matches(
|
||||
@"QueryAsync<ulong>[\s\S]*JOIN players p ON p\.id = gm\.player_id[\s\S]*p\.platform = 'Discord'[\s\S]*g\.external_group_id = @GuildId",
|
||||
source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldBePlatformNeutral()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.DoesNotContain("telegram_chat_id", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("telegram_id", source, StringComparison.Ordinal);
|
||||
Assert.Contains("platform = 'Discord'", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldUseTransactions()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("BeginTransactionAsync", source, StringComparison.Ordinal);
|
||||
Assert.Contains("CommitAsync", source, StringComparison.Ordinal);
|
||||
Assert.Contains("RollbackAsync", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldNotRollbackCommittedTransactionAfterPostCommitFailure()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("transactionCommitted = false", source, StringComparison.Ordinal);
|
||||
Assert.Contains("transactionCommitted = true", source, StringComparison.Ordinal);
|
||||
Assert.Contains("if (!transactionCommitted)", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldRespectCancellationToken()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("CancellationToken", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_ShouldRenderEmbedOnSuccess()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var commandPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionCommand.cs");
|
||||
var source = File.ReadAllText(commandPath);
|
||||
|
||||
Assert.Contains("DiscordSessionBatchRenderer.Render", source, StringComparison.Ordinal);
|
||||
Assert.Contains("message.Embeds = embeds", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldLeaveScheduleMessageCreationToInteractionResponse()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.DoesNotContain("SendScheduleAsync", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("PlatformScheduleMessage", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Handler_ShouldStoreReadableDiscordGroupNameForWebCards()
|
||||
{
|
||||
var repoRoot = GetRepoRoot();
|
||||
var handlerPath = Path.Combine(repoRoot, "src", "GmRelay.DiscordBot", "Features", "Sessions", "DiscordNewSessionHandler.cs");
|
||||
var source = File.ReadAllText(handlerPath);
|
||||
|
||||
Assert.Contains("groupName", source, StringComparison.Ordinal);
|
||||
Assert.Contains("displayGroupName", source, StringComparison.Ordinal);
|
||||
Assert.Contains("VALUES (@GroupName, 'Discord'", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static DateTimeOffset FutureDateAt1930()
|
||||
{
|
||||
var future = DateTimeOffset.UtcNow.AddDays(7);
|
||||
return new DateTimeOffset(
|
||||
future.Year,
|
||||
future.Month,
|
||||
future.Day,
|
||||
19,
|
||||
30,
|
||||
0,
|
||||
TimeSpan.Zero);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using GmRelay.DiscordBot.Features.Sessions;
|
||||
using GmRelay.DiscordBot.Features.Sessions.Wizard;
|
||||
using NetCord.Services.ApplicationCommands;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Discord;
|
||||
@@ -54,7 +55,6 @@ public sealed class DiscordStartupTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(DiscordNewSessionCommand), "newsession")]
|
||||
[InlineData(typeof(DiscordListSessionsCommand), "listsessions")]
|
||||
[InlineData(typeof(DiscordRescheduleCommand), "reschedule")]
|
||||
public void DiscordSessionSlashCommands_ShouldBeDeclaredOnModuleMethods(Type moduleType, string commandName)
|
||||
@@ -76,15 +76,28 @@ public sealed class DiscordStartupTests
|
||||
{
|
||||
var service = new ApplicationCommandService<SlashCommandContext>();
|
||||
|
||||
service.AddModules(typeof(DiscordNewSessionCommand).Assembly);
|
||||
service.AddModules(typeof(DiscordListSessionsCommand).Assembly);
|
||||
|
||||
var commandNames = service.GetCommands()
|
||||
.Select(command => command.Name)
|
||||
.ToArray();
|
||||
|
||||
Assert.Contains("listsessions", commandNames);
|
||||
Assert.Contains("reschedule", commandNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscordSessionSlashCommands_ShouldIncludeNewSessionWizard()
|
||||
{
|
||||
var service = new ApplicationCommandService<SlashCommandContext>();
|
||||
|
||||
service.AddModules(typeof(DiscordWizardCommand).Assembly);
|
||||
|
||||
var commandNames = service.GetCommands()
|
||||
.Select(command => command.Name)
|
||||
.ToArray();
|
||||
|
||||
Assert.Contains("newsession", commandNames);
|
||||
Assert.Contains("listsessions", commandNames);
|
||||
Assert.Contains("reschedule", commandNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -114,7 +127,6 @@ public sealed class DiscordStartupTests
|
||||
{
|
||||
var program = ReadProgram();
|
||||
Assert.Contains("DiscordListSessionsHandler", program);
|
||||
Assert.Contains("DiscordNewSessionHandler", program);
|
||||
Assert.Contains("JoinSessionHandler", program);
|
||||
Assert.Contains("LeaveSessionHandler", program);
|
||||
Assert.Contains("DiscordPermissionChecker", program);
|
||||
|
||||
@@ -56,6 +56,29 @@ public sealed class DiscordWizardStepCapacityRenderTests
|
||||
.Select(b => b.Label ?? string.Empty)
|
||||
.ToList();
|
||||
|
||||
[Fact]
|
||||
public void RenderFormat_ContainsOnlineAndOfflineButtons()
|
||||
{
|
||||
var draft = new WizardDraft { Step = WizardStepNames.Format };
|
||||
var render = DiscordWizardStep.Render(draft, new WizardPayload());
|
||||
|
||||
var labels = ExtractButtonLabels(render);
|
||||
Assert.Contains(labels, l => l.Contains("Online", System.StringComparison.Ordinal));
|
||||
Assert.Contains(labels, l => l.Contains("Offline", System.StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(WizardSessionFormat.Online, "🔗 Ссылка")]
|
||||
[InlineData(WizardSessionFormat.Offline, "📍 Адрес")]
|
||||
public void RenderLocation_ForFormat_OpensModalAndShowsPrompt(WizardSessionFormat format, string expectedTitle)
|
||||
{
|
||||
var draft = new WizardDraft { Step = WizardStepNames.Location };
|
||||
var render = DiscordWizardStep.Render(draft, new WizardPayload { Format = format });
|
||||
|
||||
Assert.Equal(expectedTitle, render.EmbedTitle);
|
||||
Assert.Equal(WizardStepNames.Location, render.OpenModalStep);
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.List<ButtonProperties> ExtractButtons(
|
||||
DiscordWizardStep.DiscordWizardRender render) =>
|
||||
render.Components
|
||||
|
||||
@@ -82,4 +82,117 @@ public sealed class DiscordWizardSubmitterBuildCommandTests
|
||||
|
||||
Assert.Equal(5, cmd.MaxPlayers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCommand_WhenFormatIsOnline_PropagatesFormatAndJoinLink()
|
||||
{
|
||||
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,
|
||||
Format = WizardSessionFormat.Online,
|
||||
JoinLink = "https://vtt.example/game",
|
||||
Single = new WizardSingleInput
|
||||
{
|
||||
ScheduledAt = DateTimeOffset.UtcNow.AddDays(1),
|
||||
MaxPlayers = 5,
|
||||
},
|
||||
};
|
||||
|
||||
var cmd = DiscordWizardSubmitter.BuildCommand(
|
||||
draft,
|
||||
payload,
|
||||
new[] { payload.Single!.ScheduledAt!.Value },
|
||||
payload.Single.MaxPlayers,
|
||||
isOneShot: true);
|
||||
|
||||
Assert.Equal("Online", cmd.Format);
|
||||
Assert.Equal("https://vtt.example/game", cmd.Link);
|
||||
Assert.Null(cmd.LocationAddress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCommand_WhenFormatIsOffline_PropagatesFormatAndAddress()
|
||||
{
|
||||
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,
|
||||
Format = WizardSessionFormat.Offline,
|
||||
LocationAddress = "Москва, ул. Кубиков, 12",
|
||||
Single = new WizardSingleInput
|
||||
{
|
||||
ScheduledAt = DateTimeOffset.UtcNow.AddDays(1),
|
||||
MaxPlayers = 5,
|
||||
},
|
||||
};
|
||||
|
||||
var cmd = DiscordWizardSubmitter.BuildCommand(
|
||||
draft,
|
||||
payload,
|
||||
new[] { payload.Single!.ScheduledAt!.Value },
|
||||
payload.Single.MaxPlayers,
|
||||
isOneShot: true);
|
||||
|
||||
Assert.Equal("Offline", cmd.Format);
|
||||
Assert.Equal("Москва, ул. Кубиков, 12", cmd.LocationAddress);
|
||||
Assert.Equal(string.Empty, cmd.Link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCommand_WhenPoolMaxPlayersIsSet_PropagatesValueToMaxPlayers()
|
||||
{
|
||||
var draft = new WizardDraft
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ChatId = "42",
|
||||
OwnerId = "100",
|
||||
Step = "confirm",
|
||||
};
|
||||
var slotTime = DateTimeOffset.UtcNow.AddDays(1);
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Pool,
|
||||
Title = "Pool",
|
||||
System = "Dnd5e",
|
||||
DurationMinutes = 240,
|
||||
Visibility = WizardVisibility.Public,
|
||||
Format = WizardSessionFormat.Online,
|
||||
JoinLink = "https://vtt.example/game",
|
||||
Pool = new WizardPoolInput
|
||||
{
|
||||
MaxPlayers = 12,
|
||||
Slots = { new WizardSlotInput { ScheduledAt = slotTime, MaxPlayers = 8 } },
|
||||
},
|
||||
};
|
||||
|
||||
var cmd = DiscordWizardSubmitter.BuildCommand(
|
||||
draft,
|
||||
payload,
|
||||
new[] { slotTime },
|
||||
payload.Pool.MaxPlayers,
|
||||
isOneShot: false);
|
||||
|
||||
Assert.Equal(12, cmd.MaxPlayers);
|
||||
}
|
||||
}
|
||||
|
||||
+49
-2
@@ -46,7 +46,7 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PoolSystemDuration_PreselectedButton_AdvancesToFormat()
|
||||
public async Task PoolSystemDuration_PreselectedButton_AdvancesToCapacity()
|
||||
{
|
||||
var wizard = BuildWizard(out var drafts, out _);
|
||||
var payload = new WizardPayload
|
||||
@@ -60,7 +60,7 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Dnd5e:240");
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Format, draft.Step);
|
||||
Assert.Equal(WizardStepNames.Capacity, draft.Step);
|
||||
using var doc = JsonDocument.Parse(draft.PayloadJson);
|
||||
var root = doc.RootElement;
|
||||
Assert.True(root.TryGetProperty("system", out var sys));
|
||||
@@ -69,6 +69,53 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
Assert.Equal(240, dur.GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PoolCapacity_Text_AdvancesToFormat_AndSetsMaxPlayers()
|
||||
{
|
||||
var wizard = BuildWizard(out var drafts, out _);
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Pool,
|
||||
Title = "Pool",
|
||||
System = "Dnd5e",
|
||||
DurationMinutes = 240,
|
||||
};
|
||||
var draft = NewDraft(WizardStepNames.Capacity, payload);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await wizard.HandleInteractionAsync(TextInteraction("10", ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Format, draft.Step);
|
||||
using var doc = JsonDocument.Parse(draft.PayloadJson);
|
||||
var root = doc.RootElement;
|
||||
Assert.True(root.TryGetProperty("pool", out var pool));
|
||||
Assert.True(pool.TryGetProperty("maxPlayers", out var maxPlayers));
|
||||
Assert.Equal(10, maxPlayers.GetInt32());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("waitlist:on")]
|
||||
[InlineData("waitlist:off")]
|
||||
public async Task Capacity_WithMaxPlayersAndWaitlist_AdvancesToFormat(string waitlistChoice)
|
||||
{
|
||||
var wizard = BuildWizard(out var drafts, out _);
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Single,
|
||||
Title = "T",
|
||||
System = "Dnd5e",
|
||||
DurationMinutes = 240,
|
||||
Single = new WizardSingleInput { ScheduledAt = DateTimeOffset.UtcNow.AddDays(1), MaxPlayers = 5 },
|
||||
};
|
||||
var draft = NewDraft(WizardStepNames.Capacity, payload);
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.Capacity, waitlistChoice);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Format, draft.Step);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoLimitCapacityButton_AdvancesToVisibility_AndLeavesMaxPlayersNull()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user