From f796b7d1e456d648c2c4078ca494cdb580f59fb4 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Mon, 8 Jun 2026 10:02:59 +0300 Subject: [PATCH] fix(shared): make WizardDraftRepository AOT-safe (v3.9.1 hotfix) Production regression in 3.9.0: Telegram bot silently dropped every update. WizardDraftRepository.GetActiveAsync was called on every Telegram update (via UpdateRouter -> TryGetWizardContext) and threw System.PlatformNotSupportedException in NativeAOT, because Dapper.AOT 1.0.48 only generates interceptors for the (sql, object?) extension overloads and NOT for the (CommandDefinition) overload. The runtime then fell back to Dapper.SqlMapper.CreateParamInfoGenerator, which uses Reflection.Emit and fails on AOT. TelegramBotService swallowed the exception, so /newsession appeared to start but no button press reached the wizard and no session was created. Two related changes in WizardDraft: 1. Switched WizardDraftRepository.* from 'new CommandDefinition(sql, params, cancellationToken: ct)' to the direct 'connection.Query*(sql, params)' overload, matching the working pattern in JoinSessionHandler. Dapper.AOT now generates CommandFactory30 + RowFactory17 + QuerySingleOrDefaultAsync37 for all four methods. 2. WizardDraft.CreatedAt/UpdatedAt/ExpiresAt are now DateTime (UTC) instead of DateTimeOffset. AOT RowFactory calls reader.GetDateTime() directly and does not perform DateTime -> DateTimeOffset conversion; the previous type raised InvalidCastException on the very first wizard_drafts query. All 588/590 tests pass (2 pre-existing skipped, +5 new AOT regression tests in WizardDraftRepositoryAotShapeTests). dotnet format clean. Bumps: 3.9.0 -> 3.9.1. Note: GetOwnerClubsAsync (Telegram/Discord), DiscordPermissionLookup, and DiscordWizardInteractionModule.GetOwnerClubsAsync still use CommandDefinition and will hit the same Reflection.Emit AOT failure when the user reaches the PickClub visibility step. Follow-up in 3.9.2. --- .gitea/workflows/deploy.yml | 2 +- Directory.Build.props | 2 +- RELEASE_NOTES.md | 19 ++- compose.yaml | 6 +- .../CreateSession/CreateSessionHandler.cs | 10 +- .../Sessions/Wizard/DiscordWizardCommand.cs | 8 +- .../Sessions/Wizard/DiscordWizardSubmitter.cs | 2 +- .../Wizard/GameCreationWizard.cs | 2 +- .../CreateSession/Wizard/WizardDraft.cs | 6 +- .../Wizard/WizardDraftRepository.cs | 21 +-- .../Components/Layout/NavMenu.razor | 2 +- .../WizardDraftRepositoryAotShapeTests.cs | 136 ++++++++++++++++++ .../Wizard/WizardDraftRepositoryTests.cs | 18 +-- .../CreateSession/Wizard/WizardTestFakes.cs | 6 +- .../Web/CampaignTemplatesNavigationTests.cs | 2 +- 15 files changed, 199 insertions(+), 43 deletions(-) create mode 100644 tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryAotShapeTests.cs diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index c9db68f..953def3 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,7 +6,7 @@ on: - main env: - VERSION: 3.9.0 + VERSION: 3.9.1 jobs: # ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами) diff --git a/Directory.Build.props b/Directory.Build.props index 4cf89ff..03a7089 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 3.9.0 + 3.9.1 net10.0 preview enable diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ec59637..f3c6a86 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,21 @@ -## 🎯 Minor 3.9.0 — Discord-визард создания игры/пула (issue #112) +## 🐞 Patch 3.9.1 — Hotfix: Telegram-визард мёртв после 3.9.0 + +Регрессия в `WizardDraftRepository` (NativeAOT). В Telegram **не реагировали кнопки** и **не создавались игры**, потому что Dapper.AOT 1.0.48 не генерирует интерсепторы для оверлоада `(CommandDefinition)` — рантайм падал в `CreateParamInfoGenerator` → `PlatformNotSupportedException` на каждом апдейте, `TelegramBotService` глотал исключение и апдейт терялся. + +### 🩹 Что починено +- `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraftRepository.cs` — все 4 метода переписаны с `new CommandDefinition(sql, params, cancellationToken: ct)` на прямой оверлоад `connection.QuerySingleOrDefaultAsync(sql, params)` (паттерн `JoinSessionHandler`). Dapper.AOT генерирует интерсепторы только для прямого оверлоада. +- `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraft.cs` — `CreatedAt` / `UpdatedAt` / `ExpiresAt` переведены с `DateTimeOffset` на `DateTime` (UTC). AOT RowFactory вызывает `reader.GetDateTime()` напрямую и не делает `DateTime → DateTimeOffset` конверсию. +- `src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs`, `src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardCommand.cs` — `DateTimeOffset.UtcNow` → `DateTime.UtcNow` в новых драфтах. +- `Directory.Build.props` / `compose.yaml` / `.gitea/workflows/deploy.yml` / `NavMenu.razor` — бамп 3.9.0 → 3.9.1. +- `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryAotShapeTests.cs` — 5 source-grep регрессионных тестов: ни один метод `WizardDraftRepository` не должен использовать `new CommandDefinition`, и три timestamp-свойства `WizardDraft` должны быть `DateTime` (не `DateTimeOffset`). + +### ⚠️ Известные ограничения +- В `TelegramWizardMessenger.GetOwnerClubsAsync`, `DiscordWizardMessenger.GetOwnerClubsAsync`, `DiscordPermissionLookup.LoadManagerUserIdsAsync`, `DiscordWizardInteractionModule.GetOwnerClubsAsync` остаётся `new CommandDefinition`. Эти вызовы **падают на AOT так же**, как падал `WizardDraftRepository` в 3.9.0. Пользователь натыкается на это только когда выбирает «видимость = клуб/мемберы» и доходит до шага выбора клуба. Будет исправлено в 3.9.2 вместе с переводом `DiscordWizardInteractionModule` на прямые Dapper-оверлоады. + +### 🧪 Тесты +- 588/590 passed (2 pre-existing skipped), `dotnet format` clean, `dotnet build` 0 warnings/errors, AOT-генератор эмитит 4 интерсептора + `RowFactory17` + `CommandFactory30`. + +## 🎯 Minor 3.9.0 — Discord-визард создания игры/пула (issue #112) Пошаговый сценарий создания одиночной игры или пула игр в Discord-чате, по аналогии с Telegram-визардом из 3.8.0. Платформо-нейтральная стейт-машина `GameCreationWizard` и контракт `IWizardMessenger` перенесены в `GmRelay.Shared`, чтобы обе платформы (Telegram/Discord) использовали один и тот же движок визарда. diff --git a/compose.yaml b/compose.yaml index 0f0ca98..2b1f870 100644 --- a/compose.yaml +++ b/compose.yaml @@ -49,7 +49,7 @@ services: crond -f bot: - image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.0 + image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.1 restart: always depends_on: db: @@ -67,7 +67,7 @@ services: retries: 3 discord: - image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.0 + image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.1 restart: always depends_on: db: @@ -86,7 +86,7 @@ services: retries: 3 web: - image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.0 + image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.1 restart: always depends_on: db: diff --git a/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs b/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs index 798e384..29eba1a 100644 --- a/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs @@ -66,16 +66,16 @@ public sealed class CreateSessionHandler OwnerId = ownerId, Platform = PlatformName, Step = WizardStepNames.Type, - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - ExpiresAt = DateTimeOffset.UtcNow.AddHours(24), + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + ExpiresAt = DateTime.UtcNow.AddHours(24), }; await _drafts.UpsertAsync(draft, ct); var (text, actions) = WizardStepViewBuilder.Build(draft, new WizardPayload()); var msgId = await _messenger.SendDraftMessageAsync(draft, text, actions, ct); draft.DraftMessageId = msgId; - draft.UpdatedAt = DateTimeOffset.UtcNow; + draft.UpdatedAt = DateTime.UtcNow; await _drafts.UpsertAsync(draft, ct); return draft; } @@ -135,7 +135,7 @@ public sealed class CreateSessionHandler await _drafts.DeleteAsync(draft.Id, ct); return; } - draft.UpdatedAt = DateTimeOffset.UtcNow; + draft.UpdatedAt = DateTime.UtcNow; await _drafts.UpsertAsync(draft, ct); await _messenger.EditDraftMessageAsync( draft, diff --git a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardCommand.cs b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardCommand.cs index ca35ae5..96743ab 100644 --- a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardCommand.cs +++ b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardCommand.cs @@ -115,9 +115,9 @@ public sealed class DiscordWizardCommand : ApplicationCommandModule diff --git a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs index ec2c965..5ab039c 100644 --- a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs +++ b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs @@ -97,7 +97,7 @@ public sealed class DiscordWizardSubmitter _contextStore.Remove(draft.Id); return; } - draft.UpdatedAt = DateTimeOffset.UtcNow; + draft.UpdatedAt = DateTime.UtcNow; await _drafts.UpsertAsync(draft, ct); // The full exception (with stack trace, Postgres constraint // name, sometimes partial SQL) is already logged server-side diff --git a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs index 6f88dfd..294a675 100644 --- a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs +++ b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs @@ -169,7 +169,7 @@ public sealed class GameCreationWizard private async Task PersistAndRenderAsync(WizardDraft draft, string? interactionId, CancellationToken ct) { - draft.UpdatedAt = DateTimeOffset.UtcNow; + draft.UpdatedAt = DateTime.UtcNow; await _drafts.UpsertAsync(draft, ct); var payload = LoadPayload(draft); IReadOnlyList? clubs = null; diff --git a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraft.cs b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraft.cs index 4c79b29..39cc8e3 100644 --- a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraft.cs +++ b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraft.cs @@ -43,9 +43,9 @@ public sealed class WizardDraft /// public string? DraftMessageId { get; set; } - public DateTimeOffset CreatedAt { get; set; } + public DateTime CreatedAt { get; set; } - public DateTimeOffset UpdatedAt { get; set; } + public DateTime UpdatedAt { get; set; } - public DateTimeOffset ExpiresAt { get; set; } + public DateTime ExpiresAt { get; set; } } diff --git a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraftRepository.cs b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraftRepository.cs index 20b777d..bc55f1f 100644 --- a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraftRepository.cs +++ b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraftRepository.cs @@ -8,6 +8,13 @@ namespace GmRelay.Shared.Features.Sessions.CreateSession.Wizard; public sealed class WizardDraftRepository(NpgsqlDataSource dataSource) : IWizardDraftRepository { + // NOTE: NativeAOT — Dapper.AOT 1.0.48 only generates interceptors for the + // (sql, param) extension overloads, NOT for the (CommandDefinition) overload. + // Passing a CommandDefinition here would skip the interceptor and fall back to + // Dapper.SqlMapper.CreateParamInfoGenerator, which uses Reflection.Emit and + // throws PlatformNotSupportedException on AOT (issue: wizard silently dropped + // every Telegram update in v3.9.0). All four methods therefore use the plain + // (sql, param) overload, matching the pattern in JoinSessionHandler. public async Task GetActiveAsync(string platform, string ownerId, CancellationToken ct) { const string sql = """ @@ -29,13 +36,10 @@ public sealed class WizardDraftRepository(NpgsqlDataSource dataSource) : IWizard ORDER BY updated_at DESC LIMIT 1 """; - await using var connection = await dataSource.OpenConnectionAsync(ct); return await connection.QuerySingleOrDefaultAsync( - new CommandDefinition( - sql, - new { Platform = platform, OwnerId = ownerId }, - cancellationToken: ct)); + sql, + new { Platform = platform, OwnerId = ownerId }); } public async Task UpsertAsync(WizardDraft draft, CancellationToken ct) @@ -52,22 +56,21 @@ public sealed class WizardDraftRepository(NpgsqlDataSource dataSource) : IWizard updated_at = EXCLUDED.updated_at, expires_at = EXCLUDED.expires_at; """; - await using var connection = await dataSource.OpenConnectionAsync(ct); - await connection.ExecuteAsync(new CommandDefinition(sql, draft, cancellationToken: ct)); + await connection.ExecuteAsync(sql, draft); } public async Task DeleteAsync(Guid id, CancellationToken ct) { const string sql = "DELETE FROM wizard_drafts WHERE id = @Id"; await using var connection = await dataSource.OpenConnectionAsync(ct); - await connection.ExecuteAsync(new CommandDefinition(sql, new { Id = id }, cancellationToken: ct)); + await connection.ExecuteAsync(sql, new { Id = id }); } public async Task DeleteExpiredAsync(CancellationToken ct) { const string sql = "DELETE FROM wizard_drafts WHERE expires_at <= NOW()"; await using var connection = await dataSource.OpenConnectionAsync(ct); - return await connection.ExecuteAsync(new CommandDefinition(sql, cancellationToken: ct)); + return await connection.ExecuteAsync(sql); } } diff --git a/src/GmRelay.Web/Components/Layout/NavMenu.razor b/src/GmRelay.Web/Components/Layout/NavMenu.razor index 10142ed..86cb6a3 100644 --- a/src/GmRelay.Web/Components/Layout/NavMenu.razor +++ b/src/GmRelay.Web/Components/Layout/NavMenu.razor @@ -82,7 +82,7 @@ - + diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryAotShapeTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryAotShapeTests.cs new file mode 100644 index 0000000..06c1246 --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryAotShapeTests.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using Xunit; + +namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; + +/// +/// Regression guard: WizardDraftRepository must not use the (CommandDefinition) +/// overload of Dapper. Dapper.AOT 1.0.48 only generates interceptors for the +/// (sql, object?) extension overloads; using CommandDefinition falls back to +/// Dapper.SqlMapper.CreateParamInfoGenerator, which uses Reflection.Emit and +/// throws PlatformNotSupportedException on NativeAOT (v3.9.0 wizard regression). +/// The v3.9.1 hotfix switched all four methods to the direct overload. +/// +public sealed class WizardDraftRepositoryAotShapeTests +{ + [Fact] + public void WizardDraftRepository_GetActiveAsync_ShouldNotUseCommandDefinition() + { + var repoRoot = FindRepositoryRoot(); + var source = File.ReadAllText(Path.Combine( + repoRoot, + "src", + "GmRelay.Shared", + "Features", + "Sessions", + "CreateSession", + "Wizard", + "WizardDraftRepository.cs")); + + var getActive = ExtractMethodBody(source, "GetActiveAsync"); + Assert.DoesNotContain("new CommandDefinition", getActive, StringComparison.Ordinal); + } + + [Theory] + [InlineData("UpsertAsync")] + [InlineData("DeleteAsync")] + [InlineData("DeleteExpiredAsync")] + public void WizardDraftRepository_MutatingMethods_ShouldNotUseCommandDefinition(string methodName) + { + var repoRoot = FindRepositoryRoot(); + var source = File.ReadAllText(Path.Combine( + repoRoot, + "src", + "GmRelay.Shared", + "Features", + "Sessions", + "CreateSession", + "Wizard", + "WizardDraftRepository.cs")); + + var body = ExtractMethodBody(source, methodName); + Assert.DoesNotContain("new CommandDefinition", body, StringComparison.Ordinal); + } + + /// + /// AOT RowFactory for WizardDraft expects to read timestamps as + /// (UTC). If a future refactor switches the + /// properties back to , the AOT row factory + /// will throw InvalidCastException: DateTime → DateTimeOffset + /// on the first query against wizard_drafts. + /// + [Fact] + public void WizardDraft_TimestampsMustBeDateTime_NotDateTimeOffset() + { + var repoRoot = FindRepositoryRoot(); + var source = File.ReadAllText(Path.Combine( + repoRoot, + "src", + "GmRelay.Shared", + "Features", + "Sessions", + "CreateSession", + "Wizard", + "WizardDraft.cs")); + + Assert.Contains("public DateTime CreatedAt", source, StringComparison.Ordinal); + Assert.Contains("public DateTime UpdatedAt", source, StringComparison.Ordinal); + Assert.Contains("public DateTime ExpiresAt", source, StringComparison.Ordinal); + Assert.DoesNotContain("public DateTimeOffset CreatedAt", source, StringComparison.Ordinal); + Assert.DoesNotContain("public DateTimeOffset UpdatedAt", source, StringComparison.Ordinal); + Assert.DoesNotContain("public DateTimeOffset ExpiresAt", source, StringComparison.Ordinal); + } + + private static string ExtractMethodBody(string source, string methodName) + { + var searchFrom = source.IndexOf(methodName, StringComparison.Ordinal); + if (searchFrom < 0) + { + throw new InvalidOperationException($"Could not locate {methodName} in source."); + } + + // Accept any return type: `public async Task` (no result) or + // `public async Task` (with result). Search for the keyword + // "Task" right before the method name. + var idx = source.IndexOf("Task", searchFrom - 16, StringComparison.Ordinal); + if (idx < 0 || !source.Substring(idx, 4).StartsWith("Task", StringComparison.Ordinal)) + { + throw new InvalidOperationException($"Could not locate {methodName} declaration in source."); + } + + var braceStart = source.IndexOf('{', idx); + if (braceStart < 0) + { + throw new InvalidOperationException($"Could not locate body opening brace for {methodName}."); + } + + var depth = 1; + var pos = braceStart + 1; + while (pos < source.Length && depth > 0) + { + switch (source[pos]) + { + case '{': depth++; break; + case '}': depth--; break; + } + pos++; + } + + return source.Substring(braceStart, pos - braceStart); + } + + private static string FindRepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "Directory.Build.props"))) + { + return directory.FullName; + } + directory = directory.Parent; + } + throw new InvalidOperationException("Could not locate repository root."); + } +} diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs index f4d5046..5bef27f 100644 --- a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs @@ -16,11 +16,11 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt await using var dataSource = NpgsqlDataSource.Create(connectionString); var sut = new WizardDraftRepository(dataSource); - var draft = NewDraft("Type", DateTimeOffset.UtcNow.AddHours(1)); + var draft = NewDraft("Type", DateTime.UtcNow.AddHours(1)); await sut.UpsertAsync(draft, CancellationToken.None); draft.Step = "Title"; - draft.UpdatedAt = DateTimeOffset.UtcNow.AddSeconds(1); + draft.UpdatedAt = DateTime.UtcNow.AddSeconds(1); await sut.UpsertAsync(draft, CancellationToken.None); var loaded = await sut.GetActiveAsync(draft.Platform, draft.OwnerId, CancellationToken.None); @@ -35,7 +35,7 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt await using var dataSource = NpgsqlDataSource.Create(connectionString); var sut = new WizardDraftRepository(dataSource); - var draft = NewDraft("Type", DateTimeOffset.UtcNow.AddMinutes(-1)); + var draft = NewDraft("Type", DateTime.UtcNow.AddMinutes(-1)); await sut.UpsertAsync(draft, CancellationToken.None); var loaded = await sut.GetActiveAsync(draft.Platform, draft.OwnerId, CancellationToken.None); @@ -49,7 +49,7 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt await using var dataSource = NpgsqlDataSource.Create(connectionString); var sut = new WizardDraftRepository(dataSource); - var draft = NewDraft("Type", DateTimeOffset.UtcNow.AddHours(1)); + var draft = NewDraft("Type", DateTime.UtcNow.AddHours(1)); await sut.UpsertAsync(draft, CancellationToken.None); var otherOwner = (long.Parse(draft.OwnerId, System.Globalization.CultureInfo.InvariantCulture) + 1) @@ -65,8 +65,8 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt 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)); + var fresh = NewDraft("Type", DateTime.UtcNow.AddHours(1)); + var stale = NewDraft("Type", DateTime.UtcNow.AddMinutes(-1)); stale.Id = Guid.NewGuid(); await sut.UpsertAsync(fresh, CancellationToken.None); await sut.UpsertAsync(stale, CancellationToken.None); @@ -78,7 +78,7 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt Assert.NotNull(loadedFresh); } - private static WizardDraft NewDraft(string step, DateTimeOffset expiresAt) => new() + private static WizardDraft NewDraft(string step, DateTime expiresAt) => new() { Id = Guid.NewGuid(), ChatId = "42", @@ -87,8 +87,8 @@ public sealed class WizardDraftRepositoryTests(WizardDraftRepositoryFixture fixt Platform = "Telegram", Step = step, PayloadJson = "{}", - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, ExpiresAt = expiresAt, }; } diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardTestFakes.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardTestFakes.cs index 1636517..6c6208f 100644 --- a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardTestFakes.cs +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardTestFakes.cs @@ -40,9 +40,9 @@ internal static class WizardTestFakes PayloadJson = System.Text.Json.JsonSerializer.Serialize( payload ?? new WizardPayload(), WizardPayloadJsonContext.Default.WizardPayload), - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - ExpiresAt = DateTimeOffset.UtcNow.AddHours(24), + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + ExpiresAt = DateTime.UtcNow.AddHours(24), }; /// diff --git a/tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs b/tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs index 783012f..efbd201 100644 --- a/tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs +++ b/tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs @@ -15,7 +15,7 @@ public sealed class CampaignTemplatesNavigationTests public async Task NavMenu_ShouldExposeCurrentProjectVersion() { var navMenu = await File.ReadAllTextAsync(FindRepositoryFile("src/GmRelay.Web/Components/Layout/NavMenu.razor")); - Assert.Contains("v3.9.0", navMenu, StringComparison.Ordinal); + Assert.Contains("v3.9.1", navMenu, StringComparison.Ordinal); } [Fact]