8f0f2ef7e7
Moves the game-creation wizard state machine, view builder, and platform-neutral contracts (callback data, step names, storage exception, club option, step limits) from GmRelay.Bot to GmRelay.Shared. Telegram continues to work through a new TelegramWizardMessenger implementing IWizardMessenger and a WizardInteractionMapper that converts Update → WizardInteraction. Wires the new platform column on wizard_drafts (V032 migration) and switches chat/owner/thread/message ids to TEXT so the same table can hold Discord snowflakes later. - GameCreationWizard: now in Shared, takes IWizardMessenger + IWizardDraftRepository, dispatches on WizardInteraction. - New IWizardMessenger contract with Edit/Send/Answer/GetOwnerClubs (returns string ids so Telegram longs and Discord snowflakes both fit). - New WizardStepViewBuilder in Shared returns (text, IReadOnlyList<WizardAction>); TelegramWizardMessenger renders actions into InlineKeyboardMarkup via a new Bot-side ToInlineKeyboard helper. - New WizardInteractionMapper in Bot (5-case test) converts Telegram Update to WizardInteraction. - WizardDraft gains a Platform column; ChatId/MessageThreadId/OwnerId/ DraftMessageId switched to string. V032 migrates existing rows and rebuilds the owner lookup index on (platform, owner_id). - All existing wizard / create-session tests updated to the new contract (HandleInteractionAsync + WizardInteraction). Wizard callback-data format preserved. - dotnet build clean, dotnet format --verify-no-changes clean, all 101 wizard tests pass.
127 lines
4.2 KiB
C#
127 lines
4.2 KiB
C#
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
|
using Telegram.Bot.Types;
|
|
using Xunit;
|
|
|
|
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
|
|
|
/// <summary>
|
|
/// Verifies the Telegram <c>Update</c> → <c>WizardInteraction</c> mapping
|
|
/// that <see cref="WizardInteractionMapper"/> exposes. The mapper is the
|
|
/// single bridge between Telegram's native update type and the
|
|
/// platform-neutral wizard core, so its contract needs to be locked
|
|
/// down: callback queries carry the data payload, text messages carry
|
|
/// their text, and photos carry the largest photo's <c>FileId</c>.
|
|
/// </summary>
|
|
public sealed class WizardInteractionMapperTests
|
|
{
|
|
[Fact]
|
|
public void CallbackUpdate_ProducesCallbackInteraction_WithPayloadAndOwner()
|
|
{
|
|
var update = new Update
|
|
{
|
|
CallbackQuery = new CallbackQuery
|
|
{
|
|
Id = "cb-42",
|
|
Data = "wizard:choice:Type:single",
|
|
From = new User { Id = 100, FirstName = "GM" },
|
|
Message = new Message { Chat = new Chat { Id = 42 } },
|
|
},
|
|
};
|
|
|
|
var ok = WizardInteractionMapper.TryMap(update, out var interaction);
|
|
|
|
Assert.True(ok);
|
|
Assert.Equal("100", interaction.OwnerId);
|
|
Assert.Null(interaction.Text);
|
|
Assert.Equal("wizard:choice:Type:single", interaction.CallbackPayload);
|
|
Assert.Null(interaction.PhotoFileId);
|
|
Assert.Null(interaction.PhotoUrl);
|
|
Assert.Equal("cb-42", interaction.InteractionId);
|
|
}
|
|
|
|
[Fact]
|
|
public void TextUpdate_ProducesTextInteraction_WithTextAndNoCallback()
|
|
{
|
|
var update = new Update
|
|
{
|
|
Message = new Message
|
|
{
|
|
Text = "My Game Title",
|
|
Chat = new Chat { Id = 42 },
|
|
From = new User { Id = 200, FirstName = "GM" },
|
|
},
|
|
};
|
|
|
|
var ok = WizardInteractionMapper.TryMap(update, out var interaction);
|
|
|
|
Assert.True(ok);
|
|
Assert.Equal("200", interaction.OwnerId);
|
|
Assert.Equal("My Game Title", interaction.Text);
|
|
Assert.Null(interaction.CallbackPayload);
|
|
Assert.Null(interaction.PhotoFileId);
|
|
Assert.Equal("msg", interaction.InteractionId);
|
|
}
|
|
|
|
[Fact]
|
|
public void PhotoUpdate_ProducesPhotoInteraction_WithLargestFileId()
|
|
{
|
|
var update = new Update
|
|
{
|
|
Message = new Message
|
|
{
|
|
Chat = new Chat { Id = 42 },
|
|
From = new User { Id = 300, FirstName = "GM" },
|
|
Photo = new[]
|
|
{
|
|
new PhotoSize { FileId = "small-id", Width = 90, Height = 60 },
|
|
new PhotoSize { FileId = "medium-id", Width = 320, Height = 240 },
|
|
new PhotoSize { FileId = "large-id", Width = 800, Height = 600 },
|
|
},
|
|
},
|
|
};
|
|
|
|
var ok = WizardInteractionMapper.TryMap(update, out var interaction);
|
|
|
|
Assert.True(ok);
|
|
Assert.Equal("300", interaction.OwnerId);
|
|
Assert.Null(interaction.Text);
|
|
Assert.Null(interaction.CallbackPayload);
|
|
Assert.Equal("large-id", interaction.PhotoFileId);
|
|
}
|
|
|
|
[Fact]
|
|
public void CaptionedPhoto_ProducesPhotoInteraction_AndKeepsCaptionOutOfText()
|
|
{
|
|
// Telegram sometimes attaches a caption to a photo message. The
|
|
// mapper treats it as a non-text interaction (cover-step uses
|
|
// PhotoFileId, not caption). This test pins that distinction.
|
|
var update = new Update
|
|
{
|
|
Message = new Message
|
|
{
|
|
Caption = "ignored",
|
|
Chat = new Chat { Id = 42 },
|
|
From = new User { Id = 400 },
|
|
Photo = new[]
|
|
{
|
|
new PhotoSize { FileId = "only-id", Width = 100, Height = 100 },
|
|
},
|
|
},
|
|
};
|
|
|
|
var ok = WizardInteractionMapper.TryMap(update, out var interaction);
|
|
|
|
Assert.True(ok);
|
|
Assert.Equal("only-id", interaction.PhotoFileId);
|
|
}
|
|
|
|
[Fact]
|
|
public void EmptyUpdate_ReturnsFalse()
|
|
{
|
|
var ok = WizardInteractionMapper.TryMap(new Update(), out var interaction);
|
|
|
|
Assert.False(ok);
|
|
Assert.Null(interaction);
|
|
}
|
|
}
|