using System.Net.Http.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.Skills; using NeonSprawl.Server.Tests.Game.PositionState; using Npgsql; using Xunit; namespace NeonSprawl.Server.Tests.Game.Skills; [Collection("Postgres integration")] public sealed class SkillProgressionGrantPersistenceIntegrationTests(PostgresIntegrationHarness harness) { private PostgresWebApplicationFactory Factory => harness.Factory; [RequirePostgresFact] public async Task PostGrantThenGetAcrossNewFactory_ShouldPersistSkillXp() { // Arrange await ResetProgressionTableAsync(); var grant = new SkillProgressionGrantRequest { SchemaVersion = SkillProgressionGrantRequest.CurrentSchemaVersion, SkillId = "salvage", Amount = 50, SourceKind = "activity", }; HttpResponseMessage postResponse; // Act — write grant through first host using var firstClient = Factory.CreateClient(); postResponse = await firstClient.PostAsJsonAsync( "/game/players/dev-local-1/skill-progression", grant); // Act — read back through a fresh host (forces DB round-trip / new DI scope) await using var secondFactory = new PostgresWebApplicationFactory(); using var secondClient = secondFactory.CreateClient(); var snapshot = await secondClient.GetFromJsonAsync( "/game/players/dev-local-1/skill-progression"); // Assert Assert.True(postResponse.IsSuccessStatusCode); Assert.NotNull(snapshot); var salvage = Assert.Single(snapshot!.Skills!, static s => s.Id == "salvage"); Assert.Equal(50, salvage.Xp); Assert.Equal(1, salvage.Level); } private async Task ResetProgressionTableAsync() { 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 progressionDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V003__player_skill_progression.sql"); if (!File.Exists(positionDdlPath)) { throw new FileNotFoundException($"Test DDL not found at '{positionDdlPath}'.", positionDdlPath); } if (!File.Exists(progressionDdlPath)) { throw new FileNotFoundException($"Test DDL not found at '{progressionDdlPath}'.", progressionDdlPath); } var positionDdl = await File.ReadAllTextAsync(positionDdlPath); var progressionDdl = await File.ReadAllTextAsync(progressionDdlPath); 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 applyProgression = new NpgsqlCommand(progressionDdl, conn); await applyProgression.ExecuteNonQueryAsync(); await using var truncateProgression = new NpgsqlCommand("TRUNCATE player_skill_progression;", conn); await truncateProgression.ExecuteNonQueryAsync(); } }