Files
GmRelayBot/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardInteractionMapperTests.cs
T
Toutsu 8f0f2ef7e7 refactor(wizard): move core to Shared, add IWizardMessenger contract (issue #112)
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.
2026-06-05 16:23:20 +03:00

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);
}
}