From 384887a8620b34e7f81b62534ce0fe9aae649100 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Thu, 4 Jun 2026 08:13:22 +0300 Subject: [PATCH] test(wizard): add WizardDraftRepository integration tests --- .../Wizard/WizardDraftRepositoryFixture.cs | 75 ++++++++++++++++ .../Wizard/WizardDraftRepositoryTests.cs | 88 +++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryFixture.cs create mode 100644 tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryFixture.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryFixture.cs new file mode 100644 index 0000000..9634262 --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryFixture.cs @@ -0,0 +1,75 @@ +using Npgsql; +using Testcontainers.PostgreSql; + +namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; + +[CollectionDefinition(Name)] +public sealed class WizardDraftRepositoryCollection : ICollectionFixture +{ + public const string Name = "Wizard draft repository PostgreSQL"; +} + +public sealed class WizardDraftRepositoryFixture : IAsyncLifetime +{ + private static readonly TimeSpan ContainerTimeout = TimeSpan.FromMinutes(2); + private readonly PostgreSqlContainer container = new PostgreSqlBuilder("postgres:17-alpine").Build(); + + public Task InitializeAsync() + { + return container.StartAsync().WaitAsync(ContainerTimeout); + } + + public Task DisposeAsync() + { + return container.DisposeAsync().AsTask().WaitAsync(ContainerTimeout); + } + + public async Task CreateSchemaDatabaseAsync() + { + var databaseName = $"wizard_drafts_{Guid.NewGuid():N}"; + + await using (var adminConnection = new NpgsqlConnection(container.GetConnectionString())) + { + await adminConnection.OpenAsync().WaitAsync(ContainerTimeout); + await using var createDatabase = new NpgsqlCommand($"CREATE DATABASE \"{databaseName}\"", adminConnection); + await createDatabase.ExecuteNonQueryAsync().WaitAsync(ContainerTimeout); + } + + var connectionString = new NpgsqlConnectionStringBuilder(container.GetConnectionString()) + { + Database = databaseName, + Timeout = 10, + CommandTimeout = 10 + }.ConnectionString; + + await using (var connection = new NpgsqlConnection(connectionString)) + { + await connection.OpenAsync().WaitAsync(ContainerTimeout); + await using var createSchema = new NpgsqlCommand( + """ + CREATE TABLE wizard_drafts ( + id UUID PRIMARY KEY, + chat_id BIGINT NOT NULL, + message_thread_id INT, + owner_telegram_id BIGINT NOT NULL, + step TEXT NOT NULL, + payload JSONB NOT NULL, + draft_message_id BIGINT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL + ); + + CREATE INDEX idx_wizard_drafts_owner + ON wizard_drafts(chat_id, message_thread_id, owner_telegram_id); + + CREATE INDEX idx_wizard_drafts_expires + ON wizard_drafts(expires_at); + """, + connection); + await createSchema.ExecuteNonQueryAsync().WaitAsync(ContainerTimeout); + } + + return connectionString; + } +} diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs new file mode 100644 index 0000000..d2c15c4 --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs @@ -0,0 +1,88 @@ +using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; +using Npgsql; + +namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; + +[Collection(WizardDraftRepositoryCollection.Name)] +public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixture) +{ + [Fact] + public async Task UpsertAsync_InsertThenUpdate_PreservesSingleRow() + { + var connectionString = await fixture.CreateSchemaDatabaseAsync(); + await using var dataSource = NpgsqlDataSource.Create(connectionString); + var sut = new WizardDraftRepository(dataSource); + + var draft = NewDraft("Type", DateTimeOffset.UtcNow.AddHours(1)); + await sut.UpsertAsync(draft, CancellationToken.None); + + draft.Step = "Title"; + draft.UpdatedAt = DateTimeOffset.UtcNow.AddSeconds(1); + await sut.UpsertAsync(draft, CancellationToken.None); + + var loaded = await sut.GetActiveAsync(draft.ChatId, draft.MessageThreadId, draft.OwnerTelegramId, CancellationToken.None); + Assert.NotNull(loaded); + Assert.Equal("Title", loaded!.Step); + } + + [Fact] + public async Task GetActiveAsync_ExpiredDraft_ReturnsNull() + { + var connectionString = await fixture.CreateSchemaDatabaseAsync(); + await using var dataSource = NpgsqlDataSource.Create(connectionString); + var sut = new WizardDraftRepository(dataSource); + + var draft = NewDraft("Type", DateTimeOffset.UtcNow.AddMinutes(-1)); + await sut.UpsertAsync(draft, CancellationToken.None); + + var loaded = await sut.GetActiveAsync(draft.ChatId, draft.MessageThreadId, draft.OwnerTelegramId, CancellationToken.None); + Assert.Null(loaded); + } + + [Fact] + public async Task GetActiveAsync_DifferentOwner_ReturnsNull() + { + var connectionString = await fixture.CreateSchemaDatabaseAsync(); + await using var dataSource = NpgsqlDataSource.Create(connectionString); + var sut = new WizardDraftRepository(dataSource); + + var draft = NewDraft("Type", DateTimeOffset.UtcNow.AddHours(1)); + await sut.UpsertAsync(draft, CancellationToken.None); + + var loaded = await sut.GetActiveAsync(draft.ChatId, draft.MessageThreadId, ownerTelegramId: draft.OwnerTelegramId + 1, CancellationToken.None); + Assert.Null(loaded); + } + + [Fact] + public async Task DeleteExpiredAsync_DeletesOnlyExpired() + { + var connectionString = await fixture.CreateSchemaDatabaseAsync(); + await using var dataSource = NpgsqlDataSource.Create(connectionString); + var sut = new WizardDraftRepository(dataSource); + + var fresh = NewDraft("Type", DateTimeOffset.UtcNow.AddHours(1)); + var stale = NewDraft("Type", DateTimeOffset.UtcNow.AddMinutes(-1)); + stale.Id = Guid.NewGuid(); + await sut.UpsertAsync(fresh, CancellationToken.None); + await sut.UpsertAsync(stale, CancellationToken.None); + + var deleted = await sut.DeleteExpiredAsync(CancellationToken.None); + Assert.Equal(1, deleted); + + var loadedFresh = await sut.GetActiveAsync(fresh.ChatId, fresh.MessageThreadId, fresh.OwnerTelegramId, CancellationToken.None); + Assert.NotNull(loadedFresh); + } + + private static WizardDraft NewDraft(string step, DateTimeOffset expiresAt) => new() + { + Id = Guid.NewGuid(), + ChatId = 42, + MessageThreadId = null, + OwnerTelegramId = 100, + Step = step, + PayloadJson = "{}", + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + ExpiresAt = expiresAt, + }; +}