using System.Net; using System.Net.Http.Json; using System.Text; using NeonSprawl.Server.Game.Interaction; using NeonSprawl.Server.Game.PositionState; using Xunit; namespace NeonSprawl.Server.Tests.Game.Interaction; /// Integration tests for (NS-18). public class InteractionApiTests { [Fact] public async Task PostInteract_ShouldAllow_WhenExactlyAtHorizontalRadius() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var move = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 3, Y = 0, Z = 0 }, }; // Act var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, moveResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.True(body!.Allowed); } [Fact] public async Task PostInteract_ShouldAllow_WhenPlayerInHorizontalRange() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var move = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 0, Y = 0.9, Z = 0 }, }; // Act var moveResp = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); // Assert Assert.Equal(HttpStatusCode.OK, moveResp.StatusCode); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }; var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.Allowed); Assert.Null(body.ReasonCode); Assert.Equal(PrototypeInteractableRegistry.PrototypeTerminalId, body.InteractableId); Assert.NotNull(body.Payload); Assert.Equal("placeholder", body.Payload!.Kind); } [Fact] public async Task PostInteract_ShouldDenyOutOfRange_WhenPlayerSeededFarFromTerminal() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Allowed); Assert.Equal(InteractionApi.ReasonOutOfRange, body.ReasonCode); } [Fact] public async Task PostInteract_ShouldDenyUnknownInteractable_WhenIdNotInRegistry() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = "no_such_thing", }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Allowed); Assert.Equal(InteractionApi.ReasonUnknownInteractable, body.ReasonCode); } [Fact] public async Task PostInteract_ShouldBeCaseInsensitive_ForInteractableId() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var move = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 0, Y = 0, Z = 0 }, }; // Act var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = " PROTOTYPE_TERMINAL ", }; var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, moveResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.True(body!.Allowed); } [Fact] public async Task PostInteract_ShouldReturnBadRequest_WhenSchemaVersionWrong() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = 999, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostInteract_ShouldReturnBadRequest_WhenInteractableIdMissing() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var content = new StringContent( $"{{\"schemaVersion\":{InteractionRequest.CurrentSchemaVersion}}}", Encoding.UTF8, "application/json"); // Act var response = await client.PostAsync("/game/players/dev-local-1/interact", content); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostInteract_ShouldReturnBadRequest_WhenInteractableIdWhitespaceOnly() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = " \t ", }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostInteract_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }; // Act var response = await client.PostAsJsonAsync("/game/players/nobody-here/interact", req); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostInteract_ShouldReflectNewPosition_AfterMove() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var far = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }); var farBody = await far.Content.ReadFromJsonAsync(); // Assert Assert.False(farBody!.Allowed); Assert.Equal(InteractionApi.ReasonOutOfRange, farBody.ReasonCode); var move = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 0, Y = 0, Z = 0 }, }; Assert.Equal(HttpStatusCode.OK, (await client.PostAsJsonAsync("/game/players/dev-local-1/move", move)).StatusCode); var near = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }); var nearBody = await near.Content.ReadFromJsonAsync(); Assert.True(nearBody!.Allowed); } [Fact] public async Task PostInteract_ShouldAllowResourceNode_WhenPlayerInRangeOfResourceAnchor() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var move = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 12, Y = 0.9, Z = -6 }, }; // Act var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, }); var body = await response.Content.ReadFromJsonAsync(); // Assert Assert.Equal(HttpStatusCode.OK, moveResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.True(body!.Allowed); Assert.Equal(PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, body.InteractableId); } [Fact] public async Task PostInteract_ShouldDenyOutOfRange_ForResourceNode_WhenPlayerFar() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, }); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Allowed); Assert.Equal(InteractionApi.ReasonOutOfRange, body.ReasonCode); } }