Skip to content

Commit

Permalink
Merge pull request openshift#14675 from vojtechszocs/csp-e2e-testing-…
Browse files Browse the repository at this point in the history
…with-puppeteer

CONSOLE-4430: Content Security Policy E2E testing with Puppeteer & Chrome
  • Loading branch information
openshift-merge-bot[bot] authored Mar 8, 2025
2 parents 5a1fe5e + afa91e9 commit 508da0c
Show file tree
Hide file tree
Showing 8 changed files with 620 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cypress-a11y-report.json
/gopath
/Godeps/_workspace/src/github.com/openshift/console
/frontend/.cache-loader
/frontend/.puppeteer
/frontend/.webpack-cycles
/frontend/__coverage__
/frontend/__chrome_browser__
Expand Down
1 change: 1 addition & 0 deletions frontend/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.puppeteer
.yarn
__coverage__
**/node_modules
Expand Down
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"test-cypress-topology": "cd packages/topology/integration-tests && yarn run test-cypress",
"test-cypress-topology-headless": "cd packages/topology/integration-tests && yarn run test-cypress-headless",
"test-cypress-topology-nightly": "cd packages/topology/integration-tests && yarn run test-cypress-headless-all",
"test-puppeteer-csp": "yarn ts-node ./test-puppeteer-csp.ts",
"cypress-merge": "mochawesome-merge ./gui_test_screenshots/cypress_report*.json > ./gui_test_screenshots/cypress.json",
"cypress-generate": "marge -o ./gui_test_screenshots/ -f cypress-report -t 'OpenShift Console Cypress Test Results' -p 'OpenShift Cypress Test Results' --showPassed false --assetsDir ./gui_test_screenshots/cypress/assets ./gui_test_screenshots/cypress.json",
"cypress-a11y-report": "echo '\nA11y Test Results:' && mv packages/integration-tests-cypress/cypress-a11y-report.json ./gui_test_screenshots/ && node -e \"console.table(JSON.parse(require('fs').readFileSync(process.argv[1])));\" ./gui_test_screenshots/cypress-a11y-report.json",
Expand Down Expand Up @@ -305,6 +306,7 @@
"monaco-editor": "^0.51.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"prettier": "2.0.5",
"puppeteer-core": "^23.9.0",
"react-refresh": "^0.10.0",
"read-pkg": "5.x",
"redux-mock-store": "^1.5.3",
Expand Down
197 changes: 197 additions & 0 deletions frontend/test-puppeteer-csp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/* eslint-env node */
/* eslint-disable no-console */

import * as fs from 'fs';
import * as path from 'path';
import {
Browser as BrowserType,
detectBrowserPlatform,
getInstalledBrowsers,
install,
resolveBuildId,
} from '@puppeteer/browsers';
import { Browser, HTTPResponse, launch } from 'puppeteer-core';

// Use 'Chrome for Testing' build of the Chrome web browser.
// https://googlechromelabs.github.io/chrome-for-testing/
const testBrowser = BrowserType.CHROME;
const testBrowserTag = 'stable';

const baseDir = path.resolve(__dirname, '.puppeteer');
const cacheDir = path.resolve(baseDir, 'cache');
const userDataDir = path.resolve(baseDir, 'user-data');

const parseNumValue = (value: string, defaultValue: number) => {
if (!value) {
return defaultValue;
}

const num = Number(value);
return !isNaN(num) && isFinite(num) ? num : defaultValue;
};

const envParameters = {
// The 'NO_SANDBOX' env. variable can be used to run Chrome under the root user.
// https://chromium.googlesource.com/chromium/src/+/HEAD/docs/design/sandbox.md
noSandbox: process.env.NO_SANDBOX === 'true',

// Base URL of Console web application.
consoleBaseURL: process.env.CONSOLE_BASE_URL || 'http://localhost:9000',

// CSP reporting endpoint to be used for testing Console pages.
cspReportURL: process.env.CSP_REPORT_URL || 'http://localhost:7777',

// Timeout [ms] when loading Console pages. Default value 60s should give
// the browser enough time to load all Console resources over the network.
pageLoadTimeout: parseNumValue(process.env.PAGE_LOAD_TIMEOUT, 60000),

// Timeout [ms] used for "no network activity" check after loading a page.
pageNetIdleTimeout: parseNumValue(process.env.PAGE_NET_IDLE_TIMEOUT, 20000),
};

