Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
Feature: License Explorer
As a Platform Eng
I want to be able to download the licenses in a CSV file format from a specific SBOM

Background:

Scenario: Verify Download Licences option on SBOM Search Results page for CycloneDX SBOM
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
And User Clicks on SBOM name hyperlink from the Search Results
And User Clicks "Action" button
Then "Download License Report" Option should be visible

Examples:
| sbomName |
| liboqs |

Scenario Outline: User Downloads license information for CycloneDX SBOM from SBOM Search Results page
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
And User Clicks on SBOM name hyperlink from the Search Results
And User Clicks "Action" button
And Selects "Download License Report" option
Then Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name

Examples:
| sbomName |
| liboqs |

Scenario: Verify Download Licences option on SBOM Explorer page for CycloneDX SBOM
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box and Navigates to Search results page
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
And User Clicks on SBOM name hyperlink from the Search Results
Then Application Navigates to SBOM Explorer page
And User Clicks "Action" button
And "Download License Report" Option should be visible

Examples:
| sbomName |
| liboqs |

Scenario: User Downloads license information for CycloneDX SBOM from SBOM Explorer page
Given User is on SBOM Explorer page for the CycloneDX SBOM "<sbomName>"
And User Clicks on "Download License Report" button
Then Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name

Examples:
| sbomName |
| liboqs |

Scenario: Verify the files on downloaded CycloneDX SBOM license TAR.GZ
Given User has Downloaded the License information for CycloneDX SBOM "<sbomName>"
When User extracts the Downloaded license TAR.GZ file
Then Extracted files should contain two CSVs, one for Package license information and another one for License reference

Examples:
| sbomName |
| liboqs |

Scenario: Verify the headers on CycloneDX SBOM package License CSV file
Given User extracted the CycloneDX SBOM "<sbomName>" license compressed file
When User Opens the package license information file
Then The file should have the following headers - SBOM name, SBOM id, package name, package group, package version, package purl, package cpe and license

Examples:
| sbomName |
| liboqs |

Scenario: Verify the headers on CycloneDX SBOM License reference CSV file
Given User extracted the CycloneDX SBOM "<sbomName>" license compressed file
When User Opens the license reference file
Then The file should have the following headers - licenseId, name, extracted text and comment

Examples:
| sbomName |
| liboqs |

Scenario: Verify the contents on CycloneDX SBOM license reference CSV file
Given User is on license reference "<sbomName>" file
Then The License reference CSV should be empty

Examples:
| sbomName |
| liboqs |
205 changes: 205 additions & 0 deletions e2e/tests/ui/features/@license-export_cdx/license-export_cdx.step.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { createBdd } from "playwright-bdd";
import * as fs from "node:fs";

import { test } from "../../fixtures";

import { expect } from "../../assertions";

import { SbomListPage } from "../../pages/sbom-list/SbomListPage";
import { SbomDetailsPage } from "../../pages/sbom-details/SbomDetailsPage";
import { DetailsPage } from "../../helpers/DetailsPage";
import { SearchPage } from "../../helpers/SearchPage";
import { clickAndVerifyDownload } from "../../pages/Helpers";
import {
downloadLicenseReport,
extractLicenseReport,
findCsvWithHeader,
} from "../../pages/LicenseExportHelpers";

export const { Given, When, Then } = createBdd(test);

let downloadedFilename: string;
let downloadedFilePath: string;
let extractionPath: string;
let packageLicenseFilePath: string;
let licenseReferenceFilePath: string;

Given(
"User Searches for CycloneDX SBOM {string} using Search Text box",
async ({ page }, sbomName: string) => {
const listPage = await SbomListPage.build(page);
const toolbar = await listPage.getToolbar();
await toolbar.applyFilter({ "Filter text": sbomName });
},
);

Given(
"User Searches for CycloneDX SBOM {string} using Search Text box and Navigates to Search results page",
async ({ page }, sbomName: string) => {
const searchPage = new SearchPage(page, "Search");
await searchPage.generalSearch("SBOMs", sbomName);
},
);

When(
"User Selects CycloneDX SBOM {string} from the Search Results",
async ({ page }, sbomName: string) => {
const listPage = await SbomListPage.fromCurrentPage(page);
const table = await listPage.getTable();
await table.waitUntilDataIsLoaded();

await expect(table).toHaveColumnWithValue("Name", sbomName);
},
);

When(
"User Clicks on SBOM name hyperlink from the Search Results",
async ({ page }) => {
const listPage = await SbomListPage.fromCurrentPage(page);
const table = await listPage.getTable();
const rows = await table.getRows();
await rows.first().getByRole("link").click();
},
);

Then("Application Navigates to SBOM Explorer page", async ({ page }) => {
await expect(page).toHaveURL(/\/sboms\/[^/]+/);
});

When('User Clicks "Action" button', async ({ page }) => {
const detailsPage = new DetailsPage(page);
await detailsPage.openActionsMenu();
});

Then('"Download License Report" Option should be visible', async ({ page }) => {
const detailsPage = new DetailsPage(page);
await detailsPage.verifyActionIsVisibleInMenu("Download License Report");
});

When('Selects "Download License Report" option', async ({ page }) => {
const detailsPage = new DetailsPage(page);
downloadedFilename = await clickAndVerifyDownload(
page,
async () =>
await detailsPage.page
.getByRole("menuitem", { name: "Download License Report" })
.click(),
);
});

Then(
"Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name",
async ({ page }) => {
const sbomName = await page.getByRole("heading").first().innerText();
expect(downloadedFilename).toContain(sbomName);
expect(downloadedFilename.endsWith(".tar.gz")).toBeTruthy();
},
);

Given(
"User is on SBOM Explorer page for the CycloneDX SBOM {string}",
async ({ page }, sbomName: string) => {
await SbomDetailsPage.build(page, sbomName);
},
);

When('User Clicks on "Download License Report" button', async ({ page }) => {
const detailsPage = new DetailsPage(page);
await detailsPage.openActionsMenu();

downloadedFilename = await clickAndVerifyDownload(
page,
async () =>
await detailsPage.page
.getByRole("menuitem", { name: "Download License Report" })
.click(),
);
});

Given(
"User has Downloaded the License information for CycloneDX SBOM {string}",
async ({ page }, sbomName: string) => {
await SbomDetailsPage.build(page, sbomName);
downloadedFilePath = await downloadLicenseReport(page);
},
);

When("User extracts the Downloaded license TAR.GZ file", async () => {
extractionPath = await extractLicenseReport(downloadedFilePath);
});

Then(
"Extracted files should contain two CSVs, one for Package license information and another one for License reference",
async () => {
const files = fs.readdirSync(extractionPath);
const csvFiles = files.filter((file) => file.endsWith(".csv"));
expect(csvFiles.length).toBe(2);
},
);

Given(
"User extracted the CycloneDX SBOM {string} license compressed file",
async ({ page }, sbomName: string) => {
await SbomDetailsPage.build(page, sbomName);
downloadedFilePath = await downloadLicenseReport(page);
extractionPath = await extractLicenseReport(downloadedFilePath);
},
);

When("User Opens the package license information file", async () => {
packageLicenseFilePath = findCsvWithHeader(
extractionPath,
"SBOM name",
"Package license information file",
);
});

Then(
"The file should have the following headers - SBOM name, SBOM id, package name, package group, package version, package purl, package cpe and license",
async () => {
const content = fs.readFileSync(packageLicenseFilePath, "utf-8");
const headers = content.split("\n")[0].trim();
const expectedHeaders =
'"SBOM name"\t"SBOM id"\t"package name"\t"package group"\t"package version"\t"package purl"\t"package cpe"\t"license"\t"license type"';
expect(headers).toBe(expectedHeaders);
},
);

When("User Opens the license reference file", async () => {
licenseReferenceFilePath = findCsvWithHeader(
extractionPath,
"licenseId",
"License reference file",
);
});

Then(
"The file should have the following headers - licenseId, name, extracted text and comment",
async () => {
const content = fs.readFileSync(licenseReferenceFilePath, "utf-8");
const headers = content.split("\n")[0].trim();
const expectedHeaders = '"licenseId"\t"name"\t"extracted text"\t"comment"';
expect(headers).toBe(expectedHeaders);
},
);

Given(
"User is on license reference {string} file",
async ({ page }, sbomName: string) => {
await SbomDetailsPage.build(page, sbomName);
downloadedFilePath = await downloadLicenseReport(page);
extractionPath = await extractLicenseReport(downloadedFilePath);
licenseReferenceFilePath = findCsvWithHeader(
extractionPath,
"licenseId",
"License reference file",
);
},
);

Then("The License reference CSV should be empty", async () => {
const content = fs.readFileSync(licenseReferenceFilePath, "utf-8");
const lines = content.trim().split("\n");
// Only header should be present
expect(lines.length).toBe(1);
});
12 changes: 12 additions & 0 deletions e2e/tests/ui/helpers/DetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export class DetailsPage {
await this.page.getByRole("menuitem", { name: actionName }).click();
}

async openActionsMenu() {
const actionsButton = this.page.getByRole("button", { name: "Actions" });
await actionsButton.click();
await expect(actionsButton).toHaveAttribute("aria-expanded", "true");
}

async clickOnPageButton(buttonName: string) {
await this.page.getByRole("button", { name: buttonName }).click();
}
Expand All @@ -37,6 +43,12 @@ export class DetailsPage {
).toBeVisible();
}

async verifyActionIsVisibleInMenu(actionName: string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the verifyActionIsVisibleInMenu now deals only with verification part and user actions is under openActionsMenu. Which is good comparing to, verifyActionIsAvailable. But shall we move the verification part into assertions? This ensures type safety and consistency.

await expect(
this.page.getByRole("menuitem", { name: actionName }),
).toBeVisible();
}

async verifyButtonIsVisible(button: string) {
await expect(this.page.getByRole("button", { name: button })).toBeVisible();
}
Expand Down
17 changes: 17 additions & 0 deletions e2e/tests/ui/pages/Helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ export const clickAndVerifyDownload = async (
return await verifyDownload(download);
};

/**
* Handles click action that triggers a download and returns the Download object
* @param page The Playwright Page object
* @param clickAction The async function that performs the click
* @returns The Download object
*/
export const clickAndDownload = async (
page: Page,
clickAction: () => Promise<void>,
): Promise<Download> => {
const [download] = await Promise.all([
page.waitForEvent("download"),
clickAction(),
]);
return download;
};

/**
* Verifies comma-delimited expected values against child elements
* Useful for comparing lists like qualifiers, tags, labels, severity values, etc.
Expand Down
Loading