using System.Linq; using System.Net; using System.Net.Http.Json; using NeonSprawl.Server.Game.Items; using Xunit; namespace NeonSprawl.Server.Tests.Game.Items; /// Regression tests for GET/POST …/inventory (NEO-55). public sealed class PlayerInventoryApiTests { private static PlayerInventoryMutationRequest Mutation( string mutationKind, string itemId, int quantity, int schemaVersion = PlayerInventoryMutationRequest.CurrentSchemaVersion) => new() { SchemaVersion = schemaVersion, MutationKind = mutationKind, ItemId = itemId, Quantity = quantity, }; [Fact] public async Task GetInventory_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.GetAsync("/game/players/missing-player/inventory"); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task GetInventory_ShouldReturnSchemaV1_WithEmptyFixedSlotArrays() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.GetAsync("/game/players/dev-local-1/inventory"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal(PlayerInventorySnapshotResponse.CurrentSchemaVersion, body!.SchemaVersion); Assert.Equal("dev-local-1", body.PlayerId); Assert.Equal(PlayerInventorySnapshot.BagSlotCount, body.BagSlots.Count); Assert.Single(body.EquipmentSlots); Assert.All(body.BagSlots, static slot => { Assert.Null(slot.ItemId); Assert.Equal(0, slot.Quantity); }); Assert.All(body.EquipmentSlots, static slot => { Assert.Null(slot.ItemId); Assert.Equal(0, slot.Quantity); }); Assert.Equal(Enumerable.Range(0, PlayerInventorySnapshot.BagSlotCount), body.BagSlots.Select(static s => s.SlotIndex)); Assert.Equal(0, body.EquipmentSlots[0].SlotIndex); } [Fact] public async Task PostInventory_ShouldReturnBadRequest_WhenSchemaVersionMismatch() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var request = Mutation("add", "scrap_metal_bulk", quantity: 1, schemaVersion: 99); // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/inventory", request); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostInventory_ShouldReturnBadRequest_WhenMutationKindUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var request = Mutation("consume", "scrap_metal_bulk", quantity: 1); // Act var response = await client.PostAsJsonAsync("/game/players/dev-local-1/inventory", request); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostInventory_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/missing-player/inventory", Mutation("add", "scrap_metal_bulk", quantity: 1)); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostInventory_ShouldApplyAdd_WithUpdatedSnapshot() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/inventory", Mutation("add", "scrap_metal_bulk", quantity: 10)); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.Applied); Assert.Null(body.ReasonCode); Assert.Equal("dev-local-1", body.Inventory.PlayerId); var occupied = body.Inventory.BagSlots.Where(static s => s.Quantity > 0).ToList(); Assert.Single(occupied); Assert.Equal("scrap_metal_bulk", occupied[0].ItemId); Assert.Equal(10, occupied[0].Quantity); } [Fact] public async Task PostInventory_ShouldApplyRemove_AfterAdd_WithUpdatedSnapshot() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var addResponse = await client.PostAsJsonAsync( "/game/players/dev-local-1/inventory", Mutation("add", "scrap_metal_bulk", quantity: 10)); var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/inventory", Mutation("remove", "scrap_metal_bulk", quantity: 4)); // Assert Assert.Equal(HttpStatusCode.OK, addResponse.StatusCode); var addBody = await addResponse.Content.ReadFromJsonAsync(); Assert.NotNull(addBody); Assert.True(addBody!.Applied); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.True(body!.Applied); Assert.Null(body.ReasonCode); var occupied = body.Inventory.BagSlots.Where(static s => s.Quantity > 0).ToList(); Assert.Single(occupied); Assert.Equal("scrap_metal_bulk", occupied[0].ItemId); Assert.Equal(6, occupied[0].Quantity); } [Fact] public async Task PostInventory_ShouldDenyNonPositiveQuantity_WithInvalidQuantity() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/inventory", Mutation("add", "scrap_metal_bulk", quantity: 0)); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Applied); Assert.Equal(PlayerInventoryReasonCodes.InvalidQuantity, body.ReasonCode); Assert.All(body.Inventory.BagSlots, static slot => Assert.Equal(0, slot.Quantity)); } [Fact] public async Task PostInventory_ShouldDenyUnknownItem_WithInvalidItem_AndLeaveInventoryEmpty() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/inventory", Mutation("add", "not-a-prototype-item", quantity: 1)); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Applied); Assert.Equal(PlayerInventoryReasonCodes.InvalidItem, body.ReasonCode); Assert.All(body.Inventory.BagSlots, static slot => Assert.Equal(0, slot.Quantity)); } [Fact] public async Task PostInventory_ShouldDenyRemove_WithInsufficientQuantity() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/inventory", Mutation("remove", "scrap_metal_bulk", quantity: 1)); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Applied); Assert.Equal(PlayerInventoryReasonCodes.InsufficientQuantity, body.ReasonCode); } }