fix(data): align portfolio mutation lock order

This commit is contained in:
2026-06-01 20:23:43 +03:00
parent 2b725708ef
commit a28b75dd5b
8 changed files with 220 additions and 22 deletions
@@ -3,15 +3,22 @@ namespace GmRelay.Bot.Tests.Web;
public sealed class PortfolioSessionDeletionSourceTests
{
[Fact]
public async Task SharedDeleteSessionHandler_ShouldUnpublishLinkedPortfolioCardBeforeDeletingSession()
public async Task SharedDeleteSessionHandler_ShouldLockSessionBeforeUnpublishingLinkedPortfolioCardAndDeletingSession()
{
var source = NormalizeSql(await ReadRepositoryFileAsync(
"src/GmRelay.Shared/Features/Sessions/ListSessions/DeleteSessionHandler.cs"));
const string sessionLock =
"FROM sessions s WHERE s.id = @SessionId FOR UPDATE OF s";
const string unpublish =
"UPDATE portfolio_games pg SET is_public = false, updated_at = now() FROM portfolio_game_sessions pgs WHERE pgs.portfolio_game_id = pg.id AND pgs.session_id = @SessionId AND pg.is_public = true";
Assert.Contains(sessionLock, source, StringComparison.Ordinal);
Assert.Contains(unpublish, source, StringComparison.Ordinal);
Assert.True(
source.IndexOf(sessionLock, StringComparison.Ordinal) <
source.IndexOf(unpublish, StringComparison.Ordinal),
"The shared delete path must lock the session before locking a linked portfolio card.");
Assert.True(
source.IndexOf(unpublish, StringComparison.Ordinal) <
source.IndexOf("DELETE FROM sessions WHERE id = @Id", StringComparison.Ordinal),
@@ -19,16 +26,23 @@ public sealed class PortfolioSessionDeletionSourceTests
}
[Fact]
public async Task DiscordDeleteSessionHandler_ShouldUnpublishOnlyCardsFromTheInteractionGuildBeforeDeletingSession()
public async Task DiscordDeleteSessionHandler_ShouldLockGuildSessionBeforeUnpublishingLinkedPortfolioCardAndDeletingSession()
{
var source = NormalizeSql(await ReadRepositoryFileAsync(
"src/GmRelay.DiscordBot/Features/Sessions/DiscordDeleteSessionHandler.cs"));
const string sessionLock =
"SELECT s.id FROM sessions s JOIN game_groups g ON g.id = s.group_id WHERE s.id = @SessionId AND g.platform = 'Discord' AND g.external_group_id = @GuildId FOR UPDATE OF s";
const string unpublish =
"UPDATE portfolio_games pg SET is_public = false, updated_at = now() FROM portfolio_game_sessions pgs JOIN sessions s ON s.id = pgs.session_id JOIN game_groups g ON g.id = s.group_id WHERE pgs.portfolio_game_id = pg.id AND s.id = @SessionId AND g.platform = 'Discord' AND g.external_group_id = @GuildId AND pg.is_public = true";
Assert.Contains(sessionLock, source, StringComparison.Ordinal);
Assert.Contains(unpublish, source, StringComparison.Ordinal);
Assert.Contains("AND p.platform = 'Discord'", source, StringComparison.Ordinal);
Assert.True(
source.IndexOf(sessionLock, StringComparison.Ordinal) <
source.IndexOf(unpublish, StringComparison.Ordinal),
"The Discord delete path must lock the guild-scoped session before locking a linked portfolio card.");
Assert.True(
source.IndexOf(unpublish, StringComparison.Ordinal) <
source.IndexOf("DELETE FROM sessions s", StringComparison.Ordinal),