mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-06 04:03:50 +08:00
135 lines
5.1 KiB
JavaScript
135 lines
5.1 KiB
JavaScript
const assert = require("node:assert/strict");
|
|
const test = require("node:test");
|
|
|
|
// Use the existing TypeScript dev dependency to exercise the real mock routes,
|
|
// including their extensionless imports, without adding a test runtime.
|
|
require("ts-node").register({
|
|
skipProject: true,
|
|
transpileOnly: true,
|
|
compilerOptions: { target: "ES2022", module: "CommonJS", moduleResolution: "Node" },
|
|
});
|
|
const data = require("./data.ts");
|
|
const finding = (id, task, severity, day, status = "pending") => ({
|
|
id,
|
|
task_id: task,
|
|
severity,
|
|
ts: `2026-01-${String(day).padStart(2, "0")}T12:00:00Z`,
|
|
status,
|
|
name: "needle",
|
|
vulnclass: "Demo",
|
|
summary: "Synthetic sorting fixture",
|
|
evidence: "",
|
|
assets: [],
|
|
});
|
|
const fixtures = [
|
|
finding("1", "2", "low", 2),
|
|
finding("2", "2", "high", 4),
|
|
finding("10", "10", "high", 4),
|
|
finding("11", "10", "critical", 1),
|
|
finding("12", "20", "high", 4),
|
|
finding("13", undefined, "high", 4),
|
|
finding("14", "30", "low", 5),
|
|
finding("15", "40", "critical", 1),
|
|
finding("16", "10", "high", 3, "resolved"),
|
|
];
|
|
// handler.ts snapshots fixtures on import. Restore exported demo data immediately;
|
|
// node:test runs this test file in its own process, separate from the application.
|
|
const original = data.findings.slice();
|
|
let mockHandle;
|
|
try {
|
|
data.findings.splice(0, data.findings.length, ...fixtures);
|
|
({ mockHandle } = require("./handler.ts"));
|
|
} finally {
|
|
data.findings.splice(0, data.findings.length, ...original);
|
|
}
|
|
|
|
const rowOrders = {
|
|
time: {
|
|
asc: ["15", "11", "1", "16", "13", "12", "10", "2", "14"],
|
|
desc: ["14", "13", "12", "10", "2", "16", "1", "15", "11"],
|
|
},
|
|
severity: {
|
|
asc: ["15", "11", "16", "13", "12", "10", "2", "1", "14"],
|
|
desc: ["15", "11", "13", "12", "10", "2", "16", "14", "1"],
|
|
},
|
|
};
|
|
const groupOrders = {
|
|
time: {
|
|
asc: ["40", "20", "10", "2", null, "30"],
|
|
desc: ["30", "20", "10", "2", null, "40"],
|
|
},
|
|
severity: {
|
|
asc: ["40", "10", "20", "2", null, "30"],
|
|
desc: ["10", "40", "20", "2", null, "30"],
|
|
},
|
|
};
|
|
const routes = [
|
|
{ path: "/exploration/findings", key: (row) => row.id, orders: rowOrders },
|
|
{ path: "/exploration/finding-cases", key: (row) => row.finding.id, orders: rowOrders },
|
|
{ path: "/exploration/findings/groups", key: (row) => row.task_id, orders: groupOrders },
|
|
];
|
|
const read = (route, query = {}) =>
|
|
mockHandle("GET", `${route.path}?${new URLSearchParams({ page: "1", limit: "100", ...query })}`);
|
|
|
|
for (const route of routes) {
|
|
for (const sort of ["time", "severity"]) {
|
|
for (const direction of ["asc", "desc"]) {
|
|
test(`${route.path}: ${sort}/${direction} sorts before pagination with stable ID ties`, async () => {
|
|
const expected = route.orders[sort][direction];
|
|
const full = await read(route, { sort, time_direction: direction });
|
|
assert.deepEqual(full.items.map(route.key), expected);
|
|
assert.equal(full.total, expected.length);
|
|
const paged = [];
|
|
for (let page = 1; page <= Math.ceil(expected.length / 2); page++) {
|
|
const result = await read(route, { sort, time_direction: direction, page: String(page), limit: "2" });
|
|
assert.equal(result.total, expected.length);
|
|
assert.equal(result.page, page);
|
|
assert.equal(result.page_size, 2);
|
|
paged.push(...result.items.map(route.key));
|
|
}
|
|
assert.deepEqual(paged, expected);
|
|
});
|
|
}
|
|
}
|
|
|
|
test(`${route.path}: missing or invalid direction defaults to descending`, async () => {
|
|
for (const sort of ["time", "severity"]) {
|
|
for (const direction of [undefined, "", "ASC", "invalid", "asc; DROP TABLE findings"]) {
|
|
const query = { sort, ...(direction === undefined ? {} : { time_direction: direction }) };
|
|
assert.deepEqual((await read(route, query)).items.map(route.key), route.orders[sort].desc);
|
|
}
|
|
}
|
|
});
|
|
|
|
test(`${route.path}: direction preserves combined filters and leaves the snapshot unchanged`, async () => {
|
|
const before = await read(route, { sort: "time", time_direction: "desc" });
|
|
const grouped = route.path.endsWith("/groups");
|
|
for (const direction of ["asc", "desc"]) {
|
|
const result = await read(route, {
|
|
sort: "time",
|
|
time_direction: direction,
|
|
severity: "high",
|
|
status: "pending",
|
|
q: "needle",
|
|
});
|
|
assert.deepEqual(result.items.map(route.key), grouped ? ["20", "10", "2", null] : ["13", "12", "10", "2"]);
|
|
assert.equal(result.total, 4);
|
|
if (grouped) assert.equal(result.finding_total, 4);
|
|
const empty = await read(route, { time_direction: direction, q: "absent fixture text" });
|
|
assert.deepEqual(empty.items, []);
|
|
assert.equal(empty.total, 0);
|
|
}
|
|
const after = await read(route, { sort: "time", time_direction: "desc" });
|
|
assert.deepEqual(after, before);
|
|
});
|
|
}
|
|
|
|
test("group dates use the latest matching finding after filters", async () => {
|
|
const route = routes[2];
|
|
const result = await read(route, { status: "resolved", time_direction: "asc" });
|
|
assert.equal(result.total, 1);
|
|
assert.equal(result.finding_total, 1);
|
|
assert.equal(result.items[0].task_id, "10");
|
|
assert.equal(result.items[0].last_found_at, fixtures[8].ts);
|
|
});
|