Skip to content

Add LicenseExport feature CDX tests#942

Open
a-oren wants to merge 2 commits intoguacsec:mainfrom
a-oren:tests/license-explorer
Open

Add LicenseExport feature CDX tests#942
a-oren wants to merge 2 commits intoguacsec:mainfrom
a-oren:tests/license-explorer

Conversation

@a-oren
Copy link

@a-oren a-oren commented Mar 1, 2026

test(e2e): add license export tests for CycloneDX SBOMs

Add end-to-end tests that verify the Download License Report workflow
for CycloneDX SBOMs, covering:

  • Verifying the Download License Report option visibility on both the
    SBOM search results page and the SBOM Explorer page
  • Downloading the license report in TAR.GZ format
  • Extracting and validating the archive contents (two CSV files)
  • Verifying CSV headers for package license and license reference files
  • Validating package-level license data including SBOM name, id, group,
    version, purl, cpe, and license columns

Supporting changes:

  • Add openActionsMenu() and verifyActionIsVisibleInMenu() to DetailsPage
  • Add clickAndDownload() helper to Helpers.ts
  • Add LicenseExportHelpers with download, extract, TSV parsing, and
    package row lookup utilities

Summary by Sourcery

Add end-to-end UI tests for downloading and validating CycloneDX SBOM license reports and introduce shared helpers for license report downloads and menu interactions.

New Features:

  • Add BDD feature and step definitions to verify CycloneDX SBOM license report download workflows from search results and SBOM Explorer pages, including file format, contents, and CSV headers.
  • Introduce helpers to download, extract, and parse CycloneDX license report archives and locate specific package rows in TSV data.

Enhancements:

  • Extend shared UI helpers with generic click-and-download support and actions menu utilities on the SBOM details page to support license export flows.

Tests:

  • Add comprehensive e2e scenarios covering visibility of the Download License Report action, successful TAR.GZ download, archive structure, CSV headers, and package-level license data for CycloneDX SBOMs.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 1, 2026

Reviewer's Guide

Adds end-to-end Playwright BDD coverage for the CycloneDX SBOM "Download License Report" workflow, including new helpers for triggering and validating downloads, extracting the TAR.GZ license archive, parsing TSV/CSV contents, and asserting package-level license metadata, along with small enhancements to the shared DetailsPage and Helpers utilities.

File-Level Changes

Change Details Files
Introduce a generic helper to wait for and return a Playwright Download object when a click triggers a download.
  • Add clickAndDownload(page, clickAction) that races page.waitForEvent('download') with an injected click action.
  • Document the helper and reuse the existing click-and-verify-download pattern by returning the Download for further assertions.
e2e/tests/ui/pages/Helpers.ts
Extend DetailsPage to support opening the Actions menu and asserting that specific menu items are visible.
  • Add openActionsMenu() that clicks the Actions button and asserts aria-expanded='true'.
  • Add verifyActionIsVisibleInMenu(actionName) to check visibility of a named menu item.
e2e/tests/ui/helpers/DetailsPage.ts
Add CycloneDX License Export BDD feature and step definitions to exercise the UI download flow and validate archive structure and TSV content.
  • Create license-export_cdx.feature defining scenarios for visibility of the Download License Report action, downloading from search and explorer pages, validating TAR.GZ contents, verifying CSV headers, and checking package-level license fields for specific SBOMs and packages.
  • Implement license-export_cdx.step.ts with Given/When/Then steps that drive SbomListPage/SearchPage/SbomDetailsPage, trigger downloads, and assert URL navigation and file naming.
  • Use Node fs/path to read extracted CSV files and Playwright expect to assert headers, file counts, and that license-reference CSV is empty.
  • Add steps that select package rows from the parsed TSV and compare each relevant column to example values, handling embedded newlines in fields.
e2e/tests/ui/features/@license-export_cdx/license-export_cdx.feature
e2e/tests/ui/features/@license-export_cdx/license-export_cdx.step.ts
Introduce LicenseExportHelpers utilities to download the license report, extract the TAR.GZ, locate CSVs by header, parse TSV content robustly, and find package rows by name.
  • Add downloadLicenseReport(page) that opens the Actions menu, uses clickAndDownload, and returns the downloaded file path with error handling when path() is null.
  • Add extractLicenseReport(downloadedFilePath) that creates an 'extracted' directory next to the download and runs 'tar -xzf' via promisified child_process.exec.
  • Add findCsvWithHeader(extractionPath, headerIdentifier, fileDescription) to locate the correct CSV by inspecting its header line and throw a helpful error if not found.
  • Implement parseTsvLine, splitTsvLines (preserving quoted fields containing newlines), and parseTsvFile to turn TSV content into header + row records.
  • Add findPackageRow(filePath, packageName) to retrieve a row where "package name" matches the given package, or throw with a list of available package names.
