249 lines
9.0 KiB
C#
249 lines
9.0 KiB
C#
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;
|
|
|
|
/// <summary>Regression tests for <c>POST …/craft</c> (NEO-70).</summary>
|
|
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<CraftResponse>();
|
|
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<PlayerInventorySnapshotResponse>(
|
|
$"/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<CraftResponse>();
|
|
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<PlayerInventorySnapshotResponse>(
|
|
$"/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<CraftResponse>();
|
|
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<CraftResponse>();
|
|
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<PlayerInventorySnapshotResponse>(
|
|
$"/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<CraftResponse>();
|
|
Assert.NotNull(body);
|
|
Assert.False(body!.Success);
|
|
Assert.Equal(CraftReasonCodes.InventoryFull, body.ReasonCode);
|
|
var afterInventory = await client.GetFromJsonAsync<PlayerInventorySnapshotResponse>(
|
|
$"/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<IItemDefinitionRegistry>();
|
|
var inventoryStore = factory.Services.GetRequiredService<IPlayerInventoryStore>();
|
|
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<IPlayerInventoryStore>();
|
|
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;
|
|
}
|
|
}
|