diff --git a/e2e/tests/common/assets/advisory/cve-2022-45787.json.bz2 b/e2e/tests/common/assets/advisory/cve-2022-45787.json.bz2 new file mode 100644 index 000000000..935bddb00 Binary files /dev/null and b/e2e/tests/common/assets/advisory/cve-2022-45787.json.bz2 differ diff --git a/e2e/tests/common/assets/advisory/cve-2023-0044.json.bz2 b/e2e/tests/common/assets/advisory/cve-2023-0044.json.bz2 new file mode 100644 index 000000000..d6ed7c3f8 Binary files /dev/null and b/e2e/tests/common/assets/advisory/cve-2023-0044.json.bz2 differ diff --git a/e2e/tests/ui/features/@uploads/advisory-upload.feature b/e2e/tests/ui/features/@uploads/advisory-upload.feature new file mode 100644 index 000000000..c8cc76b4e --- /dev/null +++ b/e2e/tests/ui/features/@uploads/advisory-upload.feature @@ -0,0 +1,65 @@ +Feature: Advisory Upload - Upload advisory documents to Trustify + As a Trustify user + I want to upload advisory documents + So that I can manage and track security advisories + + Background: Authentication + Given User is authenticated + + Scenario: Navigate to Upload Advisory page from advisory list + When User navigates to the advisory list page + When User clicks Upload Advisory button from the toolbar + Then The application navigates to the Upload Advisory page + And The upload page heading is "Upload Advisory" + And The breadcrumb shows "Advisories" link and "Upload Advisory" as active item + + Scenario: Verify Upload Advisory page layout + Given User is on the Upload Advisory page + Then The upload page heading is "Upload Advisory" + And The supported formats description mentions "CSAF" and "CVE" + And The upload area displays "Drag and drop files here" + And The upload area displays "Accepted file types: .json, .bz2" + And The Upload button is visible in the upload area + + Scenario Outline: Upload a valid advisory file + Given User is on the Upload Advisory page + When User uploads file "" from "" + Then The upload summary shows "1 of 1 files uploaded" + And The uploaded file "" shows "success" status + + Examples: + | fileName | filePath | + | cve-2022-45787.json.bz2 | /tests/common/assets/advisory/ | + | cve-2023-0044.json.bz2 | /tests/common/assets/advisory/ | + + Scenario: Upload an invalid advisory file + Given User is on the Upload Advisory page + When User uploads file "invalid-file.json" from "/tests/common/assets/" + Then The uploaded file "invalid-file.json" shows "danger" status + + Scenario: Reject file with unsupported extension + Given User is on the Upload Advisory page + When User uploads file "invalid-file.txt" from "/tests/common/assets/" + Then The unsupported file modal is displayed containing "invalid-file.txt" + + Scenario: Upload multiple advisory files simultaneously + Given User is on the Upload Advisory page + When User uploads files "cve-2022-45787.json.bz2, cve-2023-0044.json.bz2" from "/tests/common/assets/advisory/" + Then The upload summary shows "2 of 2 files uploaded" + And The uploaded file "cve-2022-45787.json.bz2" shows "success" status + And The uploaded file "cve-2023-0044.json.bz2" shows "success" status + + Scenario: Upload mix of valid and invalid files + Given User is on the Upload Advisory page + When User uploads file "cve-2022-45787.json.bz2" from "/tests/common/assets/advisory/" + When User uploads file "invalid-file.json" from "/tests/common/assets/" + Then The upload summary shows "1 of 2 files uploaded" + And The uploaded file "cve-2022-45787.json.bz2" shows "success" status + And The uploaded file "invalid-file.json" shows "danger" status + + Scenario: Remove file from upload list + Given User is on the Upload Advisory page + When User uploads files "cve-2022-45787.json.bz2, cve-2023-0044.json.bz2" from "/tests/common/assets/advisory/" + Then The upload summary shows "2 of 2 files uploaded" + When User removes file "cve-2022-45787.json.bz2" from the upload list + Then The upload summary shows "1 of 1 files uploaded" diff --git a/e2e/tests/ui/features/@uploads/sbom-upload.feature b/e2e/tests/ui/features/@uploads/sbom-upload.feature new file mode 100644 index 000000000..94cfc9c0a --- /dev/null +++ b/e2e/tests/ui/features/@uploads/sbom-upload.feature @@ -0,0 +1,65 @@ +Feature: SBOM Upload - Upload SBOM documents to Trustify + As a Trustify user + I want to upload SBOM documents + So that I can manage and analyze my software supply chain + + Background: Authentication + Given User is authenticated + + Scenario: Navigate to Upload SBOM page from SBOM list + When User navigates to the SBOM list page + When User clicks Upload SBOM action from the toolbar + Then The application navigates to the Upload SBOM page + And The upload page heading is "Upload SBOM" + And The breadcrumb shows "SBOMs" link and "Upload SBOM" as active item + + Scenario: Verify Upload SBOM page layout + Given User is on the Upload SBOM page + Then The upload page heading is "Upload SBOM" + And The supported formats description mentions "CycloneDX" and "SPDX" + And The upload area displays "Drag and drop files here" + And The upload area displays "Accepted file types: .json, .bz2" + And The Upload button is visible in the upload area + + Scenario Outline: Upload a valid SBOM file + Given User is on the Upload SBOM page + When User uploads file "" from "" + Then The upload summary shows "1 of 1 files uploaded" + And The uploaded file "" shows "success" status + + Examples: + | fileName | filePath | + | examples_sbom.json | /tests/common/assets/sbom/ | + | example_product_quarkus.json.bz2 | /tests/common/assets/sbom/ | + + Scenario: Upload an invalid SBOM file + Given User is on the Upload SBOM page + When User uploads file "invalid-file.json" from "/tests/common/assets/" + Then The uploaded file "invalid-file.json" shows "danger" status + + Scenario: Reject file with unsupported extension + Given User is on the Upload SBOM page + When User uploads file "invalid-file.txt" from "/tests/common/assets/" + Then The unsupported file modal is displayed containing "invalid-file.txt" + + Scenario: Upload multiple SBOM files simultaneously + Given User is on the Upload SBOM page + When User uploads files "examples_sbom.json, example_product_quarkus.json.bz2" from "/tests/common/assets/sbom/" + Then The upload summary shows "2 of 2 files uploaded" + And The uploaded file "examples_sbom.json" shows "success" status + And The uploaded file "example_product_quarkus.json.bz2" shows "success" status + + Scenario: Upload mix of valid and invalid files + Given User is on the Upload SBOM page + When User uploads file "examples_sbom.json" from "/tests/common/assets/sbom/" + When User uploads file "invalid-file.json" from "/tests/common/assets/" + Then The upload summary shows "1 of 2 files uploaded" + And The uploaded file "examples_sbom.json" shows "success" status + And The uploaded file "invalid-file.json" shows "danger" status + + Scenario: Remove file from upload list + Given User is on the Upload SBOM page + When User uploads files "examples_sbom.json, example_product_quarkus.json.bz2" from "/tests/common/assets/sbom/" + Then The upload summary shows "2 of 2 files uploaded" + When User removes file "examples_sbom.json" from the upload list + Then The upload summary shows "1 of 1 files uploaded" diff --git a/e2e/tests/ui/features/@uploads/upload.step.ts b/e2e/tests/ui/features/@uploads/upload.step.ts new file mode 100644 index 000000000..8a51efc74 --- /dev/null +++ b/e2e/tests/ui/features/@uploads/upload.step.ts @@ -0,0 +1,171 @@ +import { createBdd } from "playwright-bdd"; + +import { test } from "../../fixtures"; + +import { expect } from "../../assertions"; + +import { FileUpload } from "../../pages/FileUpload"; +import { resolveAssetPath } from "../../pages/Helpers"; +import { AdvisoryListPage } from "../../pages/advisory-list/AdvisoryListPage"; +import { AdvisoryUploadPage } from "../../pages/advisory-upload/AdvisoryUploadPage"; +import { SBOMUploadPage } from "../../pages/sbom-upload/SBOMUploadPage"; +import { SbomListPage } from "../../pages/sbom-list/SbomListPage"; + +export const { Given, When, Then } = createBdd(test); + +// --- Given (feature-specific) --- + +Given("User is on the Upload Advisory page", async ({ page }) => { + await AdvisoryUploadPage.buildFromBrowserPath(page); +}); + +Given("User is on the Upload SBOM page", async ({ page }) => { + await SBOMUploadPage.buildFromBrowserPath(page); +}); + +// --- When (feature-specific navigation) --- + +When("User navigates to the advisory list page", async ({ page }) => { + await AdvisoryListPage.build(page); +}); + +When("User navigates to the SBOM list page", async ({ page }) => { + await SbomListPage.build(page); +}); + +When( + "User clicks Upload Advisory button from the toolbar", + async ({ page }) => { + const toolbar = page.locator("[aria-label='advisory-toolbar']"); + await expect(toolbar).toBeVisible(); + await toolbar.getByRole("button", { name: "Upload Advisory" }).click(); + }, +); + +When("User clicks Upload SBOM action from the toolbar", async ({ page }) => { + const listPage = await SbomListPage.fromCurrentPage(page); + const toolbar = await listPage.getToolbar(); + await toolbar.clickKebabAction("Upload SBOM"); +}); + +// --- When (shared upload actions) --- + +When( + "User uploads file {string} from {string}", + async ({ page }, fileName: string, filePath: string) => { + const fileUploader = await FileUpload.build(page); + const resolvedPath = resolveAssetPath(filePath, fileName); + await fileUploader.uploadFiles([resolvedPath]); + }, +); + +When( + "User uploads files {string} from {string}", + async ({ page }, fileNamesCsv: string, filePath: string) => { + const fileUploader = await FileUpload.build(page); + const fileNames = fileNamesCsv.split(",").map((f) => f.trim()); + const resolvedPaths = fileNames.map((fn) => resolveAssetPath(filePath, fn)); + await fileUploader.uploadFiles(resolvedPaths); + }, +); + +When( + "User removes file {string} from the upload list", + async ({ page }, fileName: string) => { + const fileUploader = await FileUpload.build(page); + const statusItem = await fileUploader.getUploadStatusItem(fileName); + await statusItem.getByRole("button", { name: "Remove from list" }).click(); + }, +); + +// --- Then (shared assertions) --- + +Then( + "The application navigates to the Upload Advisory page", + async ({ page }) => { + await expect(page).toHaveURL(/\/advisories\/upload$/); + }, +); + +Then("The application navigates to the Upload SBOM page", async ({ page }) => { + await expect(page).toHaveURL(/\/sboms\/upload$/); +}); + +Then( + "The upload page heading is {string}", + async ({ page }, heading: string) => { + await expect( + page.getByRole("heading", { level: 1, name: heading }), + ).toBeVisible(); + }, +); + +Then( + "The breadcrumb shows {string} link and {string} as active item", + async ({ page }, linkText: string, activeText: string) => { + const breadcrumb = page.locator("nav[aria-label='Breadcrumb']"); + await expect( + breadcrumb.getByRole("link", { name: linkText }), + ).toBeVisible(); + await expect(breadcrumb.getByText(activeText)).toBeVisible(); + }, +); + +Then( + "The supported formats description mentions {string} and {string}", + async ({ page }, format1: string, format2: string) => { + const description = page.getByText("Upload a"); + await expect(description).toBeVisible(); + await expect(description).toContainText(format1); + await expect(description).toContainText(format2); + }, +); + +Then("The upload area displays {string}", async ({ page }, text: string) => { + await expect(page.getByText(text)).toBeVisible(); +}); + +Then("The Upload button is visible in the upload area", async ({ page }) => { + const fileUploader = await FileUpload.build(page); + await expect(fileUploader.getUploadButton()).toBeVisible(); +}); + +Then( + "The upload summary shows {string}", + async ({ page }, summaryText: string) => { + const match = summaryText.match(/(\d+) of (\d+) files uploaded/); + if (!match) { + throw new Error(`Invalid summary format: "${summaryText}"`); + } + const successfulFiles = Number(match[1]); + const totalFiles = Number(match[2]); + + const fileUploader = await FileUpload.build(page); + await expect(fileUploader).toHaveSummaryUploadStatus({ + successfulFiles, + totalFiles, + }); + }, +); + +Then( + "The uploaded file {string} shows {string} status", + async ({ page }, fileName: string, status: string) => { + const fileUploader = await FileUpload.build(page); + await expect(fileUploader).toHaveItemUploadStatus({ + fileName, + status: status as "success" | "danger", + }); + }, +); + +Then( + "The unsupported file modal is displayed containing {string}", + async ({ page }, fileName: string) => { + const modal = page.getByRole("dialog", { + name: "unsupported file upload attempted", + }); + await expect(modal).toBeVisible(); + await expect(modal).toContainText(fileName); + }, +); diff --git a/e2e/tests/ui/pages/FileUpload.ts b/e2e/tests/ui/pages/FileUpload.ts index 20f967168..238b506d5 100644 --- a/e2e/tests/ui/pages/FileUpload.ts +++ b/e2e/tests/ui/pages/FileUpload.ts @@ -9,19 +9,28 @@ export class FileUpload { this._uploader = uploader; } - static async build(page: Page, ariaLabel: string) { - const locator = page.locator( - `div.pf-v6-c-multiple-file-upload[aria-label="${ariaLabel}"]`, - ); + static async build(page: Page, ariaLabel?: string) { + const selector = ariaLabel + ? `div.pf-v6-c-multiple-file-upload[aria-label="${ariaLabel}"]` + : "div.pf-v6-c-multiple-file-upload"; + const locator = page.locator(selector); + if (!ariaLabel) { + await expect(locator).toHaveCount(1); + } await expect(locator).toBeVisible(); return new FileUpload(page, locator); } + getUploadButton() { + return this._uploader.getByRole("button", { + name: "Upload", + exact: true, + }); + } + async uploadFiles(filePaths: string[]) { const fileChooserPromise = this._page.waitForEvent("filechooser"); - await this._uploader - .getByRole("button", { name: "Upload", exact: true }) - .click(); + await this.getUploadButton().click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles(filePaths); } diff --git a/e2e/tests/ui/pages/advisory-upload/AdvisoryUploadPage.ts b/e2e/tests/ui/pages/advisory-upload/AdvisoryUploadPage.ts index ed995143f..51fbcc8de 100644 --- a/e2e/tests/ui/pages/advisory-upload/AdvisoryUploadPage.ts +++ b/e2e/tests/ui/pages/advisory-upload/AdvisoryUploadPage.ts @@ -13,6 +13,10 @@ export class AdvisoryUploadPage { return new AdvisoryUploadPage(page); } + static fromCurrentPage(page: Page) { + return new AdvisoryUploadPage(page); + } + async getFileUploader() { return await FileUpload.build(this._page, "advisory-uploader"); } diff --git a/e2e/tests/ui/pages/sbom-upload/SBOMUploadPage.ts b/e2e/tests/ui/pages/sbom-upload/SBOMUploadPage.ts index a02dacf0e..883e6ad5c 100644 --- a/e2e/tests/ui/pages/sbom-upload/SBOMUploadPage.ts +++ b/e2e/tests/ui/pages/sbom-upload/SBOMUploadPage.ts @@ -13,6 +13,10 @@ export class SBOMUploadPage { return new SBOMUploadPage(page); } + static fromCurrentPage(page: Page) { + return new SBOMUploadPage(page); + } + async getFileUploader() { return await FileUpload.build(this._page, "sbom-uploader"); }