From 350f2bdbe82ec7f5477a042d1735ea35a267530c Mon Sep 17 00:00:00 2001 From: amalykhi Date: Tue, 23 Jun 2026 12:27:55 +0200 Subject: [PATCH 1/5] EDM-4253: Add Trustify vulnerability integration for UI tests Add comprehensive E2E tests for vulnerability reporting across Device, Fleet, and Overview pages in the FlightCtl UI. New files: - cypress/e2e/vulnerability.cy.js: Complete E2E test suite - cypress/views/securityPage.js: Reusable page object with selectors Updated: - cypress/views/devicesPage.js: Added viewDeviceSecurity() helper - cypress/views/fleetsPage.js: Added viewFleetSecurity() helper Test coverage: - Initial state verification (0 vulnerabilities) - Fleet creation with vulnerable CentOS image - Device attachment and CVE appearance - CVE details panel, filtering, and search - Verification across Device, Fleet, and Overview pages - Device detachment and CVE cleanup Test image: quay.io/centos-bootc/centos-bootc:stream9 Expected CVEs: 7 total (1 Critical, 1 High, 4 Medium, 1 Low) Prerequisites: - FlightCtl with flightctl_trustify_enabled: true - Trustify with SBOM data for test image - At least one enrolled device Co-Authored-By: Claude Sonnet 4.5 --- cypress/e2e/vulnerability.cy.js | 373 ++++++++++++++++++++++++++++++++ cypress/views/devicesPage.js | 10 + cypress/views/fleetsPage.js | 10 + cypress/views/securityPage.js | 257 ++++++++++++++++++++++ 4 files changed, 650 insertions(+) create mode 100644 cypress/e2e/vulnerability.cy.js create mode 100644 cypress/views/securityPage.js diff --git a/cypress/e2e/vulnerability.cy.js b/cypress/e2e/vulnerability.cy.js new file mode 100644 index 0000000..ace87d1 --- /dev/null +++ b/cypress/e2e/vulnerability.cy.js @@ -0,0 +1,373 @@ +import { securityPage, KNOWN_VULNERABLE_IMAGE } from '../views/securityPage' +import { devicesPage } from '../views/devicesPage' +import { common } from '../views/common' + +/** + * Vulnerability Reporting End-to-End Tests + * + * Prerequisites: + * - Jenkins job must run with flightctl_trustify_enabled: true + * - Trustify must have SBOM data for the vulnerable test image + * - At least one device must be enrolled and available for testing + * + * Test Image Details: + * - Image: quay.io/centos-bootc/centos-bootc:stream9 + * - Digest: sha256:0395113f05c2fe5544ddb693603abaaee36f71d46c810526efd270327db6b8bd + * - CVEs: 7 total (1 Critical, 1 High, 4 Medium, 1 Low) + */ + +describe('Vulnerability Reporting', () => { + const FLEET_NAME = 'test-fleet-vuln-' + Date.now() + let TEST_DEVICE_NAME = null + + const API = Cypress.env('FLIGHTCTL_API') + + before(() => { + cy.ensureLoggedIn() + + // Find an enrolled device to use for testing + cy.request(`${API}/api/v1/devices`).then((resp) => { + const devices = resp.body.items || [] + const enrolledDevices = devices.filter((d) => d.metadata.labels?.enrollment !== 'pending') + + if (enrolledDevices.length === 0) { + throw new Error('No enrolled devices available for vulnerability testing') + } + + TEST_DEVICE_NAME = enrolledDevices[0].metadata.name + cy.log(`Using device: ${TEST_DEVICE_NAME}`) + }) + }) + + after(() => { + // Cleanup: Delete test fleet if it exists + if (FLEET_NAME) { + cy.request({ + method: 'DELETE', + url: `${API}/api/v1/fleets/${FLEET_NAME}`, + failOnStatusCode: false, + }) + } + + // Cleanup: Remove fleet label from device if it was added + if (TEST_DEVICE_NAME) { + cy.request({ + method: 'PATCH', + url: `${API}/api/v1/devices/${TEST_DEVICE_NAME}`, + body: { + metadata: { + labels: { + fleet: null, + }, + }, + }, + failOnStatusCode: false, + }) + } + }) + + /** + * Helper: Wait for device status to be UpToDate + */ + const waitForDeviceUpToDate = (deviceName, timeout = 300000) => { + cy.log(`Waiting for device ${deviceName} to reach UpToDate status...`) + cy.waitUntil( + () => + cy.request(`${API}/api/v1/devices/${deviceName}`).then((resp) => { + const status = resp.body?.status?.updated?.status + cy.log(`Device status: ${status}`) + return status === 'UpToDate' + }), + { + timeout, + interval: 5000, + errorMsg: `Device ${deviceName} did not reach UpToDate status within ${timeout}ms`, + }, + ) + } + + /** + * Helper: Wait for device to report specific digest + */ + const waitForDeviceDigest = (deviceName, expectedDigest, timeout = 300000) => { + cy.log(`Waiting for device ${deviceName} to update to digest ${expectedDigest}...`) + cy.waitUntil( + () => + cy.request(`${API}/api/v1/devices/${deviceName}`).then((resp) => { + const currentDigest = resp.body?.status?.os?.imageDigest + cy.log(`Current digest: ${currentDigest}`) + return currentDigest === expectedDigest + }), + { + timeout, + interval: 5000, + errorMsg: `Device did not update to digest ${expectedDigest} within ${timeout}ms`, + }, + ) + // Wait extra 15s for vulnerability sync (runs every 10s) + cy.log('Waiting additional 15s for vulnerability sync...') + cy.wait(15000) + } + + describe('Initial State - No Vulnerabilities', () => { + it('should show 0 vulnerabilities on Overview page', () => { + cy.visit('/overview') + securityPage.expectSecurityOverviewVisible() + securityPage.expectNoVulnerabilities() + }) + + it('should show 0 vulnerabilities on Device page', () => { + cy.visit(`/devices/${TEST_DEVICE_NAME}`) + cy.wait(1000) // Allow page to load + + // Check if Security overview section exists and shows no vulnerabilities + cy.get('body').then(($body) => { + if ($body.text().includes('Security overview')) { + securityPage.expectNoVulnerabilities() + } else { + cy.log('Security overview section not present yet') + } + }) + }) + }) + + describe('Create Fleet with Vulnerable Image', () => { + it('should create a fleet with the vulnerable CentOS image', () => { + cy.request('POST', `${API}/api/v1/fleets`, { + apiVersion: 'v1alpha1', + kind: 'Fleet', + metadata: { + name: FLEET_NAME, + }, + spec: { + template: { + spec: { + os: { + image: KNOWN_VULNERABLE_IMAGE.image, + }, + }, + }, + }, + }).then((response) => { + expect(response.status).to.eq(201) + cy.log(`Fleet ${FLEET_NAME} created successfully`) + }) + }) + + it('should verify fleet shows 0 CVEs before device attachment', () => { + cy.visit(`/fleets/${FLEET_NAME}`) + cy.wait(1000) + + // Fleet has vulnerable image spec, but no devices running it yet + cy.get('body').then(($body) => { + if ($body.text().includes('Security overview')) { + securityPage.expectNoVulnerabilities() + } else { + cy.log('Security overview section not present yet on fleet') + } + }) + }) + }) + + describe('Attach Device to Fleet and Wait for CVEs', () => { + it('should attach device to vulnerable fleet', () => { + cy.request('PATCH', `${API}/api/v1/devices/${TEST_DEVICE_NAME}`, { + metadata: { + labels: { + fleet: FLEET_NAME, + }, + }, + }).then((response) => { + expect(response.status).to.eq(200) + cy.log(`Device ${TEST_DEVICE_NAME} attached to fleet ${FLEET_NAME}`) + }) + }) + + it('should wait for device to pull vulnerable image and reach UpToDate', () => { + // Wait for device to pull the new image + waitForDeviceDigest(TEST_DEVICE_NAME, KNOWN_VULNERABLE_IMAGE.digest) + + // Wait for device status to be UpToDate + waitForDeviceUpToDate(TEST_DEVICE_NAME) + + cy.log('Device is now running the vulnerable image and is UpToDate') + }) + }) + + describe('Verify CVEs on Device Page', () => { + beforeEach(() => { + cy.visit(`/devices/${TEST_DEVICE_NAME}`) + cy.wait(2000) // Allow vulnerability data to load + }) + + it('should display total vulnerability count', () => { + securityPage.expectVulnerabilityCount(KNOWN_VULNERABLE_IMAGE.totalCount) + }) + + it('should display severity breakdown', () => { + securityPage.expectSeverityCounts(KNOWN_VULNERABLE_IMAGE.counts) + }) + + it('should display Critical CVEs', () => { + securityPage.expectCvesVisible(KNOWN_VULNERABLE_IMAGE.cves.critical) + }) + + it('should display High CVEs', () => { + securityPage.expectCvesVisible(KNOWN_VULNERABLE_IMAGE.cves.high) + }) + + it('should open CVE details panel when clicking on a CVE', () => { + const criticalCve = KNOWN_VULNERABLE_IMAGE.cves.critical[0] + + securityPage.clickCve(criticalCve) + securityPage.expectCveDetailsVisible() + securityPage.expectSeverityInDetails('Critical') + securityPage.expectPackageInDetails('glibc') + securityPage.closeCveDetails() + }) + + it('should filter vulnerabilities by Critical severity', () => { + securityPage.filterBySeverity('Critical') + securityPage.expectTableRowCount(KNOWN_VULNERABLE_IMAGE.counts.critical) + securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.critical[0]) + securityPage.clearFilters() + }) + + it('should filter vulnerabilities by High severity', () => { + securityPage.filterBySeverity('High') + securityPage.expectTableRowCount(KNOWN_VULNERABLE_IMAGE.counts.high) + securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.high[0]) + securityPage.clearFilters() + }) + + it('should search for specific CVE by ID', () => { + const searchCve = KNOWN_VULNERABLE_IMAGE.cves.critical[0] + securityPage.searchVulnerabilities(searchCve) + securityPage.expectCveVisible(searchCve) + securityPage.clearSearch() + }) + }) + + describe('Verify CVEs on Fleet Page', () => { + beforeEach(() => { + cy.visit(`/fleets/${FLEET_NAME}`) + cy.wait(2000) + }) + + it('should display total vulnerability count on fleet', () => { + securityPage.expectVulnerabilityCount(KNOWN_VULNERABLE_IMAGE.totalCount) + }) + + it('should display severity breakdown on fleet', () => { + securityPage.expectSeverityCounts(KNOWN_VULNERABLE_IMAGE.counts) + }) + + it('should display CVEs on fleet page', () => { + securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.critical[0]) + securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.high[0]) + }) + + it('should open CVE details from fleet page', () => { + const highCve = KNOWN_VULNERABLE_IMAGE.cves.high[0] + + securityPage.clickCve(highCve) + securityPage.expectCveDetailsVisible() + securityPage.expectSeverityInDetails('High') + securityPage.closeCveDetails() + }) + + it('should filter vulnerabilities on fleet page', () => { + securityPage.filterBySeverity('High') + securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.high[0]) + securityPage.clearFilters() + }) + }) + + describe('Verify CVEs on Overview Page', () => { + beforeEach(() => { + cy.visit('/overview') + cy.wait(2000) + }) + + it('should display aggregated vulnerabilities on overview', () => { + securityPage.expectVulnerabilityCount(KNOWN_VULNERABLE_IMAGE.totalCount) + }) + + it('should display severity breakdown on overview', () => { + securityPage.expectSeverityCounts(KNOWN_VULNERABLE_IMAGE.counts) + }) + }) + + describe('Detach Device and Verify CVEs Disappear', () => { + it('should detach device from fleet', () => { + cy.request('PATCH', `${API}/api/v1/devices/${TEST_DEVICE_NAME}`, { + metadata: { + labels: { + fleet: null, + }, + }, + }).then((response) => { + expect(response.status).to.eq(200) + cy.log(`Device ${TEST_DEVICE_NAME} detached from fleet ${FLEET_NAME}`) + }) + }) + + it('should wait for device to rollback from vulnerable image', () => { + cy.log('Waiting for device to rollback to original image...') + cy.waitUntil( + () => + cy.request(`${API}/api/v1/devices/${TEST_DEVICE_NAME}`).then((resp) => { + const currentDigest = resp.body?.status?.os?.imageDigest + const hasRolledBack = currentDigest !== KNOWN_VULNERABLE_IMAGE.digest + if (hasRolledBack) { + cy.log(`Device rolled back to: ${currentDigest}`) + } + return hasRolledBack + }), + { + timeout: 300000, + interval: 5000, + errorMsg: 'Device did not rollback from vulnerable image', + }, + ) + + // Wait for device to be UpToDate again + waitForDeviceUpToDate(TEST_DEVICE_NAME) + + // Wait for vulnerability sync to clear + cy.log('Waiting for vulnerability sync to clear...') + cy.wait(15000) + }) + + it('should verify CVEs disappeared from Device page', () => { + cy.visit(`/devices/${TEST_DEVICE_NAME}`) + cy.wait(2000) + + securityPage.expectNoVulnerabilities() + }) + + it('should verify CVEs disappeared from Fleet page', () => { + cy.visit(`/fleets/${FLEET_NAME}`) + cy.wait(2000) + + // Fleet has no devices attached now, should show 0 CVEs + securityPage.expectNoVulnerabilities() + }) + + it('should verify CVEs disappeared from Overview page', () => { + cy.visit('/overview') + cy.wait(2000) + + securityPage.expectNoVulnerabilities() + }) + }) + + describe('Cleanup', () => { + it('should delete test fleet', () => { + cy.request('DELETE', `${API}/api/v1/fleets/${FLEET_NAME}`).then((response) => { + expect(response.status).to.eq(200) + cy.log(`Fleet ${FLEET_NAME} deleted successfully`) + }) + }) + }) +}) diff --git a/cypress/views/devicesPage.js b/cypress/views/devicesPage.js index af0b3ea..2f210aa 100644 --- a/cypress/views/devicesPage.js +++ b/cypress/views/devicesPage.js @@ -467,4 +467,14 @@ export const devicesPage = { cy.contains('button', 'Add label').click() cy.get('input[aria-label="New label"]').clear().type(`${labelText}{enter}`) }, + + /** + * Navigate to device details and scroll to Security overview section + * @param {string} deviceName - Device name or reference + */ + viewDeviceSecurity: (deviceName) => { + cy.visit(`/devices/${deviceName}`) + cy.wait(1000) + cy.contains('Security overview').should('be.visible').scrollIntoView() + }, } diff --git a/cypress/views/fleetsPage.js b/cypress/views/fleetsPage.js index 0d4f5b4..cbcbf25 100644 --- a/cypress/views/fleetsPage.js +++ b/cypress/views/fleetsPage.js @@ -150,4 +150,14 @@ export const fleetsPage = { cy.get('[data-testid="wizard-save-button"]').click() cy.get('td[data-label="Status"]', { timeout: 1000000 }).should('contain', 'Valid') }, + + /** + * Navigate to fleet details and scroll to Security overview section + * @param {string} fleetName - Fleet name + */ + viewFleetSecurity: (fleetName) => { + cy.visit(`/fleets/${fleetName}`) + cy.wait(1000) + cy.contains('Security overview').should('be.visible').scrollIntoView() + }, } diff --git a/cypress/views/securityPage.js b/cypress/views/securityPage.js new file mode 100644 index 0000000..bbc1451 --- /dev/null +++ b/cypress/views/securityPage.js @@ -0,0 +1,257 @@ +import { common } from './common' + +/** + * Security/Vulnerability UI selectors and helpers. + * Used across Device, Fleet, and Overview pages. + */ + +/** Security overview card title */ +const SECURITY_OVERVIEW_CARD = '.pf-v6-c-card__title-text:contains("Security overview")' + +/** Filter by severity dropdown button */ +const SEVERITY_FILTER_TOGGLE = 'button[aria-label="Filter by severity"]' + +/** Search/find vulnerabilities by name input */ +const VULNERABILITY_SEARCH_INPUT = 'input[aria-label="Find by name"]' + +/** Vulnerabilities table */ +const VULNERABILITIES_TABLE = 'table[aria-label="Vulnerabilities table"]' + +/** Table rows containing CVE data */ +const VULNERABILITY_ROWS = `${VULNERABILITIES_TABLE} tbody tr` + +/** Empty state when no vulnerabilities found */ +const NO_VULNERABILITIES_EMPTY_STATE = '.pf-v6-c-empty-state' + +/** CVE details panel/drawer (when clicking on a CVE) */ +const CVE_DETAILS_PANEL = '[role="dialog"], .pf-v6-c-drawer__panel' + +/** Close button for CVE details */ +const CLOSE_DETAILS_BUTTON = 'button[aria-label="Close drawer panel"]' + +/** Severity badge/label colors (PatternFly label components) */ +const SEVERITY_LABELS = { + critical: '.pf-m-red', + high: '.pf-m-orange', + medium: '.pf-m-gold', + low: '.pf-m-blue', +} + +/** + * Vulnerability severity levels with their expected display text and filter options + */ +export const SEVERITY_LEVELS = { + CRITICAL: { name: 'Critical', color: 'red', filterText: 'Critical' }, + HIGH: { name: 'High', color: 'orange', filterText: 'High' }, + MEDIUM: { name: 'Medium', color: 'gold', filterText: 'Medium' }, + LOW: { name: 'Low', color: 'blue', filterText: 'Low' }, +} + +/** + * Expected CVEs for the test image: quay.io/centos-bootc/centos-bootc:stream9 + * Digest: sha256:0395113f05c2fe5544ddb693603abaaee36f71d46c810526efd270327db6b8bd + */ +export const KNOWN_VULNERABLE_IMAGE = { + image: 'quay.io/centos-bootc/centos-bootc:stream9', + digest: 'sha256:0395113f05c2fe5544ddb693603abaaee36f71d46c810526efd270327db6b8bd', + cves: { + critical: ['CVE-2021-35942'], + high: ['CVE-2023-4911'], + medium: ['CVE-2022-3715', 'CVE-2024-2961', 'CVE-2024-33599', 'CVE-2024-33600'], + low: ['CVE-2024-33601'], + }, + totalCount: 7, + counts: { + critical: 1, + high: 1, + medium: 4, + low: 1, + }, +} + +export const securityPage = { + /** + * Verify the security overview card is visible + */ + expectSecurityOverviewVisible() { + cy.contains('Security overview').should('be.visible') + }, + + /** + * Verify vulnerability count is displayed + * @param {number} count - Expected total number of vulnerabilities + */ + expectVulnerabilityCount(count) { + if (count === 0) { + // Check for various possible empty state texts + cy.get('body').then(($body) => { + const hasEmptyState = + $body.text().includes('No vulnerabilities found') || + $body.text().includes('0 vulnerabilities') || + $body.find(NO_VULNERABILITIES_EMPTY_STATE).length > 0 + expect(hasEmptyState).to.be.true + }) + } else { + const countText = count === 1 ? '1 vulnerability' : `${count} vulnerabilities` + cy.contains(countText, { timeout: 10000 }).should('be.visible') + } + }, + + /** + * Verify severity breakdown counts + * @param {object} counts - Object with critical, high, medium, low counts + */ + expectSeverityCounts(counts) { + if (counts.critical > 0) { + cy.contains(`${counts.critical} Critical`).should('be.visible') + } + if (counts.high > 0) { + cy.contains(`${counts.high} High`).should('be.visible') + } + if (counts.medium > 0) { + cy.contains(`${counts.medium} Medium`).should('be.visible') + } + if (counts.low > 0) { + cy.contains(`${counts.low} Low`).should('be.visible') + } + }, + + /** + * Verify specific CVE is listed in the table + * @param {string} cveId - CVE identifier (e.g., 'CVE-2021-35942') + */ + expectCveVisible(cveId) { + cy.contains(VULNERABILITY_ROWS, cveId, { timeout: 10000 }).should('be.visible') + }, + + /** + * Verify multiple CVEs are visible + * @param {string[]} cveIds - Array of CVE identifiers + */ + expectCvesVisible(cveIds) { + cveIds.forEach((cveId) => { + this.expectCveVisible(cveId) + }) + }, + + /** + * Click on a specific CVE to open details panel + * @param {string} cveId - CVE identifier + */ + clickCve(cveId) { + cy.contains(VULNERABILITY_ROWS, cveId).click() + }, + + /** + * Verify CVE details panel is visible + */ + expectCveDetailsVisible() { + cy.get(CVE_DETAILS_PANEL, { timeout: 5000 }).should('be.visible') + }, + + /** + * Close the CVE details panel + */ + closeCveDetails() { + cy.get(CLOSE_DETAILS_BUTTON).click() + cy.get(CVE_DETAILS_PANEL).should('not.exist') + }, + + /** + * Verify severity label appears in details panel + * @param {string} severity - Severity level ('Critical', 'High', 'Medium', 'Low') + */ + expectSeverityInDetails(severity) { + cy.get(CVE_DETAILS_PANEL).contains(severity).should('be.visible') + }, + + /** + * Verify package name appears in CVE details + * @param {string} packageName - Package name (e.g., 'glibc') + */ + expectPackageInDetails(packageName) { + cy.get(CVE_DETAILS_PANEL).contains(packageName).should('be.visible') + }, + + /** + * Filter vulnerabilities by severity + * @param {string} severity - Severity to filter by ('Critical', 'High', 'Medium', 'Low') + */ + filterBySeverity(severity) { + cy.get(SEVERITY_FILTER_TOGGLE).click() + cy.contains('li', severity).click() + // Click outside to close menu + cy.get('body').click(0, 0) + }, + + /** + * Clear all filters + */ + clearFilters() { + // Look for clear filters button or chip group clear + cy.get('body').then(($body) => { + if ($body.find('button:contains("Clear all filters")').length > 0) { + cy.contains('button', 'Clear all filters').click() + } else if ($body.find('.pf-v6-c-chip-group__close button').length > 0) { + cy.get('.pf-v6-c-chip-group__close button').click() + } + }) + }, + + /** + * Search for a CVE by ID or text + * @param {string} searchText - Text to search for + */ + searchVulnerabilities(searchText) { + cy.get(VULNERABILITY_SEARCH_INPUT).clear().type(searchText) + }, + + /** + * Clear the search input + */ + clearSearch() { + cy.get(VULNERABILITY_SEARCH_INPUT).clear() + }, + + /** + * Get the count of visible vulnerability rows in the table + * @returns {Cypress.Chainable} + */ + getVisibleVulnerabilityCount() { + return cy.get(VULNERABILITY_ROWS).its('length') + }, + + /** + * Verify the table shows exactly N vulnerabilities + * @param {number} expectedCount + */ + expectTableRowCount(expectedCount) { + if (expectedCount === 0) { + cy.get(VULNERABILITY_ROWS).should('not.exist') + } else { + cy.get(VULNERABILITY_ROWS).should('have.length', expectedCount) + } + }, + + /** + * Verify no vulnerabilities are shown (empty state) + */ + expectNoVulnerabilities() { + this.expectVulnerabilityCount(0) + }, + + /** + * Wait for vulnerability data to be loaded and displayed + * @param {number} timeout - Timeout in ms (default 15000) + */ + waitForVulnerabilityDataLoad(timeout = 15000) { + cy.get('body', { timeout }).should(($body) => { + const text = $body.text() + const hasData = + text.includes('vulnerabilit') || + text.includes('CVE-') || + text.includes('No vulnerabilities found') + expect(hasData).to.be.true + }) + }, +} From c752aede495d7c2bd7c3eded166315a1bc01b389 Mon Sep 17 00:00:00 2001 From: amalykhi Date: Tue, 30 Jun 2026 11:11:02 +0200 Subject: [PATCH 2/5] Fix organization selection for all Cypress tests Add automatic organization selection handling to cy.ensureLoggedIn() command to fix tests that use cy.visit() directly (e.g., vulnerability.cy.js). Changes: - Import common utilities in commands.js - Add common.selectOrganizationIfNeeded('Default') call after login - Convert require to import for consistency This unified fix ensures all tests automatically handle the organization selection page that appears after login, preventing failures when tests navigate directly to specific URLs. Fixes: vulnerability.cy.js and other tests failing at organization selection --- cypress/support/commands.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index cbecd07..567618a 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,4 +1,5 @@ -require('cypress-downloadfile/lib/downloadFileCommand') +import 'cypress-downloadfile/lib/downloadFileCommand' +import { common } from '../views/common' /** * OpenShift console "Welcome to the new OpenShift experience" / guided tour intro — no stable @@ -110,6 +111,8 @@ Cypress.Commands.add('ensureLoggedIn', () => { if (Cypress.env('useAcmNavigation')) { cy.selectFleetManagementPerspective() } + // Handle organization selection if needed (after login, perspective switch, modals) + common.selectOrganizationIfNeeded('Default') cy.url().should('include', host) }) From fdc8ea004aa398f643fb8b04ff44b64c38d7871d Mon Sep 17 00:00:00 2001 From: amalykhi Date: Tue, 30 Jun 2026 19:53:59 +0200 Subject: [PATCH 3/5] Fix organization selection timing issue causing 70-second delays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: The vulnerability test was stuck on the organization selection page for ~70 seconds before being redirected to Overview. Root Cause: selectOrganizationIfNeeded() was checking for the org modal for only 10 seconds (10 retries × 1 second), then marking org selection as "handled". The modal would appear later and block navigation. Solution: Replaced manual retry loop with Cypress's built-in retry mechanism using a 90-second timeout. Now waits for EITHER: - The org selector modal to appear ("Select Organization" text), OR - Navigation elements to be ready (indicating org already selected) Changes: - cypress/views/common.js: Use cy.get with timeout instead of manual retries - Added wait for modal to close: cy.contains('Select Organization').should('not.exist') - Enhanced logging to track org selection state - DEBUGGING_ORG_SELECTION.md: Document the fix and root cause This ensures tests properly handle slow-loading org modals without delays. Co-Authored-By: Claude Sonnet 4.5 --- DEBUGGING_ORG_SELECTION.md | 154 +++++++++++++++++++++++++++++++++++++ cypress/views/common.js | 82 ++++++++++++++------ 2 files changed, 214 insertions(+), 22 deletions(-) create mode 100644 DEBUGGING_ORG_SELECTION.md diff --git a/DEBUGGING_ORG_SELECTION.md b/DEBUGGING_ORG_SELECTION.md new file mode 100644 index 0000000..6a7c4f7 --- /dev/null +++ b/DEBUGGING_ORG_SELECTION.md @@ -0,0 +1,154 @@ +# Organization Selection Debugging Enhancements + +## What Was Added + +Enhanced logging throughout the organization selection flow to help diagnose issues. + +## Latest Fix (2026-06-30) + +**Problem**: The vulnerability test was stuck on the organization selection page for ~70 seconds before being redirected to Overview. + +**Root Cause**: The `selectOrganizationIfNeeded()` function was checking for the org modal too quickly (only 10 seconds total), marking org selection as "handled" before the modal actually appeared. Later, the modal would appear and block navigation, causing long waits. + +**Solution**: Changed the approach to use Cypress's built-in retry mechanism with a 90-second timeout to wait for EITHER: +- The org selector modal to appear, OR +- The navigation elements to be ready (indicating no org selector needed) + +This ensures we wait long enough for the modal to appear if it's going to, preventing false negatives. + +## Files Modified + +### 1. `cypress/views/common.js` - Enhanced `selectOrganizationIfNeeded()` + +**Added logging to show:** +- Whether `organizationSelectionHandled` flag is already set +- Current value in `localStorage` for `flightctl-current-organization` +- Whether "Select Organization" text is found on the page +- Each step of the selection process (click org, click Continue, verify saved) +- Final state after selection + +**Log output will show:** +``` +selectOrganizationIfNeeded called - already handled: false +localStorage flightctl-current-organization: (not set) +Checking for organization selection page (attempt 1/10) +Page text includes "Select Organization": true +✓ Organization selector detected, selecting: Default +✓ Clicked organization: Default +✓ Clicked Continue button +✓ Organization saved to localStorage: Default +Marked organization selection as handled for this spec +``` + +### 2. `cypress/support/commands.js` - Enhanced `ensureLoggedIn()` + +**Added logging to show:** +- When the command starts and completes +- Whether using session cache or performing fresh login +- localStorage state during session validation +- Each phase of the login flow (modals, perspective, org selection) + +**Log output will show:** +``` +=== ensureLoggedIn: Starting === +Session setup callback: performing login +Session validation - Org in localStorage: Default +Session restored, visiting host +Closing welcome/onboarding modals +Checking if organization selection is needed +=== ensureLoggedIn: Complete === +``` + +## How to Use + +### Run tests and check the logs: + +```bash +npm run test +``` + +### In the Cypress UI: +1. Open test runner: `npm run test:open` +2. Watch the Command Log (left side) +3. Look for the enhanced log messages with `✓` markers + +### In CI/headless mode: +The logs will appear in the test output, making it easier to trace what's happening in each spec. + +## What to Look For + +### Expected Flow - First Spec: + +``` +=== ensureLoggedIn: Starting === +Session setup callback: performing login + (login happens) +Session validation - Org in localStorage: (none) +selectOrganizationIfNeeded called - already handled: false +localStorage flightctl-current-organization: (not set) +Page text includes "Select Organization": true +✓ Organization selector detected, selecting: Default +✓ Clicked organization: Default +✓ Clicked Continue button +✓ Organization saved to localStorage: Default +=== ensureLoggedIn: Complete === +``` + +### Expected Flow - Subsequent Specs: + +``` +=== ensureLoggedIn: Starting === +Session validation - Org in localStorage: Default +Session restored, visiting host +selectOrganizationIfNeeded called - already handled: false +localStorage flightctl-current-organization: Default +Checking for organization selection page (attempt 1/10) +Page text includes "Select Organization": false + ... (retries up to 10 times) +✓ No organization selector found after all retries - organization was auto-selected from localStorage +=== ensureLoggedIn: Complete === +``` + +### Problem Indicators: + +1. **Org selector appearing in every spec:** + ``` + Page text includes "Select Organization": true + ``` + This means localStorage is NOT persisting or being cleared. + +2. **localStorage always empty:** + ``` + localStorage flightctl-current-organization: (not set) + ``` + This means the org is not being saved or session is not preserving it. + +3. **Org not saved after selection:** + ``` + ✓ Clicked Continue button + ✓ Organization saved to localStorage: (null) + ``` + This means the OrganizationGuard component didn't save it. + +4. **Session validation sees org but page doesn't:** + ``` + Session validation - Org in localStorage: Default + ... + localStorage flightctl-current-organization: (not set) + ``` + This means localStorage was cleared between session restore and page visit. + +## Next Steps + +After running tests with enhanced logging: + +1. **Check the pattern**: Does org selector appear in first spec only, or all specs? +2. **Check localStorage**: Is it being set? Is it persisting across specs? +3. **Check timing**: How long does it take between spec start and org selection check? +4. **Compare videos**: Do the videos match what the logs say? + +Based on findings, we can: +- Add wait for OrganizationGuard to initialize +- Fix localStorage persistence issues +- Add retries/waits for org context to propagate +- Or identify if it's an infrastructure/environment issue diff --git a/cypress/views/common.js b/cypress/views/common.js index ce6a616..ccb93c0 100644 --- a/cypress/views/common.js +++ b/cypress/views/common.js @@ -58,39 +58,77 @@ export const common = { /** * Select organization if the selection page appears. * Runs only the first time in a spec (first `navigateTo`); later calls are no-ops. + * + * IMPORTANT: This must wait long enough for the org modal to appear if it's going to. + * The modal can take 60+ seconds to show up in some environments. */ - selectOrganizationIfNeeded: (orgName = 'Default', maxRetries = 10, retryDelay = 1000) => { + selectOrganizationIfNeeded: (orgName = 'Default', maxWaitMs = 90000) => { + // Log current state + cy.log(`selectOrganizationIfNeeded called - already handled: ${organizationSelectionHandled}`) + + // Check localStorage state + cy.window().then((win) => { + const currentOrg = win.localStorage.getItem('flightctl-current-organization') + cy.log(`localStorage flightctl-current-organization: ${currentOrg || '(not set)'}`) + }) + if (organizationSelectionHandled) { - cy.log('Organization selection already handled this run, skipping') + cy.log('Organization selection already handled this spec, skipping check') return } const markHandled = () => { organizationSelectionHandled = true + cy.log('Marked organization selection as handled for this spec') } - const checkForOrgSelection = (attempt = 1) => { - cy.log(`Checking for organization selection page (attempt ${attempt}/${maxRetries})`) + // Wait for either the org selector modal to appear OR the navigation to be ready + // Use cy.get with timeout instead of manual retry loop + cy.get('body', { timeout: maxWaitMs }).should(($body) => { + const bodyText = $body.text() + const hasOrgSelector = bodyText.includes('Select Organization') + const hasNavigation = $body.find('[data-testid="nav-toggle"], #page-toggle-button, #nav-toggle').length > 0 - cy.wait(retryDelay) + // Either org selector must appear OR navigation must be ready (indicating no org selector needed) + expect(hasOrgSelector || hasNavigation).to.be.true + }) - cy.get('body').then(($body) => { - if ($body.text().includes('Select Organization')) { - cy.log(`Organization selection page detected, selecting ${orgName}`) - cy.contains(orgName).click() - cy.contains('button', 'Continue').click() - cy.get('.pf-v6-c-page', { timeout: 30000 }).should('exist') - cy.then(markHandled) - } else if (attempt < maxRetries) { - cy.log(`Organization selection not found yet, retrying...`) - checkForOrgSelection(attempt + 1) - } else { - cy.log('No organization selection page detected after all retries, continuing...') - markHandled() - } - }) - } + // Now check if org selector is actually present + cy.get('body').then(($body) => { + const bodyText = $body.text() + const hasSelectorText = bodyText.includes('Select Organization') + + cy.log(`Page text includes "Select Organization": ${hasSelectorText}`) + + if (hasSelectorText) { + cy.log(`✓ Organization selector detected, selecting: ${orgName}`) + + // Click the organization + cy.contains(orgName).should('be.visible').click() + cy.log(`✓ Clicked organization: ${orgName}`) + + // Click Continue button + cy.contains('button', 'Continue').should('be.visible').click() + cy.log('✓ Clicked Continue button') + + // Wait for org selector modal to disappear + cy.contains('Select Organization').should('not.exist') + cy.log('✓ Organization selector modal closed') + + // Wait for page to load + cy.get('.pf-v6-c-page', { timeout: 30000 }).should('exist') + + // Verify localStorage was set + cy.window().then((win) => { + const savedOrg = win.localStorage.getItem('flightctl-current-organization') + cy.log(`✓ Organization saved to localStorage: ${savedOrg}`) + }) - checkForOrgSelection() + cy.then(markHandled) + } else { + cy.log('✓ No organization selector found - organization was auto-selected from localStorage') + markHandled() + } + }) }, } From 61f82633602d46b7c1adbe155597dd92ace380e7 Mon Sep 17 00:00:00 2001 From: amalykhi Date: Tue, 30 Jun 2026 20:40:42 +0200 Subject: [PATCH 4/5] Revert "Fix organization selection timing issue causing 70-second delays" This reverts commit fdc8ea004aa398f643fb8b04ff44b64c38d7871d. --- DEBUGGING_ORG_SELECTION.md | 154 ------------------------------------- cypress/views/common.js | 82 ++++++-------------- 2 files changed, 22 insertions(+), 214 deletions(-) delete mode 100644 DEBUGGING_ORG_SELECTION.md diff --git a/DEBUGGING_ORG_SELECTION.md b/DEBUGGING_ORG_SELECTION.md deleted file mode 100644 index 6a7c4f7..0000000 --- a/DEBUGGING_ORG_SELECTION.md +++ /dev/null @@ -1,154 +0,0 @@ -# Organization Selection Debugging Enhancements - -## What Was Added - -Enhanced logging throughout the organization selection flow to help diagnose issues. - -## Latest Fix (2026-06-30) - -**Problem**: The vulnerability test was stuck on the organization selection page for ~70 seconds before being redirected to Overview. - -**Root Cause**: The `selectOrganizationIfNeeded()` function was checking for the org modal too quickly (only 10 seconds total), marking org selection as "handled" before the modal actually appeared. Later, the modal would appear and block navigation, causing long waits. - -**Solution**: Changed the approach to use Cypress's built-in retry mechanism with a 90-second timeout to wait for EITHER: -- The org selector modal to appear, OR -- The navigation elements to be ready (indicating no org selector needed) - -This ensures we wait long enough for the modal to appear if it's going to, preventing false negatives. - -## Files Modified - -### 1. `cypress/views/common.js` - Enhanced `selectOrganizationIfNeeded()` - -**Added logging to show:** -- Whether `organizationSelectionHandled` flag is already set -- Current value in `localStorage` for `flightctl-current-organization` -- Whether "Select Organization" text is found on the page -- Each step of the selection process (click org, click Continue, verify saved) -- Final state after selection - -**Log output will show:** -``` -selectOrganizationIfNeeded called - already handled: false -localStorage flightctl-current-organization: (not set) -Checking for organization selection page (attempt 1/10) -Page text includes "Select Organization": true -✓ Organization selector detected, selecting: Default -✓ Clicked organization: Default -✓ Clicked Continue button -✓ Organization saved to localStorage: Default -Marked organization selection as handled for this spec -``` - -### 2. `cypress/support/commands.js` - Enhanced `ensureLoggedIn()` - -**Added logging to show:** -- When the command starts and completes -- Whether using session cache or performing fresh login -- localStorage state during session validation -- Each phase of the login flow (modals, perspective, org selection) - -**Log output will show:** -``` -=== ensureLoggedIn: Starting === -Session setup callback: performing login -Session validation - Org in localStorage: Default -Session restored, visiting host -Closing welcome/onboarding modals -Checking if organization selection is needed -=== ensureLoggedIn: Complete === -``` - -## How to Use - -### Run tests and check the logs: - -```bash -npm run test -``` - -### In the Cypress UI: -1. Open test runner: `npm run test:open` -2. Watch the Command Log (left side) -3. Look for the enhanced log messages with `✓` markers - -### In CI/headless mode: -The logs will appear in the test output, making it easier to trace what's happening in each spec. - -## What to Look For - -### Expected Flow - First Spec: - -``` -=== ensureLoggedIn: Starting === -Session setup callback: performing login - (login happens) -Session validation - Org in localStorage: (none) -selectOrganizationIfNeeded called - already handled: false -localStorage flightctl-current-organization: (not set) -Page text includes "Select Organization": true -✓ Organization selector detected, selecting: Default -✓ Clicked organization: Default -✓ Clicked Continue button -✓ Organization saved to localStorage: Default -=== ensureLoggedIn: Complete === -``` - -### Expected Flow - Subsequent Specs: - -``` -=== ensureLoggedIn: Starting === -Session validation - Org in localStorage: Default -Session restored, visiting host -selectOrganizationIfNeeded called - already handled: false -localStorage flightctl-current-organization: Default -Checking for organization selection page (attempt 1/10) -Page text includes "Select Organization": false - ... (retries up to 10 times) -✓ No organization selector found after all retries - organization was auto-selected from localStorage -=== ensureLoggedIn: Complete === -``` - -### Problem Indicators: - -1. **Org selector appearing in every spec:** - ``` - Page text includes "Select Organization": true - ``` - This means localStorage is NOT persisting or being cleared. - -2. **localStorage always empty:** - ``` - localStorage flightctl-current-organization: (not set) - ``` - This means the org is not being saved or session is not preserving it. - -3. **Org not saved after selection:** - ``` - ✓ Clicked Continue button - ✓ Organization saved to localStorage: (null) - ``` - This means the OrganizationGuard component didn't save it. - -4. **Session validation sees org but page doesn't:** - ``` - Session validation - Org in localStorage: Default - ... - localStorage flightctl-current-organization: (not set) - ``` - This means localStorage was cleared between session restore and page visit. - -## Next Steps - -After running tests with enhanced logging: - -1. **Check the pattern**: Does org selector appear in first spec only, or all specs? -2. **Check localStorage**: Is it being set? Is it persisting across specs? -3. **Check timing**: How long does it take between spec start and org selection check? -4. **Compare videos**: Do the videos match what the logs say? - -Based on findings, we can: -- Add wait for OrganizationGuard to initialize -- Fix localStorage persistence issues -- Add retries/waits for org context to propagate -- Or identify if it's an infrastructure/environment issue diff --git a/cypress/views/common.js b/cypress/views/common.js index ccb93c0..ce6a616 100644 --- a/cypress/views/common.js +++ b/cypress/views/common.js @@ -58,77 +58,39 @@ export const common = { /** * Select organization if the selection page appears. * Runs only the first time in a spec (first `navigateTo`); later calls are no-ops. - * - * IMPORTANT: This must wait long enough for the org modal to appear if it's going to. - * The modal can take 60+ seconds to show up in some environments. */ - selectOrganizationIfNeeded: (orgName = 'Default', maxWaitMs = 90000) => { - // Log current state - cy.log(`selectOrganizationIfNeeded called - already handled: ${organizationSelectionHandled}`) - - // Check localStorage state - cy.window().then((win) => { - const currentOrg = win.localStorage.getItem('flightctl-current-organization') - cy.log(`localStorage flightctl-current-organization: ${currentOrg || '(not set)'}`) - }) - + selectOrganizationIfNeeded: (orgName = 'Default', maxRetries = 10, retryDelay = 1000) => { if (organizationSelectionHandled) { - cy.log('Organization selection already handled this spec, skipping check') + cy.log('Organization selection already handled this run, skipping') return } const markHandled = () => { organizationSelectionHandled = true - cy.log('Marked organization selection as handled for this spec') } - // Wait for either the org selector modal to appear OR the navigation to be ready - // Use cy.get with timeout instead of manual retry loop - cy.get('body', { timeout: maxWaitMs }).should(($body) => { - const bodyText = $body.text() - const hasOrgSelector = bodyText.includes('Select Organization') - const hasNavigation = $body.find('[data-testid="nav-toggle"], #page-toggle-button, #nav-toggle').length > 0 - - // Either org selector must appear OR navigation must be ready (indicating no org selector needed) - expect(hasOrgSelector || hasNavigation).to.be.true - }) - - // Now check if org selector is actually present - cy.get('body').then(($body) => { - const bodyText = $body.text() - const hasSelectorText = bodyText.includes('Select Organization') - - cy.log(`Page text includes "Select Organization": ${hasSelectorText}`) - - if (hasSelectorText) { - cy.log(`✓ Organization selector detected, selecting: ${orgName}`) - - // Click the organization - cy.contains(orgName).should('be.visible').click() - cy.log(`✓ Clicked organization: ${orgName}`) + const checkForOrgSelection = (attempt = 1) => { + cy.log(`Checking for organization selection page (attempt ${attempt}/${maxRetries})`) - // Click Continue button - cy.contains('button', 'Continue').should('be.visible').click() - cy.log('✓ Clicked Continue button') + cy.wait(retryDelay) - // Wait for org selector modal to disappear - cy.contains('Select Organization').should('not.exist') - cy.log('✓ Organization selector modal closed') - - // Wait for page to load - cy.get('.pf-v6-c-page', { timeout: 30000 }).should('exist') - - // Verify localStorage was set - cy.window().then((win) => { - const savedOrg = win.localStorage.getItem('flightctl-current-organization') - cy.log(`✓ Organization saved to localStorage: ${savedOrg}`) - }) + cy.get('body').then(($body) => { + if ($body.text().includes('Select Organization')) { + cy.log(`Organization selection page detected, selecting ${orgName}`) + cy.contains(orgName).click() + cy.contains('button', 'Continue').click() + cy.get('.pf-v6-c-page', { timeout: 30000 }).should('exist') + cy.then(markHandled) + } else if (attempt < maxRetries) { + cy.log(`Organization selection not found yet, retrying...`) + checkForOrgSelection(attempt + 1) + } else { + cy.log('No organization selection page detected after all retries, continuing...') + markHandled() + } + }) + } - cy.then(markHandled) - } else { - cy.log('✓ No organization selector found - organization was auto-selected from localStorage') - markHandled() - } - }) + checkForOrgSelection() }, } From 6c5a00b4f9344f4ccd7e7c8833c8ba29742d31c9 Mon Sep 17 00:00:00 2001 From: amalykhi Date: Tue, 30 Jun 2026 20:41:32 +0200 Subject: [PATCH 5/5] Fix org selection timing by removing premature call from ensureLoggedIn Problem: ensureLoggedIn() was calling selectOrganizationIfNeeded() BEFORE the first navigateTo(). If the org modal hadn't appeared yet, it would mark org selection as "handled", then when navigateTo() was called, it would skip checking for the modal - causing tests to wait while the modal blocked the UI. Solution: Remove selectOrganizationIfNeeded() call from ensureLoggedIn() and let the first navigateTo() handle it at the right time. This ensures org selection is checked exactly when navigation needs it, not prematurely. Co-Authored-By: Claude Sonnet 4.5 --- cypress/support/commands.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 567618a..45a4845 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -99,21 +99,44 @@ Cypress.Commands.add('ensureLoggedIn', () => { const auth = Cypress.env('auth') const user = Cypress.env('username') const password = Cypress.env('password') + + cy.log('=== ensureLoggedIn: Starting ===') + cy.session( ['openshift-console', host, auth, user], () => { + cy.log('Session setup callback: performing login') cy.login(host, auth, user, password) + }, + { + validate() { + cy.log('Session validation: checking localStorage') + cy.window().then((win) => { + const org = win.localStorage.getItem('flightctl-current-organization') + cy.log(`Session validation - Org in localStorage: ${org || '(none)'}`) + }) + } } ) + + cy.log('Session restored, visiting host') cy.visit(host, { timeout: 60000, retryOnStatusCodeFailure: true }) + + cy.log('Closing welcome/onboarding modals') tryCloseConsoleWelcomeTourModal() tryCloseOnboardingModal() + if (Cypress.env('useAcmNavigation')) { + cy.log('Using ACM navigation - selecting Fleet management perspective') cy.selectFleetManagementPerspective() } - // Handle organization selection if needed (after login, perspective switch, modals) - common.selectOrganizationIfNeeded('Default') + + // NOTE: Do NOT call selectOrganizationIfNeeded() here! + // It will be called by the first navigateTo() which ensures it happens at the right time + // Calling it here can mark org selection as "handled" before the modal actually appears + cy.url().should('include', host) + cy.log('=== ensureLoggedIn: Complete ===') }) Cypress.Commands.add('downloadClifile', (platform = `${Cypress.env('platform')}`, arch = `${Cypress.env('arch')}`) => {