Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions e2e/tests/api/features/advisory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test } from "../fixtures";
import {
testBasicSort,
validateDateSorting,
validateSortDirectionDiffers,
} from "../helpers/sorting-helpers";

test.describe("Advisory sorting validation", () => {
test("Sort advisories by ID ascending", async ({ axios }) => {
await testBasicSort(axios, "/api/v2/advisory", "id", "asc");
});

test("Sort advisories by ID descending", async ({ axios }) => {
await validateSortDirectionDiffers(
axios,
"/api/v2/advisory",
"id",
(item) => item.identifier,
);
});

test("Sort advisories by modified date ascending", async ({ axios }) => {
const items = await testBasicSort(
axios,
"/api/v2/advisory",
"modified",
"asc",
);
validateDateSorting(items, "modified", "ascending");
});

test("Sort advisories by modified date descending", async ({ axios }) => {
const items = await testBasicSort(
axios,
"/api/v2/advisory",
"modified",
"desc",
);
validateDateSorting(items, "modified", "descending");
});
});
53 changes: 53 additions & 0 deletions e2e/tests/api/features/purl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { expect, test } from "../fixtures";
import {
testBasicSort,
validateSortDirectionDiffers,
} from "../helpers/sorting-helpers";

test.skip("Purl by alias - vanilla", async ({ axios }) => {
const vanillaResponse = await axios.get(
Expand Down Expand Up @@ -36,3 +40,52 @@ test.skip("Purl by alias - vanilla", async ({ axios }) => {
]),
);
});

test.describe("PURL sorting validation", () => {
test("Sort PURLs by name ascending", async ({ axios }) => {
await testBasicSort(axios, "/api/v2/purl", "name", "asc");
});

test("Sort PURLs by name descending", async ({ axios }) => {
await validateSortDirectionDiffers(
axios,
"/api/v2/purl",
"name",
(item) => {
// Extract name from purl string
const match = item.purl.match(/pkg:[^/]+\/(?:[^/]+\/)?([^@?]+)/);
return match ? match[1] : item.purl;
},
);
});

test("Sort PURLs by namespace ascending", async ({ axios }) => {
await testBasicSort(axios, "/api/v2/purl", "namespace", "asc");
});

test("Sort PURLs by namespace descending", async ({ axios }) => {
await validateSortDirectionDiffers(
axios,
"/api/v2/purl",
"namespace",
(item) => {
// Extract namespace from purl string
const match = item.purl.match(/pkg:[^/]+\/([^/]+)/);
return match ? match[1] : null;
},
);
});

test("Sort PURLs by version ascending", async ({ axios }) => {
await testBasicSort(axios, "/api/v2/purl", "version", "asc");
});

test("Sort PURLs by version descending", async ({ axios }) => {
await validateSortDirectionDiffers(
axios,
"/api/v2/purl",
"version",
(item) => item.version.version,
);
});
});
25 changes: 25 additions & 0 deletions e2e/tests/api/features/sboms.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { expect, test } from "../fixtures";
import {
testBasicSort,
validateDateSorting,
validateStringSorting,
} from "../helpers/sorting-helpers";

test.skip("List first 10 sboms by name - vanilla", async ({ axios }) => {
const vanillaResponse = await axios.get(
Expand All @@ -10,3 +15,23 @@ test.skip("List first 10 sboms by name - vanilla", async ({ axios }) => {
}),
);
});

test("Sort SBOMs by name ascending", async ({ axios }) => {
const items = await testBasicSort(axios, "/api/v2/sbom", "name", "asc");
validateStringSorting(items, "name", "ascending");
});

test("Sort SBOMs by name descending", async ({ axios }) => {
const items = await testBasicSort(axios, "/api/v2/sbom", "name", "desc");
validateStringSorting(items, "name", "descending");
});

test("Sort SBOMs by published date ascending", async ({ axios }) => {
const items = await testBasicSort(axios, "/api/v2/sbom", "published", "asc");
validateDateSorting(items, "published", "ascending");
});

test("Sort SBOMs by published date descending", async ({ axios }) => {
const items = await testBasicSort(axios, "/api/v2/sbom", "published", "desc");
validateDateSorting(items, "published", "descending");
});
65 changes: 65 additions & 0 deletions e2e/tests/api/features/vulnerability.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { expect, test } from "../fixtures";
import {
testBasicSort,
validateDateSorting,
validateNumericSorting,
validateSortDirectionDiffers,
} from "../helpers/sorting-helpers";

test.skip("Filter Vulnerabilities by severity - vanilla", async ({ axios }) => {
// URLSearchParams ensures escaping special characters
Expand All @@ -20,3 +26,62 @@ test.skip("Filter Vulnerabilities by severity - vanilla", async ({ axios }) => {
}),
);
});

