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.pull/90/head
parent
00ada50b95
commit
1cbe6302b5
|
|
@ -6,12 +6,11 @@ meta {
|
||||||
|
|
||||||
docs {
|
docs {
|
||||||
NEO-55: per-player inventory read model; join with GET /game/world/item-definitions for stackMax and slot kinds.
|
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).
|
Order-independent: validates fixed slot arrays and slot JSON shape only (not empty-grid assumptions).
|
||||||
Restart server (or truncate DB in Postgres mode) to see an all-empty grid again.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/game/players/dev-local-1/inventory
|
url: {{baseUrl}}/game/players/{{playerId}}/inventory
|
||||||
body: none
|
body: none
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +21,7 @@ tests {
|
||||||
expect(res.getHeader("content-type")).to.contain("application/json");
|
expect(res.getHeader("content-type")).to.contain("application/json");
|
||||||
const body = res.getBody();
|
const body = res.getBody();
|
||||||
expect(body.schemaVersion).to.equal(1);
|
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).to.be.an("array");
|
||||||
expect(body.bagSlots.length).to.equal(24);
|
expect(body.bagSlots.length).to.equal(24);
|
||||||
expect(body.equipmentSlots).to.be.an("array");
|
expect(body.equipmentSlots).to.be.an("array");
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,17 @@ meta {
|
||||||
}
|
}
|
||||||
|
|
||||||
docs {
|
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 {
|
post {
|
||||||
url: {{baseUrl}}/game/players/dev-local-1/inventory
|
url: {{baseUrl}}/game/players/{{playerId}}/inventory
|
||||||
body: json
|
body: json
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +30,7 @@ body:json {
|
||||||
}
|
}
|
||||||
|
|
||||||
tests {
|
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);
|
expect(res.getStatus()).to.equal(200);
|
||||||
const body = res.getBody();
|
const body = res.getBody();
|
||||||
expect(body.schemaVersion).to.equal(1);
|
expect(body.schemaVersion).to.equal(1);
|
||||||
|
|
@ -32,5 +38,10 @@ tests {
|
||||||
expect(body.reasonCode).to.equal("invalid_item");
|
expect(body.reasonCode).to.equal("invalid_item");
|
||||||
expect(body.inventory).to.be.an("object");
|
expect(body.inventory).to.be.an("object");
|
||||||
expect(body.inventory.bagSlots.length).to.equal(24);
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,18 @@ meta {
|
||||||
}
|
}
|
||||||
|
|
||||||
docs {
|
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 {
|
post {
|
||||||
url: {{baseUrl}}/game/players/dev-local-1/inventory
|
url: {{baseUrl}}/game/players/{{playerId}}/inventory
|
||||||
body: json
|
body: json
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
@ -24,18 +31,19 @@ body:json {
|
||||||
}
|
}
|
||||||
|
|
||||||
tests {
|
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);
|
expect(res.getStatus()).to.equal(200);
|
||||||
const body = res.getBody();
|
const body = res.getBody();
|
||||||
expect(body.schemaVersion).to.equal(1);
|
expect(body.schemaVersion).to.equal(1);
|
||||||
expect(body.applied).to.equal(true);
|
expect(body.applied).to.equal(true);
|
||||||
expect(body.reasonCode).to.equal(null);
|
expect(body.reasonCode).to.equal(null);
|
||||||
expect(body.inventory).to.be.an("object");
|
expect(body.inventory).to.be.an("object");
|
||||||
expect(body.inventory.playerId).to.equal("dev-local-1");
|
expect(body.inventory.playerId).to.equal(bru.getEnvVar("playerId") || bru.getVar("playerId"));
|
||||||
const occupied = body.inventory.bagSlots.filter((s) => s.quantity > 0);
|
const before = bru.getVar("scrapBeforeAdd");
|
||||||
expect(occupied.length).to.be.at.least(1);
|
const addQuantity = bru.getVar("addQuantity");
|
||||||
const scrap = occupied.find((s) => s.itemId === "scrap_metal_bulk");
|
const after = [...body.inventory.bagSlots, ...body.inventory.equipmentSlots]
|
||||||
expect(scrap).to.be.an("object");
|
.filter((slot) => slot.itemId === "scrap_metal_bulk")
|
||||||
expect(scrap.quantity).to.be.at.least(5);
|
.reduce((total, slot) => total + slot.quantity, 0);
|
||||||
|
expect(after).to.equal(before + addQuantity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,19 @@ meta {
|
||||||
}
|
}
|
||||||
|
|
||||||
docs {
|
docs {
|
||||||
NEO-55: remove stack via mutationKind remove. Run Post inventory add first on a fresh server,
|
NEO-55: remove stack via mutationKind remove. Pre-request seeds scrap_metal_bulk stock when needed so this request is order-independent.
|
||||||
or expect applied true when dev-local-1 already holds scrap_metal_bulk.
|
}
|
||||||
|
|
||||||
|
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 {
|
post {
|
||||||
url: {{baseUrl}}/game/players/dev-local-1/inventory
|
url: {{baseUrl}}/game/players/{{playerId}}/inventory
|
||||||
body: json
|
body: json
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
@ -25,17 +32,18 @@ body:json {
|
||||||
}
|
}
|
||||||
|
|
||||||
tests {
|
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);
|
expect(res.getStatus()).to.equal(200);
|
||||||
const body = res.getBody();
|
const body = res.getBody();
|
||||||
expect(body.schemaVersion).to.equal(1);
|
expect(body.schemaVersion).to.equal(1);
|
||||||
expect(body.applied).to.equal(true);
|
expect(body.applied).to.equal(true);
|
||||||
expect(body.reasonCode).to.equal(null);
|
expect(body.reasonCode).to.equal(null);
|
||||||
expect(body.inventory).to.be.an("object");
|
expect(body.inventory).to.be.an("object");
|
||||||
const scrap = body.inventory.bagSlots.find((s) => s.itemId === "scrap_metal_bulk");
|
const before = bru.getVar("scrapBeforeRemove");
|
||||||
if (scrap) {
|
const removeQuantity = bru.getVar("removeQuantity");
|
||||||
expect(scrap.quantity).to.be.a("number");
|
const after = [...body.inventory.bagSlots, ...body.inventory.equipmentSlots]
|
||||||
expect(scrap.quantity).to.be.at.least(0);
|
.filter((slot) => slot.itemId === "scrap_metal_bulk")
|
||||||
}
|
.reduce((total, slot) => total + slot.quantity, 0);
|
||||||
|
expect(after).to.equal(before - removeQuantity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,5 @@ meta {
|
||||||
}
|
}
|
||||||
|
|
||||||
docs {
|
docs {
|
||||||
NEO-55 inventory HTTP. GET does not assume an empty grid — dev-local-1 inventory persists across runs.
|
NEO-55 inventory HTTP. Requests are order-independent: pre-request scripts record baselines or seed stock via inventory-api-helper.js.
|
||||||
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.
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -147,7 +147,7 @@ public sealed class PlayerInventoryApiTests
|
||||||
var client = factory.CreateClient();
|
var client = factory.CreateClient();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
_ = await client.PostAsJsonAsync(
|
var addResponse = await client.PostAsJsonAsync(
|
||||||
"/game/players/dev-local-1/inventory",
|
"/game/players/dev-local-1/inventory",
|
||||||
Mutation("add", "scrap_metal_bulk", quantity: 10));
|
Mutation("add", "scrap_metal_bulk", quantity: 10));
|
||||||
var response = await client.PostAsJsonAsync(
|
var response = await client.PostAsJsonAsync(
|
||||||
|
|
@ -155,6 +155,10 @@ public sealed class PlayerInventoryApiTests
|
||||||
Mutation("remove", "scrap_metal_bulk", quantity: 4));
|
Mutation("remove", "scrap_metal_bulk", quantity: 4));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, addResponse.StatusCode);
|
||||||
|
var addBody = await addResponse.Content.ReadFromJsonAsync<PlayerInventoryMutationResponse>();
|
||||||
|
Assert.NotNull(addBody);
|
||||||
|
Assert.True(addBody!.Applied);
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
var body = await response.Content.ReadFromJsonAsync<PlayerInventoryMutationResponse>();
|
var body = await response.Content.ReadFromJsonAsync<PlayerInventoryMutationResponse>();
|
||||||
Assert.NotNull(body);
|
Assert.NotNull(body);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue