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() { 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 }, }; Assert.Equal(HttpStatusCode.OK, (await client.PostAsJsonAsync("/game/players/dev-local-1/move", move)).StatusCode); var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.Allowed); } [Fact] public async Task PostInteract_ShouldAllow_WhenPlayerInHorizontalRange() { 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 }, }; var moveResp = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); 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() { await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); 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.False(body!.Allowed); Assert.Equal(InteractionApi.ReasonOutOfRange, body.ReasonCode); } [Fact] public async Task PostInteract_ShouldDenyUnknownInteractable_WhenIdNotInRegistry() { await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = "no_such_thing", }; 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.False(body!.Allowed); Assert.Equal(InteractionApi.ReasonUnknownInteractable, body.ReasonCode); } [Fact] public async Task PostInteract_ShouldBeCaseInsensitive_ForInteractableId() { 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 }, }; Assert.Equal(HttpStatusCode.OK, (await client.PostAsJsonAsync("/game/players/dev-local-1/move", move)).StatusCode); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = " PROTOTYPE_TERMINAL ", }; 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); } [Fact] public async Task PostInteract_ShouldReturnBadRequest_WhenSchemaVersionWrong() { await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = 999, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }; var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostInteract_ShouldReturnBadRequest_WhenInteractableIdMissing() { await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var content = new StringContent( $"{{\"schemaVersion\":{InteractionRequest.CurrentSchemaVersion}}}", Encoding.UTF8, "application/json"); var response = await client.PostAsync("/game/players/dev-local-1/interact", content); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostInteract_ShouldReturnBadRequest_WhenInteractableIdWhitespaceOnly() { await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = " \t ", }; var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostInteract_ShouldReturnNotFound_WhenPlayerUnknown() { await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }; var response = await client.PostAsJsonAsync("/game/players/nobody-here/interact", req); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostInteract_ShouldReflectNewPosition_AfterMove() { await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); 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.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() { 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 }, }; Assert.Equal(HttpStatusCode.OK, (await client.PostAsJsonAsync("/game/players/dev-local-1/move", move)).StatusCode); var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.Allowed); Assert.Equal(PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, body.InteractableId); } [Fact] public async Task PostInteract_ShouldDenyOutOfRange_ForResourceNode_WhenPlayerFar() { await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, }); 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); } }