using System.Net; using System.Net.Http.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.PositionState; using Npgsql; using Xunit; namespace NeonSprawl.Server.Tests.Game.PositionState; /// NS-17: real PostgreSQL + HTTP; collection serializes tests that share the database. [Collection("Postgres integration")] public sealed class PostgresPositionStateIntegrationTests(PostgresIntegrationHarness harness) { private PostgresWebApplicationFactory Factory => harness.Factory; [RequirePostgresFact] public async Task PostMove_ThenGet_ShouldReflectPersistedPositionAndSequence() { // Arrange await ResetPlayerPositionTableAsync(); using var client = Factory.CreateClient(); var cmd = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 2, Y = 0.5, Z = 3.5 }, }; // Act var before = await client.GetFromJsonAsync("/game/players/dev-local-1/position"); var post = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd); var postBody = await post.Content.ReadFromJsonAsync(); var after = await client.GetFromJsonAsync("/game/players/dev-local-1/position"); // Assert Assert.NotNull(before); Assert.Equal(HttpStatusCode.OK, post.StatusCode); Assert.NotNull(postBody); Assert.NotNull(after); Assert.Equal(PositionStateResponse.CurrentSchemaVersion, postBody!.SchemaVersion); Assert.Equal(before!.Sequence + 1, postBody.Sequence); Assert.Equal(2, postBody.Position.X); Assert.Equal(0.5, postBody.Position.Y); Assert.Equal(3.5, postBody.Position.Z); Assert.Equal(postBody.Sequence, after!.Sequence); Assert.Equal(postBody.Position.X, after.Position.X); Assert.Equal(postBody.Position.Y, after.Position.Y); Assert.Equal(postBody.Position.Z, after.Position.Z); } [RequirePostgresFact] public async Task SecondProcess_ShouldReadLastPosition_AfterFirstFactoryDisposed() { // Arrange await ResetPlayerPositionTableAsync(); var cmd = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 7, Y = 0.8, Z = 7 }, }; HttpStatusCode postStatus = default; PositionStateResponse? persisted = null; // Act using (var first = Factory.CreateClient()) { var post = await first.PostAsJsonAsync("/game/players/dev-local-1/move", cmd); postStatus = post.StatusCode; persisted = await post.Content.ReadFromJsonAsync(); } await using var secondFactory = new PostgresWebApplicationFactory(); using var secondClient = secondFactory.CreateClient(); var after = await secondClient.GetFromJsonAsync("/game/players/dev-local-1/position"); // Assert Assert.Equal(HttpStatusCode.OK, postStatus); Assert.NotNull(persisted); Assert.NotNull(after); Assert.Equal(7, after!.Position.X); Assert.Equal(0.8, after.Position.Y); Assert.Equal(7, after.Position.Z); Assert.Equal(1ul, after.Sequence); } [RequirePostgresFact] public async Task PostMove_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await ResetPlayerPositionTableAsync(); using var client = Factory.CreateClient(); var cmd = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 0, Y = 0, Z = 0 }, }; // Act var response = await client.PostAsJsonAsync("/game/players/not-a-seeded-player/move", cmd); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } /// DDL + empty table + dev row matching the test host's (parity with startup seed). private async Task ResetPlayerPositionTableAsync() { 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 ddlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V001__player_position.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 player_position;", conn)) { await truncate.ExecuteNonQueryAsync(); } PostgresPositionBootstrap.SeedDevPlayer(conn, options); } }