refactor(wizard): make CreateSessionHandler wizard-driven and remove legacy parser
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using BotRescheduleHandler = GmRelay.Bot.Features.Sessions.RescheduleSession.HandleRescheduleTimeInputHandler;
|
||||
using GmRelay.DiscordBot.Rendering;
|
||||
using GmRelay.Shared.Domain;
|
||||
@@ -11,13 +10,17 @@ namespace GmRelay.Bot.Tests.Features.Landing;
|
||||
|
||||
public sealed class DiscordLandingPromisesSmokeTests
|
||||
{
|
||||
private sealed record SmokeParseResult(
|
||||
string Title,
|
||||
string Link,
|
||||
int? MaxPlayers,
|
||||
IReadOnlyList<DateTimeOffset> ScheduledTimes);
|
||||
|
||||
[Fact]
|
||||
public void Smoke_ShouldCoverDiscordLandingPromisesWithoutExternalDiscordApi()
|
||||
{
|
||||
var nowUtc = new DateTimeOffset(2026, 5, 1, 12, 0, 0, TimeSpan.Zero);
|
||||
var parseResult = NewSessionCommandParser.Parse(BuildRecurringSessionCommand(), nowUtc);
|
||||
var parseResult = BuildRecurringSessionParseResult();
|
||||
|
||||
Assert.True(parseResult.IsValid);
|
||||
Assert.Equal(3, parseResult.ScheduledTimes.Count);
|
||||
Assert.Equal(2, parseResult.MaxPlayers);
|
||||
Assert.Equal(TimeSpan.FromDays(7), parseResult.ScheduledTimes[1] - parseResult.ScheduledTimes[0]);
|
||||
@@ -126,16 +129,17 @@ public sealed class DiscordLandingPromisesSmokeTests
|
||||
Assert.Contains("Carol", firstSessionEmbed.Description);
|
||||
}
|
||||
|
||||
private static string BuildRecurringSessionCommand() =>
|
||||
string.Join(
|
||||
'\n',
|
||||
"/newsession",
|
||||
"Название: Landing Promise Smoke",
|
||||
"Время: 15.05.2026 19:30",
|
||||
"Игр: 3",
|
||||
"Интервал: 7",
|
||||
"Мест: 2",
|
||||
"Ссылка: https://example.test/table");
|
||||
private static SmokeParseResult BuildRecurringSessionParseResult() =>
|
||||
new(
|
||||
Title: "Landing Promise Smoke",
|
||||
Link: "https://example.test/table",
|
||||
MaxPlayers: 2,
|
||||
ScheduledTimes: new[]
|
||||
{
|
||||
new DateTimeOffset(2026, 5, 15, 16, 30, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 5, 22, 16, 30, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 5, 29, 16, 30, 0, TimeSpan.Zero),
|
||||
});
|
||||
|
||||
private static IReadOnlyList<string> CallbackData(IReadOnlyList<ActionRowProperties> actionRows) =>
|
||||
actionRows
|
||||
@@ -183,14 +187,14 @@ public sealed class DiscordLandingPromisesSmokeTests
|
||||
public FakeDiscordMessage LastMessage => Messenger.LastMessage;
|
||||
|
||||
public static DiscordLandingSmokeScenario Publish(
|
||||
NewSessionParseResult parseResult,
|
||||
SmokeParseResult parseResult,
|
||||
SessionNotificationMode notificationMode)
|
||||
{
|
||||
var scenario = new DiscordLandingSmokeScenario(
|
||||
parseResult.Title!,
|
||||
parseResult.Title,
|
||||
parseResult.ScheduledTimes,
|
||||
parseResult.MaxPlayers,
|
||||
parseResult.Link!,
|
||||
parseResult.Link,
|
||||
notificationMode);
|
||||
|
||||
scenario.RenderBatch();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using BotRescheduleHandler = GmRelay.Bot.Features.Sessions.RescheduleSession.HandleRescheduleTimeInputHandler;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||
@@ -10,13 +9,17 @@ namespace GmRelay.Bot.Tests.Features.Landing;
|
||||
|
||||
public sealed class TelegramLandingPromisesSmokeTests
|
||||
{
|
||||
private sealed record SmokeParseResult(
|
||||
string Title,
|
||||
string Link,
|
||||
int? MaxPlayers,
|
||||
IReadOnlyList<DateTimeOffset> ScheduledTimes);
|
||||
|
||||
[Fact]
|
||||
public void Smoke_ShouldCoverTelegramLandingPromisesWithoutExternalTelegramApi()
|
||||
{
|
||||
var nowUtc = new DateTimeOffset(2026, 5, 1, 12, 0, 0, TimeSpan.Zero);
|
||||
var parseResult = NewSessionCommandParser.Parse(BuildRecurringSessionCommand(), nowUtc);
|
||||
var parseResult = BuildRecurringSessionParseResult();
|
||||
|
||||
Assert.True(parseResult.IsValid);
|
||||
Assert.Equal(3, parseResult.ScheduledTimes.Count);
|
||||
Assert.Equal(2, parseResult.MaxPlayers);
|
||||
Assert.Equal(TimeSpan.FromDays(7), parseResult.ScheduledTimes[1] - parseResult.ScheduledTimes[0]);
|
||||
@@ -120,16 +123,17 @@ public sealed class TelegramLandingPromisesSmokeTests
|
||||
Assert.Contains("@carol", scenario.LastMessage.Text);
|
||||
}
|
||||
|
||||
private static string BuildRecurringSessionCommand() =>
|
||||
string.Join(
|
||||
'\n',
|
||||
"/newsession",
|
||||
"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435: Landing Promise Smoke",
|
||||
"\u0412\u0440\u0435\u043c\u044f: 15.05.2026 19:30",
|
||||
"\u0418\u0433\u0440: 3",
|
||||
"\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b: 7",
|
||||
"\u041c\u0435\u0441\u0442: 2",
|
||||
"\u0421\u0441\u044b\u043b\u043a\u0430: https://example.test/table");
|
||||
private static SmokeParseResult BuildRecurringSessionParseResult() =>
|
||||
new(
|
||||
Title: "Landing Promise Smoke",
|
||||
Link: "https://example.test/table",
|
||||
MaxPlayers: 2,
|
||||
ScheduledTimes: new[]
|
||||
{
|
||||
new DateTimeOffset(2026, 5, 15, 16, 30, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 5, 22, 16, 30, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 5, 29, 16, 30, 0, TimeSpan.Zero),
|
||||
});
|
||||
|
||||
private static IReadOnlyList<string> CallbackData(InlineKeyboardMarkup markup) =>
|
||||
markup.InlineKeyboard
|
||||
@@ -169,14 +173,14 @@ public sealed class TelegramLandingPromisesSmokeTests
|
||||
public FakeTelegramMessage LastMessage => Messenger.LastMessage;
|
||||
|
||||
public static TelegramLandingSmokeScenario Publish(
|
||||
NewSessionParseResult parseResult,
|
||||
SmokeParseResult parseResult,
|
||||
SessionNotificationMode notificationMode)
|
||||
{
|
||||
var scenario = new TelegramLandingSmokeScenario(
|
||||
parseResult.Title!,
|
||||
parseResult.Title,
|
||||
parseResult.ScheduledTimes,
|
||||
parseResult.MaxPlayers,
|
||||
parseResult.Link!,
|
||||
parseResult.Link,
|
||||
notificationMode);
|
||||
|
||||
scenario.RenderBatch();
|
||||
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using Telegram.Bot.Types;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession;
|
||||
|
||||
public sealed class NewSessionCommandParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_ShouldExtractTitleLinkAndUpcomingTimes()
|
||||
{
|
||||
var nowUtc = new DateTimeOffset(2026, 4, 23, 12, 0, 0, TimeSpan.Zero);
|
||||
var text = """
|
||||
/newsession
|
||||
Название: Curse of Strahd
|
||||
Время: 24.04.2026 19:30
|
||||
Время: 01.05.2026 20:00
|
||||
Мест: 4
|
||||
Ссылка: https://example.test/room
|
||||
""";
|
||||
|
||||
var result = NewSessionCommandParser.Parse(text, nowUtc);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("Curse of Strahd", result.Title);
|
||||
Assert.Equal("https://example.test/room", result.Link);
|
||||
Assert.Equal(4, result.MaxPlayers);
|
||||
Assert.Equal(
|
||||
[
|
||||
new DateTimeOffset(2026, 4, 24, 16, 30, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 5, 1, 17, 0, 0, TimeSpan.Zero)
|
||||
],
|
||||
result.ScheduledTimes);
|
||||
Assert.Empty(result.PastTimeInputs);
|
||||
Assert.Empty(result.InvalidTimeInputs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldExtractOptionalImageUrl()
|
||||
{
|
||||
var nowUtc = new DateTimeOffset(2026, 4, 23, 12, 0, 0, TimeSpan.Zero);
|
||||
var text = """
|
||||
/newsession
|
||||
Название: Curse of Strahd
|
||||
Время: 24.04.2026 19:30
|
||||
Ссылка: https://example.test/room
|
||||
Картинка: https://example.test/strahd.jpg
|
||||
""";
|
||||
|
||||
var result = NewSessionCommandParser.Parse(text, nowUtc);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("https://example.test/strahd.jpg", result.ImageUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBatchImageReference_ShouldPreferAttachedPhotoOverParsedUrl()
|
||||
{
|
||||
var message = new Message
|
||||
{
|
||||
Photo =
|
||||
[
|
||||
new PhotoSize { FileId = "small-photo", Width = 320, Height = 180, FileSize = 10 },
|
||||
new PhotoSize { FileId = "large-photo", Width = 1280, Height = 720, FileSize = 20 }
|
||||
]
|
||||
};
|
||||
|
||||
var imageReference = CreateSessionHandler.GetBatchImageReference(
|
||||
message,
|
||||
"https://example.test/cover.jpg");
|
||||
|
||||
Assert.Equal("large-photo", imageReference);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldExpandRecurringSchedule_WhenRepeatCountAndIntervalProvided()
|
||||
{
|
||||
var nowUtc = new DateTimeOffset(2026, 4, 23, 12, 0, 0, TimeSpan.Zero);
|
||||
var text = """
|
||||
/newsession
|
||||
Название: Kingmaker
|
||||
Время: 30.04.2026 19:30
|
||||
Игр: 4
|
||||
Интервал: 14
|
||||
Ссылка: https://example.test/kingmaker
|
||||
""";
|
||||
|
||||
var result = NewSessionCommandParser.Parse(text, nowUtc);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(
|
||||
[
|
||||
new DateTimeOffset(2026, 4, 30, 16, 30, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 5, 14, 16, 30, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 5, 28, 16, 30, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2026, 6, 11, 16, 30, 0, TimeSpan.Zero)
|
||||
],
|
||||
result.ScheduledTimes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldCollectPastAndInvalidTimes()
|
||||
{
|
||||
var nowUtc = new DateTimeOffset(2026, 4, 23, 12, 0, 0, TimeSpan.Zero);
|
||||
var text = """
|
||||
Название: Delta Green
|
||||
Время: 20.04.2026 19:30
|
||||
Время: 31.04.2026 19:30
|
||||
Время: 25.04.2026 18:00
|
||||
Ссылка: https://example.test/dg
|
||||
""";
|
||||
|
||||
var result = NewSessionCommandParser.Parse(text, nowUtc);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Single(result.ScheduledTimes);
|
||||
Assert.Equal(["20.04.2026 19:30"], result.PastTimeInputs);
|
||||
Assert.Equal(["31.04.2026 19:30"], result.InvalidTimeInputs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldBeInvalid_WhenRequiredFieldsMissing()
|
||||
{
|
||||
var text = """
|
||||
/newsession
|
||||
Название: Blades in the Dark
|
||||
Время: 25.04.2026 19:30
|
||||
""";
|
||||
|
||||
var result = NewSessionCommandParser.Parse(text, DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Null(result.Link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ShouldCollectInvalidSeatLimit()
|
||||
{
|
||||
var text = """
|
||||
/newsession
|
||||
Название: Blades in the Dark
|
||||
Время: 25.04.2026 19:30
|
||||
Мест: 0
|
||||
Ссылка: https://example.test/blades
|
||||
""";
|
||||
|
||||
var result = NewSessionCommandParser.Parse(text, DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Null(result.MaxPlayers);
|
||||
Assert.Equal(["0"], result.InvalidSeatLimitInputs);
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -8,12 +8,18 @@ public sealed class TelegramTopicIntegrationSmokeTests
|
||||
var migration = await ReadRepositoryFileAsync("src/GmRelay.Bot/Migrations/V015__add_topic_ownership.sql");
|
||||
var createHandler = await ReadRepositoryFileAsync("src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs");
|
||||
var deleteHandler = await ReadRepositoryFileAsync("src/GmRelay.Bot/Features/Sessions/ListSessions/DeleteSessionHandler.cs");
|
||||
var topicRouting = await ReadRepositoryFileAsync("src/GmRelay.Bot/Infrastructure/Telegram/TelegramTopicRouting.cs");
|
||||
|
||||
Assert.Contains("topic_created_by_bot", migration, StringComparison.Ordinal);
|
||||
Assert.Contains("ResolveNewScheduleDestination", createHandler, StringComparison.Ordinal);
|
||||
Assert.Contains("message.MessageThreadId", createHandler, StringComparison.Ordinal);
|
||||
Assert.Contains("topicCreatedByBot", createHandler, StringComparison.Ordinal);
|
||||
Assert.Contains("MissingForumTopicRightsMessage", createHandler, StringComparison.Ordinal);
|
||||
|
||||
// The wizard-driven CreateSessionHandler threads the existing forum topic
|
||||
// (if any) into the draft; the shared creation command inherits it. Topic
|
||||
// auto-creation and rights handling live in TelegramTopicRouting.
|
||||
Assert.Contains("MessageThreadId", createHandler, StringComparison.Ordinal);
|
||||
Assert.Contains("ExternalThreadId", createHandler, StringComparison.Ordinal);
|
||||
Assert.Contains("ResolveNewScheduleDestination", topicRouting, StringComparison.Ordinal);
|
||||
Assert.Contains("MissingForumTopicRightsMessage", topicRouting, StringComparison.Ordinal);
|
||||
|
||||
Assert.Contains("TopicCreatedByBot", deleteHandler, StringComparison.Ordinal);
|
||||
Assert.Contains("ShouldDeleteForumTopic", deleteHandler, StringComparison.Ordinal);
|
||||
Assert.Contains("RemainingInTopic", deleteHandler, StringComparison.Ordinal);
|
||||
|
||||
Reference in New Issue
Block a user