using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.Factions; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Tests.Game.PositionState; using Npgsql; using Xunit; namespace NeonSprawl.Server.Tests.Game.Factions; [Collection("Postgres integration")] public sealed class FactionStandingPersistenceIntegrationTests(PostgresIntegrationHarness harness) { private PostgresWebApplicationFactory Factory => harness.Factory; private const string PlayerId = "dev-local-1"; private const string GridFactionId = "prototype_faction_grid_operators"; [RequirePostgresFact] public async Task ApplyDeltaThenGetAcrossNewFactory_ShouldPersistStanding() { // Arrange await ResetFactionStandingTableAsync(); // Act — write through first host FactionStandingMutationOutcome writeOutcome; using (var firstScope = Factory.Services.CreateScope()) { var store = firstScope.ServiceProvider.GetRequiredService(); writeOutcome = store.TryApplyStandingDelta(PlayerId, GridFactionId, 15); } FactionStandingReadOutcome readBack; await using var secondFactory = new PostgresWebApplicationFactory(); using var secondScope = secondFactory.Services.CreateScope(); var readStore = secondScope.ServiceProvider.GetRequiredService(); readBack = readStore.TryGetStanding(PlayerId, GridFactionId); // Assert Assert.True(writeOutcome.Success); Assert.Equal(15, writeOutcome.NewStanding); Assert.True(readBack.Success); Assert.Equal(15, readBack.Standing); } [RequirePostgresFact] public async Task TryGetStanding_ShouldClampRead_WhenStoredValueExceedsMax() { // Arrange await ResetFactionStandingTableAsync(); var cs = Environment.GetEnvironmentVariable("ConnectionStrings__NeonSprawl") ?? throw new InvalidOperationException("ConnectionStrings__NeonSprawl is not set."); await using (var conn = new NpgsqlConnection(cs)) { await conn.OpenAsync(); await using var insert = new NpgsqlCommand( """ INSERT INTO player_faction_standing (player_id, faction_id, standing, updated_at) VALUES (@pid, @fid, @standing, now()); """, conn); insert.Parameters.AddWithValue("pid", PlayerId); insert.Parameters.AddWithValue("fid", GridFactionId); insert.Parameters.AddWithValue("standing", 999); await insert.ExecuteNonQueryAsync(); } // Act FactionStandingReadOutcome readBack; using var scope = Factory.Services.CreateScope(); var store = scope.ServiceProvider.GetRequiredService(); readBack = store.TryGetStanding(PlayerId, GridFactionId); // Assert Assert.True(readBack.Success); Assert.Equal(100, readBack.Standing); } private async Task ResetFactionStandingTableAsync() { var cs = Environment.GetEnvironmentVariable("ConnectionStrings__NeonSprawl"); if (string.IsNullOrWhiteSpace(cs)) { throw new InvalidOperationException("ConnectionStrings__NeonSprawl is not set."); } _ = Factory.Services; var options = Factory.Services.GetRequiredService>().Value; var positionDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V001__player_position.sql"); var standingDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V009__player_faction_standing.sql"); if (!File.Exists(positionDdlPath) || !File.Exists(standingDdlPath)) { throw new FileNotFoundException("Test DDL for faction standing persistence not found."); } var positionDdl = await File.ReadAllTextAsync(positionDdlPath); var standingDdl = await File.ReadAllTextAsync(standingDdlPath); await using var conn = new NpgsqlConnection(cs); await conn.OpenAsync(); await using var applyPosition = new NpgsqlCommand(positionDdl, conn); await applyPosition.ExecuteNonQueryAsync(); await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn); await truncate.ExecuteNonQueryAsync(); PostgresPositionBootstrap.SeedDevPlayer(conn, options); await using var applyStanding = new NpgsqlCommand(standingDdl, conn); await applyStanding.ExecuteNonQueryAsync(); } }