diff --git a/e2e/tests/ui/features/@importer-explorer/importer-explorer.feature b/e2e/tests/ui/features/@importer-explorer/importer-explorer.feature index 6a9a71733..e4a5d6e26 100644 --- a/e2e/tests/ui/features/@importer-explorer/importer-explorer.feature +++ b/e2e/tests/ui/features/@importer-explorer/importer-explorer.feature @@ -1,6 +1,7 @@ Feature: Importer Explorer - View importers details Background: Authentication Given User is authenticated + And All importers are present Scenario: Navigate to Importers page and verify basic elements When User navigates to Importers page @@ -52,9 +53,9 @@ Feature: Importer Explorer - View importers details Then The "" importer state should be "Disabled" Examples: - | importerName | - | cve | - + | importerName | + | fake-importer-disable | + Scenario: Verify disabled importer does not show Disable option When User navigates to Importers page @@ -74,8 +75,8 @@ Feature: Importer Explorer - View importers details Then The "" importer state should be "Disabled" Examples: - | importerName | - | clearly-defined-curations | + | importerName | + | fake-importer-enable | Scenario Outline: Run an enabled importer When User navigates to Importers page @@ -90,6 +91,5 @@ Feature: Importer Explorer - View importers details Then The "" importer state should be "Disabled" Examples: - | importerName | - | cve | - \ No newline at end of file + | importerName | + | fake-importer-run | diff --git a/e2e/tests/ui/features/@importer-explorer/importer-explorer.step.ts b/e2e/tests/ui/features/@importer-explorer/importer-explorer.step.ts index 1c8a626cd..85d5d97c2 100644 --- a/e2e/tests/ui/features/@importer-explorer/importer-explorer.step.ts +++ b/e2e/tests/ui/features/@importer-explorer/importer-explorer.step.ts @@ -1,6 +1,7 @@ import { createBdd } from "playwright-bdd"; import { expect } from "../../assertions"; import { test } from "../../fixtures"; +import { ensureAllImportersExist } from "../../helpers/Importer"; import { DeletionConfirmDialog } from "../../pages/ConfirmDialog"; import { ImporterListPage } from "../../pages/importer-list/ImporterListPage"; @@ -11,6 +12,10 @@ When("User navigates to Importers page", async ({ page }) => { await ImporterListPage.build(page); }); +When("All importers are present", async ({ page }) => { + await ensureAllImportersExist(page); +}); + // Assertions - Page structure Then("The page title is {string}", async ({ page }, expectedTitle) => { await expect( @@ -361,6 +366,8 @@ When("User disables the {string} importer", async ({ page }, importerName) => { When( "User enables a disabled importer {string}", async ({ page }, importerName) => { + test.setTimeout(90_000); + const listPage = new ImporterListPage(page); // Verify the importer is actually disabled before enabling diff --git a/e2e/tests/ui/helpers/Auth.ts b/e2e/tests/ui/helpers/Auth.ts index 993a66b2c..c7b3e8f90 100644 --- a/e2e/tests/ui/helpers/Auth.ts +++ b/e2e/tests/ui/helpers/Auth.ts @@ -5,6 +5,27 @@ import { AUTH_USER, } from "../../common/constants"; +export const getAccessToken = (page: Page): Promise => + page.evaluate(() => { + for (let i = 0; i < window.sessionStorage.length; i++) { + const key = window.sessionStorage.key(i); + if (key?.startsWith("oidc.user:")) { + const raw = window.sessionStorage.getItem(key); + if (raw) { + try { + return ( + (JSON.parse(raw) as { access_token?: string }).access_token ?? + null + ); + } catch { + return null; + } + } + } + } + return null; + }); + export const login = async (page: Page) => { if (AUTH_REQUIRED === "true") { const userName = AUTH_USER; diff --git a/e2e/tests/ui/helpers/Importer.ts b/e2e/tests/ui/helpers/Importer.ts new file mode 100644 index 000000000..9861f9acd --- /dev/null +++ b/e2e/tests/ui/helpers/Importer.ts @@ -0,0 +1,135 @@ +import { type Page } from "@playwright/test"; +import { AUTH_REQUIRED, TRUSTIFY_API_URL } from "../../common/constants"; +import { getAccessToken } from "./Auth"; + +export interface ImporterConfig { + [importerType: string]: object; +} + +export const IMPORTER_CONFIGS: Record = { + "clearly-defined-curations": { + clearlyDefinedCuration: { + disabled: true, + period: "1h", + description: "Community-curated ClearlyDefined licenses", + source: "https://github.com/clearlydefined/curated-data", + types: [ + "pypi", + "deb", + "npm", + "pod", + "nuget", + "crate", + "git", + "composer", + "go", + "maven", + "gem", + ], + }, + }, + importer1: { cwe: { disabled: true, period: "1d" } }, + importer2: { cwe: { disabled: true, period: "1d" } }, + importer3: { cwe: { disabled: true, period: "1d" } }, + importer4: { cwe: { disabled: true, period: "1d" } }, + importer5: { cwe: { disabled: true, period: "1d" } }, + cve: { + cve: { + disabled: true, + period: "5m", + description: "CVE List V5", + source: "https://github.com/CVEProject/cvelistV5", + }, + }, + "cve-from-2024": { + cve: { + disabled: true, + period: "5m", + description: "CVE List V5 (starting 2024)", + source: "https://github.com/CVEProject/cvelistV5", + startYear: 2024, + }, + }, + "fake-importer-disable": { + osv: { + disabled: true, + period: "1day", + description: "GitHub Advisory Database", + source: "https://github.com/matejnesuta/sample_advisories", + path: "advisories", + }, + }, + "fake-importer-enable": { + osv: { + disabled: true, + period: "1day", + description: "GitHub Advisory Database", + source: "https://github.com/matejnesuta/sample_advisories", + path: "advisories", + }, + }, + "fake-importer-run": { + osv: { + disabled: true, + period: "1day", + description: "GitHub Advisory Database", + source: "https://github.com/matejnesuta/sample_advisories", + path: "advisories", + }, + }, +}; + +export const ensureAllImportersExist = async (page: Page): Promise => { + // Ensure the app is loaded so sessionStorage (and the OIDC token) is + // accessible. When auth is disabled the page may still be on about:blank, + // where page.evaluate() throws a SecurityError. + await page.goto("/importers"); + + // Use the API base URL (which may differ from the UI host on downstream + // deployments) so importer creation targets the real API and not the UI's + // SPA fallback, which would answer /api/v3/... with a 200 HTML page. + const baseUrl = TRUSTIFY_API_URL.replace(/\/+$/, ""); + const accessToken = await getAccessToken(page); + if (AUTH_REQUIRED === "true" && !accessToken) { + throw new Error( + "ensureAllImportersExist: auth is required but no OIDC access token was " + + "found in sessionStorage. Ensure login() ran and succeeded before this.", + ); + } + const headers: Record = {}; + if (accessToken) headers["Authorization"] = `Bearer ${accessToken}`; + + for (const [importerName, importerConfig] of Object.entries( + IMPORTER_CONFIGS, + )) { + const url = `${baseUrl}/api/v3/importer/${encodeURIComponent(importerName)}`; + const checkResponse = await page.request.get(url, { headers }); + const checkStatus = checkResponse.status(); + + if (checkStatus === 200) { + continue; // Already exists. + } + if (checkStatus !== 404) { + throw new Error( + `ensureAllImportersExist: unexpected status ${checkStatus} checking ` + + `"${importerName}" at ${url}. Body: ${await checkResponse.text()}`, + ); + } + + const createResponse = await page.request.post(url, { + headers: { ...headers, "Content-Type": "application/json" }, + data: importerConfig, + }); + // 409 means another parallel worker created it between our GET and POST; + // the importer now exists, which is exactly what we want. + if (!createResponse.ok() && createResponse.status() !== 409) { + throw new Error( + `ensureAllImportersExist: failed to create "${importerName}" ` + + `(status ${createResponse.status()}). Body: ${await createResponse.text()}`, + ); + } + } + // Importers are created server-side via the API above. Each scenario's first + // step navigates to the Importers page (fetching fresh data), so there is no + // need to reload/re-render here. +};