Skip to content
Merged
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
1,223 changes: 1,223 additions & 0 deletions e2e/tests/api/features/sbom-groups.ts

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions e2e/tests/api/helpers/sbom-group-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import type { AxiosInstance } from "axios";
import { logger } from "../../common/constants";

type CreateGroupOptions = {
description?: string;
parent?: string;
labels?: Record<string, string>;
};

type GroupResponse = {
id: string;
name: string;
parent: string | null;
description: string | null;
labels: Record<string, string>;
};

type ListGroupsParams = {
q?: string;
sort?: string;
offset?: number;
limit?: number;
total?: boolean;
totals?: boolean;
parents?: "skip" | "id" | "resolve";
};

export async function createGroup(
axios: AxiosInstance,
name: string,
options?: CreateGroupOptions,
): Promise<{ id: string; etag: string }> {
const body: Record<string, unknown> = { name };
if (options?.description != null) body.description = options.description;
if (options?.parent != null) body.parent = options.parent;
if (options?.labels) body.labels = options.labels;

const response = await axios.post("/api/v3/group/sbom", body);

return {
id: response.data.id,
etag: response.headers["etag"] as string,
};
}

export async function readGroup(
axios: AxiosInstance,
id: string,
): Promise<{ body: GroupResponse; etag: string }> {
const response = await axios.get(`/api/v3/group/sbom/${id}`);

return {
body: response.data,
etag: response.headers["etag"] as string,
};
}

export async function updateGroup(
axios: AxiosInstance,
id: string,
body: {
name: string;
description?: string | null;
parent?: string | null;
labels?: Record<string, string>;
},
etag?: string,
): Promise<{ etag: string }> {
const headers: Record<string, string> = {};
if (etag) {
headers["If-Match"] = etag;
}

const response = await axios.put(`/api/v3/group/sbom/${id}`, body, {
headers,
});
return {
etag: response.headers["etag"] as string,
};
}

export async function deleteGroup(
axios: AxiosInstance,
id: string,
etag?: string,
): Promise<void> {
const headers: Record<string, string> = {};
if (etag) {
headers["If-Match"] = etag;
}

await axios.delete(`/api/v3/group/sbom/${id}`, { headers });
}

export async function listGroups(
axios: AxiosInstance,
params?: ListGroupsParams,
) {
const response = await axios.get("/api/v3/group/sbom", { params });
return response.data;
}

export async function readAssignments(
axios: AxiosInstance,
sbomId: string,
): Promise<{ groupIds: string[]; etag: string }> {
const response = await axios.get(`/api/v3/group/sbom-assignment/${sbomId}`);

return {
groupIds: response.data,
etag: response.headers["etag"] as string,
};
}

export async function updateAssignments(
axios: AxiosInstance,
sbomId: string,
groupIds: string[],
etag?: string,
): Promise<void> {
const headers: Record<string, string> = {};
if (etag) {
headers["If-Match"] = etag;
}

await axios.put(`/api/v3/group/sbom-assignment/${sbomId}`, groupIds, {
headers,
});
}

export async function bulkAssign(
axios: AxiosInstance,
sbomIds: string[],
groupIds: string[],
): Promise<void> {
await axios.put("/api/v3/group/sbom-assignment", {
sbom_ids: sbomIds,
group_ids: groupIds,
});
}

export async function patchAssignments(
axios: AxiosInstance,
sbomIds: string[],
add: string[],
remove: string[],
): Promise<void> {
await axios.patch("/api/v3/group/sbom-assignment", {
sbom_ids: sbomIds,
add,
remove,
});
}

export async function findFirstSbomId(axios: AxiosInstance): Promise<string> {
const response = await axios.get("/api/v3/sbom", {
params: { limit: 1, offset: 0 },
});
if (response.data.items.length === 0) {
throw new Error("No SBOMs found in database");
}
return response.data.items[0].id as string;
}

