fix(data): enforce portfolio validation isolation
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Npgsql;
|
||||
using System.Data;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Web;
|
||||
|
||||
@@ -184,6 +185,78 @@ public sealed class PortfolioMigrationPostgresTests(PortfolioMigrationPostgresFi
|
||||
parameters: new NpgsqlParameter("portfolioGameId", seed.PortfolioGameId)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("portfolio_game_sessions", "session_id")]
|
||||
[InlineData("portfolio_game_masters", "player_id")]
|
||||
public async Task RepeatableReadConcurrentRequiredLinkDeletes_ShouldBeRejectedWithoutInvalidPublicCard(
|
||||
string linkTable,
|
||||
string linkColumn)
|
||||
{
|
||||
var database = await fixture.CreateMigratedDatabaseAsync();
|
||||
await using var seedConnection = await database.OpenConnectionAsync();
|
||||
var seed = await SeedCardAsync(
|
||||
seedConnection,
|
||||
isPublic: true,
|
||||
sessionCount: linkTable == "portfolio_game_sessions" ? 2 : 1,
|
||||
masterCount: linkTable == "portfolio_game_masters" ? 2 : 1);
|
||||
await using var firstConnection = await database.OpenConnectionAsync();
|
||||
await using var secondConnection = await database.OpenConnectionAsync();
|
||||
await using var firstTransaction = await firstConnection.BeginTransactionAsync(IsolationLevel.RepeatableRead);
|
||||
await using var secondTransaction = await secondConnection.BeginTransactionAsync(IsolationLevel.RepeatableRead);
|
||||
var linkIds = linkTable == "portfolio_game_sessions" ? seed.SessionIds : seed.MasterIds;
|
||||
|
||||
await ExecuteNonQueryAsync(
|
||||
firstConnection,
|
||||
$"DELETE FROM {linkTable} WHERE portfolio_game_id = @portfolioGameId AND {linkColumn} = @linkId",
|
||||
firstTransaction,
|
||||
new NpgsqlParameter("portfolioGameId", seed.PortfolioGameId),
|
||||
new NpgsqlParameter("linkId", linkIds[0]));
|
||||
await ExecuteNonQueryAsync(
|
||||
secondConnection,
|
||||
$"DELETE FROM {linkTable} WHERE portfolio_game_id = @portfolioGameId AND {linkColumn} = @linkId",
|
||||
secondTransaction,
|
||||
new NpgsqlParameter("portfolioGameId", seed.PortfolioGameId),
|
||||
new NpgsqlParameter("linkId", linkIds[1]));
|
||||
|
||||
var commitStates = await Task.WhenAll(
|
||||
CommitAndCaptureSqlStateAsync(firstTransaction),
|
||||
CommitAndCaptureSqlStateAsync(secondTransaction));
|
||||
|
||||
Assert.All(commitStates, state => Assert.Equal(PostgresErrorCodes.FeatureNotSupported, state));
|
||||
|
||||
await using var verificationConnection = await database.OpenConnectionAsync();
|
||||
Assert.True(await ExecuteScalarAsync<bool>(
|
||||
verificationConnection,
|
||||
"SELECT is_public FROM portfolio_games WHERE id = @portfolioGameId",
|
||||
parameters: new NpgsqlParameter("portfolioGameId", seed.PortfolioGameId)));
|
||||
Assert.Equal(2, await ExecuteScalarAsync<long>(
|
||||
verificationConnection,
|
||||
$"SELECT COUNT(*) FROM {linkTable} WHERE portfolio_game_id = @portfolioGameId",
|
||||
parameters: new NpgsqlParameter("portfolioGameId", seed.PortfolioGameId)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(IsolationLevel.RepeatableRead)]
|
||||
[InlineData(IsolationLevel.Serializable)]
|
||||
public async Task NonReadCommittedPublishedCardLinkDelete_ShouldBeRejected(IsolationLevel isolationLevel)
|
||||
{
|
||||
var database = await fixture.CreateMigratedDatabaseAsync();
|
||||
await using var connection = await database.OpenConnectionAsync();
|
||||
var seed = await SeedCardAsync(connection, isPublic: true);
|
||||
await using var transaction = await connection.BeginTransactionAsync(isolationLevel);
|
||||
|
||||
await ExecuteNonQueryAsync(
|
||||
connection,
|
||||
"DELETE FROM portfolio_game_sessions WHERE portfolio_game_id = @portfolioGameId",
|
||||
transaction,
|
||||
new NpgsqlParameter("portfolioGameId", seed.PortfolioGameId));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<PostgresException>(
|
||||
() => transaction.CommitAsync().WaitAsync(CommandTimeout));
|
||||
|
||||
Assert.Equal(PostgresErrorCodes.FeatureNotSupported, exception.SqlState);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("portfolio_game_sessions")]
|
||||
[InlineData("portfolio_game_masters")]
|
||||
|
||||
Reference in New Issue
Block a user