Skip to content

Commit 03fd60f

Browse files
committed
LicenseExport feature CDX
1 parent 56d5583 commit 03fd60f

File tree

5 files changed

+391
-0
lines changed

5 files changed

+391
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
Feature: License Explorer
2+
As a Platform Eng
3+
I want to be able to download the licenses in a CSV file format from a specific SBOM
4+
5+
Background:
6+
7+
Scenario: Verify Download Licences option on SBOM Search Results page for CycloneDX SBOM
8+
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box
9+
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
10+
And User Clicks on SBOM name hyperlink from the Search Results
11+
And User Clicks "Action" button
12+
Then "Download License Report" Option should be visible
13+
14+
Examples:
15+
| sbomName |
16+
| liboqs |
17+
18+
Scenario Outline: User Downloads license information for CycloneDX SBOM from SBOM Search Results page
19+
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box
20+
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
21+
And User Clicks on SBOM name hyperlink from the Search Results
22+
And User Clicks "Action" button
23+
And Selects "Download License Report" option
24+
Then Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name
25+
26+
Examples:
27+
| sbomName |
28+
| liboqs |
29+
30+
Scenario: Verify Download Licences option on SBOM Explorer page for CycloneDX SBOM
31+
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box and Navigates to Search results page
32+
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
33+
And User Clicks on SBOM name hyperlink from the Search Results
34+
Then Application Navigates to SBOM Explorer page
35+
And User Clicks "Action" button
36+
And "Download License Report" Option should be visible
37+
38+
Examples:
39+
| sbomName |
40+
| liboqs |
41+
42+
Scenario: User Downloads license information for CycloneDX SBOM from SBOM Explorer page
43+
Given User is on SBOM Explorer page for the CycloneDX SBOM "<sbomName>"
44+
And User Clicks on "Download License Report" button
45+
Then Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name
46+
47+
Examples:
48+
| sbomName |
49+
| liboqs |
50+
51+
Scenario: Verify the files on downloaded CycloneDX SBOM license TAR.GZ
52+
Given User has Downloaded the License information for CycloneDX SBOM "<sbomName>"
53+
When User extracts the Downloaded license TAR.GZ file
54+
Then Extracted files should contain two CSVs, one for Package license information and another one for License reference
55+
56+
Examples:
57+
| sbomName |
58+
| liboqs |
59+
60+
Scenario: Verify the headers on CycloneDX SBOM package License CSV file
61+
Given User extracted the CycloneDX SBOM "<sbomName>" license compressed file
62+
When User Opens the package license information file
63+
Then The file should have the following headers - SBOM name, SBOM id, package name, package group, package version, package purl, package cpe and license
64+
65+
Examples:
66+
| sbomName |
67+
| liboqs |
68+
69+
Scenario: Verify the headers on CycloneDX SBOM License reference CSV file
70+
Given User extracted the CycloneDX SBOM "<sbomName>" license compressed file
71+
When User Opens the license reference file
72+
Then The file should have the following headers - licenseId, name, extracted text and comment
73+
74+
Examples:
75+
| sbomName |
76+
| liboqs |
77+
78+
Scenario: Verify the contents on CycloneDX SBOM license reference CSV file
79+
Given User is on license reference "<sbomName>" file
80+
Then The License reference CSV should be empty
81+
82+
Examples:
83+
| sbomName |
84+
| liboqs |
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { createBdd } from "playwright-bdd";
2+
import * as fs from "node:fs";
3+
4+
import { test } from "../../fixtures";
5+
6+
import { expect } from "../../assertions";
7+
8+
import { SbomListPage } from "../../pages/sbom-list/SbomListPage";
9+
import { SbomDetailsPage } from "../../pages/sbom-details/SbomDetailsPage";
10+
import { DetailsPage } from "../../helpers/DetailsPage";
11+
import { SearchPage } from "../../helpers/SearchPage";
12+
import { clickAndVerifyDownload } from "../../pages/Helpers";
13+
import {
14+
downloadLicenseReport,
15+
extractLicenseReport,
16+
findCsvWithHeader,
17+
} from "../../pages/LicenseExportHelpers";
18+
19+
export const { Given, When, Then } = createBdd(test);
20+
21+
let downloadedFilename: string;
22+
let downloadedFilePath: string;
23+
let extractionPath: string;
24+
let packageLicenseFilePath: string;
25+
let licenseReferenceFilePath: string;
26+
27+
Given(
28+
"User Searches for CycloneDX SBOM {string} using Search Text box",
29+
async ({ page }, sbomName: string) => {
30+
const listPage = await SbomListPage.build(page);
31+
const toolbar = await listPage.getToolbar();
32+
await toolbar.applyFilter({ "Filter text": sbomName });
33+
},
34+
);
35+
36+
Given(
37+
"User Searches for CycloneDX SBOM {string} using Search Text box and Navigates to Search results page",
38+
async ({ page }, sbomName: string) => {
39+
const searchPage = new SearchPage(page, "Search");
40+
await searchPage.generalSearch("SBOMs", sbomName);
41+
},
42+
);
43+
44+
When(
45+
"User Selects CycloneDX SBOM {string} from the Search Results",
46+
async ({ page }, sbomName: string) => {
47+
const listPage = await SbomListPage.fromCurrentPage(page);
48+
const table = await listPage.getTable();
49+
await table.waitUntilDataIsLoaded();
50+
51+
await expect(table).toHaveColumnWithValue("Name", sbomName);
52+
},
53+
);
54+
55+
When(
56+
"User Clicks on SBOM name hyperlink from the Search Results",
57+
async ({ page }) => {
58+
const listPage = await SbomListPage.fromCurrentPage(page);
59+
const table = await listPage.getTable();
60+
const rows = await table.getRows();
61+
await rows.first().getByRole("link").click();
62+
},
63+
);
64+
65+
Then("Application Navigates to SBOM Explorer page", async ({ page }) => {
66+
await expect(page).toHaveURL(/\/sboms\/[^/]+/);
67+
});
68+
69+
When('User Clicks "Action" button', async ({ page }) => {
70+
const detailsPage = new DetailsPage(page);
71+
await detailsPage.openActionsMenu();
72+
});
73+
74+
Then('"Download License Report" Option should be visible', async ({ page }) => {
75+
const detailsPage = new DetailsPage(page);
76+
await detailsPage.verifyActionIsVisibleInMenu("Download License Report");
77+
});
78+
79+
When('Selects "Download License Report" option', async ({ page }) => {
80+
const detailsPage = new DetailsPage(page);
81+
downloadedFilename = await clickAndVerifyDownload(
82+
page,
83+
async () =>
84+
await detailsPage.page
85+
.getByRole("menuitem", { name: "Download License Report" })
86+
.click(),
87+
);
88+
});
89+
90+
Then(
91+
"Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name",
92+
async ({ page }) => {
93+
const sbomName = await page.getByRole("heading").first().innerText();
94+
expect(downloadedFilename).toContain(sbomName);
95+
expect(downloadedFilename.endsWith(".tar.gz")).toBeTruthy();
96+
},
97+
);
98+
99+
Given(
100+
"User is on SBOM Explorer page for the CycloneDX SBOM {string}",
101+
async ({ page }, sbomName: string) => {
102+
await SbomDetailsPage.build(page, sbomName);
103+
},
104+
);
105+
106+
When('User Clicks on "Download License Report" button', async ({ page }) => {
107+
const detailsPage = new DetailsPage(page);
108+
await detailsPage.openActionsMenu();
109+
110+
downloadedFilename = await clickAndVerifyDownload(
111+
page,
112+
async () =>
113+
await detailsPage.page
114+
.getByRole("menuitem", { name: "Download License Report" })
115+
.click(),
116+
);
117+
});
118+
119+
Given(
120+
"User has Downloaded the License information for CycloneDX SBOM {string}",
121+
async ({ page }, sbomName: string) => {
122+
await SbomDetailsPage.build(page, sbomName);
123+
downloadedFilePath = await downloadLicenseReport(page);
124+
},
125+
);
126+
127+
When("User extracts the Downloaded license TAR.GZ file", async () => {
128+
extractionPath = await extractLicenseReport(downloadedFilePath);
129+
});
130+
131+
Then(
132+
"Extracted files should contain two CSVs, one for Package license information and another one for License reference",
133+
async () => {
134+
const files = fs.readdirSync(extractionPath);
135+
const csvFiles = files.filter((file) => file.endsWith(".csv"));
136+
expect(csvFiles.length).toBe(2);
137+
},
138+
);
139+
140+
Given(
141+
"User extracted the CycloneDX SBOM {string} license compressed file",
142+
async ({ page }, sbomName: string) => {
143+
await SbomDetailsPage.build(page, sbomName);
144+
downloadedFilePath = await downloadLicenseReport(page);
145+
extractionPath = await extractLicenseReport(downloadedFilePath);
146+
},
147+
);
148+
149+
When("User Opens the package license information file", async () => {
150+
packageLicenseFilePath = findCsvWithHeader(
151+
extractionPath,
152+
"SBOM name",
153+
"Package license information file",
154+
);
155+
});
156+
157+
Then(
158+
"The file should have the following headers - SBOM name, SBOM id, package name, package group, package version, package purl, package cpe and license",
159+
async () => {
160+
const content = fs.readFileSync(packageLicenseFilePath, "utf-8");
161+
const headers = content.split("\n")[0].trim();
162+
const expectedHeaders =
163+
'"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"';
164+
expect(headers).toBe(expectedHeaders);
165+
},
166+
);
167+
168+
When("User Opens the license reference file", async () => {
169+
licenseReferenceFilePath = findCsvWithHeader(
170+
extractionPath,
171+
"licenseId",
172+
"License reference file",
173+
);
174+
});
175+
176+
Then(
177+
"The file should have the following headers - licenseId, name, extracted text and comment",
178+
async () => {
179+
const content = fs.readFileSync(licenseReferenceFilePath, "utf-8");
180+
const headers = content.split("\n")[0].trim();
181+
const expectedHeaders = '"licenseId"\t"name"\t"extracted text"\t"comment"';
182+
expect(headers).toBe(expectedHeaders);
183+
},
184+
);
185+
186+
Given(
187+
"User is on license reference {string} file",
188+
async ({ page }, sbomName: string) => {
189+
await SbomDetailsPage.build(page, sbomName);
190+
downloadedFilePath = await downloadLicenseReport(page);
191+
extractionPath = await extractLicenseReport(downloadedFilePath);
192+
licenseReferenceFilePath = findCsvWithHeader(
193+
extractionPath,
194+
"licenseId",
195+
"License reference file",
196+
);
197+
},
198+
);
199+
200+
Then("The License reference CSV should be empty", async () => {
201+
const content = fs.readFileSync(licenseReferenceFilePath, "utf-8");
202+
const lines = content.trim().split("\n");
203+
// Only header should be present
204+
expect(lines.length).toBe(1);
205+
});

