Harden owner and suggestion invariants for concurrent writes

This commit is contained in:
2026-02-08 21:37:46 +01:00
parent 569cea161f
commit fe6a9d5da4
13 changed files with 472 additions and 22 deletions

View File

@@ -1,6 +1,8 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using GameList.Data;
using GameList.Domain;
using GameList.Infrastructure;
using GameList.Tests.Support;
using Microsoft.EntityFrameworkCore;
@@ -247,4 +249,32 @@ public class AuthTests
resp.EnsureSuccessStatusCode();
Assert.True(resp.Headers.TryGetValues("Set-Cookie", out var cookies) && cookies.Any(c => c.Contains("player")));
}
[Fact]
public async Task Owner_uniqueness_is_enforced_by_database_constraint()
{
await using var factory = new TestWebApplicationFactory();
var ownerClient = factory.CreateClientWithCookies();
await ownerClient.RegisterAsync("owner1", admin: true);
var thrown = await Assert.ThrowsAsync<DbUpdateException>(() => factory.WithDbContextAsync(async db =>
{
var (hash, salt) = PasswordHasher.HashPassword("Pass123!");
db.Players.Add(new Player
{
Id = Guid.NewGuid(),
Username = "owner2",
NormalizedUsername = "owner2",
PasswordHash = hash,
PasswordSalt = salt,
DisplayName = "Owner2",
IsOwner = true,
IsAdmin = true
});
await db.SaveChangesAsync();
}));
Assert.Contains("Players.IsOwner", thrown.InnerException?.Message ?? thrown.Message, StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -1,6 +1,7 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using GameList.Domain;
using GameList.Tests.Support;
using Microsoft.EntityFrameworkCore;
@@ -626,4 +627,41 @@ public class SuggestionTests
Assert.False(db.Votes.Any(v => v.SuggestionId == id));
});
}
[Fact]
public async Task Suggestion_limit_is_enforced_by_database_trigger_without_joker()
{
await using var factory = new TestWebApplicationFactory();
var client = factory.CreateClientWithCookies();
await client.RegisterAsync("dbcap");
var playerId = await factory.WithDbContextAsync(async db => await db.Players.Select(p => p.Id).SingleAsync());
await factory.WithDbContextAsync(async db =>
{
for (var i = 0; i < 5; i++)
{
db.Suggestions.Add(new Suggestion
{
PlayerId = playerId,
Name = $"Seed {i}"
});
}
await db.SaveChangesAsync();
});
var thrown = await Assert.ThrowsAsync<DbUpdateException>(() => factory.WithDbContextAsync(async db =>
{
db.Suggestions.Add(new Suggestion
{
PlayerId = playerId,
Name = "Blocked by trigger"
});
await db.SaveChangesAsync();
}));
Assert.Contains("suggestion_limit_exceeded", thrown.InnerException?.Message ?? thrown.Message, StringComparison.OrdinalIgnoreCase);
}
}