using System.Net; using System.Net.Http.Json; using System.Text; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.PositionState; using Xunit; namespace NeonSprawl.Server.Tests.Game.PositionState; /// /// NS-16 / NS-19: integration tests for POST /game/players/{{id}}/move on . /// Isolated host per test (fresh in-memory store). /// public class MoveCommandApiTests { [Fact] public async Task PostMove_ShouldPersistTargetAndIncrementSequence_WhenDevPlayerPostsValidMove() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var cmd = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 1.5, Y = 0.0, Z = 2.25 }, }; var beforeMove = await client.GetFromJsonAsync("/game/players/dev-local-1/position"); // Act var postResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd); // Assert Assert.NotNull(beforeMove); var seq0 = beforeMove!.Sequence; Assert.Equal(HttpStatusCode.OK, postResponse.StatusCode); var postBody = await postResponse.Content.ReadFromJsonAsync(); var afterMove = await client.GetFromJsonAsync("/game/players/dev-local-1/position"); Assert.NotNull(postBody); Assert.Equal(PositionStateResponse.CurrentSchemaVersion, postBody!.SchemaVersion); Assert.Equal("dev-local-1", postBody.PlayerId); Assert.Equal(seq0 + 1, postBody.Sequence); Assert.Equal(1.5, postBody.Position.X); Assert.Equal(0.0, postBody.Position.Y); Assert.Equal(2.25, postBody.Position.Z); Assert.NotNull(afterMove); Assert.Equal(postBody.Position.X, afterMove!.Position.X); Assert.Equal(postBody.Position.Y, afterMove.Position.Y); Assert.Equal(postBody.Position.Z, afterMove.Position.Z); Assert.Equal(postBody.Sequence, afterMove.Sequence); } [Fact] public async Task PostMove_ShouldReturnNotFound_WhenPlayerIsUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); 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/unknown-player/move", cmd); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostMove_ShouldReturnBadRequest_WhenSchemaVersionIsWrong() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var cmd = new MoveCommandRequest { SchemaVersion = 999, Target = new PositionVector { X = 0, Y = 0, Z = 0 }, }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostMove_ShouldReturnBadRequest_WhenTargetIsMissing() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var content = new StringContent( "{\"schemaVersion\":1}", Encoding.UTF8, "application/json"); // Act var response = await client.PostAsync("/game/players/dev-local-1/move", content); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostMove_ShouldReturn400WithReason_WhenHorizontalStepTooLarge() { // Arrange await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(b => { b.ConfigureTestServices(services => { services.PostConfigure(o => { o.MovementValidation.HorizontalStepEnabled = true; o.MovementValidation.MaxHorizontalStep = 1.0; }); }); }); var client = factory.CreateClient(); var cmd = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 0, Y = 0.9, Z = 0 }, }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal(MoveCommandRejectedResponse.CurrentSchemaVersion, body!.SchemaVersion); Assert.Equal(MoveCommandReasonCodes.HorizontalStepExceeded, body.ReasonCode); } [Fact] public async Task PostMove_ShouldReturn400WithReason_WhenVerticalStepTooLarge() { // Arrange await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(b => { b.ConfigureTestServices(services => { services.PostConfigure(o => o.MovementValidation.MaxVerticalStep = 0.5); }); }); var client = factory.CreateClient(); var cmd = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = -5, Y = 2.0, Z = -5 }, }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal(MoveCommandReasonCodes.VerticalStepExceeded, body!.ReasonCode); } [Fact] public async Task PostMove_ShouldReturn400WithoutReasonBody_WhenSchemaInvalid() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsync( "/game/players/dev-local-1/move", new StringContent("{\"schemaVersion\":999,\"target\":{\"x\":0,\"y\":0,\"z\":0}}", Encoding.UTF8, "application/json")); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var text = await response.Content.ReadAsStringAsync(); Assert.DoesNotContain("reasonCode", text, StringComparison.OrdinalIgnoreCase); } }