const findInstalledBrowser = async () => {
const allBrowsers = await getInstalledBrowsers({ cacheDir });
return allBrowsers.find((b) => b.browser === testBrowser);
};

const initBrowserInstance = async () => {
let browser = await findInstalledBrowser();

if (!browser) {
console.info(`Browser ${testBrowser} not found, installing...`);

browser = await install({
browser: testBrowser,
buildId: await resolveBuildId(testBrowser, detectBrowserPlatform(), testBrowserTag),
cacheDir,
});
}

console.info(
`Using browser ${browser.browser} on ${browser.platform} with build ID ${browser.buildId}`,
);

fs.rmSync(userDataDir, { recursive: true, force: true });

return launch({
headless: true,
browser: testBrowser,
executablePath: browser.executablePath,
userDataDir,
args: envParameters.noSandbox ? ['--no-sandbox'] : [],
});
};

/**
* Use `browser` to test `pageURL` for Content Security Policy (CSP) violations.
*/
const testPage = async (
browser: Browser,
pageURL: URL,
cspReportURL: URL,
errorCallback: VoidFunction,
) => {
const page = await browser.newPage();

// Create a Chrome DevTools Protocol (CDP) session for the page.
const cdpSession = await page.createCDPSession();

// This will trigger 'Fetch.requestPaused' events for the matching requests.
await cdpSession.send('Fetch.enable', {
patterns: [{ resourceType: 'Document' }, { resourceType: 'CSPViolationReport' }],
});

// Handle network requests that get paused through 'Fetch.enable' command.
cdpSession.on('Fetch.requestPaused', ({ resourceType, request, requestId }) => {
// When requesting the web page, add custom 'Test-CSP-Reporting-Endpoint' HTTP header
// in order to instruct Console Bridge server to use the given CSP reporting endpoint.
if (resourceType === 'Document' && request.url === pageURL.href) {
const headers = Object.entries(request.headers).map(([name, value]) => ({ name, value }));

headers.push({ name: 'Test-CSP-Reporting-Endpoint', value: cspReportURL.href });
cdpSession.send('Fetch.continueRequest', { requestId, headers });
}

// The browser will attempt to send any CSP violations to the CSP reporting endpoint.
// When such request occurs, we manually fulfill that request before it is sent over
// the network and therefore avoiding the need to implement that reporting endpoint.
else if (resourceType === 'CSPViolationReport' && request.url === cspReportURL.href) {
try {
console.error('CSP violation detected:', JSON.parse(request.postData));
} catch (e) {
console.error('CSP violation detected, but request POST data failed to parse as JSON');
}

errorCallback();
cdpSession.send('Fetch.fulfillRequest', { requestId, responseCode: 200 });
}

// Resume other requests that were not explicitly handled above.
else {
cdpSession.send('Fetch.continueRequest', { requestId });
}
});

console.info(`Loading page ${pageURL}`);

let response: HTTPResponse;

try {
console.time('page-load');
response = await page.goto(pageURL.href, { timeout: envParameters.pageLoadTimeout });
} catch (e) {
console.warn('Page was not fully loaded within the timeout');
} finally {
console.timeEnd('page-load');
}

if (response && response.ok()) {
try {
console.time('page-net-idle');
await page.waitForNetworkIdle({ idleTime: 2000, timeout: envParameters.pageNetIdleTimeout });
} catch (e) {
console.warn('Page could not reach idle network state within the timeout');
} finally {
console.timeEnd('page-net-idle');
}
} else if (response && !response.ok()) {
console.error(`Non-OK response: status ${response.status()} ${response.statusText()}`);
errorCallback();
}

await cdpSession.detach();
await page.close();
};

(async () => {
try {
let errorsDetected = false;

console.info('Using env. parameters:', envParameters);

const browser = await initBrowserInstance();

const errorCallback = () => {
errorsDetected = true;
};

await testPage(
browser,
new URL('/dashboards', envParameters.consoleBaseURL),
new URL(envParameters.cspReportURL),
errorCallback,
);

await browser.close();

if (errorsDetected) {
process.exit(1);
} else {
console.info('No errors detected');
process.exit(0);
}
} catch (e) {
console.error(e);
process.exit(1);
}
})();
Loading

0 comments on commit 508da0c

Please sign in to comment.