e2e/tests/ui/pages/LicenseExportHelpers.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In extractLicenseReport, the tar -xzf ${downloadedFilePath} -C ${extractionPath} command is built without quoting or escaping paths, which will break on paths with spaces or special characters; consider using proper shell escaping or spawn with argument arrays instead of exec with string interpolation.
  • The step definitions rely on shared module-level variables like downloadedFilename, extractionPath, and selectedPackageRow, which can lead to subtle interference between scenarios (especially under parallel execution); it would be more robust to keep this state in the test context/fixtures or a per-scenario world object.
  • In license-export_cdx.step.ts there is now both direct usage of clickAndVerifyDownload and the new downloadLicenseReport helper (which in turn uses clickAndDownload); consider standardizing on one approach to avoid duplicated download logic and make future changes easier.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `extractLicenseReport`, the `tar -xzf ${downloadedFilePath} -C ${extractionPath}` command is built without quoting or escaping paths, which will break on paths with spaces or special characters; consider using proper shell escaping or `spawn` with argument arrays instead of `exec` with string interpolation.
- The step definitions rely on shared module-level variables like `downloadedFilename`, `extractionPath`, and `selectedPackageRow`, which can lead to subtle interference between scenarios (especially under parallel execution); it would be more robust to keep this state in the test context/fixtures or a per-scenario world object.
- In `license-export_cdx.step.ts` there is now both direct usage of `clickAndVerifyDownload` and the new `downloadLicenseReport` helper (which in turn uses `clickAndDownload`); consider standardizing on one approach to avoid duplicated download logic and make future changes easier.

## Individual Comments

