-
Notifications
You must be signed in to change notification settings - Fork 618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CONSOLE-4430: Content Security Policy E2E testing with Puppeteer & Chrome #14675
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
.puppeteer | ||
.yarn | ||
__coverage__ | ||
**/node_modules | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,168 @@ | ||||||
/* 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, Page, 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 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', | ||||||
}; | ||||||
|
||||||
const baseDir = path.resolve(__dirname, '.puppeteer'); | ||||||
const cacheDir = path.resolve(baseDir, 'cache'); | ||||||
const userDataDir = path.resolve(baseDir, 'user-data'); | ||||||
|
||||||
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, | ||||||
pageLoadCallback: (page: Page) => Promise<void>, | ||||||
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 }) => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets add more comments explaining non-obvious logic, especially for the Chrome DevTools Protocol configuration (Fetch.enable, Fetch.requestPaused). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK will do. |
||||||
// 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 }); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have purposely omitted the CDP session emitted events are async in nature. This line is the last bit of code of an if (resourceType === 'Document' && request.url === pageURL) {
const headers = Object.entries(request.headers).map(([name, value]) => ({ name, value }));
headers.push({ name: 'Test-CSP-Reporting-Endpoint', value: cspReportURL });
cdpSession.send('Fetch.continueRequest', { requestId, headers });
} It makes very little sense to We simply call the command and let it run. We don't need to handle its result, nor do we need to wait for its completion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, if we just add
If we really wanted to wait for this operation to finish, we'd have to use the standard Promise API functions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interesting, Im a bit surprised that its working this way, but I probably need read docs in detail. |
||||||
} | ||||||
|
||||||
// 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. | ||||||
if (resourceType === 'CSPViolationReport' && request.url === cspReportURL.href) { | ||||||
console.error('CSP violation detected', request.postData); | ||||||
errorCallback(); | ||||||
|
||||||
cdpSession.send('Fetch.fulfillRequest', { requestId, responseCode: 200 }); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is effectively the same thing as #14675 (comment) |
||||||
} | ||||||
}); | ||||||
|
||||||
console.info(`Loading page ${pageURL}`); | ||||||
|
||||||
// At this point, CDP session is already set up. | ||||||
const httpResponse = await page.goto(pageURL.href); | ||||||
|
||||||
if (httpResponse.ok()) { | ||||||
try { | ||||||
// Wait for the page to finish loading. | ||||||
await pageLoadCallback(page); | ||||||
} catch (e) { | ||||||
console.error(e); | ||||||
errorCallback(); | ||||||
} | ||||||
} else { | ||||||
// Treat non-OK HTTP status code as error. | ||||||
console.error(`Non-OK response: status ${httpResponse.status()} ${httpResponse.statusText()}`); | ||||||
errorCallback(); | ||||||
} | ||||||
|
||||||
await cdpSession.detach(); | ||||||
await page.close(); | ||||||
}; | ||||||
|
||||||
const waitForNetworkIdle = async (page: Page) => { | ||||||
await page.waitForNetworkIdle({ idleTime: 2000 }); | ||||||
}; | ||||||
|
||||||
(async () => { | ||||||
try { | ||||||
let errorsDetected = false; | ||||||
|
||||||
const browser = await initBrowserInstance(); | ||||||
|
||||||
const errorCallback = () => { | ||||||
errorsDetected = true; | ||||||
}; | ||||||
|
||||||
await testPage( | ||||||
browser, | ||||||
new URL('/dashboards', envParameters.consoleBaseURL), | ||||||
new URL(envParameters.cspReportURL), | ||||||
waitForNetworkIdle, | ||||||
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); | ||||||
} | ||||||
})(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that the change is actually not changing the logic, so not sure why the block got moved?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The block got moved so that
OPENSHIFT_CI
env. variable handling is located together.Technically, this is init/config
while this is validation
and I think init/config should logically come before validation.