using NeonSprawl.Server.Game.AbilityInput; using NeonSprawl.Server.Game.Gigs; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.Targeting; using NeonSprawl.Server.Tests.Game.PositionState; namespace NeonSprawl.Server.Tests.Game.Gigs; [Collection("Postgres integration")] public sealed class GigProgressionGrantPersistenceIntegrationTests(PostgresIntegrationHarness harness) { private PostgresWebApplicationFactory Factory => harness.Factory; [RequirePostgresFact] public async Task DefeatGrantThenGetAcrossNewFactory_ShouldPersistGigXp() { // Arrange await ResetGigProgressionTableAsync(); var cast = new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }; // Act — defeat alpha through first host (four pulses) using (var firstClient = Factory.CreateClient()) { await firstClient.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [new HotbarSlotBindingJson { SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse }], }); await firstClient.PostAsJsonAsync( "/game/players/dev-local-1/target/select", new TargetSelectRequest { SchemaVersion = TargetSelectRequest.CurrentSchemaVersion, TargetId = PrototypeNpcRegistry.PrototypeNpcMeleeId, }); for (var i = 0; i < 4; i++) { var response = await firstClient.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); response.EnsureSuccessStatusCode(); if (i < 3) { await Task.Delay(TimeSpan.FromSeconds(3.2)); } } } await using var secondFactory = new PostgresWebApplicationFactory(); using var secondClient = secondFactory.CreateClient(); var snapshot = await secondClient.GetFromJsonAsync( "/game/players/dev-local-1/gig-progression"); // Assert Assert.NotNull(snapshot); var breach = Assert.Single(snapshot!.Gigs!); Assert.Equal(GigProgressionConstants.CombatDefeatXpAmount, breach.Xp); } private async Task ResetGigProgressionTableAsync() { 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 gigDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V007__player_gig_progression.sql"); if (!File.Exists(positionDdlPath)) { throw new FileNotFoundException($"Test DDL not found at '{positionDdlPath}'.", positionDdlPath); } if (!File.Exists(gigDdlPath)) { throw new FileNotFoundException($"Test DDL not found at '{gigDdlPath}'.", gigDdlPath); } var positionDdl = await File.ReadAllTextAsync(positionDdlPath); var gigDdl = await File.ReadAllTextAsync(gigDdlPath); 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 applyGig = new NpgsqlCommand(gigDdl, conn); await applyGig.ExecuteNonQueryAsync(); await using var truncateGig = new NpgsqlCommand("TRUNCATE player_gig_progression;", conn); await truncateGig.ExecuteNonQueryAsync(); } }