Files
pleno-vue/tests/unit/authenticated-request.spec.js
T

242 lines
6.8 KiB
JavaScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from "vitest";
const { axiosMock } = vi.hoisted(() => ({
axiosMock: 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 };
};
describe("authenticatedRequest", () => {
beforeEach(() => {
axiosMock.mockReset();
__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(1);
expect(requestQueueState.batchFailed).toBe(0);
expect(requestQueueState.errorRequests).toHaveLength(0);
});
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);
});
});