From 1cbe6302b5d84cbde38e0d3e9ca622ece85b38c7 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sat, 23 May 2026 22:57:53 -0400 Subject: [PATCH] NEO-55: make inventory Bruno tests order-independent Shared inventory-api-helper seeds stock and records baselines in pre-request scripts; assertions use exact deltas instead of cumulative at.least checks. Harden C# remove round-trip add assertion. --- .../inventory/Get inventory.bru | 7 +- .../Post inventory add deny invalid item.bru | 17 ++++- .../inventory/Post inventory add.bru | 26 +++++--- .../inventory/Post inventory remove.bru | 26 +++++--- bruno/neon-sprawl-server/inventory/folder.bru | 4 +- .../inventory/inventory-api-helper.js | 65 +++++++++++++++++++ .../Game/Items/PlayerInventoryApiTests.cs | 6 +- 7 files changed, 122 insertions(+), 29 deletions(-) create mode 100644 bruno/neon-sprawl-server/inventory/inventory-api-helper.js diff --git a/bruno/neon-sprawl-server/inventory/Get inventory.bru b/bruno/neon-sprawl-server/inventory/Get inventory.bru index 93730cd..4919224 100644 --- a/bruno/neon-sprawl-server/inventory/Get inventory.bru +++ b/bruno/neon-sprawl-server/inventory/Get inventory.bru @@ -6,12 +6,11 @@ meta { docs { NEO-55: per-player inventory read model; join with GET /game/world/item-definitions for stackMax and slot kinds. - Does not assume a fresh server — dev-local-1 inventory persists across Bruno runs (in-memory/Postgres). - Restart server (or truncate DB in Postgres mode) to see an all-empty grid again. + Order-independent: validates fixed slot arrays and slot JSON shape only (not empty-grid assumptions). } get { - url: {{baseUrl}}/game/players/dev-local-1/inventory + url: {{baseUrl}}/game/players/{{playerId}}/inventory body: none auth: none } @@ -22,7 +21,7 @@ tests { expect(res.getHeader("content-type")).to.contain("application/json"); const body = res.getBody(); expect(body.schemaVersion).to.equal(1); - expect(body.playerId).to.equal("dev-local-1"); + expect(body.playerId).to.equal(bru.getEnvVar("playerId") || bru.getVar("playerId")); expect(body.bagSlots).to.be.an("array"); expect(body.bagSlots.length).to.equal(24); expect(body.equipmentSlots).to.be.an("array"); diff --git a/bruno/neon-sprawl-server/inventory/Post inventory add deny invalid item.bru b/bruno/neon-sprawl-server/inventory/Post inventory add deny invalid item.bru index 58961dd..78875b3 100644 --- a/bruno/neon-sprawl-server/inventory/Post inventory add deny invalid item.bru +++ b/bruno/neon-sprawl-server/inventory/Post inventory add deny invalid item.bru @@ -5,11 +5,17 @@ meta { } docs { - NEO-55: unknown itemId denies with invalid_item and echoes unchanged inventory. + NEO-55: unknown itemId denies with invalid_item. Pre-request snapshots scrap_metal_bulk total to prove inventory unchanged on deny. +} + +script:pre-request { + const { getInventory, sumItemQuantity } = require("./inventory-api-helper.js"); + const inventory = await getInventory(bru); + bru.setVar("scrapBeforeDeny", sumItemQuantity(inventory, "scrap_metal_bulk")); } post { - url: {{baseUrl}}/game/players/dev-local-1/inventory + url: {{baseUrl}}/game/players/{{playerId}}/inventory body: json auth: none } @@ -24,7 +30,7 @@ body:json { } tests { - test("denies with invalid_item and echoes inventory", function () { + test("denies with invalid_item and leaves scrap_metal_bulk unchanged", function () { expect(res.getStatus()).to.equal(200); const body = res.getBody(); expect(body.schemaVersion).to.equal(1); @@ -32,5 +38,10 @@ tests { expect(body.reasonCode).to.equal("invalid_item"); expect(body.inventory).to.be.an("object"); expect(body.inventory.bagSlots.length).to.equal(24); + const before = bru.getVar("scrapBeforeDeny"); + const after = [...body.inventory.bagSlots, ...body.inventory.equipmentSlots] + .filter((slot) => slot.itemId === "scrap_metal_bulk") + .reduce((total, slot) => total + slot.quantity, 0); + expect(after).to.equal(before); }); } diff --git a/bruno/neon-sprawl-server/inventory/Post inventory add.bru b/bruno/neon-sprawl-server/inventory/Post inventory add.bru index 1be951e..8c64e5c 100644 --- a/bruno/neon-sprawl-server/inventory/Post inventory add.bru +++ b/bruno/neon-sprawl-server/inventory/Post inventory add.bru @@ -5,11 +5,18 @@ meta { } docs { - NEO-55: add stack via mutationKind add; see server README inventory HTTP section. + NEO-55: add stack via mutationKind add. Pre-request records scrap_metal_bulk baseline so assertions are order-independent. +} + +script:pre-request { + const { getInventory, sumItemQuantity } = require("./inventory-api-helper.js"); + const inventory = await getInventory(bru); + bru.setVar("scrapBeforeAdd", sumItemQuantity(inventory, "scrap_metal_bulk")); + bru.setVar("addQuantity", 5); } post { - url: {{baseUrl}}/game/players/dev-local-1/inventory + url: {{baseUrl}}/game/players/{{playerId}}/inventory body: json auth: none } @@ -24,18 +31,19 @@ body:json { } tests { - test("add returns 200 with applied true and updated inventory", function () { + test("add returns 200 with applied true and increases scrap by requested quantity", function () { expect(res.getStatus()).to.equal(200); const body = res.getBody(); expect(body.schemaVersion).to.equal(1); expect(body.applied).to.equal(true); expect(body.reasonCode).to.equal(null); expect(body.inventory).to.be.an("object"); - expect(body.inventory.playerId).to.equal("dev-local-1"); - const occupied = body.inventory.bagSlots.filter((s) => s.quantity > 0); - expect(occupied.length).to.be.at.least(1); - const scrap = occupied.find((s) => s.itemId === "scrap_metal_bulk"); - expect(scrap).to.be.an("object"); - expect(scrap.quantity).to.be.at.least(5); + expect(body.inventory.playerId).to.equal(bru.getEnvVar("playerId") || bru.getVar("playerId")); + const before = bru.getVar("scrapBeforeAdd"); + const addQuantity = bru.getVar("addQuantity"); + const after = [...body.inventory.bagSlots, ...body.inventory.equipmentSlots] + .filter((slot) => slot.itemId === "scrap_metal_bulk") + .reduce((total, slot) => total + slot.quantity, 0); + expect(after).to.equal(before + addQuantity); }); } diff --git a/bruno/neon-sprawl-server/inventory/Post inventory remove.bru b/bruno/neon-sprawl-server/inventory/Post inventory remove.bru index 1e3fbd7..67e455f 100644 --- a/bruno/neon-sprawl-server/inventory/Post inventory remove.bru +++ b/bruno/neon-sprawl-server/inventory/Post inventory remove.bru @@ -5,12 +5,19 @@ meta { } docs { - NEO-55: remove stack via mutationKind remove. Run Post inventory add first on a fresh server, - or expect applied true when dev-local-1 already holds scrap_metal_bulk. + NEO-55: remove stack via mutationKind remove. Pre-request seeds scrap_metal_bulk stock when needed so this request is order-independent. +} + +script:pre-request { + const { ensureItemQuantity } = require("./inventory-api-helper.js"); + const removeQuantity = 2; + const before = await ensureItemQuantity(bru, "scrap_metal_bulk", removeQuantity); + bru.setVar("scrapBeforeRemove", before); + bru.setVar("removeQuantity", removeQuantity); } post { - url: {{baseUrl}}/game/players/dev-local-1/inventory + url: {{baseUrl}}/game/players/{{playerId}}/inventory body: json auth: none } @@ -25,17 +32,18 @@ body:json { } tests { - test("remove returns 200 with applied true and reduced quantity", function () { + test("remove returns 200 with applied true and decreases scrap by requested quantity", function () { expect(res.getStatus()).to.equal(200); const body = res.getBody(); expect(body.schemaVersion).to.equal(1); expect(body.applied).to.equal(true); expect(body.reasonCode).to.equal(null); expect(body.inventory).to.be.an("object"); - const scrap = body.inventory.bagSlots.find((s) => s.itemId === "scrap_metal_bulk"); - if (scrap) { - expect(scrap.quantity).to.be.a("number"); - expect(scrap.quantity).to.be.at.least(0); - } + const before = bru.getVar("scrapBeforeRemove"); + const removeQuantity = bru.getVar("removeQuantity"); + const after = [...body.inventory.bagSlots, ...body.inventory.equipmentSlots] + .filter((slot) => slot.itemId === "scrap_metal_bulk") + .reduce((total, slot) => total + slot.quantity, 0); + expect(after).to.equal(before - removeQuantity); }); } diff --git a/bruno/neon-sprawl-server/inventory/folder.bru b/bruno/neon-sprawl-server/inventory/folder.bru index 508c5b3..6f9a880 100644 --- a/bruno/neon-sprawl-server/inventory/folder.bru +++ b/bruno/neon-sprawl-server/inventory/folder.bru @@ -3,7 +3,5 @@ meta { } docs { - NEO-55 inventory HTTP. GET does not assume an empty grid — dev-local-1 inventory persists across runs. - POST add/remove mutate dev-local-1; cumulative stacks from prior runs make add assertions use at.least(5). - Restart server (or truncate DB in Postgres mode) for a clean inventory baseline. + NEO-55 inventory HTTP. Requests are order-independent: pre-request scripts record baselines or seed stock via inventory-api-helper.js. } diff --git a/bruno/neon-sprawl-server/inventory/inventory-api-helper.js b/bruno/neon-sprawl-server/inventory/inventory-api-helper.js new file mode 100644 index 0000000..ecf4dec --- /dev/null +++ b/bruno/neon-sprawl-server/inventory/inventory-api-helper.js @@ -0,0 +1,65 @@ +const axios = require("axios"); + +function resolveConfig(bru) { + return { + baseUrl: bru.getEnvVar("baseUrl") || bru.getVar("baseUrl"), + playerId: bru.getEnvVar("playerId") || bru.getVar("playerId"), + jsonHeaders: { headers: { "Content-Type": "application/json" } }, + }; +} + +function inventoryUrl(baseUrl, playerId) { + return `${baseUrl}/game/players/${playerId}/inventory`; +} + +function sumItemQuantity(inventory, itemId) { + const slots = [...inventory.bagSlots, ...inventory.equipmentSlots]; + return slots + .filter((slot) => slot.itemId === itemId) + .reduce((total, slot) => total + slot.quantity, 0); +} + +async function getInventory(bru) { + const { baseUrl, playerId } = resolveConfig(bru); + const response = await axios.get(inventoryUrl(baseUrl, playerId)); + if (response.status !== 200) { + throw new Error(`GET inventory failed: ${response.status}`); + } + + return response.data; +} + +async function postMutation(bru, body) { + const { baseUrl, playerId, jsonHeaders } = resolveConfig(bru); + return axios.post(inventoryUrl(baseUrl, playerId), body, jsonHeaders); +} + +async function ensureItemQuantity(bru, itemId, minQuantity) { + const inventory = await getInventory(bru); + const current = sumItemQuantity(inventory, itemId); + if (current >= minQuantity) { + return current; + } + + const response = await postMutation(bru, { + schemaVersion: 1, + mutationKind: "add", + itemId, + quantity: minQuantity - current, + }); + if (response.status !== 200 || response.data?.applied !== true) { + throw new Error( + `seed add ${itemId} failed: ${response.status} ${JSON.stringify(response.data)}`, + ); + } + + const after = await getInventory(bru); + return sumItemQuantity(after, itemId); +} + +module.exports = { + sumItemQuantity, + getInventory, + postMutation, + ensureItemQuantity, +}; diff --git a/server/NeonSprawl.Server.Tests/Game/Items/PlayerInventoryApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Items/PlayerInventoryApiTests.cs index 5d20d3f..814eb60 100644 --- a/server/NeonSprawl.Server.Tests/Game/Items/PlayerInventoryApiTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/Items/PlayerInventoryApiTests.cs @@ -147,7 +147,7 @@ public sealed class PlayerInventoryApiTests var client = factory.CreateClient(); // Act - _ = await client.PostAsJsonAsync( + var addResponse = await client.PostAsJsonAsync( "/game/players/dev-local-1/inventory", Mutation("add", "scrap_metal_bulk", quantity: 10)); var response = await client.PostAsJsonAsync( @@ -155,6 +155,10 @@ public sealed class PlayerInventoryApiTests 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);