using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.Gathering; using NeonSprawl.Server.Tests.Game.PositionState; using Npgsql; using Xunit; namespace NeonSprawl.Server.Tests.Game.Gathering; [Collection("Postgres integration")] public sealed class ResourceNodeInstancePersistenceIntegrationTests(PostgresIntegrationHarness harness) { private PostgresWebApplicationFactory Factory => harness.Factory; private const string AlphaId = "prototype_resource_node_alpha"; [RequirePostgresFact] public async Task TryCommitSuccessfulGatherDecrementAcrossNewFactory_ShouldPersistRemainingGathers() { // Arrange await ResetInstanceTableAsync(); ResourceNodeInstanceMutationOutcome firstOutcome; // Act — write through first host using (var firstScope = Factory.Services.CreateScope()) { var registry = firstScope.ServiceProvider.GetRequiredService(); var store = firstScope.ServiceProvider.GetRequiredService(); firstOutcome = ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(AlphaId, registry, store); } // Act — read back through a fresh host await using var secondFactory = new PostgresWebApplicationFactory(); using var secondScope = secondFactory.Services.CreateScope(); var readStore = secondScope.ServiceProvider.GetRequiredService(); var readBack = readStore.TryGetRemainingGathers(AlphaId, out var snapshot); // Assert Assert.Equal(ResourceNodeInstanceMutationKind.Applied, firstOutcome.Kind); Assert.Equal(9, firstOutcome.Snapshot!.Value.RemainingGathers); Assert.True(readBack); Assert.Equal(9, snapshot.RemainingGathers); } [RequirePostgresFact] public async Task TryCommitSuccessfulGatherDecrement_WhenDepleted_ShouldPersistZeroRow() { // Arrange await ResetInstanceTableAsync(); using (var seedScope = Factory.Services.CreateScope()) { var registry = seedScope.ServiceProvider.GetRequiredService(); var store = seedScope.ServiceProvider.GetRequiredService(); for (var i = 0; i < 10; i++) { _ = ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(AlphaId, registry, store); } } // Act ResourceNodeInstanceMutationOutcome denyOutcome; await using var secondFactory = new PostgresWebApplicationFactory(); using (var scope = secondFactory.Services.CreateScope()) { var registry = scope.ServiceProvider.GetRequiredService(); var store = scope.ServiceProvider.GetRequiredService(); denyOutcome = ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(AlphaId, registry, store); store.TryGetRemainingGathers(AlphaId, out var snapshot); // Assert Assert.Equal(ResourceNodeInstanceMutationKind.Denied, denyOutcome.Kind); Assert.Equal(ResourceNodeInstanceReasonCodes.NodeDepleted, denyOutcome.ReasonCode); Assert.Equal(0, snapshot.RemainingGathers); } } private async Task ResetInstanceTableAsync() { var cs = Environment.GetEnvironmentVariable("ConnectionStrings__NeonSprawl"); if (string.IsNullOrWhiteSpace(cs)) { throw new InvalidOperationException("ConnectionStrings__NeonSprawl is not set."); } _ = Factory.Services; var ddlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V006__resource_node_instance.sql"); if (!File.Exists(ddlPath)) { throw new FileNotFoundException($"Test DDL not found at '{ddlPath}'.", ddlPath); } var ddl = await File.ReadAllTextAsync(ddlPath); await using var conn = new NpgsqlConnection(cs); await conn.OpenAsync(); await using (var apply = new NpgsqlCommand(ddl, conn)) { await apply.ExecuteNonQueryAsync(); } await using (var truncate = new NpgsqlCommand("TRUNCATE resource_node_instance;", conn)) { await truncate.ExecuteNonQueryAsync(); } } }