Files
pleno-vue/tests/unit/objects-global-select-xss.spec.js
T
Jeppe B c237fce38d Fix null product query parameters
Fixes product detail requests so null optional query params are omitted while final_price=false is preserved.
2026-07-06 20:14:45 +02:00

334 lines
8.5 KiB
JavaScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("sweetalert2", () => ({
default: {
fire: vi.fn(),
showValidationMessage: vi.fn(),
},
}));
vi.mock("@/i18n", () => ({
default: {
global: {
t: (key) => key,
},
},
}));
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
authenticatedRequest: vi.fn(),
unauthenticatedRequest: vi.fn(),
}));
vi.mock("@/components/session/token/SessionUser/Objects/systemUserIds.js", () => ({
getSystemUserIds: () => [],
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
objects: {
global: {
language: {
no_data: "No data",
},
},
products: {
get: {
all: vi.fn(),
},
},
},
},
}));
import Swal from "sweetalert2";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import { Products } from "@/components/session/token/SessionUser/Objects/Products.vue";
beforeEach(() => {
vi.clearAllMocks();
ObjectsGlobal.clearCache();
document.body.innerHTML = "";
});
describe("ObjectsGlobal query parameters", () => {
it("omits nullish object GET parameters while preserving false and zero", async () => {
authenticatedRequest.mockResolvedValueOnce({
data: {
data: {
id: 40,
},
},
});
await expect(
ObjectsGlobal.get.object("/products", {
id: 40,
department_id: null,
customer_id: undefined,
category_id: null,
final_price: false,
page: 0,
})
).resolves.toEqual({
id: 40,
});
expect(authenticatedRequest).toHaveBeenCalledWith("/products?id=40&final_price=false&page=0", "GET");
});
it("omits nullish object list parameters while preserving false", async () => {
authenticatedRequest.mockResolvedValueOnce({
data: {
data: [],
},
});
await expect(
ObjectsGlobal.get.objects("/products", {
department_id: null,
customer_id: undefined,
final_price: false,
})
).resolves.toEqual([]);
expect(authenticatedRequest).toHaveBeenCalledWith("/products", "GET", {
final_price: false,
});
});
});
describe("Products query parameters", () => {
it("omits nullish default pricing parameters for single product GETs", async () => {
authenticatedRequest.mockResolvedValueOnce({
data: {
data: {
id: 40,
},
},
});
await expect(Products.get.single(40)).resolves.toEqual({
id: 40,
});
expect(authenticatedRequest).toHaveBeenCalledWith("/products?id=40&final_price=false", "GET");
});
it("preserves explicit product pricing parameters", async () => {
authenticatedRequest.mockResolvedValueOnce({
data: {
data: {
id: 40,
},
},
});
await expect(
Products.get.single(40, {
department_id: 2,
customer_id: 12345,
category_id: 8,
final_price: true,
})
).resolves.toEqual({
id: 40,
});
expect(authenticatedRequest).toHaveBeenCalledWith(
"/products?id=40&department_id=2&customer_id=12345&category_id=8&final_price=true",
"GET"
);
});
});
describe("ObjectsGlobal select editor escaping", () => {
it("escapes option ids and names before rendering SweetAlert HTML", async () => {
const object = {
columns: {
relay_in_id: {
label: "Relay",
type: "select",
options: vi.fn().mockResolvedValue([
{
id: 'relay-1" autofocus onfocus="alert(1)',
name: '</option></select><iframe src="javascript:parent.localStorage.token"></iframe>',
},
]),
},
},
};
const html = await ObjectsGlobal.generateEditObjectFieldForm(object, "relay_in_id", null);
expect(html).not.toContain("</option></select><iframe");
expect(html).not.toContain("<iframe");
expect(html).not.toContain('value="relay-1" autofocus');
expect(html).toContain("relay-1&quot; autofocus onfocus=&quot;alert(1)");
expect(html).toContain(
"&lt;/option&gt;&lt;/select&gt;&lt;iframe src=&quot;javascript:parent.localStorage.token&quot;&gt;&lt;/iframe&gt;"
);
});
});
describe("ObjectsGlobal create form locked values", () => {
it("renders boolean create fields with an explicit false default selected", async () => {
const html = await ObjectsGlobal.generateCreateObjectForm(
{
wash_subscription: {
label: "Wash subscription",
type: "boolean",
creation: {
required: true,
default: false,
},
},
},
{}
);
document.body.innerHTML = html;
expect(document.querySelector("#wash_subscription")?.value).toBe("false");
});
it("submits required locked values without reading them back from the DOM", async () => {
const add = vi.fn().mockResolvedValue({});
const object = {
meta: {
title: "Vehicles",
labels: {
single: "Vehicle",
},
},
columns: {
type: {
label: "Type",
type: "number",
creation: {
required: true,
},
},
reg: {
label: "Registration",
type: "string",
creation: {
required: true,
},
},
customer_id: {
label: "Customer",
type: "number",
creation: {
required: true,
},
},
},
add,
};
Swal.fire.mockImplementationOnce(async (options) => {
document.body.innerHTML = `
<input id="type" value="5">
<input id="reg" value="AB12345">
`;
await options.preConfirm();
return {};
});
await ObjectsGlobal.showCreateObjectForm(object, null, { customer_id: 12345679 });
expect(add).toHaveBeenCalledWith("5", "AB12345", 12345679);
expect(Swal.showValidationMessage).not.toHaveBeenCalled();
});
it("waits for the create callback before resolving preConfirm", async () => {
const add = vi.fn().mockResolvedValue({});
const onAfterSubmit = vi.fn().mockResolvedValue("reloaded");
const object = {
meta: {
title: "Vehicles",
labels: {
single: "Vehicle",
},
},
columns: {
reg: {
label: "Registration",
type: "string",
creation: {
required: true,
},
},
},
add,
};
Swal.fire.mockImplementationOnce(async (options) => {
document.body.innerHTML = '<input id="reg" value="AB12345">';
return options.preConfirm();
});
await expect(ObjectsGlobal.showCreateObjectForm(object, onAfterSubmit)).resolves.toBe("reloaded");
expect(onAfterSubmit).toHaveBeenCalledOnce();
});
});
describe("ObjectsGlobal edit and delete callbacks", () => {
it("waits for the edit callback before resolving preConfirm", async () => {
const setReg = vi.fn().mockResolvedValue({});
const onAfterSubmit = vi.fn().mockResolvedValue("reloaded");
const object = {
meta: {
title: "Vehicles",
labels: {
single: "Vehicle",
},
},
columns: {
reg: {
label: "Registration",
type: "string",
},
},
set: {
reg: setReg,
},
};
Swal.fire.mockImplementationOnce(async (options) => {
document.body.innerHTML = '<input id="reg" value="CD67890">';
return options.preConfirm();
});
await expect(ObjectsGlobal.showEditObjectFieldForm(object, 10, "reg", "AB12345", onAfterSubmit)).resolves.toBe(
"reloaded"
);
expect(setReg).toHaveBeenCalledWith(10, "CD67890");
expect(onAfterSubmit).toHaveBeenCalledOnce();
});
it("waits for the delete callback before resolving preConfirm", async () => {
const deleteObject = vi.fn().mockResolvedValue({});
const onAfterSubmit = vi.fn().mockResolvedValue("reloaded");
const object = {
meta: {
title: "Vehicles",
labels: {
single: "Vehicle",
},
},
delete: deleteObject,
};
Swal.fire.mockImplementationOnce(async (options) => options.preConfirm());
await expect(ObjectsGlobal.showDeleteObjectForm(object, 10, onAfterSubmit)).resolves.toBe("reloaded");
expect(deleteObject).toHaveBeenCalledWith(10);
expect(onAfterSubmit).toHaveBeenCalledOnce();
});
});