using System.Net; using System.Net.Http.Json; using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.Crafting; using NeonSprawl.Server.Game.Items; using NeonSprawl.Server.Game.Skills; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.Crafting; /// Regression tests for POST …/craft (NEO-70). public sealed class PlayerCraftApiTests { private const string PlayerId = "dev-local-1"; private static CraftRequest Request( string recipeId, int? quantity = null, int schemaVersion = CraftRequest.CurrentSchemaVersion) => new() { SchemaVersion = schemaVersion, RecipeId = recipeId, Quantity = quantity, }; [Fact] public async Task PostCraft_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( "/game/players/missing-player/craft", Request("refine_scrap_standard")); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task PostCraft_ShouldReturnBadRequest_WhenSchemaVersionMismatch() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( $"/game/players/{PlayerId}/craft", Request("refine_scrap_standard", schemaVersion: 99)); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostCraft_ShouldReturnBadRequest_WhenRecipeIdMissing() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( $"/game/players/{PlayerId}/craft", new CraftRequest { SchemaVersion = CraftRequest.CurrentSchemaVersion, RecipeId = " " }); // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task PostCraft_RefineScrapStandard_ShouldReturnSuccessEnvelope() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); SeedStack(factory, "scrap_metal_bulk", 5); // Act var response = await client.PostAsJsonAsync( $"/game/players/{PlayerId}/craft", Request("refine_scrap_standard")); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.Equal(CraftResponse.CurrentSchemaVersion, body!.SchemaVersion); Assert.True(body.Success); Assert.Null(body.ReasonCode); Assert.Single(body.InputsConsumed); Assert.Equal("scrap_metal_bulk", body.InputsConsumed[0].ItemId); Assert.Equal(5, body.InputsConsumed[0].Quantity); Assert.Single(body.OutputsGranted); Assert.Equal("refined_plate_stock", body.OutputsGranted[0].ItemId); Assert.Equal(1, body.OutputsGranted[0].Quantity); Assert.NotNull(body.XpGrantSummary); Assert.Equal("refine", body.XpGrantSummary!.SkillId); Assert.Equal(RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion, body.XpGrantSummary.Amount); Assert.Equal(RefineSkillXpConstants.ActivitySourceKind, body.XpGrantSummary.SourceKind); } [Fact] public async Task PostCraft_WhenMaterialsMissing_ShouldDenyWithInsufficientMaterials() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var beforeInventory = await client.GetFromJsonAsync( $"/game/players/{PlayerId}/inventory"); // Act var response = await client.PostAsJsonAsync( $"/game/players/{PlayerId}/craft", Request("refine_scrap_standard")); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Success); Assert.Equal(CraftReasonCodes.InsufficientMaterials, body.ReasonCode); Assert.Empty(body.InputsConsumed); Assert.Empty(body.OutputsGranted); Assert.Null(body.XpGrantSummary); var afterInventory = await client.GetFromJsonAsync( $"/game/players/{PlayerId}/inventory"); Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!)); } [Fact] public async Task PostCraft_ForUnknownRecipe_ShouldDenyWithUnknownRecipe() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.PostAsJsonAsync( $"/game/players/{PlayerId}/craft", Request("not_a_real_recipe")); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Success); Assert.Equal(CraftReasonCodes.UnknownRecipe, body.ReasonCode); Assert.Empty(body.InputsConsumed); Assert.Empty(body.OutputsGranted); Assert.Null(body.XpGrantSummary); } [Fact] public async Task PostCraft_WhenQuantityInvalid_ShouldDenyWithInvalidQuantity() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); SeedStack(factory, "scrap_metal_bulk", 5); // Act var response = await client.PostAsJsonAsync( $"/game/players/{PlayerId}/craft", Request("refine_scrap_standard", quantity: 0)); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Success); Assert.Equal(CraftReasonCodes.InvalidQuantity, body.ReasonCode); Assert.Equal(5, CountBagItem(factory, "scrap_metal_bulk")); } [Fact] public async Task PostCraft_MakePrototypeArmor_WhenEquipmentOccupied_ShouldDenyWithInventoryFull() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); SeedStack(factory, "refined_plate_stock", 8); SeedStack(factory, "scrap_metal_bulk", 5); SeedStack(factory, "prototype_armor_shell", 1); var beforeInventory = await client.GetFromJsonAsync( $"/game/players/{PlayerId}/inventory"); // Act var response = await client.PostAsJsonAsync( $"/game/players/{PlayerId}/craft", Request("make_prototype_armor")); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.NotNull(body); Assert.False(body!.Success); Assert.Equal(CraftReasonCodes.InventoryFull, body.ReasonCode); var afterInventory = await client.GetFromJsonAsync( $"/game/players/{PlayerId}/inventory"); Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!)); } private static void SeedStack(InMemoryWebApplicationFactory factory, string itemId, int quantity) { var itemRegistry = factory.Services.GetRequiredService(); var inventoryStore = factory.Services.GetRequiredService(); var outcome = PlayerInventoryOperations.TryAddStack( PlayerId, itemId, quantity, itemRegistry, inventoryStore); Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind); } private static int CountBagItem(InMemoryWebApplicationFactory factory, string itemId) { var inventoryStore = factory.Services.GetRequiredService(); inventoryStore.TryGetSnapshot(PlayerId, out var snapshot); var total = 0; foreach (var slot in snapshot!.BagSlots) { if (string.Equals(slot.ItemId, itemId, StringComparison.Ordinal)) { total += slot.Quantity; } } return total; } private static int TotalBagQuantity(PlayerInventorySnapshotResponse snapshot) { var total = 0; foreach (var slot in snapshot.BagSlots) { total += slot.Quantity; } return total; } }