From efd86bca0a40d61ce97ae951d53b891bdab3915b Mon Sep 17 00:00:00 2001 From: Toutsu Date: Tue, 9 Jun 2026 15:16:54 +0300 Subject: [PATCH] fix(shared): bind platform when creating group manager Add a PostgreSQL integration regression test for new-platform-group session creation. The production failure was a missing Platform parameter in the group_managers insert, leaving @Platform in SQL and causing PostgreSQL 42883. Bump version to 3.9.8. --- .gitea/workflows/deploy.yml | 2 +- Directory.Build.props | 2 +- compose.yaml | 6 +- .../CreateSession/CreateSessionHandler.cs | 8 +- .../Components/Layout/NavMenu.razor | 2 +- .../CreateSessionHandlerIntegrationTests.cs | 130 ++++++++++++++++++ 6 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/CreateSessionHandlerIntegrationTests.cs diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index eaa389a..8543e4c 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,7 +6,7 @@ on: - main env: - VERSION: 3.9.7 + VERSION: 3.9.8 jobs: # ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами) diff --git a/Directory.Build.props b/Directory.Build.props index da1a835..aaff13b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 3.9.7 + 3.9.8 net10.0 preview enable diff --git a/compose.yaml b/compose.yaml index a790505..e27dd62 100644 --- a/compose.yaml +++ b/compose.yaml @@ -49,7 +49,7 @@ services: crond -f bot: - image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.7 + image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.8 restart: always depends_on: db: @@ -67,7 +67,7 @@ services: retries: 3 discord: - image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.7 + image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.8 restart: always depends_on: db: @@ -86,7 +86,7 @@ services: retries: 3 web: - image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.7 + image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.8 restart: always depends_on: db: diff --git a/src/GmRelay.Shared/Features/Sessions/CreateSession/CreateSessionHandler.cs b/src/GmRelay.Shared/Features/Sessions/CreateSession/CreateSessionHandler.cs index b4bedb5..0e2c7f4 100644 --- a/src/GmRelay.Shared/Features/Sessions/CreateSession/CreateSessionHandler.cs +++ b/src/GmRelay.Shared/Features/Sessions/CreateSession/CreateSessionHandler.cs @@ -82,7 +82,13 @@ public sealed class CreateSessionHandler( AND p.external_user_id = @ExternalGmId ON CONFLICT (group_id, player_id) DO NOTHING """, - new { GroupId = groupId, ExternalGmId = externalUserId, OwnerRole = GroupManagerRoleExtensions.OwnerValue }, + new + { + GroupId = groupId, + Platform = platform, + ExternalGmId = externalUserId, + OwnerRole = GroupManagerRoleExtensions.OwnerValue + }, transaction); } else diff --git a/src/GmRelay.Web/Components/Layout/NavMenu.razor b/src/GmRelay.Web/Components/Layout/NavMenu.razor index b7b076d..dd73424 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/CreateSessionHandlerIntegrationTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/CreateSessionHandlerIntegrationTests.cs new file mode 100644 index 0000000..94f4c1f --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/CreateSessionHandlerIntegrationTests.cs @@ -0,0 +1,130 @@ +using GmRelay.Shared.Domain; +using GmRelay.Shared.Features.Sessions.CreateSession; +using GmRelay.Shared.Platform; +using Npgsql; +using Testcontainers.PostgreSql; + +namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession; + +[CollectionDefinition(Name)] +public sealed class CreateSessionHandlerPostgresCollection : ICollectionFixture +{ + public const string Name = "Create session handler PostgreSQL"; +} + +public sealed class CreateSessionHandlerPostgresFixture : 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 CreateMigratedDatabaseAsync() + { + var databaseName = $"create_session_{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 = 30 + }.ConnectionString; + + await using var connection = new NpgsqlConnection(connectionString); + await connection.OpenAsync().WaitAsync(ContainerTimeout); + + foreach (var migration in GetMigrationPaths()) + { + await using var command = new NpgsqlCommand(await File.ReadAllTextAsync(migration), connection) + { + CommandTimeout = 30 + }; + await command.ExecuteNonQueryAsync().WaitAsync(ContainerTimeout); + } + + return connectionString; + } + + private static IReadOnlyList GetMigrationPaths() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + var migrationsDirectory = Path.Combine(directory.FullName, "src", "GmRelay.Bot", "Migrations"); + if (Directory.Exists(migrationsDirectory)) + { + return Directory.GetFiles(migrationsDirectory, "V*.sql") + .OrderBy(path => Path.GetFileName(path), StringComparer.Ordinal) + .ToArray(); + } + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException("Could not locate the bot migrations directory."); + } +} + +[Collection(CreateSessionHandlerPostgresCollection.Name)] +public sealed class CreateSessionHandlerIntegrationTests(CreateSessionHandlerPostgresFixture fixture) +{ + [Fact] + public async Task HandleAsync_NewPlatformGroup_AddsOwnerAndPersistsSession() + { + var connectionString = await fixture.CreateMigratedDatabaseAsync(); + await using var dataSource = NpgsqlDataSource.Create(connectionString); + var sut = new CreateSessionHandler(dataSource); + + var result = await sut.HandleAsync( + new CreateSessionCommand( + new PlatformUser(PlatformKind.Telegram, "111111111", "Test GM", "test_gm"), + new PlatformGroup(PlatformKind.Telegram, "222222222", "Test Group"), + "Test Adventure", + string.Empty, + [DateTimeOffset.UtcNow.AddDays(1)], + null, + null, + GameSystem.Dnd5e, + "Integration regression test", + "Online", + 240, + true), + CancellationToken.None); + + Assert.True(result.Success, result.ErrorMessage); + Assert.NotNull(result.BatchId); + Assert.NotNull(result.GroupId); + + await using var connection = await dataSource.OpenConnectionAsync(); + await using var command = new NpgsqlCommand( + """ + SELECT count(*) + FROM group_managers gm + JOIN players p ON p.id = gm.player_id + WHERE gm.group_id = @group_id + AND gm.role = 'Owner' + AND p.platform = 'Telegram' + AND p.external_user_id = '111111111' + """, + connection); + command.Parameters.AddWithValue("group_id", result.GroupId.Value); + + var ownerCount = (long)(await command.ExecuteScalarAsync() ?? 0L); + Assert.Equal(1, ownerCount); + } +}