f796b7d1e4
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<WizardDraft> + RowFactory17<WizardDraft> + QuerySingleOrDefaultAsync37<WizardDraft> 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.
95 lines
3.6 KiB
C#
95 lines
3.6 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
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", DateTime.UtcNow.AddHours(1));
|
|
await sut.UpsertAsync(draft, CancellationToken.None);
|
|
|
|
draft.Step = "Title";
|
|
draft.UpdatedAt = DateTime.UtcNow.AddSeconds(1);
|
|
await sut.UpsertAsync(draft, CancellationToken.None);
|
|
|
|
var loaded = await sut.GetActiveAsync(draft.Platform, draft.OwnerId, 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", DateTime.UtcNow.AddMinutes(-1));
|
|
await sut.UpsertAsync(draft, CancellationToken.None);
|
|
|
|
var loaded = await sut.GetActiveAsync(draft.Platform, draft.OwnerId, 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", DateTime.UtcNow.AddHours(1));
|
|
await sut.UpsertAsync(draft, CancellationToken.None);
|
|
|
|
var otherOwner = (long.Parse(draft.OwnerId, System.Globalization.CultureInfo.InvariantCulture) + 1)
|
|
.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
var loaded = await sut.GetActiveAsync(draft.Platform, otherOwner, 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", 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);
|
|
|
|
var deleted = await sut.DeleteExpiredAsync(CancellationToken.None);
|
|
Assert.Equal(1, deleted);
|
|
|
|
var loadedFresh = await sut.GetActiveAsync(fresh.Platform, fresh.OwnerId, CancellationToken.None);
|
|
Assert.NotNull(loadedFresh);
|
|
}
|
|
|
|
private static WizardDraft NewDraft(string step, DateTime expiresAt) => new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ChatId = "42",
|
|
MessageThreadId = null,
|
|
OwnerId = "100",
|
|
Platform = "Telegram",
|
|
Step = step,
|
|
PayloadJson = "{}",
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
ExpiresAt = expiresAt,
|
|
};
|
|
}
|