I'm creating an API automation suite with Cypress.
I have API tests which don't require logging in, and ones that do.
I have written a utility method called 'callAPI' which all my tests will use - it has a bunch of error checking and automatically augments calls with relevant headers/authorization token, depending on what the caller passes into the method.
I also have a 'logIn' method which creates an alias storing an authorization token.
In the scenario where the automation engineer calls callAPI to do an API request that requires logging in, I want to be able to detect that they haven't logged in and throw an error message advising as such.
Approach 1: If the alias 'accessToken' doesn't exist, throw the error.
Problem: It appears that the use of cy.state('aliases') has been deprecated.
Approach 2: Create the alias in the 'before' hook with the default value 'accessToken not set'...
before(() => {
cy.wrap('accessToken not set').as('accessToken');
});
Then in callAPI, check if the alias's value is equal to 'accessToken not set' and throw the error.
Problems:
if (useAccessToken && cy.get('@accessToken') == 'accessToken not set') {
throw new Error('callAPI() - access token not set. Log in first.');
}
if (useAccessToken && cy.get('@accessToken').invoke('text') == 'accessToken not set') {
throw new Error('callAPI() - access token not set. Log in first.');
}
First 'if' statement displays "This comparison appears to be unintentional because the types 'Chainable' and 'string' have no overlap."
Second 'if' statement displays "This comparison appears to be unintentional because the types 'Chainable' and 'string' have no overlap."
I've perhaps mistakenly assumed that it should be trivial to compare the text within an alias in an 'if' statement, but apparently this is not a thing...?
Approach 3: The intention here would be to do the comparison within the 'then'....
cy.get('@accessToken').then(data => {
cy.log(data.text());
});
Problem: "data.text is not a function"
Approach 4: Within my 'if' statement, assert that the alias's text equals 'accessToken not set'.
if (useAccessToken && cy.get('@accessToken').should('equal', 'accessToken not set')) {
throw new Error('callAPI() - access token not set. Log in first.');
}
Problem: If this assertion fails, Cypress throws its own error, defeating the object of throwing my custom error.
My code:
const apiURL = Cypress.env('apiUrl');
export const APIURLs = {
Login: `${apiURL}access/login`,
Info: `${apiURL}info`,
InfoHealth: `${apiURL}info/health`
};
export class Common {
logInAndSetAccessToken(emailAddress: string, password: string) {
this.callAPI({
requestObject: {
method: 'POST',
url: APIURLs.Login,
body: {
email: emailAddress,
password: password
}
}, useAccessToken: false
}).then(response => {
cy.wrap(response.body.tokens.accessToken).as('accessToken');
});
}
/**
* 'headers' property, along with the following headers are automatically added to requestObject:
*
* 'Content-Type': 'application/json; charset=utf-8'
*
* 'Authorization': `Bearer ${accessToken}`
*/
callAPI({ url = '', requestObject = {}, useAccessToken = true } = {}) {
if (url.length > 3 && Object.keys(requestObject).length > 0) {
throw new Error('callAPI() - method call ambigious. Pass url or requestObject, not both.');
}
if (url.length < 4 && Object.keys(requestObject).length == 0) {
throw new Error('callAPI() - method call missing necessary information to make API call. Pass url or requestObject.');
}
if (useAccessToken && cy.get('@accessToken') == 'accessToken not set') { // This comparison appears to be unintentional because the types 'Chainable<JQuery<HTMLElement>>' and 'string' have no overlap.
throw new Error('callAPI() - access token not set. Log in first.');
}
if (Object.keys(requestObject).length > 0) {
if (!requestObject.method || !requestObject.url || !requestObject.body) {
throw new Error('callAPI() - method, url or body properties are missing in the requestObject.');
}
if (!requestObject.headers) {
Object.assign(requestObject, { headers: {} });
}
if (!requestObject.headers['Content-Type']) {
Object.assign(requestObject.headers, { 'Content-Type': 'application/json; charset=utf-8' });
}
if (useAccessToken && !requestObject.headers['Authorization']) {
Object.assign(requestObject.headers, { 'Authorization': `Bearer ${cy.get('@accessToken')}` });
}
return cy.request(requestObject);
} else {
if (url.length < 4) {
throw new Error('callAPI() - invalid url, cannot call API.');
}
return cy.request(url);
}
}
}
Unless I'm missing the obvious (wouldn't surprise me), is there any way to address this problem? Or should I just rely on the API informing the automation engineer that they need to log in?
I'm using TypeScript if that matters.
Thank you for any help you may be able to offer.
Thank you everyone for helping.
I ended up separating the logIn method and callAPI method, calling the logIn method within callAPI if the access token needs to be used.
export class Common {
/**
* Strictly for testing logging in - DOES NOT save access token for future calls.
*/
logIn(emailAddress: string, password: string) {
return cy.request({
method: 'POST',
url: APIURLs.AccessLogin,
body: {
email: emailAddress,
password: password
}
});
}
/**
* For API calls that require logging in/access token, pass userObject.
*/
callAPI({ url = '', requestObject = {}, useAccessToken = true, userObject = {} } = {}) {
if (url.length > 0 && Object.keys(requestObject).length > 0) {
throw new Error('callAPI() - method call ambigious. Pass url or requestObject, not both.');
}
if (url.length < (apiURL.length + 4) && Object.keys(requestObject).length == 0) {
throw new Error('callAPI() - method call missing necessary information to make API call. Pass url or requestObject.');
}
if (useAccessToken && Object.keys(userObject).length == 0) {
throw new Error('callAPI() - cannot use access token without passing a userObject from configuration.');
}
if (Object.keys(requestObject).length > 0) {
if (!requestObject.method || !requestObject.url || !requestObject.body) {
throw new Error('callAPI() - method, url or body properties are missing in the requestObject.');
}
if (!requestObject.headers) {
Object.assign(requestObject, { headers: {} });
}
if (!requestObject.headers['Content-Type']) {
Object.assign(requestObject.headers, { 'Content-Type': 'application/json; charset=utf-8' });
}
} else {
if (url.length < (apiURL.length + 4)) {
throw new Error('callAPI() - invalid url, cannot call API.');
}
}
if (useAccessToken) {
return this.logIn(userObject.Email, userObject.m_UIPassword).then(response => {
const accessToken = response.body.tokens.accessToken;
if (!requestObject.headers['Authorization']) {
Object.assign(requestObject.headers, { 'Authorization': `Bearer ${accessToken}` });
} else {
requestObject.headers['Authorization'] = `Bearer ${accessToken}`;
}
return cy.request(requestObject);
});
} else {
if (Object.keys(requestObject).length > 0) {
return cy.request(requestObject);
} else {
return cy.request(url);
}
}
}
}