test.describe("Vulnerability sorting validation", () => {
test("Sort vulnerabilities by ID ascending", async ({ axios }) => {
await testBasicSort(axios, "/api/v2/vulnerability", "id", "asc");
});

test("Sort vulnerabilities by ID descending", async ({ axios }) => {
await validateSortDirectionDiffers(
axios,
"/api/v2/vulnerability",
"id",
(item) => item.identifier,
);
});

test("Sort vulnerabilities by CVSS score ascending", async ({ axios }) => {
const items = await testBasicSort(
axios,
"/api/v2/vulnerability",
"base_score",
"asc",
);
validateNumericSorting(items, "average_score", "ascending");
});

test("Sort vulnerabilities by CVSS score descending", async ({ axios }) => {
const items = await testBasicSort(
axios,
"/api/v2/vulnerability",
"base_score",
"desc",
);
validateNumericSorting(items, "average_score", "descending");
});

test("Sort vulnerabilities by published date ascending", async ({
axios,
}) => {
const items = await testBasicSort(
axios,
"/api/v2/vulnerability",
"published",
"asc",
);
validateDateSorting(items, "published", "ascending");
});

test("Sort vulnerabilities by published date descending", async ({
axios,
}) => {
const items = await testBasicSort(
axios,
"/api/v2/vulnerability",
"published",
"desc",
);
validateDateSorting(items, "published", "descending");
});
});
142 changes: 142 additions & 0 deletions e2e/tests/api/helpers/sorting-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { AxiosInstance } from "axios";
import { expect } from "../fixtures";

/**
* Helper to validate that dates in an array are sorted in the specified order
*/
export function validateDateSorting(
// biome-ignore lint/suspicious/noExplicitAny: Generic helper accepts any API response type
items: any[],
dateField: string,
order: "ascending" | "descending",
) {
// Extract date values and convert to timestamps for comparison
// biome-ignore lint/suspicious/noExplicitAny: API response types are not strictly typed in tests
const dates = items.map((item: any) =>
item[dateField] !== null ? new Date(item[dateField]).getTime() : null,
);

// Create sorted copy
const sorted = [...dates].sort((a, b) => {
// Handle null values
if (a === null && b === null) return 0;

if (order === "ascending") {
// Ascending: nulls at end
if (a === null) return 1;
if (b === null) return -1;
return a - b;
}

// Descending: nulls at beginning
if (a === null) return -1;
if (b === null) return 1;
return b - a;
});

expect(dates).toEqual(sorted);
}

/**
* Helper to validate that numeric scores in an array are sorted in the specified order
*/
export function validateNumericSorting(
// biome-ignore lint/suspicious/noExplicitAny: Generic helper accepts any API response type
items: any[],
scoreField: string,
order: "ascending" | "descending",
) {
// biome-ignore lint/suspicious/noExplicitAny: API response types are not strictly typed in tests
const scores = items.map((item: any) => item[scoreField]);

// Create sorted copy
const sorted = [...scores].sort((a, b) => {
// Handle null values
if (a === null && b === null) return 0;

if (order === "ascending") {
// Ascending: nulls at end
if (a === null) return 1;
if (b === null) return -1;
return a - b;
}

// Descending: nulls at beginning
if (a === null) return -1;
if (b === null) return 1;
return b - a;
});

expect(scores).toEqual(sorted);
}

/**
* Helper to validate that string values are sorted using localeCompare
*/
export function validateStringSorting(
// biome-ignore lint/suspicious/noExplicitAny: Generic helper accepts any API response type
items: any[],
field: string,
order: "ascending" | "descending",
) {
// biome-ignore lint/suspicious/noExplicitAny: API response types are not strictly typed in tests
const values = items.map((item: any) => item[field]);

const sorted = [...values].sort((a, b) => {
if (order === "ascending") {
return a.localeCompare(b, undefined, { sensitivity: "base" });
}
return b.localeCompare(a, undefined, { sensitivity: "base" });
});

expect(values).toEqual(sorted);
}

/**
* Helper to test that ascending and descending sorts return different first items
*/
export async function validateSortDirectionDiffers(
axios: AxiosInstance,
endpoint: string,
sortField: string,
// biome-ignore lint/suspicious/noExplicitAny: Generic helper accepts any API response type
extractValue: (item: any) => any,
) {
const [ascResponse, descResponse] = await Promise.all([
axios.get(endpoint, {
params: { offset: 0, limit: 100, sort: `${sortField}:asc` },
}),
axios.get(endpoint, {
params: { offset: 0, limit: 100, sort: `${sortField}:desc` },
}),
]);

const ascFirst = extractValue(ascResponse.data.items[0]);
const descFirst = extractValue(descResponse.data.items[0]);

expect(descFirst).not.toEqual(ascFirst);
}

/**
* Generic test for basic sorting (just validates API accepts the parameter)
*/
export async function testBasicSort(
axios: AxiosInstance,
endpoint: string,
sortField: string,
order: "asc" | "desc",
) {
const response = await axios.get(endpoint, {
params: {
offset: 0,
limit: 100,
sort: `${sortField}:${order}`,
},
});

expect(response.status).toBe(200);
expect(response.data.total).toBeGreaterThan(0);
expect(response.data.items.length).toBeGreaterThan(0);

return response.data.items;
}