export async function findTwoSbomIds(
axios: AxiosInstance,
): Promise<[string, string]> {
const response = await axios.get("/api/v3/sbom", {
params: { limit: 2, offset: 0 },
});
if (response.data.items.length < 2) {
throw new Error("Expected at least 2 SBOMs in database");
}
return [
response.data.items[0].id as string,
response.data.items[1].id as string,
];
}

/**
* Deletes groups in order. Pass children before parents to avoid 409 conflicts.
*/
export async function cleanupGroups(
axios: AxiosInstance,
groupIds: string[],
): Promise<void> {
logger.info("Teardown: starting to delete SBOM groups.");

for (const id of groupIds) {
logger.info(`Teardown: deleting SBOM group with ID ${id}`);
try {
await axios.delete(`/api/v3/group/sbom/${id}`);
} catch (error: unknown) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
logger.warn(`Teardown: SBOM group ${id} not found during cleanup.`);
} else {
throw error;
}
}
}
}
26 changes: 26 additions & 0 deletions e2e/tests/ui/assertions/ToolbarMatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface ToolbarMatchers<
filters: Partial<FilterValueType<TFilter>>,
): Promise<MatcherResult>;
toHaveNoLabels(): Promise<MatcherResult>;
toHaveBulkSelectedCount(count: string): Promise<MatcherResult>;
}

type ToolbarMatcherDefinitions = {
Expand Down Expand Up @@ -120,4 +121,29 @@ export const toolbarAssertions = baseExpect.extend<ToolbarMatcherDefinitions>({
};
}
},
toHaveBulkSelectedCount: async <
TFilter extends Record<string, TFilterValue>,
TFilterName extends Extract<keyof TFilter, string>,
TKebabActions extends readonly string[],
>(
toolbar: Toolbar<TFilter, TFilterName, TKebabActions>,
count: string,
): Promise<MatcherResult> => {
try {
const bulkCheckbox = toolbar._toolbar
.page()
.locator("#bulk-selected-items-checkbox");
await baseExpect(bulkCheckbox).toContainText(count);

return {
pass: true,
message: () => `Bulk selected count is ${count}`,
};
} catch (error) {
return {
pass: false,
message: () => (error instanceof Error ? error.message : String(error)),
};
}
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ Feature: Importer Explorer - View importers details
Then A confirmation dialog should appear
And User confirms the action
Then The "<importerName>" importer should show "Scheduled" state or progress indicator
When User disables the "<importerName>" importer
Then A confirmation dialog should appear
And User confirms the action
Then The "<importerName>" importer state should be "Disabled"

Examples:
| importerName |
Expand All @@ -80,6 +84,10 @@ Feature: Importer Explorer - View importers details
And User confirms the action
Then A success message should be displayed
And The "<importerName>" importer should show progress indicator
When User disables the "<importerName>" importer
Then A confirmation dialog should appear
And User confirms the action
Then The "<importerName>" importer state should be "Disabled"

Examples:
| importerName |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ Then(
const maxPages = 10; // Safety limit to prevent infinite loops

// If looking for enabled importers, first check if any exist, if not enable one
let enabledImporterName = "";
if (!isDisabledState) {
let hasEnabledImporter = false;
let checkPage = 1;
Expand Down Expand Up @@ -237,7 +238,8 @@ Then(
const firstNameCell = nameColumn.first();
const importerName = (await firstNameCell.textContent()) || "";

// Enable the first importer
// Enable the first importer and track it for cleanup
enabledImporterName = importerName;
await listPage.performImporterAction(importerName, "Enable");

// Confirm the action
Expand Down Expand Up @@ -309,6 +311,13 @@ Then(
}

expect(foundMatchingRow).toBe(true);

// Cleanup: disable the importer if we enabled one for this test
if (enabledImporterName) {
await listPage.performImporterAction(enabledImporterName, "Disable");
const dialog = await DeletionConfirmDialog.build(page, "Confirm dialog");
await dialog.clickConfirm();
}
},
);

Expand Down
Loading
Loading