using System.Threading; using System.Threading.Tasks; using GmRelay.Bot.Features.Sessions.CreateSession.Wizard; using GmRelay.Bot.Infrastructure.Telegram; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Telegram.Bot; using Telegram.Bot.Types; using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes; namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; /// /// Verifies that the delegates to the wizard when /// the GM has an active (non-expired) draft, and falls through to normal /// handling when no draft is active. We instrument a real wizard via the /// shared / /// pair and verify side effects on the messenger (the wizard edits the /// draft message) — that is the observable signal that /// wizard.HandleUpdateAsync was called. /// public sealed class UpdateRouterDelegationTests { [Fact] public async Task ActiveDraft_Existing_RoutesToWizard() { var sut = BuildRouter(out var drafts, out var messenger); var draft = NewDraft(WizardStepNames.Title); drafts.Seed(draft); var update = TextUpdate("Curse of Strahd", ownerId: draft.OwnerTelegramId); await sut.RouteAsync(update, CancellationToken.None); // Wizard edits the draft message when it processes a title. Assert.NotEmpty(messenger.Edits); } [Fact] public async Task ActiveDraft_Existing_OnCallback_AlsoRoutesToWizard() { var sut = BuildRouter(out var drafts, out _); var draft = NewDraft(WizardStepNames.Title); drafts.Seed(draft); // "wizard:cancel" — wizard owns the cancel callback. The router // delegates control-callbacks (resume/reset) but lets the wizard // handle wizard:* callbacks. var update = CallbackUpdate(WizardCallbackData.Cancel(), ownerId: draft.OwnerTelegramId); await sut.RouteAsync(update, CancellationToken.None); // Cancel deletes the draft via the wizard. Assert.Contains(draft.Id, drafts.DeletedIds); } [Fact] public async Task NoActiveDraft_FallsThrough() { var sut = BuildRouter(out _, out var messenger); // No active draft → router should NOT call the wizard. It will // attempt to run the /help command via the fallback command path. // We send a /help message; the router has no draft to act on. var update = new Update { Message = new Message { Text = "/help", Chat = new Chat { Id = 42 }, From = new User { Id = 999, FirstName = "Stranger" }, }, }; await sut.RouteAsync(update, CancellationToken.None); // The wizard should not have edited anything (no draft was active). Assert.Empty(messenger.Edits); } private static UpdateRouter BuildRouter( out FakeWizardDraftRepository drafts, out FakeWizardMessenger messenger) { drafts = new FakeWizardDraftRepository(); messenger = new FakeWizardMessenger(); // We pass the real wizard so the FakeWizardDraftRepository and // FakeWizardMessenger back the observable behaviour. var wizard = new GameCreationWizard(drafts, messenger, NullLogger.Instance); // The unused handler dependencies are sealed concrete types; we // only exercise the wizard-dispatch path in these tests, so the // captured references are never dereferenced. var router = new UpdateRouter( rsvpHandler: null!, createSessionHandler: null!, joinSessionHandler: null!, leaveSessionHandler: null!, promoteWaitlistedPlayerHandler: null!, cancelSessionHandler: null!, deleteSessionHandler: null!, listSessionsHandler: null!, exportCalendarHandler: null!, initiateRescheduleHandler: null!, rescheduleTimeInputHandler: null!, rescheduleVoteHandler: null!, wizard: wizard, drafts: drafts, bot: Substitute.For(), configuration: Substitute.For(), logger: NullLogger.Instance); return router; } }