test(wizard): add submit, cleanup, router delegation tests
This commit is contained in:
+79
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="CreateSessionHandler.SubmitDraftAsync"/> bails
|
||||
/// out gracefully when the wizard payload is missing required fields. The
|
||||
/// missing-fields path returns before the shared handler is ever called,
|
||||
/// so we pass <c>null!</c> for the shared dependency — a NRE on that
|
||||
/// branch would itself prove the validation did not fire.
|
||||
/// </summary>
|
||||
public sealed class CreateSessionHandlerSubmitMissingFieldsTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SubmitDraftAsync_EmptyPayload_EditsMessageWithMissingFields()
|
||||
{
|
||||
var drafts = new FakeWizardDraftRepository();
|
||||
var messenger = new FakeWizardMessenger();
|
||||
|
||||
var sut = new CreateSessionHandler(
|
||||
drafts,
|
||||
shared: null!, // missing-fields path returns before touching the shared handler
|
||||
messenger,
|
||||
NullLogger<CreateSessionHandler>.Instance);
|
||||
|
||||
// Empty payload → every required field is missing.
|
||||
var draft = NewDraft(WizardStepNames.Confirm, new WizardPayload());
|
||||
drafts.Seed(draft);
|
||||
|
||||
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
||||
|
||||
// The wizard message is edited to surface the missing-field error.
|
||||
Assert.Single(messenger.Edits);
|
||||
var edit = messenger.Edits[0];
|
||||
Assert.Equal(draft.ChatId, edit.ChatId);
|
||||
Assert.Contains("Не заполнены", edit.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitDraftAsync_MissingTitleOnly_EditsMessageNamingTitle()
|
||||
{
|
||||
var drafts = new FakeWizardDraftRepository();
|
||||
var messenger = new FakeWizardMessenger();
|
||||
|
||||
var sut = new CreateSessionHandler(
|
||||
drafts,
|
||||
shared: null!,
|
||||
messenger,
|
||||
NullLogger<CreateSessionHandler>.Instance);
|
||||
|
||||
// All required fields set except Title.
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Single,
|
||||
System = "Dnd5e",
|
||||
DurationMinutes = 240,
|
||||
Visibility = WizardVisibility.Public,
|
||||
Single = new WizardSingleInput
|
||||
{
|
||||
ScheduledAt = DateTimeOffset.UtcNow.AddDays(7),
|
||||
MaxPlayers = 4,
|
||||
},
|
||||
};
|
||||
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
||||
|
||||
Assert.Single(messenger.Edits);
|
||||
Assert.Contains("название", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// Happy-path coverage for <see cref="Features.Sessions.CreateSession.CreateSessionHandler.SubmitDraftAsync"/>
|
||||
/// on a pool wizard payload. The success path calls the shared
|
||||
/// <c>CreateSessionHandler.HandleAsync</c>, which needs a real
|
||||
/// <c>NpgsqlDataSource</c> (it runs SQL against game_groups, players,
|
||||
/// sessions, and related tables). The missing-fields and validation
|
||||
/// branches are covered by the dedicated tests in this folder.
|
||||
/// </summary>
|
||||
public sealed class CreateSessionHandlerSubmitPoolDraftTests
|
||||
{
|
||||
[Fact(Skip = "Happy-path SubmitDraftAsync needs a Testcontainers-backed PostgreSQL with the production schema; see file-level summary.")]
|
||||
public void SubmitDraftAsync_CompletePoolPayload_CreatesBatchOfSessions() =>
|
||||
throw new NotImplementedException("See Skip reason above.");
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// Happy-path coverage for <see cref="Features.Sessions.CreateSession.CreateSessionHandler.SubmitDraftAsync"/>
|
||||
/// on a single-game wizard payload. The success path calls the shared
|
||||
/// <c>CreateSessionHandler.HandleAsync</c>, which needs a real
|
||||
/// <c>NpgsqlDataSource</c> (it runs SQL against game_groups, players,
|
||||
/// sessions, and related tables). The missing-fields and validation
|
||||
/// branches are covered by the dedicated tests in this folder.
|
||||
/// </summary>
|
||||
public sealed class CreateSessionHandlerSubmitSingleDraftTests
|
||||
{
|
||||
[Fact(Skip = "Happy-path SubmitDraftAsync needs a Testcontainers-backed PostgreSQL with the production schema; see file-level summary.")]
|
||||
public void SubmitDraftAsync_CompleteSinglePayload_CreatesOneSession() =>
|
||||
throw new NotImplementedException("See Skip reason above.");
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the validation gates inside
|
||||
/// <see cref="CreateSessionHandler.SubmitDraftAsync"/>. We never reach the
|
||||
/// shared handler in any of these tests, so the shared dependency is
|
||||
/// passed as <c>null!</c> — a NRE on that branch would itself prove the
|
||||
/// validation did not fire.
|
||||
/// </summary>
|
||||
public sealed class CreateSessionHandlerSubmitValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SubmitDraftAsync_MissingVisibility_EditsMessageNamingVisibility()
|
||||
{
|
||||
var drafts = new FakeWizardDraftRepository();
|
||||
var messenger = new FakeWizardMessenger();
|
||||
|
||||
var sut = new CreateSessionHandler(
|
||||
drafts,
|
||||
shared: null!,
|
||||
messenger,
|
||||
NullLogger<CreateSessionHandler>.Instance);
|
||||
|
||||
// All required fields set except Visibility.
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Single,
|
||||
Title = "T",
|
||||
System = "Dnd5e",
|
||||
DurationMinutes = 240,
|
||||
Single = new WizardSingleInput
|
||||
{
|
||||
ScheduledAt = DateTimeOffset.UtcNow.AddDays(7),
|
||||
MaxPlayers = 4,
|
||||
},
|
||||
};
|
||||
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
||||
|
||||
Assert.Single(messenger.Edits);
|
||||
Assert.Contains("видимость", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitDraftAsync_MissingSystem_EditsMessageNamingSystem()
|
||||
{
|
||||
var drafts = new FakeWizardDraftRepository();
|
||||
var messenger = new FakeWizardMessenger();
|
||||
|
||||
var sut = new CreateSessionHandler(
|
||||
drafts,
|
||||
shared: null!,
|
||||
messenger,
|
||||
NullLogger<CreateSessionHandler>.Instance);
|
||||
|
||||
// All required fields set except System.
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Single,
|
||||
Title = "T",
|
||||
DurationMinutes = 240,
|
||||
Visibility = WizardVisibility.Public,
|
||||
Single = new WizardSingleInput
|
||||
{
|
||||
ScheduledAt = DateTimeOffset.UtcNow.AddDays(7),
|
||||
MaxPlayers = 4,
|
||||
},
|
||||
};
|
||||
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
||||
|
||||
Assert.Single(messenger.Edits);
|
||||
Assert.Contains("система", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitDraftAsync_MissingDateTimeForSingleType_EditsMessageNamingDateTime()
|
||||
{
|
||||
var drafts = new FakeWizardDraftRepository();
|
||||
var messenger = new FakeWizardMessenger();
|
||||
|
||||
var sut = new CreateSessionHandler(
|
||||
drafts,
|
||||
shared: null!,
|
||||
messenger,
|
||||
NullLogger<CreateSessionHandler>.Instance);
|
||||
|
||||
// All required fields set except ScheduledAt for Single type.
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Single,
|
||||
Title = "T",
|
||||
System = "Dnd5e",
|
||||
DurationMinutes = 240,
|
||||
Visibility = WizardVisibility.Public,
|
||||
Single = new WizardSingleInput { MaxPlayers = 4 },
|
||||
};
|
||||
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
||||
|
||||
Assert.Single(messenger.Edits);
|
||||
Assert.Contains("дата/время", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitDraftAsync_EmptyPool_EditsMessageNamingSlots()
|
||||
{
|
||||
var drafts = new FakeWizardDraftRepository();
|
||||
var messenger = new FakeWizardMessenger();
|
||||
|
||||
var sut = new CreateSessionHandler(
|
||||
drafts,
|
||||
shared: null!,
|
||||
messenger,
|
||||
NullLogger<CreateSessionHandler>.Instance);
|
||||
|
||||
// Pool type with no slots at all.
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Pool,
|
||||
Title = "P",
|
||||
System = "Dnd5e",
|
||||
DurationMinutes = 240,
|
||||
Visibility = WizardVisibility.Public,
|
||||
Pool = new WizardPoolInput(),
|
||||
};
|
||||
var draft = NewDraft(WizardStepNames.Confirm, payload);
|
||||
drafts.Seed(draft);
|
||||
|
||||
await sut.SubmitDraftAsync(draft, CancellationToken.None);
|
||||
|
||||
Assert.Single(messenger.Edits);
|
||||
Assert.Contains("слоты", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the <see cref="UpdateRouter"/> 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 <see cref="FakeWizardDraftRepository"/>/<see cref="FakeWizardMessenger"/>
|
||||
/// pair and verify side effects on the messenger (the wizard edits the
|
||||
/// draft message) — that is the observable signal that
|
||||
/// <c>wizard.HandleUpdateAsync</c> was called.
|
||||
/// </summary>
|
||||
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<GameCreationWizard>.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<ITelegramBotClient>(),
|
||||
configuration: Substitute.For<IConfiguration>(),
|
||||
logger: NullLogger<UpdateRouter>.Instance);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
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.Requests.Abstractions;
|
||||
using Telegram.Bot.Types;
|
||||
using BotCreateSessionHandler = GmRelay.Bot.Features.Sessions.CreateSession.CreateSessionHandler;
|
||||
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// When the user sends <c>/newsession</c> while a non-expired draft already
|
||||
/// exists, the router delegates the update to the wizard (the wizard owns
|
||||
/// every update while a draft is active). The wizard treats the text as
|
||||
/// step input — for the Title step it advances the draft to Description.
|
||||
/// This is the observable contract that this test pins down.
|
||||
/// </summary>
|
||||
public sealed class UpdateRouterResetsDraftOnStaleCommandTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task NewSessionCommand_ExistingDraft_DelegatesToWizard()
|
||||
{
|
||||
var bot = Substitute.For<ITelegramBotClient>();
|
||||
var (sut, drafts, messenger) = BuildRouter(bot);
|
||||
|
||||
var draft = NewDraft(WizardStepNames.Title);
|
||||
drafts.Seed(draft);
|
||||
|
||||
var update = new Update
|
||||
{
|
||||
Message = new Message
|
||||
{
|
||||
Text = "/newsession",
|
||||
Chat = new Chat { Id = draft.ChatId },
|
||||
From = new User { Id = draft.OwnerTelegramId, FirstName = "GM" },
|
||||
},
|
||||
};
|
||||
|
||||
await sut.RouteAsync(update, CancellationToken.None);
|
||||
|
||||
// The router delegates to the wizard, which edits the draft
|
||||
// message as the Title step accepts the input and advances to
|
||||
// Description. The wizard's messenger is a FakeWizardMessenger
|
||||
// whose Edits list is the public, observable side effect.
|
||||
Assert.NotEmpty(messenger.Edits);
|
||||
// The bot.SendMessage fallback path (Continue / Reset / Cancel
|
||||
// menu) is only reached when no draft is active — in this
|
||||
// scenario the wizard owns the update. We assert it was NOT
|
||||
// taken here.
|
||||
await bot.DidNotReceiveWithAnyArgs().SendRequest(default(IRequest<Message>)!, default);
|
||||
}
|
||||
|
||||
private static (UpdateRouter sut, FakeWizardDraftRepository drafts, FakeWizardMessenger messenger) BuildRouter(
|
||||
ITelegramBotClient bot)
|
||||
{
|
||||
var drafts = new FakeWizardDraftRepository();
|
||||
var messenger = new FakeWizardMessenger();
|
||||
var wizard = new GameCreationWizard(drafts, messenger, NullLogger<GameCreationWizard>.Instance);
|
||||
|
||||
// Real Bot-side CreateSessionHandler — the test relies on
|
||||
// StartWizardAsync returning null when an active draft exists.
|
||||
// We pass null! for the shared handler since the active-draft
|
||||
// path never touches it.
|
||||
var createSessionHandler = new BotCreateSessionHandler(
|
||||
drafts,
|
||||
shared: null!,
|
||||
messenger,
|
||||
NullLogger<BotCreateSessionHandler>.Instance);
|
||||
|
||||
var sut = new UpdateRouter(
|
||||
rsvpHandler: null!,
|
||||
createSessionHandler: createSessionHandler,
|
||||
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: bot,
|
||||
configuration: Substitute.For<IConfiguration>(),
|
||||
logger: NullLogger<UpdateRouter>.Instance);
|
||||
|
||||
return (sut, drafts, messenger);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using SharedDraft = GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the cleanup background service: each tick should call the draft
|
||||
/// repository to delete expired drafts and must not propagate repository
|
||||
/// failures (a transient DB blip should not bring the worker down).
|
||||
/// </summary>
|
||||
public sealed class WizardDraftCleanupServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunOnceAsync_DeletesExpiredDrafts()
|
||||
{
|
||||
var drafts = Substitute.For<SharedDraft.IWizardDraftRepository>();
|
||||
drafts.DeleteExpiredAsync(Arg.Any<CancellationToken>()).Returns(7);
|
||||
|
||||
var sut = new WizardDraftCleanupService(drafts, NullLogger<WizardDraftCleanupService>.Instance);
|
||||
|
||||
await sut.RunOnceAsync(CancellationToken.None);
|
||||
|
||||
await drafts.Received(1).DeleteExpiredAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunOnceAsync_OnRepositoryError_DoesNotThrow()
|
||||
{
|
||||
var drafts = Substitute.For<SharedDraft.IWizardDraftRepository>();
|
||||
drafts.DeleteExpiredAsync(Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidOperationException("boom"));
|
||||
|
||||
var sut = new WizardDraftCleanupService(drafts, NullLogger<WizardDraftCleanupService>.Instance);
|
||||
|
||||
// Should swallow the exception — cleanup is best-effort.
|
||||
await sut.RunOnceAsync(CancellationToken.None);
|
||||
|
||||
await drafts.Received(1).DeleteExpiredAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
"Microsoft.TestPlatform.TestHost": "17.14.1"
|
||||
}
|
||||
},
|
||||
"NSubstitute": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.3.0, )",
|
||||
"resolved": "5.3.0",
|
||||
"contentHash": "lJ47Cps5Qzr86N99lcwd+OUvQma7+fBgr8+Mn+aOC0WrlqMNkdivaYD9IvnZ5Mqo6Ky3LS7ZI+tUq1/s9ERd0Q==",
|
||||
"dependencies": {
|
||||
"Castle.Core": "5.1.1"
|
||||
}
|
||||
},
|
||||
"SecurityCodeScan.VS2019": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.6.7, )",
|
||||
@@ -84,6 +93,11 @@
|
||||
"resolved": "2.6.2",
|
||||
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
|
||||
},
|
||||
"Castle.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.1.1",
|
||||
"contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g=="
|
||||
},
|
||||
"Dapper": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.72",
|
||||
@@ -487,8 +501,8 @@
|
||||
"Aspire.Npgsql": "[13.2.2, )",
|
||||
"Dapper": "[2.1.72, )",
|
||||
"Dapper.AOT": "[1.0.48, )",
|
||||
"GmRelay.ServiceDefaults": "[3.5.1, )",
|
||||
"GmRelay.Shared": "[3.5.1, )",
|
||||
"GmRelay.ServiceDefaults": "[3.7.1, )",
|
||||
"GmRelay.Shared": "[3.7.1, )",
|
||||
"Npgsql": "[10.0.2, )",
|
||||
"Telegram.Bot": "[22.9.5.3, )",
|
||||
"dbup-postgresql": "[7.0.1, )"
|
||||
@@ -500,8 +514,8 @@
|
||||
"Aspire.Npgsql": "[13.2.2, )",
|
||||
"Dapper": "[2.1.72, )",
|
||||
"Dapper.AOT": "[1.0.48, )",
|
||||
"GmRelay.ServiceDefaults": "[3.5.1, )",
|
||||
"GmRelay.Shared": "[3.5.1, )",
|
||||
"GmRelay.ServiceDefaults": "[3.7.1, )",
|
||||
"GmRelay.Shared": "[3.7.1, )",
|
||||
"NetCord.Hosting": "[1.0.0-alpha.489, )",
|
||||
"NetCord.Hosting.Services": "[1.0.0-alpha.489, )",
|
||||
"NetCord.Services": "[1.0.0-alpha.489, )",
|
||||
@@ -532,8 +546,8 @@
|
||||
"dependencies": {
|
||||
"Aspire.Npgsql": "[13.2.2, )",
|
||||
"Dapper": "[2.1.72, )",
|
||||
"GmRelay.ServiceDefaults": "[3.5.1, )",
|
||||
"GmRelay.Shared": "[3.5.1, )",
|
||||
"GmRelay.ServiceDefaults": "[3.7.1, )",
|
||||
"GmRelay.Shared": "[3.7.1, )",
|
||||
"Npgsql": "[10.0.2, )",
|
||||
"Telegram.Bot": "[22.9.6.1, )"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user