e2e/tests/ui/helpers/DetailsPage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export class DetailsPage {
2222
await this.page.getByRole("menuitem", { name: actionName }).click();
2323
}
2424

25+
async openActionsMenu() {
26+
const actionsButton = this.page.getByRole("button", { name: "Actions" });
27+
await actionsButton.click();
28+
await expect(actionsButton).toHaveAttribute("aria-expanded", "true");
29+
}
30+
2531
async clickOnPageButton(buttonName: string) {
2632
await this.page.getByRole("button", { name: buttonName }).click();
2733
}
@@ -37,6 +43,12 @@ export class DetailsPage {
3743
).toBeVisible();
3844
}
3945

46+
async verifyActionIsVisibleInMenu(actionName: string) {
47+
await expect(
48+
this.page.getByRole("menuitem", { name: actionName }),
49+
).toBeVisible();
50+
}
51+
4052
async verifyButtonIsVisible(button: string) {
4153
await expect(this.page.getByRole("button", { name: button })).toBeVisible();
4254
}

e2e/tests/ui/pages/Helpers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ export const clickAndVerifyDownload = async (
5959
return await verifyDownload(download);
6060
};
6161

62+
/**
63+
* Handles click action that triggers a download and returns the Download object
64+
* @param page The Playwright Page object
65+
* @param clickAction The async function that performs the click
66+
* @returns The Download object
67+
*/
68+
export const clickAndDownload = async (
69+
page: Page,
70+
clickAction: () => Promise<void>,
71+
): Promise<Download> => {
72+
const [download] = await Promise.all([
73+
page.waitForEvent("download"),
74+
clickAction(),
75+
]);
76+
return download;
77+
};
78+
6279
/**
6380
* Verifies comma-delimited expected values against child elements
6481
* Useful for comparing lists like qualifiers, tags, labels, severity values, etc.

0 commit comments

Comments
 (0)