220 lines
6.5 KiB
JavaScript
220 lines
6.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { buildApp } from "../../src/app/build-app.js";
|
|
|
|
test("accepts a protected pricing observation and valuation request", async (testContext) => {
|
|
let csrfChecks = 0;
|
|
const app = buildApp({
|
|
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
|
authorizeUnsafeRequest: async () => {
|
|
csrfChecks += 1;
|
|
},
|
|
pricingService: {
|
|
async addObservation(input) {
|
|
return { id: "observation-1", ...input };
|
|
},
|
|
async calculate() {
|
|
return {
|
|
valuation: {
|
|
id: "valuation-1",
|
|
inventoryItemId: "inventory-1",
|
|
valueMinor: 2500,
|
|
currency: "USD",
|
|
strategy: "MEDIAN",
|
|
confidence: "LOW",
|
|
sourceCount: 1,
|
|
},
|
|
observations: [],
|
|
};
|
|
},
|
|
},
|
|
});
|
|
testContext.after(() => app.close());
|
|
|
|
const observationResponse = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/admin/pricing/observations",
|
|
headers: { "x-csrf-token": "csrf" },
|
|
payload: {
|
|
productId: "d2f67d74-9647-48d5-b3a4-74d3b16ea9f8",
|
|
inventoryItemId: "21eb218b-7a95-4f80-a9e3-e306f7ca1f2e",
|
|
providerId: "manual",
|
|
priceMinor: 2500,
|
|
sourceIdentifier: "source-1",
|
|
conditionId: "ab8150c7-96c1-4b55-8d69-7d0b77d48958",
|
|
},
|
|
});
|
|
const valuationResponse = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/admin/inventory/21eb218b-7a95-4f80-a9e3-e306f7ca1f2e/valuation",
|
|
});
|
|
|
|
assert.equal(observationResponse.statusCode, 201);
|
|
assert.equal(valuationResponse.statusCode, 200);
|
|
assert.equal(valuationResponse.json().data.valueMinor, 2500);
|
|
assert.equal(csrfChecks, 2);
|
|
});
|
|
|
|
test("accepts a CSRF-protected market-value override", async (testContext) => {
|
|
let received;
|
|
const app = buildApp({
|
|
authenticateAdmin: async () => ({ userId: "user-1", sellerId: "seller-1" }),
|
|
authorizeUnsafeRequest: async () => {},
|
|
pricingService: {
|
|
async override(input) {
|
|
received = input;
|
|
return { id: "override-1", ...input };
|
|
},
|
|
},
|
|
});
|
|
testContext.after(() => app.close());
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/admin/inventory/21eb218b-7a95-4f80-a9e3-e306f7ca1f2e/valuation/override",
|
|
headers: { "x-csrf-token": "csrf" },
|
|
payload: { valueMinor: 2600, reason: "Verified recent sale" },
|
|
});
|
|
assert.equal(response.statusCode, 201);
|
|
assert.equal(received.actorUserId, "user-1");
|
|
assert.equal(response.json().data.valueMinor, 2600);
|
|
});
|
|
|
|
test("enqueues a pricing refresh and exposes seller-scoped job status", async (testContext) => {
|
|
const app = buildApp({
|
|
authenticateAdmin: async () => ({ userId: "user-1", sellerId: "seller-1" }),
|
|
authorizeUnsafeRequest: async () => {},
|
|
pricingService: {
|
|
async enqueueRefresh(input) {
|
|
return { id: "job-1", status: "PENDING", createdAt: input.sellerId };
|
|
},
|
|
async getJob(input) {
|
|
assert.equal(input.sellerId, "seller-1");
|
|
return { id: input.jobId, status: "COMPLETED", error: null };
|
|
},
|
|
},
|
|
});
|
|
testContext.after(() => app.close());
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/admin/inventory/inventory-1/pricing/refresh",
|
|
headers: { "x-csrf-token": "csrf" },
|
|
payload: { providerId: "missing" },
|
|
});
|
|
|
|
assert.equal(response.statusCode, 202);
|
|
assert.equal(response.json().data.status, "PENDING");
|
|
|
|
const jobResponse = await app.inject({
|
|
method: "GET",
|
|
url: "/api/v1/admin/jobs/job-1",
|
|
});
|
|
assert.equal(jobResponse.statusCode, 200);
|
|
assert.equal(jobResponse.json().data.status, "COMPLETED");
|
|
});
|
|
|
|
test("returns a conflict when refresh has no active product mapping", async (testContext) => {
|
|
const app = buildApp({
|
|
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
|
authorizeUnsafeRequest: async () => {},
|
|
pricingService: {
|
|
async enqueueRefresh() {
|
|
const error = new Error(
|
|
"No active pricing provider mapping exists for this product.",
|
|
);
|
|
error.code = "PRICING_PROVIDER_MAPPING_NOT_FOUND";
|
|
error.statusCode = 409;
|
|
throw error;
|
|
},
|
|
},
|
|
});
|
|
testContext.after(() => app.close());
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/admin/inventory/item-1/pricing/refresh",
|
|
headers: { "x-csrf-token": "csrf" },
|
|
payload: { productId: "product-1", conditionId: "condition-1" },
|
|
});
|
|
|
|
assert.equal(response.statusCode, 409);
|
|
assert.equal(
|
|
response.json().error.code,
|
|
"PRICING_PROVIDER_MAPPING_NOT_FOUND",
|
|
);
|
|
});
|
|
|
|
test("lists jobs and retries a failed job with CSRF protection", async (testContext) => {
|
|
let csrfChecks = 0;
|
|
const app = buildApp({
|
|
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
|
authorizeUnsafeRequest: async () => {
|
|
csrfChecks += 1;
|
|
},
|
|
pricingService: {
|
|
async listJobs() {
|
|
return [
|
|
{
|
|
id: "job-1",
|
|
status: "FAILED",
|
|
providerId: "fixture",
|
|
inventoryItemId: "item-1",
|
|
},
|
|
];
|
|
},
|
|
async retryJob() {
|
|
return { id: "job-1", status: "PENDING" };
|
|
},
|
|
},
|
|
});
|
|
testContext.after(() => app.close());
|
|
const listResponse = await app.inject({
|
|
method: "GET",
|
|
url: "/api/v1/admin/jobs",
|
|
});
|
|
const retryResponse = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/admin/jobs/job-1/retry",
|
|
headers: { "x-csrf-token": "csrf" },
|
|
});
|
|
assert.equal(listResponse.statusCode, 200);
|
|
assert.equal(retryResponse.statusCode, 202);
|
|
assert.equal(csrfChecks, 1);
|
|
});
|
|
|
|
test("lists seller-scoped latest market valuations", async (testContext) => {
|
|
const app = buildApp({
|
|
authenticateAdmin: async () => ({ sellerId: "seller-1" }),
|
|
pricingService: {
|
|
async listValuations({ sellerId }) {
|
|
assert.equal(sellerId, "seller-1");
|
|
return [
|
|
{
|
|
id: "valuation-1",
|
|
inventoryItemId: "item-1",
|
|
valueMinor: 2500,
|
|
currency: "USD",
|
|
strategy: "MEDIAN",
|
|
confidence: "MEDIUM",
|
|
sourceCount: 2,
|
|
calculatedAt: new Date().toISOString(),
|
|
},
|
|
];
|
|
},
|
|
},
|
|
});
|
|
testContext.after(() => app.close());
|
|
|
|
const response = await app.inject({
|
|
method: "GET",
|
|
url: "/api/v1/admin/valuations",
|
|
});
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
const [valuation] = response.json().data;
|
|
assert.equal(valuation.inventoryItemId, "item-1");
|
|
assert.equal(valuation.valueMinor, 2500);
|
|
assert.equal(valuation.stale, false);
|
|
});
|