Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f796b7d1e4 | |||
| 415c13bf00 |
@@ -6,7 +6,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
VERSION: 3.9.0
|
||||
VERSION: 3.9.1
|
||||
|
||||
jobs:
|
||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>3.9.0</Version>
|
||||
<Version>3.9.1</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
+18
-1
@@ -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<WizardDraft>(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<WizardDraft>` + `CommandFactory30<WizardDraft>`.
|
||||
|
||||
## 🎯 Minor 3.9.0 — Discord-визард создания игры/пула (issue #112)
|
||||
|
||||
Пошаговый сценарий создания одиночной игры или пула игр в Discord-чате, по аналогии с Telegram-визардом из 3.8.0. Платформо-нейтральная стейт-машина `GameCreationWizard` и контракт `IWizardMessenger` перенесены в `GmRelay.Shared`, чтобы обе платформы (Telegram/Discord) использовали один и тот же движок визарда.
|
||||
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -115,9 +115,9 @@ public sealed class DiscordWizardCommand : ApplicationCommandModule<SlashCommand
|
||||
Step = NormalizeMode(mode) is { } m && m == WizardCreationType.Pool
|
||||
? WizardStepNames.Title
|
||||
: WizardStepNames.Type,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddHours(24),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
ExpiresAt = DateTime.UtcNow.AddHours(24),
|
||||
};
|
||||
// If the user passed `mode=pool` we pre-seed the payload so the
|
||||
// wizard's own branching lands on the pool flow.
|
||||
@@ -147,7 +147,7 @@ public sealed class DiscordWizardCommand : ApplicationCommandModule<SlashCommand
|
||||
var (text, actions) = WizardStepViewBuilder.Build(draft, payload);
|
||||
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);
|
||||
|
||||
await Context.Interaction.ModifyResponseAsync(msg =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<WizardClubOption>? clubs = null;
|
||||
|
||||
@@ -43,9 +43,9 @@ public sealed class WizardDraft
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -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<WizardDraft?> 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<WizardDraft>(
|
||||
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<int> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="nav-version">v3.9.0</div>
|
||||
<div class="nav-version">v3.9.1</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AOT RowFactory for WizardDraft expects to read timestamps as
|
||||
/// <see cref="DateTime"/> (UTC). If a future refactor switches the
|
||||
/// properties back to <see cref="DateTimeOffset"/>, the AOT row factory
|
||||
/// will throw <c>InvalidCastException: DateTime → DateTimeOffset</c>
|
||||
/// on the first query against wizard_drafts.
|
||||
/// </summary>
|
||||
[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<int>` (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.");
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user