neon-sprawl/server/NeonSprawl.Server.Tests/Game/PositionState/PostgresPositionStateIntegr...

138 lines
5.3 KiB
C#

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;
/// <summary>NS-17: real PostgreSQL + HTTP; collection serializes tests that share the database.</summary>
[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 },
};
var before = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
// Act
var post = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
// Assert
Assert.NotNull(before);
Assert.Equal(HttpStatusCode.OK, post.StatusCode);
var postBody = await post.Content.ReadFromJsonAsync<PositionStateResponse>();
var after = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
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<PositionStateResponse>();
}
// Assert
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondClient = secondFactory.CreateClient();
var after = await secondClient.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
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);
}
/// <summary>DDL + empty table + dev row matching the test host's <see cref="GamePositionOptions"/> (parity with startup seed).</summary>
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<IOptions<GamePositionOptions>>().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 CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
}
}