@cuppet/core
Version:
Core testing framework components for Cuppet - BDD framework based on Cucumber and Puppeteer
324 lines (301 loc) • 12.3 kB
JavaScript
const axios = require('axios');
const config = require('config');
const storage = require('./dataStorage');
const helper = require('./helperFunctions');
const xml2js = require('xml2js');
const assert = require('chai').assert;
const fs = require('fs');
const mime = require('mime-types');
const FormData = require('form-data');
module.exports = {
/** @type {object} */
response: null,
/** @type {object} */
request: null,
/**
* Prepare path for API test usage
* @param url - It can be absolute/relative path or even placeholder for saved variable
* @returns {Promise<*>} - Returns a working path
*/
prepareUrl: async function (url) {
const path = await storage.checkForMultipleVariables(url);
if (!path.startsWith('http') && config.has('api.baseApiUrl')) {
// Replace placeholders in base path with variables
const resolveBasePath = await storage.checkForMultipleVariables(config.get('api.baseApiUrl'));
return resolveBasePath + path;
}
// Return the path as is if it's already an absolute URL or if no base path is configured
return path;
},
/**
* Function used to generate the needed headers for each request
* @async
* @function setHeaders
* @param headers
* @param {boolean} defaultHeadersFlag
* @returns {Promise<Object>} - Returns an object with the headers
*/
setHeaders: async function (headers = {}, defaultHeadersFlag = false) {
if (!defaultHeadersFlag) {
return headers;
}
let defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
if (this.request instanceof FormData) {
defaultHeaders = {};
Object.assign(defaultHeaders, this.request.getHeaders());
}
if (config.has('api.x-api-key')) {
defaultHeaders['X-Api-Key'] = config.get('api.x-api-key');
}
if (config.has('api.Authorization')) {
defaultHeaders['Authorization'] = config.get('api.Authorization');
}
if (headers && defaultHeaders) {
defaultHeaders = {
...defaultHeaders,
...headers,
};
}
return defaultHeaders;
},
/**
* Prepare and set the basic auth (if needed).
* This method supports if the API and the website have different basic auth.
* @async
* @function setHeaders
* @returns {Promise<{Object}>}
*/
setBasicAuth: async function () {
let basicAuth = {};
if (config.has('api.authUser')) {
basicAuth = {
username: config.get('api.authUser'),
password: config.get('api.authPass'),
};
} else if (config.has('basicAuth.authUser')) {
basicAuth = {
username: config.get('basicAuth.authUser'),
password: config.get('basicAuth.authPass'),
};
}
return basicAuth;
},
/**
* Sends an HTTP request using axios.
*
* @async
* @function sendRequest
* @param {string} method - The HTTP method to use for the request.
* @param {string} [url="/"] - The URL to send the request to. Defaults to "/".
* @param {Object} [headers={}] - An object containing HTTP headers to include with the request. Defaults to an empty object.
* @param {Object} [data={}] - An object containing data to send in the body of the request. Defaults to an empty object.
* @returns {Promise<Object>} Returns a Promise that resolves to the response from the server.
* @throws {Error} Throws an error if the request fails.
*/
sendRequest: async function (method, url = '/', headers = {}, data = {}) {
const apiUrl = await this.prepareUrl(url);
const requestHeaders = await this.setHeaders(headers, true);
const auth = await this.setBasicAuth();
if (this.request) {
data = this.request;
}
try {
this.response = await axios.request({
url: apiUrl,
method: method.toLowerCase(),
...(Object.keys(auth).length && { auth }),
// The data is conditionally added to the request, because it's not used with GET requests and creates conflict.
// The following checks if data object is not empty, returns data object if not empty or skip if empty.
...((data instanceof FormData || Object.keys(data).length) && { data }),
headers: requestHeaders,
});
// Delete the request object after the request is sent to avoid conflicts with the next request.
delete this.request;
return this.response;
} catch (error) {
console.log('Request has failed, use response code step definition to validate the response!');
if (!error.response) {
throw new Error(`Request failed with: ${error}`);
}
return (this.response = error.response);
}
},
/**
* Replace placeholders of type %var% and prepare request body
* @async
* @function prepareRequestBody
* @param body - the request body needs to be passed in string format
* @returns {Promise<Object>} - returns the request body object
*/
prepareRequestBody: async function (body) {
const preparedBody = await storage.checkForMultipleVariables(body);
this.request = JSON.parse(preparedBody);
return this.request;
},
/**
* Put values in request 1 by 1
* Example object: {
* property: value
* }
* @async
* @function prepareRequestBody
* @param value - the value to set
* @param objectPath - the path in dot notation (e.g., "home.user.firstName")
* @returns {Promise<Object>} - returns the request body object
*/
iPutValuesInRequestBody: async function (value, objectPath) {
if (!this.request) {
this.request = {};
}
const keys = objectPath.split('.');
let current = this.request;
// Navigate/create the path, stopping before the last key
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
// Set the final value
current[keys[keys.length - 1]] = value;
return this.request;
},
/**
* This step is used to validate the status code of the response
* @param code
* @returns {Promise<void>}
*/
validateResponseCode: async function (code) {
if (this.response.status !== Number(code)) {
throw new Error(
`Unexpected response code, code: ${this.response.status}. Response: ${JSON.stringify(this.response.data)}`
);
}
},
/**
* Use this step whether the response is of type array or object
* @param type
* @returns {Promise<void>}
*/
validateResponseType: async function (type) {
await assert.typeOf(this.response.data, type, `Response is not an ${type}`);
},
/**
* Asynchronously checks if a property of the response data is of a specified type.
*
* @async
* @function propertyIs
* @param {string} property - The property of the response data to check. Written in root.parent.child syntax.
* @param {string} type - The type that the property should be.
* @throws {Error} - Will throw an error if the property is not of the specified type.
*/
propertyIs: async function (property, type) {
const value = await helper.getPropertyValue(this.response.data, property);
assert.typeOf(value, type, `The property is not an ${type}`);
},
/**
* Validate value of property from the http response.
* Automatically converts expectedValue from Gherkin string to match the actual type (string, number, boolean, null).
* @param property
* @param expectedValue
* @returns {Promise<void>}
*/
propertyHasValue: async function (property, expectedValue) {
const actualValue = await helper.getPropertyValue(this.response.data, property);
const castedExpectedValue = helper.castPrimitiveType(expectedValue);
assert.strictEqual(
actualValue,
castedExpectedValue,
`Property "${property}" does not have the expected value. Expected: ${castedExpectedValue} , Actual: ${actualValue}`
);
},
/**
* @async
* @function iRememberVariable
* @param property - the name of the JSON property, written in root.parent.child syntax
* @param variable - the name of the variable to which it will be stored
* @throws {Error} - if no property is found in the response data
* @returns {Promise<void>}
*/
iRememberVariable: async function (property, variable) {
const propValue = await helper.getPropertyValue(this.response.data, property);
await storage.iStoreVariableWithValueToTheJsonFile(propValue, variable);
},
/**
* Load custom json file and make a request body from it
* @param path
* @returns {Promise<Object>}
*/
createRequestBodyFromFile: async function (path) {
this.request = storage.getJsonFile(path);
return this.request;
},
/**
* Send request to an endpoint and validate whether the response is valid xml.
* @param url
* @returns {Promise<void>}
*/
validateXMLEndpoint: async function (url) {
const xmlUrl = await this.prepareUrl(url);
let response;
try {
const auth = await this.setBasicAuth();
response = await axios.request({
url: xmlUrl,
method: 'get',
...(Object.keys(auth).length && { auth }),
});
} catch (error) {
throw new Error(`Request failed with: ${error}`);
}
const isValid = await xml2js.parseStringPromise(response.data);
if (!isValid) {
throw new Error('XML is not valid!');
}
},
/**
* Validate response header
* @param {string} header - header name
* @param {string} value - header value
* @returns {Promise<void>}
*/
validateResponseHeader: async function (header, value) {
// Resolve header and value from variables or user input.
// The header is checked directly for faster execution as it is less likely to contain variables.
const actualValue = this.response.headers[header.toLowerCase()];
assert.isDefined(actualValue, `The response header "${header}" is not found!`);
assert.strictEqual(actualValue, value, `The response header "${header}" does not have the expected value`);
},
/**
* Build multipart/form-data request body
* If you want to send a file, you need to pass the file name (not the path) as a string.
* Please use the files from the files folder.
* @param {object} data - the data to be sent in the request body
* @returns {Promise<Object>} - returns the request body object
*/
buildMultipartFormData: async function (data) {
const filePath = config.get('filePath');
const formData = new FormData();
for (const [key, value] of Object.entries(data)) {
if (key === 'file') {
const mimeType = mime.contentType(value) || 'application/octet-stream';
if (fs.existsSync(filePath + value)) {
formData.append('file', fs.createReadStream(filePath + value), {
filename: value,
contentType: mimeType,
});
} else {
throw new Error(`File ${filePath + value} does not exist in the files folder!`);
}
formData.append('type', mimeType);
} else {
formData.append(key, await storage.checkForSavedVariable(value));
}
}
this.request = formData;
return this.request;
},
};