Manage error in custom step for autologin in CodeceptJS - javascript

I'm using autoLogin plugin in CodeceptJS project with Puppeteer as testing lib.
This is my first project with CodeceptJS, the autoLogin workflow is working fine, but I'm not using the application pages (UI) to login or check the current token, I'm using directly REST API calls to make all the process a bit faster.
My autoLogin config is something like:
autoLogin: {
enabled: true,
saveToFile: true,
inject: 'login',
users: {
admin: {
login: async (I) => {
I.amOnPage('/login');
await I.loginWithCredentials(adminUser.username, adminUser.password);
},
check: (I) => {
const token = codeceptjs.store['admin_session'];
I.validateToken(token);
},
fetch: async (I) => {
const cookie = await I.grabCookie('token');
return cookie.value;
},
restore: (I, sessionToken) => {
I.amOnPage('/login');
I.saveTokenData(sessionToken);
}
},
}
}
The steps saveTokenData(), validateToken() and loginWithCredentials() are custom steps defined with actor(), for instance:
module.exports = function() {
return actor({
async validateToken(token) {
let response = await this.sendGetRequest(`/api/session/validate?token=${token}`);
if (response.status === 200) {
if (response.data === true) {
return true;
}
}
throw new Error('Invalid token !!');
}
});
The line throw new Error('Invalid token !!'); It's generating a "unexpected" error in the workflow, showing this line in the log:
(node:5986) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
I've tried several approaches like using the recorder.add() (throwing the error inside the recorder), I also tried to use chained promises (without async keyword) and the assert lib with assert.fail() , but always the same problem.
How can I "mark" my custom step as a failure without odd log messages?, I just want to implement the check step in the autoLogin without the exiting API to validate the user interface (I.see(), I.seeElement(), I.dontSee()...)
I'm using an ugly workaround until I get a better solution, the custom step returns a boolean, without throwing any error, and in the check code I've got this:
check: async (I) => {
console.log('admin.check');
const token = codeceptjs.store['admin_session'];
const isValid = await I.validateToken(token);
if (!isValid) {
I.see("NOTHING TO SEE HERE, so It'll fail");
}
},
I opened an issue in CodeceptJS GitHub project #2600

How about use the assert.fail method? It will fail your entire Scenario and you can customize the message!
const assert = require("assert");
module.exports = function() {
return actor({
async validateToken(token) {
let response = await this.sendGetRequest("/api/session/validate?token=${token}");
if (response.status !== 200 || response.data !== true) {
assert.fail("Invalid token!");
}
}
});
Check method:
check: async (I) => {
console.log('admin.check');
const token = codeceptjs.store['admin_session'];
await I.validateToken(token);
}

Related

React Native fetch abortController - figuring out abort reason

I'm in need of implementing following features for network/http client using fetch API.
Timeout
Also abort a previous request when user make multiple requests.
I was able to implement both using the abortController. But on the case of "Timeout" (no 1), I want to catch the abort and show a proper error message with "retry" option.
But when I wrap my network request inside try catch, I can't distinguish between above 1 and 2 cases. Cause both abort are thrown with same exception name/message.
The web implementation does support passing a "reason" into the abort() call. But looks like reactNative doesn't have that implemented ( Using react-native 0.63.3 )
async function request(url, abortController) {
// Manually timing out, as I did not find any support for timeout on react-native
const timeoutRef = setTimeout(() => abortController.abort(), 90000); // CASE 1 : Timeout abort
return await fetch(url,
{
signal: controller.signal
})
}
var abortController = null;
var requestPending = false;
async function searchWebsite(searchQuery) {
// If there is already pending requesting - we cancel that previous
// pending request.
if ( abortController && !controller.signal.aborted && requestPending) {
abortController.abort(); // CASE 2 : abort previous request
}
// Create a new request
try {
abortController = new AbortController();
requestPending = true;
let apiRequest = await request("http://someurl.com", abortController);
// Do whatever with `apiRequest`
requestPending = false;
} catch(e) {
requestPending = false;
if (e.name == 'AbortError') {
// HERE I'M STRUGGLING WITH
// figure out how to distinguish between "timeout" and "previous request" abort
}
}
}
How can I distinguish between different type of abortController abort on react-native?
I was in this situation before and decided to move the cancellation logic to the async function itself instead of the fetch, and create another abort controller instance, then you can throw a different message based on which abort controller you used to abort the request, based on that message you'll know which abort controller caused the error (the cancellation)
Here is an example of what i did (in a react hook), but you should be able to apply the same logic and throw your own errors in whichever way you like
return new Promise((resolve, reject) => {
if (abortControllerCancel.current.signal.aborted) {
reject({
message: 'canceled',
reason: 'canceled',
});
}
if (abortControllerDuplicate.current.signal.aborted) {
reject({
message: 'canceled',
reason: 'duplicate',
});
}
// the rest of the async function and resolving the promise
abortControllerCancel.current.signal.addEventListener(
'abort',
() => {
reject({
message: 'canceled',
reason: 'canceled',
});
}
);
abortControllerDuplicate.current.signal.addEventListener(
'abort',
() => {
reject({
message: 'canceled',
reason: 'duplicate',
});
}
);
}

UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'replace' of undefined

I got an error UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'replace' of undefined on line 36 of my file index.js (see script below). I'm calling it from a Python subprocess
Note: I must call index.js through a subprocess
For some odd reason, the program works 20% of the time, other than that I keep getting this error. How do I fix this error?
main.py
import os
import subprocess
filePath = "/file/path/index.js"
command = subprocess.Popen(["node", filePath, "https://video.com/link"])
stdout, stderr = command.communicate()
command.wait()
Error
(node:64821) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'replace' of undefined
at /full/path/index.js:36:23
at processTicksAndRejections (internal/process/task_queues.js:93:5)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:64821) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:64821) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
index.js
const urlRegex = require("url-regex");
const fetch = require("node-fetch");
const fs = require("fs");
const https = require("https");
if (process.argv.length <= 2) {
console.log('Missing URL.');
console.log('Usage: node index.js <url>');
process.exit(0);
}
let url = process.argv[2];
if (url.endsWith('/')) url = url.substring(0, url.length - 1);
const urlParts = url.split("/");
let id = urlParts[urlParts.length - 1];
if (id.includes("?")) id = id.split("?")[0];
console.log(`URL > ${url}`);
console.log(`ID > ${id}`);
fetch(url)
.then(res => {
return res.text();
})
.then(body => {
const urls = body.match(urlRegex());
const baseUrl = urls.find(url => url.includes("playlist.m3u8"))
let liveUrl = baseUrl;
liveUrl = liveUrl.replace("pull-hls", "pull-flv");
liveUrl = liveUrl.split("/").slice(0, 5).join("/")
liveUrl += ".flv"
console.log(`Found live playlist (m3u8) URL: ${baseUrl}`);
console.log(`Found live flv URL: ${liveUrl}`);
console.log(`Writing live to output.flv. Press Ctrl C to STOP.`)
const file = fs.createWriteStream('output_path/output.flv')
https.get(liveUrl, function (response) {
response.pipe(file);
});
fs.writeFile('output_path/completion_check.txt', 'Hello World!', function (err) {
if (err) return console.log(err);
console.log('completion_check.txt has been created');
});
});
The reason is that on your line 32, the .find function can return undefined and you cannot call any methods (.replace in this case) on an undefined value:
const baseUrl = urls.find(url => url.includes("playlist.m3u8"))
Add a check after this line before proceeding. For example:
if (baseUrl === undefined) {
console.log("Did not find any matching URLs")
return;
}
More on how .find works:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
As the terminal shows try to launch the node script adding --trace-warnings to show where the warning was created.
You should try:
command = subprocess.Popen(["node --trace-warnings", filePath, "https://video.com/link"])
Then you should also add a catch block inside your promise, you can see the message in your terminal (This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch()). Whit this is easy to see where is originated the error.
fetch(url)
.then(res => {
return res.text();
})
.then(body => {
const urls = body.match(urlRegex());
const baseUrl = urls.find(url => url.includes("playlist.m3u8"))
let liveUrl = baseUrl;
liveUrl = liveUrl.replace("pull-hls", "pull-flv");
liveUrl = liveUrl.split("/").slice(0, 5).join("/")
liveUrl += ".flv"
console.log(`Found live playlist (m3u8) URL: ${baseUrl}`);
console.log(`Found live flv URL: ${liveUrl}`);
console.log(`Writing live to output.flv. Press Ctrl C to STOP.`)
const file = fs.createWriteStream('output_path/output.flv')
https.get(liveUrl, function (response) {
response.pipe(file);
});
fs.writeFile('output_path/completion_check.txt', 'Hello World!', function (err) {
if (err) return console.log(err);
console.log('completion_check.txt has been created');
});
})
.catch((error) => {
console.log(error)
});
A tricky thing about String.prototype.match is that unless the RegExp has the g (global) flag, you will not match multiple values. It looks like you're using the url-regex package, which does not return a RegExp with the g flag set, so you're not going to be getting the array of matches like you expect. So that might be why finding the URL you expect is turning up undefined.
You'll have to convert that URL to a global, something like this:
const allUrls = new RegExp(urlRegex().source, 'ig'); // Case insensitive, global
// ...
const urls = body.match(allUrls);
const baseUrl = urls.find((url => url.includes("playlist.m3u8"));
Of course, you'll also want to check that baseUrl was found:
if (!baseUrl) {
// Or however you want to handle this case...
throw new Error('Base URL containing playlist.m3u8 was not found');
}
I don't know the library url-regex, but reading this source file seem that the default option for the strict parameter is true. You can try to print out the partial results with the console api, es console.log(body, body.match(urlRegex())) or using the debugger api of node (or you can use vscode that has a very nice debugger for JavaScript)
Also maybe, in urls, there is no url that includes the "playlist.m3u8" string, so urls.find(url => url.includes("playlist.m3u8")) returns undefined. You can chain a .catch(error => { ... }) to the fetch() call, after the .then(body => { ... }) in order to handle exceptions

Testcafe V1.9.0 UnhandledPromiseRejectionWarning: TypeError: Cannot convert undefined or null to object

I am losing my marbles trying to figure out why testcafe is being such a pain. So here is my scenario:
I have a runner which launches my tests against browserstack. fine. Once I bump my testcafe version from 1.6.1 to 1.9.0 the browserstack runner fails to launch. I get this error:
(node:12310) UnhandledPromiseRejectionWarning: TypeError: Cannot convert undefined or null to object
at Function.entries (<anonymous>)
at TestCafeConfiguration.mergeOptions (/Users/testcafe/node_modules/testcafe/lib/configuration/configuration-base.js:50:16)
at TestCafeConfiguration.init (/Users/testcafe/node_modules/testcafe/lib/configuration/testcafe-configuration.js:48:14)
at async getConfiguration (/Users/testcafe/node_modules/testcafe/lib/index.js:41:9)
at async createTestCafe (/Users/testcafe/node_modules/testcafe/lib/index.js:57:27)
(node:12310) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:12310) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Weird enough if I were to bump my version to 1.7.0 I do not experience any issues. Any version 1.8.0 and up I get the above error.
Any one experienced this as well? Can someone shed some light on the issue?
Here is my runner SetupRunners.js file:
import getRunnerSrc from "./getRunnerSrc";
import getRunnerBrowsers from "./getRunnerBrowsers";
export default function setupRunner(runner, options = {}, cb) {
let stream = false;
const src = options.src || getRunnerSrc(options.breakpoint);
if (src && src.length) {
runner.src(src);
runner.browsers(
options.browsers || getRunnerBrowsers(options.breakpoint)
);
return cb(runner).then(data => {
if (stream) {
stream.end();
}
return data;
});
}
}
Here is the file that launches the tests against browserstack
import getArgs from "./getArgs";
import sanitizeArg from "./sanitizeArg";
import isBrowserStack from "./getIsBrowserStack";
const defaultBrowsers = {
desktop: "Chrome:OS X",
mobile: "Safari:iOS",
};
const browserStackPrefix = "browserstack:";
export default function getRunnerBrowsers(breakpoint) {
const useBrowserStack = isBrowserStack();
if (useBrowserStack && breakpoint) {
return getBrowserstackDevices(breakpoint);
}
return `${useBrowserStack ? browserStackPrefix : ""}${sanitizeArg(
getArgs("browsers") || defaultBrowsers.desktop
)}`;
}
function getBrowserstackDevices(breakpoint) {
return `${browserStackPrefix}${getArgs(`${breakpoint}_devices`) ||
defaultBrowsers[breakpoint]}`;
}
here is the helper getArgs.js:
function getArgs(key) {
if (process && process.argv && process.argv.length) {
return process.argv.reduce((val, arg) => {
const split = arg.split("=");
if (split[0] === key || split[0] === "--" + key) {
val = split[1];
}
return val;
}, null);
}
return null;
}
module.exports = getArgs;
and santizeArgs.js
export default function sanitizeArg(ar) {
return filterEmpty((ar || "").split(",").map(str => str.trim()));
}
export function filterEmpty(items) {
return items.filter(item => item && item.length);
}
======= UPDATE ========
CREATE TESTCAFE FUNCTION at v1.7.0
async function createTestCafe(hostname, port1, port2, sslOptions, developmentMode, retryTestPages) {
console.dir
const configuration = new testcafe_configuration_1.default();
await configuration.init({
hostname,
port1,
port2,
ssl: sslOptions,
developmentMode,
retryTestPages
});
CREATE TESTCAFE FUNCTION AFTER UPDATING TESTCAFE TO v1.9.2
async function createTestCafe(...args) {
const configuration = await getConfiguration(args); // HERE IS WHERE THE ERROR IS
const [hostname, port1, port2] = await Promise.all([
getValidHostname(configuration.getOption(option_names_1.default.hostname)),
getValidPort(configuration.getOption(option_names_1.default.port1)),
getValidPort(configuration.getOption(option_names_1.default.port2))
]);
configuration.mergeOptions({ hostname, port1, port2 });
const testcafe = new TestCafe(configuration);
setupExitHook(cb => testcafe.close().then(cb));
return testcafe;
}
I reproduced the problem based on your information and created an issue in the TestCafe GitHub repository. As a workaround, you can correct parameter values passed to the 'createTestCafe' function.
According to the output you shared, you passed the 'null' value as the 'hostname' parameter. I guess the 'null' value means that the 'hostname' value is not specified. In this case, you need to return 'undefined' instead of 'null' to the 'createTestCafe' function.

Use Try/Catch Ideally

I have two function, the controller and the service. Here is the service code.
const getVersion = async (type) => {
const version = await Version.findOne({ TYPE: type }, { _id: false, VERSION: true })
return version
}
And the controller code call the getVersion function that exist in service
const getVersion = async (req, res) => {
try {
......
const version = await Version.findOne({ TYPE: type }, { _id: false, VERSION: true })
......
} catch (error) {
......
}
}
So my question is, in getVersion() function, there's an asynchronous call. Should I wrap the function in try catch, so it would look like this:
const getVersion = async (type) => {
try {
const version = await Version.findOne({ TYPE: type }, { _id: false, VERSION: true })
return version
} catch (error) {
return error
}
}
Or should i leave as it to be like the original one that use the try/catch in the root of the function? What are the advantages and the disadvantages of those two method? Thank you.
This is an anti-pattern -
const getVersion = async (type) => {
try {
const version = await Version.findOne({ TYPE: type }, { _id: false, VERSION: true })
return version
} catch (error) {
return error
}
}
The reason being that your function is labeled async it already returns a Promise. So it will either resolve version or it will reject the error.1
This is the idiomatic way to write it -
const getVersion = type =>
Version.findOne({ TYPE: type }, { _id: false, VERSION: true })
Now when you call it, a valid version response will be resolved, or some error will be rejected -
getVersion("foo").then(console.log, console.error)
1.In your getVersion you actually resolve both the successful case and the error case. This is effectively silencing the error instead of letting it bubble up to the caller. By rejecting the error, you allow the caller to handle it appropriately.
This is a similar anti-pattern -
function foo (s = "") {
if (s.length > 5)
return true
else
return false
}
Which is the less idiomatic version of -
function foo (s = "") {
return s.length > 5
}
Or as an arrow function -
const foo = (s = "") =>
s.length > 5
I suggest you leave as it is. There is no need to add try catch at different places to handle same exception. Suppose assume you are logging exception message to database. If catch is at 2 places, you will end up writing 2 logs into Db. It gives wrong perception that 2 exceptions occurred!
You should only wrap around to the code that's actually asynchronously fetching data (i.e, liable to be successful or fail). For example, this line - const version = await Version.findOne({ TYPE: type }, { _id: false, VERSION: true }). So, that if that's fails Catch block would run. You should wrap the whole function with try/catch
Should I wrap the function in try catch
You need to wrap async functions, whenever a promise is being resolved.
The await statement means there's an asynchronous call that could potentially fail and reject. If it fails and it's not within a trycatch block, then it will create an Unhandled Promise Rejection.
TLDR:
If you use the await keyword, then wrap it with a try catch.
This function requires a trycatch block
async function DoSomething() {
try {
const result = await MakeRequest(); // failure must be handled here
return result;
} catch (error) {
// Handle error
}
}
This function doesn't require a trycatch block
// This requires a trycatch block
async function DoSomething() {
return MakeRequest(); // failure can be handled by parent function
}

Usage of async/await in Mocha - test hangs or ends up with unhandled promise rejection

Running very simple test where I'm passing invalid credentials within the basic authorization header and I'm expecting the server to return 401
const request = require('request-promise');
const expect = require('chai').expect;
const basicToken = require('basic-auth-token');
describe('PUT Endpoint', function () {
it.only('should return unauthorized if basic token is incorrect', async function (done) {
let options = {
url: `http://url_to_handle_request`,
resolveWithFullResponse: true,
headers: {
Authorization: `Basic ${basicToken('INVALID', 'CREDENTIALS')}`
}
};
try {
await request.put(options); // this should throw exception
} catch (err) {
expect(err.statusCode).to.be.equal(401); // this is called
}
done();
});
});
Problem with this code is that the expect clause resolves to false (because the server responded e.g. with 403) and the test ends up with error:
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): AssertionError: expected 403 to equal 401
If I omit the done callback, the test just hangs (the name is in red) and is apparently "waiting" for something to finish
I know it will work if I would rewrite it to use standard promises approach. I'm just curious how to do it via async/await.
Thanks
Remove done from parameter (and as well from function body), then Mocha will expect that you return Promise.
Async functions returns promises by default.
If you dont throw error, it returns resolved promise.
Change this code block
try {
await request.put(options); // this should throw exception
} catch (err) {
expect(err.statusCode).to.be.equal(401); // this is called
}
done();
to
await request.put(options)
.then(()=>{ })
.catch(function (err)){
expect(err.statusCode).to.be.equal(401);
}

Categories

Resources