139 lines
5.3 KiB
C#
139 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 },
|
|
};
|
|
|
|
// Act
|
|
var before = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
|
|
var post = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
|
|
var postBody = await post.Content.ReadFromJsonAsync<PositionStateResponse>();
|
|
var after = await client.GetFromJsonAsync<PositionStateResponse>("/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<PositionStateResponse>();
|
|
}
|
|
|
|
await using var secondFactory = new PostgresWebApplicationFactory();
|
|
using var secondClient = secondFactory.CreateClient();
|
|
var after = await secondClient.GetFromJsonAsync<PositionStateResponse>("/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);
|
|
}
|
|
|
|
/// <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;", conn))
|
|
{
|
|
await truncate.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
|
|
}
|
|
}
|