|
const assert = require('assert'); |
|
const Proxy = require('browsermob-proxy').Proxy; |
|
const fs = require('fs'); |
|
const event = require('codeceptjs').event; |
|
const output = require('codeceptjs').output; |
|
const container = require('codeceptjs').container; |
|
|
|
class Browsermob extends Helper { |
|
|
|
constructor(config) |
|
{ |
|
super(config); |
|
this.bmpProxyPort = config.bmpProxyPort || Math.floor(Math.random() * 99 + 8081); // if not configured: random number between 8081-8180 |
|
this.bmpPort = config.bmpPort || 8080; |
|
this.bmpHost = config.host || 'localhost'; |
|
|
|
this.proxy = new Proxy({ |
|
host: this.bmpHost, |
|
port: this.bmpPort, |
|
proxyPort: this.bmpProxyPort |
|
}); |
|
|
|
// Add behavior "Before" each test |
|
|
|
event.dispatcher.on(event.test.before, async function (test) { |
|
if (test.title.indexOf('@bmp') !== -1) { |
|
|
|
// Configure browser driver to use proxy |
|
const proxyConfig = { |
|
proxyType:"MANUAL", |
|
httpProxy: this.bmpHost + ':' + this.bmpProxyPort, |
|
sslProxy: this.bmpHost + ':' + this.bmpProxyPort |
|
}; |
|
// Save original proxy settings. We will reset to them after the test. |
|
this.previousProxyConfig = detectBrowserHelper().config.capabilities.proxy; |
|
detectBrowserHelper().config.capabilities.proxy = proxyConfig; |
|
} |
|
}.bind(this)); |
|
|
|
// Add behavior "After" each test |
|
|
|
event.dispatcher.on(event.test.after, async function (test) { |
|
if (test.title.indexOf('@bmp') !== -1) { |
|
// Recover original proxy config to not sabotage other non-browsermobproxy tests. |
|
detectBrowserHelper().config.capabilities.proxy = this.previousProxyConfig; |
|
} |
|
}.bind(this)); |
|
|
|
return this; |
|
} |
|
|
|
/** |
|
* |
|
* @returns {Promise<void>} |
|
*/ |
|
startProxy() { |
|
|
|
return new Promise(function (resolve, reject) { |
|
this.proxy.start(this.bmpProxyPort, function (err, resp) { |
|
if (err) { |
|
const errMessage = '[ERROR] (BrowserMobProxy) Error starting proxy.' + |
|
` Address of server: ${this.bmpHost}:${this.bmpPort}. Error message: '${err}'`; |
|
reject(errMessage); |
|
} else { |
|
this.proxy.startHAR(this.bmpProxyPort, null, null, true, function (err, resp) { |
|
if (!err) { |
|
output.debug('[DEBUG] (BrowserMobProxy) Recording of HAR started on port ' + this.bmpProxyPort + ' ...'); |
|
|
|
resolve() |
|
} else { |
|
const errorMessage = ('[ERROR] (BrowserMobProxy) Error starting HAR: ' + err); |
|
this.proxy.stop(data.port, function () { |
|
}); |
|
reject(errorMessage); |
|
} |
|
}.bind(this)); |
|
} |
|
}.bind(this)); |
|
}.bind(this)); |
|
} |
|
|
|
getHar() { |
|
return new Promise(function (resolve, reject) { |
|
this.proxy.getHAR(this.bmpProxyPort, function (err, resp) { |
|
if (! err) { |
|
resolve(JSON.parse(resp)) |
|
} else { |
|
reject('[ERROR] (BrowserMobProxy) Error getting HAR file: "' + err + '"') |
|
} |
|
}); |
|
}.bind(this)); |
|
} |
|
|
|
stopProxy() { |
|
return new Promise(function (resolve, reject) { |
|
this.proxy.stop(this.bmpProxyPort, function(err) { |
|
if (err) { |
|
reject('[WARNING] (BrowserMobProxy) Error occured during stop of proxy on port ' + this.bmpProxyPort); |
|
} else { |
|
output.debug('[DEBUG] (BrowserMobProxy) Proxy stopped on port ' + this.bmpProxyPort); |
|
resolve(); |
|
} |
|
}.bind(this)); |
|
}.bind(this)); |
|
} |
|
|
|
/** |
|
* Verifies that network traffic contains a certain entry. |
|
* |
|
* @param name Short description of what you test. This value has no functionality, it just helps with the reports. |
|
* @param url Part of URL is sufficient, f.e. the domain. |
|
* @param parameters (optional) Expected parameters. |
|
* This is an object of multiple key/value pairs, where the key is the name of the parameter |
|
* and the value the expected value of the parameter. |
|
* @param timeout timeout in seconds to wait for traffic to be present. Default value: 5 |
|
* @param advancedParameters Specify even more parameters that must be present in traffic. These parameters are only |
|
* tested when environment variable "STRICT" exists and is set to value "true" |
|
* @returns {Promise<void>} |
|
*/ |
|
async seeTraffic(name, url, parameters, advancedParameters, timeout = 5 ) { |
|
let testresult = false; |
|
|
|
if (typeof parameters !== "object" && ! typeof parameters === "undefined") { |
|
throw Error( |
|
'Invalid argument for method "seeTraffic": Parameter "parameters" is not of type "Object".' |
|
+ ' This would be a valid example: { "t" : "pageview"}' |
|
); |
|
} |
|
const browserHelper = detectBrowserHelper(); |
|
if (typeof browserHelper.config.capabilities.proxy !== 'undefined' && ! browserHelper.config.capabilities.proxy.sslProxy) { |
|
throw Error( |
|
'[ERROR] BrowserMobProxy is not used as a proxy. This is expected when using "I.seeTraffic".' |
|
+ 'To fix this you should add "@bmp" to the name of your test Scenario.' |
|
); |
|
} |
|
|
|
for (let i = 0; i <= timeout * 2; i++) { |
|
testresult = await this.isInTraffic(name, url, parameters, advancedParameters); |
|
if (true === testresult) { |
|
// console.log('INFO: traffic found for "' + name + '" after -- ' + (i * 500) + ' -- milliseconds'); |
|
return true; |
|
} |
|
await new Promise(done => setTimeout(done, 500)); |
|
} |
|
assert.fail(testresult); |
|
} |
|
|
|
async isInTraffic(name, url, parameters, advancedParameters) { |
|
let success = false; |
|
let matches = 0; |
|
let advancedResults = false; |
|
|
|
const har = await this.getHar(); |
|
|
|
// Get entries from HAR |
|
const entries = har.log.entries; |
|
|
|
// Iterate over all entries |
|
for (let entry of entries) { |
|
|
|
// Does "url" match? |
|
if (entry.request.url.indexOf(url) !== -1) { |
|
|
|
// Does all "parameterValuePairs" match? |
|
if (typeof parameters === "undefined" |
|
|| allParameterValuePairsMatch(entry.request.queryString, entry.request.postData, parameters) |
|
) { |
|
matches++; |
|
success = true; |
|
if (typeof advancedParameters !== "undefined" && process.env.STRICT !== 'false') { |
|
advancedResults = allParameterValuePairsMatchExtreme(entry.request.queryString, advancedParameters); |
|
if (advancedResults !== true) { |
|
success = false; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (matches > 1) { |
|
assert.fail( |
|
`Network traffic for "${name}" was found ${matches} times. Only 1 time is expected. |
|
Expected: Part of URL: "${url}", Parameters: "${JSON.stringify(parameters)}"` |
|
); |
|
} |
|
if (success) { |
|
return true; |
|
} |
|
// No matching entry found in traffic. Let the test fail. |
|
// Use this for debugging the traffic: |
|
//fs.writeFileSync('output/traffic-debug.har', JSON.stringify(har, null, "\t"), 'utf8'); |
|
return `Expected content in network traffic was not found for "${name}". |
|
Expected: Part of URL: "${url}", Parameters: "${JSON.stringify(parameters)}" |
|
${advancedResults ? advancedResults : ''}` |
|
; |
|
} |
|
|
|
async dontSeeTraffic(name, url, parameters) { |
|
const I = this; |
|
try { |
|
await I.seeTraffic(name,url,parameters, undefined, 0); |
|
} catch (err) { |
|
if (err.code === 'ERR_ASSERTION') { |
|
// verification succesful |
|
return true; |
|
} |
|
} |
|
assert.fail(`Content in network traffic was unexpectedly found for "${name}". |
|
Expected: Part of URL: "${url}", Parameters: "${JSON.stringify(parameters)}" |
|
Content of network traffic saved in dir: "./output/traffic"`) |
|
} |
|
|
|
/** |
|
* Modifies rewrite rules of BrowserMobProxy |
|
* |
|
* Example rewrite configuration to block traffic: |
|
* { |
|
* matchRegex : 'https://cdn.optimizely.com.*', |
|
* replace : '' |
|
* } |
|
*/ |
|
rewriteTraffic(configuration) { |
|
this.proxy.rewrite(this.bmpProxyPort, configuration, () => {}); |
|
} |
|
|
|
/** |
|
* Saves network traffic to .har file on disk. |
|
* |
|
* @param name Prefix added to filename. Filename will always contain timestamp. Pattern "name-1239809122.har". Default value: "Traffic" |
|
* @param folder The folder the .har file will saved in. Default value: "output/traffic" |
|
*/ |
|
async saveTraffic(name = 'Traffic', folder = 'output/traffic/') { |
|
const I = this; |
|
try { |
|
if (folder.slice(-1) != '/' ) { |
|
folder = folder + '/'; |
|
} |
|
// Use this for debugging the traffic: |
|
!fs.existsSync(folder) && fs.mkdirSync(folder); |
|
const harFilename = name + '-' + new Date().getTime() + '.har'; |
|
fs.writeFileSync(folder + harFilename, JSON.stringify(await I.getHar(), null, "\t"), 'utf8'); |
|
console.log('Traffic saved to "file://' + process.cwd() + '/' + folder + harFilename + '"'); |
|
} catch (err) { |
|
console.log('WARN: Writing traffic to HAR-file failed. Error message: "' + err + '"') |
|
} |
|
} |
|
} |
|
|
|
module.exports = Browsermob; |
|
|
|
function allParameterValuePairsMatch(queryString, postData, parameterValuePairs) { |
|
let parameterMatched = 0; |
|
let success = false; |
|
// Check content of "queryString" |
|
|
|
for (let expectedKey in parameterValuePairs) { |
|
let continueSearch = false; |
|
let expectedValue = parameterValuePairs[expectedKey]; |
|
for (let queryParameter of queryString) { |
|
if (queryParameter.name === expectedKey) { |
|
if (queryParameter.value === expectedValue) { |
|
parameterMatched++; |
|
if (parameterMatched === Object.keys(parameterValuePairs).length) { |
|
continueSearch = false; |
|
success = true; |
|
break; |
|
} else { |
|
continueSearch = true; |
|
} |
|
} |
|
} |
|
} |
|
if (continueSearch === false) { |
|
break; |
|
} |
|
} |
|
|
|
// Nothing found so far, check content of "postData" |
|
if (success === false && postData && postData.text) { |
|
for (let parameterValuePairKey in parameterValuePairs) { |
|
let continueSearch = false; |
|
let expectedKey = parameterValuePairKey; |
|
let expectedValue = parameterValuePairs[parameterValuePairKey]; |
|
|
|
if ( (expectedValue === undefined && postData.text.indexOf(expectedKey + '=') !== -1) |
|
|| (postData.text.indexOf(expectedKey + '=' + expectedValue) !== -1) |
|
) { |
|
parameterMatched++; |
|
if (parameterMatched === Object.keys(parameterValuePairs).length) { |
|
continueSearch = false; |
|
success = true; |
|
} else { |
|
continueSearch = true; |
|
} |
|
} |
|
|
|
if (! continueSearch) { |
|
break; |
|
} |
|
} |
|
} |
|
|
|
return success; |
|
} |
|
|
|
/** |
|
* More advanced check if all request parameters match with the expectations |
|
* |
|
* @param queryString |
|
* @param advancedExpectedParameterValuePairs |
|
* @returns {*} |
|
*/ |
|
function allParameterValuePairsMatchExtreme(queryString, advancedExpectedParameterValuePairs) { |
|
let littleReport = '\nAdvanced test results:\n'; |
|
let success = true; |
|
|
|
for (let expectedKey in advancedExpectedParameterValuePairs) { |
|
let paramaterFound = false; |
|
let expectedValue = advancedExpectedParameterValuePairs[expectedKey]; |
|
|
|
for (let queryParameter of queryString) { |
|
if (queryParameter.name === expectedKey) { |
|
paramaterFound = true; |
|
if (expectedValue === undefined) { |
|
littleReport += ' ' + expectedKey.padStart(10, ' ') + '\n'; |
|
} else if (queryParameter.value === expectedValue) { |
|
littleReport += ' ' + expectedKey.padStart(10, ' ') + ' = ' + expectedValue + '\n' |
|
} else { |
|
littleReport += ' ✖ ' + expectedKey.padStart(10, ' ') + ' = ' + expectedValue + ' -> actual value: "' + queryParameter.value + '"\n'; |
|
success = false; |
|
} |
|
} |
|
} |
|
|
|
if (paramaterFound === false) { |
|
littleReport += ' ✖ ' + expectedKey.padStart(10, ' ') + (expectedValue ? ' = ' + expectedValue : '') + ' -> parameter not found in request\n'; |
|
success = false; |
|
} |
|
} |
|
|
|
return success ? true : littleReport; |
|
} |
|
|
|
function detectBrowserHelper() { |
|
const browserHelpers = ['WebDriver','Protractor','Puppeteer','TestCafe','Nigthmare','Playwright']; |
|
const configuredHelpers = codeceptjs.container.helpers(); |
|
for (const helper of browserHelpers) { |
|
if (Object.keys(configuredHelpers).indexOf(helper) >= 0) return codeceptjs.container.helpers(helper); |
|
} |
|
throw new Error(`[ERROR] No browser helper configured in CodeceptJS config. Expecting one of these: ${browserHelpers}`); |
|
} |