using NeonSprawl.Server.Game.Contracts; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Tests.Game.PositionState; namespace NeonSprawl.Server.Tests.Game.Contracts; [Collection("Postgres integration")] public sealed class ContractInstancePersistenceIntegrationTests(PostgresIntegrationHarness harness) { private PostgresWebApplicationFactory Factory => harness.Factory; private const string PlayerId = "dev-local-1"; private const string InstanceId = "prototype_contract_instance_pg_001"; private const string TemplateId = "prototype_contract_clear_combat_pocket"; private const string SeedBucket = "2026-06-22"; private static readonly DateTimeOffset IssuedAt = new(2026, 6, 22, 10, 0, 0, TimeSpan.Zero); private static readonly DateTimeOffset CompletedAt = new(2026, 6, 22, 14, 0, 0, TimeSpan.Zero); [RequirePostgresFact] public async Task TryCreateActive_ShouldReturnOneTrueAndRestFalse_WhenCalledConcurrently() { // Arrange await ResetContractInstanceTableAsync(); const int concurrentCalls = 8; // Act var results = await Task.WhenAll( Enumerable.Range(0, concurrentCalls).Select(_ => Task.Run(() => { using var scope = Factory.Services.CreateScope(); var store = scope.ServiceProvider.GetRequiredService(); return store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out ContractInstanceState _); }))); // Assert Assert.Equal(1, results.Count(static r => r)); Assert.Equal(concurrentCalls - 1, results.Count(static r => !r)); Assert.True( Factory.Services.CreateScope().ServiceProvider .GetRequiredService() .TryGet(PlayerId, InstanceId, out var snapshot)); Assert.Equal(ContractInstanceStatus.Active, snapshot.Status); Assert.Equal(TemplateId, snapshot.TemplateId); } [RequirePostgresFact] public async Task TryCreateActive_ShouldAllowOnlyOneActive_WhenDistinctInstanceIdsRace() { // Arrange await ResetContractInstanceTableAsync(); const int concurrentCalls = 8; // Act var results = await Task.WhenAll( Enumerable.Range(0, concurrentCalls).Select(i => Task.Run(() => { using var scope = Factory.Services.CreateScope(); var store = scope.ServiceProvider.GetRequiredService(); var instanceId = $"prototype_contract_instance_pg_race_{i:D3}"; return ( Success: store.TryCreateActive( PlayerId, instanceId, TemplateId, SeedBucket, IssuedAt, out _), InstanceId: instanceId); }))); // Assert Assert.Equal(1, results.Count(static r => r.Success)); Assert.Equal(concurrentCalls - 1, results.Count(static r => !r.Success)); Assert.True( Factory.Services.CreateScope().ServiceProvider .GetRequiredService() .TryGetActiveForPlayer(PlayerId, out var active)); Assert.Equal(ContractInstanceStatus.Active, active.Status); Assert.Contains(results.Where(static r => r.Success).Select(static r => r.InstanceId), id => id == active.ContractInstanceId); } [RequirePostgresFact] public async Task TryMarkComplete_ShouldBeIdempotent_OnPostgres() { // Arrange await ResetContractInstanceTableAsync(); using var scope = Factory.Services.CreateScope(); var store = scope.ServiceProvider.GetRequiredService(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); // Act var first = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out var firstSnapshot); var second = store.TryMarkComplete( PlayerId, InstanceId, CompletedAt.AddHours(1), out var secondSnapshot); // Assert Assert.True(first); Assert.False(second); Assert.Equal(CompletedAt, secondSnapshot.CompletedAt); Assert.Equal(firstSnapshot.ContractInstanceId, secondSnapshot.ContractInstanceId); } [RequirePostgresFact] public async Task CreateComplete_ShouldPersistAcrossNewFactory() { // Arrange await ResetContractInstanceTableAsync(); // Act — write through first host using (var firstScope = Factory.Services.CreateScope()) { var store = firstScope.ServiceProvider.GetRequiredService(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); Assert.True(store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out _)); } ContractInstanceState readBack; await using var secondFactory = new PostgresWebApplicationFactory(); using var secondScope = secondFactory.Services.CreateScope(); var readStore = secondScope.ServiceProvider.GetRequiredService(); readBack = readStore.TryGet(PlayerId, InstanceId, out var snapshot) ? snapshot : null!; // Assert Assert.NotNull(readBack); Assert.Equal(ContractInstanceStatus.Completed, readBack.Status); Assert.Equal(SeedBucket, readBack.SeedBucket); Assert.Equal(CompletedAt, readBack.CompletedAt); } private async Task ResetContractInstanceTableAsync() { 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 instanceDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V011__contract_instance.sql"); if (!File.Exists(positionDdlPath) || !File.Exists(instanceDdlPath)) { throw new FileNotFoundException("Test DDL for contract instance persistence not found."); } var positionDdl = await File.ReadAllTextAsync(positionDdlPath); var instanceDdl = await File.ReadAllTextAsync(instanceDdlPath); 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 applyInstance = new NpgsqlCommand(instanceDdl, conn); await applyInstance.ExecuteNonQueryAsync(); } }