690 lines
22 KiB
JavaScript
690 lines
22 KiB
JavaScript
// @vitest-environment jsdom
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { axiosMock, fetchMock } = vi.hoisted(() => ({
|
|
axiosMock: vi.fn(),
|
|
fetchMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("axios", () => ({
|
|
default: axiosMock,
|
|
}));
|
|
|
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
|
import { __resetReleaseTimelineForTests, configureReleaseRuntime } from "@/services/releaseTimeline.js";
|
|
import {
|
|
__configureRequestQueueForTests,
|
|
__resetRequestQueueForTests,
|
|
requestQueueState,
|
|
} from "@/services/requestQueue.js";
|
|
|
|
const flushMicrotasks = async () => {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
};
|
|
|
|
const flushManyMicrotasks = async (rounds = 10) => {
|
|
for (let index = 0; index < rounds; index += 1) {
|
|
await Promise.resolve();
|
|
}
|
|
};
|
|
|
|
const createDeferred = () => {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
|
|
return { promise, resolve, reject };
|
|
};
|
|
|
|
const createFetchResponse = ({ body = "", headers = {}, status = 200, statusText = "OK" } = {}) => {
|
|
const normalizedHeaders = Object.fromEntries(
|
|
Object.entries(headers).map(([key, value]) => [String(key).toLowerCase(), value])
|
|
);
|
|
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
statusText,
|
|
headers: {
|
|
get: vi.fn((name) => normalizedHeaders[String(name).toLowerCase()] ?? null),
|
|
},
|
|
text: vi.fn(() => Promise.resolve(body)),
|
|
};
|
|
};
|
|
|
|
describe("authenticatedRequest", () => {
|
|
beforeEach(() => {
|
|
axiosMock.mockReset();
|
|
fetchMock.mockReset();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
__resetRequestQueueForTests();
|
|
__resetReleaseTimelineForTests();
|
|
__configureRequestQueueForTests({
|
|
maxConcurrentGet: 1,
|
|
maxConcurrentOther: 1,
|
|
spacingMs: 0,
|
|
retryByStatusCode: {},
|
|
});
|
|
localStorage.clear();
|
|
localStorage.setItem("token", "token");
|
|
});
|
|
|
|
it("resolves scanner lpr no-plate responses without recording a failed request", async () => {
|
|
const response = {
|
|
status: 200,
|
|
data: {
|
|
success: false,
|
|
data: {
|
|
message: "No license plate detected.",
|
|
reason: "no_license_plate_detected",
|
|
},
|
|
meta: [],
|
|
includes: [],
|
|
},
|
|
};
|
|
|
|
axiosMock.mockResolvedValueOnce(response);
|
|
const catchCallable = vi.fn();
|
|
|
|
const result = await authenticatedRequest("/modules/scanner/lpr", "POST", { base64_image: "x" }, catchCallable);
|
|
|
|
expect(result).toBe(response);
|
|
expect(catchCallable).not.toHaveBeenCalled();
|
|
expect(requestQueueState.batchCompleted).toBe(0);
|
|
expect(requestQueueState.batchFailed).toBe(0);
|
|
expect(requestQueueState.recentRequests).toHaveLength(0);
|
|
expect(requestQueueState.requestInsights.scanner).toEqual(
|
|
expect.objectContaining({
|
|
url: expect.stringContaining("/modules/scanner/lpr"),
|
|
success: true,
|
|
statusCode: 200,
|
|
})
|
|
);
|
|
expect(requestQueueState.errorRequests).toHaveLength(0);
|
|
});
|
|
|
|
it("passes scanner lpr FormData through without generic network accounting", async () => {
|
|
const response = {
|
|
status: 200,
|
|
data: {
|
|
success: true,
|
|
data: {
|
|
license_plate_number: "AB12345",
|
|
},
|
|
},
|
|
};
|
|
const payload = new FormData();
|
|
payload.append("image", new Blob(["frame"], { type: "image/jpeg" }), "license-plate.jpg");
|
|
|
|
axiosMock.mockResolvedValueOnce(response);
|
|
|
|
await authenticatedRequest("/modules/scanner/lpr", "POST", payload);
|
|
|
|
const axiosConfig = axiosMock.mock.calls[0]?.[0];
|
|
expect(axiosConfig).toEqual(
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
data: payload,
|
|
__skipRequestQueue: true,
|
|
})
|
|
);
|
|
expect(axiosConfig.headers["Content-Type"]).toBeUndefined();
|
|
expect(requestQueueState.networkTotals.outgoingRequests).toBe(0);
|
|
expect(requestQueueState.networkTotals.outgoingBytes).toBe(0);
|
|
expect(requestQueueState.networkTotals.ingoingResponses).toBe(0);
|
|
expect(requestQueueState.networkTotals.ingoingBytes).toBe(0);
|
|
expect(requestQueueState.recentRequests).toHaveLength(0);
|
|
expect(requestQueueState.requestInsights.scanner?.url).toContain("/modules/scanner/lpr");
|
|
});
|
|
|
|
it("passes scanner lpr raw image content type through without generic network accounting", async () => {
|
|
const response = {
|
|
status: 200,
|
|
data: {
|
|
success: true,
|
|
data: {
|
|
license_plate_number: "AB12345",
|
|
},
|
|
},
|
|
};
|
|
const payload = new Blob(["frame"], { type: "image/jpeg" });
|
|
|
|
axiosMock.mockResolvedValueOnce(response);
|
|
|
|
await authenticatedRequest("/modules/scanner/lpr", "POST", payload, null, null, {
|
|
headers: {
|
|
"Content-Type": "image/jpeg",
|
|
},
|
|
});
|
|
|
|
const axiosConfig = axiosMock.mock.calls[0]?.[0];
|
|
expect(axiosConfig).toEqual(
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
data: payload,
|
|
__skipRequestQueue: true,
|
|
})
|
|
);
|
|
expect(axiosConfig.headers).toEqual(
|
|
expect.objectContaining({
|
|
"Content-Type": "image/jpeg",
|
|
})
|
|
);
|
|
expect(requestQueueState.networkTotals.outgoingRequests).toBe(0);
|
|
expect(requestQueueState.networkTotals.ingoingResponses).toBe(0);
|
|
expect(requestQueueState.recentRequests).toHaveLength(0);
|
|
expect(requestQueueState.requestInsights.scanner?.url).toContain("/modules/scanner/lpr");
|
|
});
|
|
|
|
it("uses fetch transport for scanner raw image requests when requested", async () => {
|
|
const payload = new Blob(["frame"], { type: "image/jpeg" });
|
|
const signal = new AbortController().signal;
|
|
fetchMock.mockResolvedValueOnce(
|
|
createFetchResponse({
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
license_plate_number: "AB12345",
|
|
},
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"server-timing": "lpr_total;dur=17.000",
|
|
},
|
|
})
|
|
);
|
|
|
|
const result = await authenticatedRequest("/modules/scanner/lpr", "POST", payload, null, null, {
|
|
headers: {
|
|
"Content-Type": "image/jpeg",
|
|
},
|
|
signal,
|
|
transport: "fetch",
|
|
});
|
|
|
|
expect(result.data).toEqual({
|
|
success: true,
|
|
data: {
|
|
license_plate_number: "AB12345",
|
|
},
|
|
});
|
|
expect(axiosMock).not.toHaveBeenCalled();
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
expect.stringContaining("/modules/scanner/lpr"),
|
|
expect.objectContaining({
|
|
body: payload,
|
|
method: "POST",
|
|
signal,
|
|
headers: expect.objectContaining({
|
|
Authorization: "Bearer token",
|
|
"Content-Type": "image/jpeg",
|
|
}),
|
|
})
|
|
);
|
|
expect(requestQueueState.networkTotals.outgoingRequests).toBe(0);
|
|
expect(requestQueueState.recentRequests).toHaveLength(0);
|
|
expect(requestQueueState.requestInsights.scanner?.serverTiming).toBe("lpr_total;dur=17.000");
|
|
});
|
|
|
|
it("rejects failed fetch transport responses with an axios-like response", async () => {
|
|
fetchMock.mockResolvedValueOnce(
|
|
createFetchResponse({
|
|
body: JSON.stringify({
|
|
success: false,
|
|
data: {
|
|
message: "Plate Recognizer unavailable.",
|
|
},
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"server-timing": "lpr_total;dur=44.000",
|
|
},
|
|
status: 503,
|
|
statusText: "Service Unavailable",
|
|
})
|
|
);
|
|
const catchCallable = vi.fn();
|
|
|
|
await expect(
|
|
authenticatedRequest("/modules/scanner/lpr", "POST", new Blob(["frame"]), catchCallable, null, {
|
|
headers: {
|
|
"Content-Type": "image/jpeg",
|
|
},
|
|
transport: "fetch",
|
|
})
|
|
).rejects.toMatchObject({
|
|
name: "AxiosError",
|
|
response: {
|
|
status: 503,
|
|
statusText: "Service Unavailable",
|
|
data: {
|
|
success: false,
|
|
data: {
|
|
message: "Plate Recognizer unavailable.",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(catchCallable).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
name: "AxiosError",
|
|
response: expect.objectContaining({
|
|
status: 503,
|
|
}),
|
|
})
|
|
);
|
|
expect(requestQueueState.errorRequests[0].serverTiming).toBe("lpr_total;dur=44.000");
|
|
});
|
|
|
|
it("continues to reject scanner errors", async () => {
|
|
const error = {
|
|
response: {
|
|
status: 500,
|
|
data: {
|
|
success: false,
|
|
data: {
|
|
message: "Internal server error: Different scanner problem.",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
axiosMock.mockRejectedValueOnce(error);
|
|
const catchCallable = vi.fn();
|
|
|
|
await expect(
|
|
authenticatedRequest("/modules/scanner/lpr", "POST", { base64_image: "x" }, catchCallable)
|
|
).rejects.toBe(error);
|
|
|
|
expect(catchCallable).toHaveBeenCalledWith(error);
|
|
});
|
|
|
|
it("does not send bearer credentials to untrusted absolute request URLs", async () => {
|
|
const response = { status: 200, data: { ok: true } };
|
|
axiosMock.mockResolvedValueOnce(response);
|
|
localStorage.setItem("is_subuser", "true");
|
|
localStorage.setItem("selected_customer_number", "1234");
|
|
configureReleaseRuntime({ api_base_url: "https://attacker.example/api" });
|
|
|
|
await authenticatedRequest("https://attacker.example/api/orders", "GET");
|
|
|
|
expect(axiosMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
url: "https://attacker.example/api/orders",
|
|
headers: expect.not.objectContaining({
|
|
Authorization: "Bearer token",
|
|
"X-Customer-Number": 1234,
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("does not expose preview-only active wash reservations as in-progress washes", async () => {
|
|
const response = {
|
|
status: 200,
|
|
data: {
|
|
data: {
|
|
lane_id: 3,
|
|
in_progress: true,
|
|
status: "READY_FOR_MACHINE_START",
|
|
session: {
|
|
id: 379,
|
|
status: "READY_FOR_MACHINE_START",
|
|
machine_relay_enabled: false,
|
|
machine_start_triggered: false,
|
|
wash_started_at: null,
|
|
machine_start_triggered_at: null,
|
|
machine_relay_enabled_at: null,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const thenCallable = vi.fn();
|
|
|
|
axiosMock.mockResolvedValueOnce(response);
|
|
|
|
const result = await authenticatedRequest(
|
|
"/modules/self-serve/lane/wash/my-active-wash",
|
|
"GET",
|
|
undefined,
|
|
null,
|
|
thenCallable
|
|
);
|
|
|
|
expect(result).not.toBe(response);
|
|
expect(result.data.data).toMatchObject({
|
|
lane_id: 3,
|
|
in_progress: false,
|
|
status: "READY_FOR_MACHINE_START",
|
|
});
|
|
expect(result.data.data.session).toBe(response.data.data.session);
|
|
expect(thenCallable).toHaveBeenCalledWith(result);
|
|
});
|
|
|
|
it("keeps relay-enabled active wash responses in progress", async () => {
|
|
const response = {
|
|
status: 200,
|
|
data: {
|
|
data: {
|
|
lane_id: 3,
|
|
in_progress: true,
|
|
status: "MACHINE_RELAY_ENABLED",
|
|
session: {
|
|
id: 380,
|
|
status: "MACHINE_RELAY_ENABLED",
|
|
machine_relay_enabled: true,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
axiosMock.mockResolvedValueOnce(response);
|
|
|
|
const result = await authenticatedRequest("/modules/self-serve/lane/wash/my-active-wash", "GET");
|
|
|
|
expect(result).toBe(response);
|
|
expect(result.data.data.in_progress).toBe(true);
|
|
});
|
|
|
|
it("queues requests and executes them sequentially", async () => {
|
|
const first = createDeferred();
|
|
const second = createDeferred();
|
|
const third = createDeferred();
|
|
const deferredCalls = [first, second, third];
|
|
|
|
axiosMock.mockImplementation(() => deferredCalls.shift()?.promise);
|
|
|
|
const request1 = authenticatedRequest("/one", "GET");
|
|
const request2 = authenticatedRequest("/two", "GET");
|
|
const request3 = authenticatedRequest("/three", "GET");
|
|
|
|
await flushMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(1);
|
|
expect(requestQueueState.active).toBe(1);
|
|
expect(requestQueueState.pending).toBe(2);
|
|
expect(requestQueueState.batchTotal).toBe(3);
|
|
|
|
first.resolve({ status: 200, data: { id: 1 } });
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(2);
|
|
expect(requestQueueState.active).toBe(1);
|
|
expect(requestQueueState.pending).toBe(1);
|
|
|
|
second.resolve({ status: 200, data: { id: 2 } });
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(3);
|
|
expect(requestQueueState.active).toBe(1);
|
|
expect(requestQueueState.pending).toBe(0);
|
|
|
|
third.resolve({ status: 200, data: { id: 3 } });
|
|
await expect(Promise.all([request1, request2, request3])).resolves.toEqual([
|
|
{ status: 200, data: { id: 1 } },
|
|
{ status: 200, data: { id: 2 } },
|
|
{ status: 200, data: { id: 3 } },
|
|
]);
|
|
|
|
await flushMicrotasks();
|
|
|
|
expect(requestQueueState.active).toBe(0);
|
|
expect(requestQueueState.pending).toBe(0);
|
|
expect(requestQueueState.batchCompleted).toBe(3);
|
|
expect(requestQueueState.batchFailed).toBe(0);
|
|
});
|
|
|
|
it("keeps self-serve hardware commands serial without blocking ordinary POST requests", async () => {
|
|
const hardwareOne = createDeferred();
|
|
const hardwareTwo = createDeferred();
|
|
const ordinaryPost = createDeferred();
|
|
axiosMock.mockImplementation(({ url }) => {
|
|
if (url.includes("/modules/self-serve/lane/command")) {
|
|
return hardwareOne.promise;
|
|
}
|
|
if (url.includes("/modules/self-serve/lane/relay/machine/enable")) {
|
|
return hardwareTwo.promise;
|
|
}
|
|
if (url.endsWith("/orders")) {
|
|
return ordinaryPost.promise;
|
|
}
|
|
return Promise.reject(new Error(`Unexpected request URL: ${url}`));
|
|
});
|
|
|
|
const request1 = authenticatedRequest("/modules/self-serve/lane/command", "post", {
|
|
lane_id: 7,
|
|
command: "STOP",
|
|
});
|
|
const request2 = authenticatedRequest("/modules/self-serve/lane/relay/machine/enable", "post", {
|
|
lane_id: 7,
|
|
});
|
|
const request3 = authenticatedRequest("/orders", "post", {
|
|
reference: "ordinary-post",
|
|
});
|
|
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(2);
|
|
expect(axiosMock.mock.calls.map(([config]) => config.url)).toEqual([
|
|
expect.stringContaining("/modules/self-serve/lane/command"),
|
|
expect.stringMatching(/\/orders$/),
|
|
]);
|
|
expect(requestQueueState.active).toBe(2);
|
|
expect(requestQueueState.pending).toBe(1);
|
|
|
|
ordinaryPost.resolve({ status: 200, data: { id: 1 } });
|
|
await flushManyMicrotasks();
|
|
expect(axiosMock).toHaveBeenCalledTimes(2);
|
|
|
|
hardwareOne.resolve({ status: 200, data: { ok: true } });
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(3);
|
|
expect(axiosMock.mock.calls[2][0].url).toContain("/modules/self-serve/lane/relay/machine/enable");
|
|
|
|
hardwareTwo.resolve({ status: 200, data: { ok: true } });
|
|
await expect(Promise.all([request1, request2, request3])).resolves.toHaveLength(3);
|
|
});
|
|
|
|
it("serializes POS scanner requests without blocking Stripe invoices or ordinary POS order mutations", async () => {
|
|
const scannerOne = createDeferred();
|
|
const scannerTwo = createDeferred();
|
|
const stripeInvoice = createDeferred();
|
|
const orderCreate = createDeferred();
|
|
const scannerResponses = [scannerOne, scannerTwo];
|
|
axiosMock.mockImplementation(({ url }) => {
|
|
if (url.includes("/modules/scanner/lpr")) {
|
|
return scannerResponses.shift()?.promise;
|
|
}
|
|
if (url.includes("/modules/stripe/invoice")) {
|
|
return stripeInvoice.promise;
|
|
}
|
|
if (url.endsWith("/orders")) {
|
|
return orderCreate.promise;
|
|
}
|
|
return Promise.reject(new Error(`Unexpected request URL: ${url}`));
|
|
});
|
|
|
|
const request1 = authenticatedRequest("/modules/scanner/lpr", "post", { base64_image: "image-one" });
|
|
const request2 = authenticatedRequest("/modules/scanner/lpr", "post", { base64_image: "image-two" });
|
|
const request3 = authenticatedRequest("/modules/stripe/invoice", "post", { order_id: 42, email: "a@example.test" });
|
|
const request4 = authenticatedRequest("/orders", "post", { department_id: 3, reference: "pos-order" });
|
|
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(3);
|
|
expect(axiosMock.mock.calls.map(([config]) => config.url)).toEqual([
|
|
expect.stringContaining("/modules/scanner/lpr"),
|
|
expect.stringContaining("/modules/stripe/invoice"),
|
|
expect.stringMatching(/\/orders$/),
|
|
]);
|
|
expect(requestQueueState.active).toBe(2);
|
|
expect(requestQueueState.pending).toBe(0);
|
|
expect(requestQueueState.activeRequests.map((request) => request.queueGroup)).toEqual(["POS_STRIPE", null]);
|
|
expect(requestQueueState.activeRequests.some((request) => request.queueGroup === "POS_SCANNER")).toBe(false);
|
|
|
|
scannerOne.resolve({ status: 200, data: { success: true } });
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(4);
|
|
expect(axiosMock.mock.calls[3][0].url).toContain("/modules/scanner/lpr");
|
|
|
|
scannerTwo.resolve({ status: 200, data: { success: true } });
|
|
stripeInvoice.resolve({ status: 200, data: { id: "in_1" } });
|
|
orderCreate.resolve({ status: 200, data: { id: 42 } });
|
|
|
|
await expect(Promise.all([request1, request2, request3, request4])).resolves.toHaveLength(4);
|
|
});
|
|
|
|
it("drops queued scanner LPR requests when their signal aborts before Axios starts", async () => {
|
|
const scannerOne = createDeferred();
|
|
const scannerTwo = createDeferred();
|
|
axiosMock.mockImplementation(({ data }) => {
|
|
if (data?.base64_image === "image-one") {
|
|
return scannerOne.promise;
|
|
}
|
|
if (data?.base64_image === "image-two") {
|
|
return scannerTwo.promise;
|
|
}
|
|
return Promise.reject(new Error("stale scanner frame should not start"));
|
|
});
|
|
|
|
const request1 = authenticatedRequest("/modules/scanner/lpr", "post", { base64_image: "image-one" });
|
|
const request2 = authenticatedRequest("/modules/scanner/lpr", "post", { base64_image: "image-two" });
|
|
const controller = new AbortController();
|
|
const staleRequest = authenticatedRequest(
|
|
"/modules/scanner/lpr",
|
|
"post",
|
|
{ base64_image: "stale-image" },
|
|
null,
|
|
null,
|
|
{ signal: controller.signal }
|
|
);
|
|
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(1);
|
|
expect(requestQueueState.active).toBe(0);
|
|
expect(requestQueueState.pending).toBe(0);
|
|
expect(requestQueueState.batchTotal).toBe(0);
|
|
|
|
controller.abort();
|
|
await expect(staleRequest).rejects.toMatchObject({ name: "AbortError" });
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(1);
|
|
expect(requestQueueState.pending).toBe(0);
|
|
expect(requestQueueState.batchTotal).toBe(0);
|
|
expect(requestQueueState.batchFailed).toBe(0);
|
|
expect(requestQueueState.errorRequests).toHaveLength(0);
|
|
|
|
scannerOne.resolve({ status: 200, data: { success: true } });
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(2);
|
|
expect(requestQueueState.active).toBe(0);
|
|
expect(requestQueueState.pending).toBe(0);
|
|
|
|
scannerTwo.resolve({ status: 200, data: { success: true } });
|
|
|
|
await expect(Promise.all([request1, request2])).resolves.toHaveLength(2);
|
|
await flushManyMicrotasks();
|
|
|
|
expect(requestQueueState.batchCompleted).toBe(0);
|
|
expect(requestQueueState.batchFailed).toBe(0);
|
|
});
|
|
|
|
it("does not record active scanner LPR aborts as failed API requests", async () => {
|
|
axiosMock.mockImplementation(
|
|
({ signal }) =>
|
|
new Promise((resolve, reject) => {
|
|
signal.addEventListener("abort", () => {
|
|
const error = new Error("canceled");
|
|
error.code = "ERR_CANCELED";
|
|
reject(error);
|
|
});
|
|
})
|
|
);
|
|
|
|
const controller = new AbortController();
|
|
const request = authenticatedRequest("/modules/scanner/lpr", "post", { base64_image: "image-one" }, null, null, {
|
|
signal: controller.signal,
|
|
});
|
|
|
|
await flushManyMicrotasks();
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(1);
|
|
expect(requestQueueState.active).toBe(0);
|
|
expect(requestQueueState.batchTotal).toBe(0);
|
|
|
|
controller.abort();
|
|
|
|
await expect(request).rejects.toMatchObject({ code: "ERR_CANCELED" });
|
|
await flushManyMicrotasks();
|
|
|
|
expect(requestQueueState.active).toBe(0);
|
|
expect(requestQueueState.pending).toBe(0);
|
|
expect(requestQueueState.batchTotal).toBe(0);
|
|
expect(requestQueueState.batchFailed).toBe(0);
|
|
expect(requestQueueState.errorRequests).toHaveLength(0);
|
|
});
|
|
|
|
it("does not retry POS Stripe invoice mutations", async () => {
|
|
__configureRequestQueueForTests({
|
|
retryByStatusCode: { 500: 1 },
|
|
retryDelayBaseMs: 0,
|
|
retryDelayMaxMs: 0,
|
|
retryDelayJitterMs: 0,
|
|
});
|
|
axiosMock.mockRejectedValue({
|
|
response: {
|
|
status: 500,
|
|
data: { message: "temporary Stripe failure" },
|
|
},
|
|
message: "Request failed",
|
|
});
|
|
|
|
await expect(
|
|
authenticatedRequest("/modules/stripe/invoice", "post", {
|
|
order_id: 42,
|
|
email: "a@example.test",
|
|
})
|
|
).rejects.toMatchObject({ response: { status: 500 } });
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(1);
|
|
expect(requestQueueState.batchFailed).toBe(1);
|
|
});
|
|
|
|
it("does not retry non-idempotent self-serve hardware mutations", async () => {
|
|
__configureRequestQueueForTests({
|
|
retryByStatusCode: { 500: 1 },
|
|
retryDelayBaseMs: 0,
|
|
retryDelayMaxMs: 0,
|
|
retryDelayJitterMs: 0,
|
|
});
|
|
axiosMock.mockRejectedValue({
|
|
response: {
|
|
status: 500,
|
|
data: { message: "temporary backend failure" },
|
|
},
|
|
message: "Request failed",
|
|
});
|
|
|
|
await expect(
|
|
authenticatedRequest("/modules/self-serve/lane/command", "post", {
|
|
lane_id: 7,
|
|
command: "STOP",
|
|
})
|
|
).rejects.toMatchObject({ response: { status: 500 } });
|
|
|
|
expect(axiosMock).toHaveBeenCalledTimes(1);
|
|
expect(requestQueueState.batchFailed).toBe(1);
|
|
});
|
|
});
|