### Comment 1
<location path="e2e/tests/ui/features/@license-export_cdx/license-export_cdx.step.ts" line_range="23-28" />
<code_context>
+
+export const { Given, When, Then } = createBdd(test);
+
+let downloadedFilename: string;
+let downloadedFilePath: string;
+let extractionPath: string;
+let packageLicenseFilePath: string;
+let licenseReferenceFilePath: string;
+let selectedPackageRow: Record<string, string>;
+
+Given(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Shared mutable state between steps can cause flakiness and cross-scenario leakage; consider scoping state per scenario/test.

These `let` variables are effectively global to all steps in this module, so scenarios can interfere with each other (especially under parallel execution or reuse within a worker). Please move this state to a per-scenario context (e.g., Playwright test fixtures or the BDD world/context) or, at minimum, fully reset each variable in `Before`/`After` hooks so no scenario can see stale values from another.

Suggested implementation:

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

```

```typescript
let downloadedFilename: string;
let downloadedFilePath: string;
let extractionPath: string;
let packageLicenseFilePath: string;
let licenseReferenceFilePath: string;
let selectedPackageRow: Record<string, string>;

Before(async () => {
  downloadedFilename = "";
  downloadedFilePath = "";
  extractionPath = "";
  packageLicenseFilePath = "";
  licenseReferenceFilePath = "";
  selectedPackageRow = {};
});

Given(

```
</issue_to_address>

### Comment 2
<location path="e2e/tests/ui/pages/LicenseExportHelpers.ts" line_range="26-33" />
<code_context>
+  return downloadPath;
+};
+
+export const extractLicenseReport = async (downloadedFilePath: string): Promise<string> => {
+  const execAsync = promisify(exec);
+  const extractionPath = path.join(path.dirname(downloadedFilePath), "extracted");
+  if (!fs.existsSync(extractionPath)) {
+    fs.mkdirSync(extractionPath);
+  }
+
+  await execAsync(`tar -xzf ${downloadedFilePath} -C ${extractionPath}`);
+  return extractionPath;
+};
</code_context>
<issue_to_address>
**suggestion:** Extraction helper assumes a `tar` binary and paths without spaces; consider hardening and/or adding tests for these edge cases.

This helper shells out to `tar` and interpolates `downloadedFilePath` directly into the command, which will fail if the download/extraction paths contain spaces or shell metacharacters, or if `tar` is missing or incompatible. To keep e2e tests robust, consider switching to a Node tar library, or at least add tests covering paths with spaces and asserting a clear error when `tar` fails.

Suggested implementation:

```typescript
export const extractLicenseReport = async (downloadedFilePath: string): Promise<string> => {
  const execFileAsync = promisify(execFile);
  const extractionPath = path.join(path.dirname(downloadedFilePath), "extracted");

  if (!fs.existsSync(extractionPath)) {
    // Use recursive to be robust against nested paths and concurrent tests
    fs.mkdirSync(extractionPath, { recursive: true });
  }

  try {
    await execFileAsync("tar", ["-xzf", downloadedFilePath, "-C", extractionPath]);
  } catch (error: any) {
    const stderr = error?.stderr ?? "";
    throw new Error(
      `Failed to extract license report with tar: ${stderr || error?.message || "unknown error"}`,
    );
  }

  return extractionPath;
}

```

1. Ensure `execFile` is imported at the top of `e2e/tests/ui/pages/LicenseExportHelpers.ts`:
   - Add `execFile` to the existing `child_process` import, e.g.:
     - `import { execFile } from "child_process";`
2. Add e2e tests that:
   - Verify extraction works when the download path and extraction directory contain spaces.
   - Assert that when `tar` fails (e.g., corrupt archive or `tar` missing in PATH, if you can simulate it), the thrown error includes the clear message `"Failed to extract license report with tar"`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +23 to +28
let downloadedFilename: string;
let downloadedFilePath: string;
let extractionPath: string;
let packageLicenseFilePath: string;
let licenseReferenceFilePath: string;
let selectedPackageRow: Record<string, string>;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Shared mutable state between steps can cause flakiness and cross-scenario leakage; consider scoping state per scenario/test.

These let variables are effectively global to all steps in this module, so scenarios can interfere with each other (especially under parallel execution or reuse within a worker). Please move this state to a per-scenario context (e.g., Playwright test fixtures or the BDD world/context) or, at minimum, fully reset each variable in Before/After hooks so no scenario can see stale values from another.

Suggested implementation:

export const { Given, When, Then, Before } = createBdd(test);
let downloadedFilename: string;
let downloadedFilePath: string;
let extractionPath: string;
let packageLicenseFilePath: string;
let licenseReferenceFilePath: string;
let selectedPackageRow: Record<string, string>;

Before(async () => {
  downloadedFilename = "";
  downloadedFilePath = "";
  extractionPath = "";
  packageLicenseFilePath = "";
  licenseReferenceFilePath = "";
  selectedPackageRow = {};
});

Given(

Comment on lines +26 to +33
export const extractLicenseReport = async (downloadedFilePath: string): Promise<string> => {
const execAsync = promisify(exec);
const extractionPath = path.join(path.dirname(downloadedFilePath), "extracted");
if (!fs.existsSync(extractionPath)) {
fs.mkdirSync(extractionPath);
}

await execAsync(`tar -xzf ${downloadedFilePath} -C ${extractionPath}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Extraction helper assumes a tar binary and paths without spaces; consider hardening and/or adding tests for these edge cases.

This helper shells out to tar and interpolates downloadedFilePath directly into the command, which will fail if the download/extraction paths contain spaces or shell metacharacters, or if tar is missing or incompatible. To keep e2e tests robust, consider switching to a Node tar library, or at least add tests covering paths with spaces and asserting a clear error when tar fails.

Suggested implementation:

export const extractLicenseReport = async (downloadedFilePath: string): Promise<string> => {
  const execFileAsync = promisify(execFile);
  const extractionPath = path.join(path.dirname(downloadedFilePath), "extracted");

  if (!fs.existsSync(extractionPath)) {
    // Use recursive to be robust against nested paths and concurrent tests
    fs.mkdirSync(extractionPath, { recursive: true });
  }

  try {
    await execFileAsync("tar", ["-xzf", downloadedFilePath, "-C", extractionPath]);
  } catch (error: any) {
    const stderr = error?.stderr ?? "";
    throw new Error(
      `Failed to extract license report with tar: ${stderr || error?.message || "unknown error"}`,
    );
  }

  return extractionPath;
}
  1. Ensure execFile is imported at the top of e2e/tests/ui/pages/LicenseExportHelpers.ts:
    • Add execFile to the existing child_process import, e.g.:
      • import { execFile } from "child_process";
  2. Add e2e tests that:
    • Verify extraction works when the download path and extraction directory contain spaces.
    • Assert that when tar fails (e.g., corrupt archive or tar missing in PATH, if you can simulate it), the thrown error includes the clear message "Failed to extract license report with tar".

@a-oren a-oren force-pushed the tests/license-explorer branch 5 times, most recently from 864b6d3 to b886f7e Compare March 2, 2026 11:47
@codecov
Copy link

codecov bot commented Mar 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.18%. Comparing base (56d5583) to head (b4b2858).
⚠️ Report is 36 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #942      +/-   ##
==========================================
+ Coverage   64.98%   67.18%   +2.19%     
==========================================
  Files         195      218      +23     
  Lines        3339     3828     +489     
  Branches      751      873     +122     
==========================================
+ Hits         2170     2572     +402     
- Misses        872      918      +46     
- Partials      297      338      +41     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@a-oren a-oren force-pushed the tests/license-explorer branch from b886f7e to 26694fa Compare March 2, 2026 12:17
@a-oren a-oren force-pushed the tests/license-explorer branch from 26694fa to 03fd60f Compare March 2, 2026 12:20
@carlosthe19916
Copy link
Collaborator

@a-oren seems like this PR is passing CI, good job! Please assign the QE engineers as reviewers, otherwise they might not see this PR

Copy link
Contributor

@mrrajan mrrajan left a comment

Choose a reason for hiding this comment

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

Thanks for the PR @a-oren I have requested for some changes.

).toBeVisible();
}

async verifyActionIsVisibleInMenu(actionName: string) {
Copy link
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.

fs.mkdirSync(extractionPath);
}

await execAsync(`tar -xzf ${downloadedFilePath} -C ${extractionPath}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Use execFileAsync instead


const savePath = path.join(
os.tmpdir(),
`license-report-${Date.now()}-${download.suggestedFilename()}`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion - Sanitize the filepath to replace special characters like "/","" and "."

@a-oren
Copy link
Author

a-oren commented Mar 15, 2026

@mrrajan Thanks for the review, I added your recommendation

@a-oren a-oren force-pushed the tests/license-explorer branch from 31d7378 to b4b2858 Compare March 15, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants