using NeonSprawl.Server.Game.Interaction; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Tests.Game.Interaction; /// Integration tests for (NS-18). NEO-63: covers gather via interact. 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 }, }; var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }); // Assert var body = await response.Content.ReadFromJsonAsync(); 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 }, }; var moveResp = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); 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, moveResp.StatusCode); 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 }, }; var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); var req = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = " PROTOTYPE_TERMINAL ", }; // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", req); // Assert var body = await response.Content.ReadFromJsonAsync(); 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, }); // Assert Assert.Equal(HttpStatusCode.OK, far.StatusCode); var farBody = await far.Content.ReadFromJsonAsync(); Assert.False(farBody!.Allowed); Assert.Equal(InteractionApi.ReasonOutOfRange, farBody.ReasonCode); // Act var move = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 0, Y = 0, Z = 0 }, }; var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); // Assert Assert.Equal(HttpStatusCode.OK, moveResponse.StatusCode); // Act var near = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId, }); // Assert Assert.Equal(HttpStatusCode.OK, near.StatusCode); 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 }, }; var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/interact", new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, }); // Assert var body = await response.Content.ReadFromJsonAsync(); 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); } }