repo_description: Supercharged End 2 End Testing Framework for NodeJS
repo_topics: acceptance-testing bdd bdd-framework codeceptjs e2e-tests end-to-end-testing graphql-testing hacktoberfest javascript nodejs pageobject playwright selenium selenium-webdriver testing webdriver webdriverio
code: const axios = require('axios').default
  const Helper = require('@codeceptjs/helper')
  const { Agent } = require('https')
  const Secret = require('../secret')
  
  const { beautify } = require('../utils')
  
  /**
   * ## Configuration
   *
   * @typedef RESTConfig
   * @type {object}
   * @prop {string} [endpoint] - API base URL
   * @prop {boolean} [prettyPrintJson=false] - pretty print json for response/request on console logs.
   * @prop {boolean} [printCurl=false] - print cURL request on console logs. False by default.
   * @prop {number} [timeout=1000] - timeout for requests in milliseconds. 10000ms by default.
   * @prop {object} [defaultHeaders] - a list of default headers.
   * @prop {object} [httpAgent] - create an agent with SSL certificate
   * @prop {function} [onRequest] - an async function which can update request object.
   * @prop {function} [onResponse] - an async function which can update response object.
   * @prop {number} [maxUploadFileSize] - set the max content file size in MB when performing api calls.
   */
  const config = {}
  
  /**
   * REST helper allows to send additional requests to the REST API during acceptance tests.
   * [Axios](https://github.com/axios/axios) library is used to perform requests.
   *
   * <!-- configuration -->
   *
   * ## Example
   *
   * ```js
   *{
   *   helpers: {
   *     REST: {
   *       endpoint: 'http://site.com/api',
   *       prettyPrintJson: true,
   *       onRequest: (request) => {
   *         request.headers.auth = '123';
   *       }
   *     }
   *   }
   *}
   * ```
   *
   *  With httpAgent
   *
   * ```js
   * {
   *   helpers: {
   *     REST: {
   *       endpoint: 'http://site.com/api',
   *       prettyPrintJson: true,
   *       httpAgent: {
   *          key: fs.readFileSync(__dirname + '/path/to/keyfile.key'),
   *          cert: fs.readFileSync(__dirname + '/path/to/certfile.cert'),
   *          rejectUnauthorized: false,
   *          keepAlive: true
   *       }
   *     }
   *   }
   * }
   * ```
   *
   * ```js
   * {
   *   helpers: {
   *     REST: {
   *       endpoint: 'http://site.com/api',
   *       prettyPrintJson: true,
   *       httpAgent: {
   *          ca: fs.readFileSync(__dirname + '/path/to/ca.pem'),
   *          rejectUnauthorized: false,
   *          keepAlive: true
   *       }
   *     }
   *   }
   * }
   * ```
   *
   * ## Access From Helpers
   *
   * Send REST requests by accessing `_executeRequest` method:
   *
   * ```js
   * this.helpers['REST']._executeRequest({
   *    url,
   *    data,
   * });
   * ```
   *
   * ## Methods
   */
  class REST extends Helper {
    constructor(config) {
      super(config)
      this.options = {
        timeout: 10000,
        defaultHeaders: {},
        endpoint: '',
        prettyPrintJson: false,
        onRequest: null,
        onResponse: null,
      }
  
      if (this.options.maxContentLength) {
        const maxContentLength = this.options.maxUploadFileSize * 1024 * 1024
        this.options.maxContentLength = maxContentLength
        this.options.maxBodyLength = maxContentLength
      }
  
      // override defaults with config
      this._setConfig(config)
  
      this.headers = { ...this.options.defaultHeaders }
  
      // Create an agent with SSL certificate
      if (this.options.httpAgent) {
        // if one of those keys is there, all good to go
        if (this.options.httpAgent.ca || this.options.httpAgent.key || this.options.httpAgent.cert) {
          this.httpsAgent = new Agent(this.options.httpAgent)
        } else {
          // otherwise, throws an error of httpAgent config
          throw Error('Please recheck your httpAgent config!')
        }
      }
  
      this.axios = this.httpsAgent ? axios.create({ httpsAgent: this.httpsAgent }) : axios.create()
      // @ts-ignore
      this.axios.defaults.headers = this.options.defaultHeaders
    }
  
    static _config() {
      return [
        {
          name: 'endpoint',
          message: 'Endpoint of API you are going to test',
          default: 'http://localhost:3000/api',
        },
      ]
    }
  
    static _checkRequirements() {
      try {
        require('axios')
      } catch (e) {
        return ['axios']
      }
    }
  
    _before() {
      this.headers = { ...this.options.defaultHeaders }
    }
  
    /**
     * Sets request headers for all requests of this test
     *
     * @param {object} headers headers list
     */
    haveRequestHeaders(headers) {
      this.headers = { ...this.headers, ...headers }
    }
  
    /**
     * Adds a header for Bearer authentication
     *
     * ```js
     * // we use secret function to hide token from logs
     * I.amBearerAuthenticated(secret('heregoestoken'))
     * ```
     *
     * @param {string | CodeceptJS.Secret} accessToken  Bearer access token
     */
    amBearerAuthenticated(accessToken) {
      this.haveRequestHeaders({ Authorization: `Bearer ${accessToken}` })
    }
  
    /**
     * Executes axios request
     *
     * @param {*} request
     *
     * @returns {Promise<*>} response
     */
    async _executeRequest(request) {
      // Add custom headers. They can be set by amBearerAuthenticated() or haveRequestHeaders()
      request.headers = { ...this.headers, ...request.headers }
  
      const _debugRequest = { ...request }
      this.axios.defaults.timeout = request.timeout || this.options.timeout
  
      if (this.headers && this.headers.auth) {
        request.auth = this.headers.auth
      }
  
      if (typeof request.data === 'object') {
        const returnedValue = {}
        for (const [key, value] of Object.entries(request.data)) {
          returnedValue[key] = value
          if (value instanceof Secret) returnedValue[key] = value.getMasked()
        }
        _debugRequest.data = returnedValue
      }
  
      if (request.data instanceof Secret) {
        _debugRequest.data = '*****'
        request.data = typeof request.data === 'object' && !(request.data instanceof Secret) ? { ...request.data.toString() } : request.data.toString()
      }
  
      if (typeof request.data === 'string') {
        if (!request.headers || !request.headers['Content-Type']) {
          request.headers = { ...request.headers, ...{ 'Content-Type': 'application/x-www-form-urlencoded' } }
        }
      }
  
      if (this.config.onRequest) {
        await this.config.onRequest(request)
      }
  
      this.options.prettyPrintJson ? this.debugSection('Request', beautify(JSON.stringify(_debugRequest))) : this.debugSection('Request', JSON.stringify(_debugRequest))
  
      if (this.options.printCurl) {
        this.debugSection('CURL Request', curlize(request))
      }
  
      let response
      try {
        response = await this.axios(request)
      } catch (err) {
        if (!err.response) throw err
        this.debugSection('Response', `Response error. Status code: ${err.response.status}`)
        response = err.response
      }
      if (this.config.onResponse) {
        await this.config.onResponse(response)
      }
      this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data))
      return response
    }
  
    /**
     * Generates url based on format sent (takes endpoint + url if latter lacks 'http')
     *
     * @param {*} url
     */
    _url(url) {
      return /^\w+\:\/\//.test(url) ? url : this.options.endpoint + url
    }
  
    /**
     * Set timeout for the request
     *
     * ```js
     * I.setRequestTimeout(10000); // In milliseconds
     * ```
     *
     * @param {number} newTimeout - timeout in milliseconds
     */
    setRequestTimeout(newTimeout) {
      this.options.timeout = newTimeout
    }
  
    /**
     * Send GET request to REST API
     *
     * ```js
     * I.sendGetRequest('/api/users.json');
     * ```
     *
     * @param {*} url
     * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
     *
     * @returns {Promise<*>} response
     */
    async sendGetRequest(url, headers = {}) {
      const request = {
        baseURL: this._url(url),
        headers,
      }
      return this._executeRequest(request)
    }
  
    /**
     * Send HEAD request to REST API
     *
     * ```js
     * I.sendHeadRequest('/api/users.json');
     * ```
     *
     * @param {*} url
     * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
     *
     * @returns {Promise<*>} response
     */
    async sendHeadRequest(url, headers = {}) {
      const request = {
        baseURL: this._url(url),
        method: 'HEAD',
        headers,
      }
      return this._executeRequest(request)
    }
  
    /**
     * Sends POST request to API.
     *
     * ```js
     * I.sendPostRequest('/api/users.json', { "email": "user@user.com" });
     *
     * // To mask the payload in logs
     * I.sendPostRequest('/api/users.json', secret({ "email": "user@user.com" }));
     *
     * ```
     *
     * @param {*} url
     * @param {*} [payload={}] - the payload to be sent. By default, it is sent as an empty object
     * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
     *
     * @returns {Promise<*>} response
     */
    async sendPostRequest(url, payload = {}, headers = {}) {
      const request = {
        baseURL: this._url(url),
        method: 'POST',
        data: payload,
        headers,
      }
  
      if (this.options.maxContentLength) {
        request.maxContentLength = this.options.maxContentLength
        request.maxBodyLength = this.options.maxContentLength
      }
  
      return this._executeRequest(request)
    }
  
    /**
     * Sends PATCH request to API.
     *
     * ```js
     * I.sendPatchRequest('/api/users.json', { "email": "user@user.com" });
     *
     * // To mask the payload in logs
     * I.sendPatchRequest('/api/users.json', secret({ "email": "user@user.com" }));
     *
     * ```
     *
     * @param {string} url
     * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
     * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
     *
     * @returns {Promise<*>} response
     */
    async sendPatchRequest(url, payload = {}, headers = {}) {
      const request = {
        baseURL: this._url(url),
        method: 'PATCH',
        data: payload,
        headers,
      }
  
      if (this.options.maxContentLength) {
        request.maxContentLength = this.options.maxContentLength
        request.maxBodyLength = this.options.maxBodyLength
      }
  
      return this._executeRequest(request)
    }
  
    /**
     * Sends PUT request to API.
     *
     * ```js
     * I.sendPutRequest('/api/users.json', { "email": "user@user.com" });
     *
     * // To mask the payload in logs
     * I.sendPutRequest('/api/users.json', secret({ "email": "user@user.com" }));
     *
     * ```
     *
     * @param {string} url
     * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
     * @param {object} [headers={}] - the headers object to be sent. By default it is sent as an empty object
     *
     * @returns {Promise<*>} response
     */
    async sendPutRequest(url, payload = {}, headers = {}) {
      const request = {
        baseURL: this._url(url),
        method: 'PUT',
        data: payload,
        headers,
      }
  
      if (this.options.maxContentLength) {
        request.maxContentLength = this.options.maxContentLength
        request.maxBodyLength = this.options.maxBodyLength
      }
  
      return this._executeRequest(request)
    }
  
    /**
     * Sends DELETE request to API.
     *
     * ```js
     * I.sendDeleteRequest('/api/users/1');
     * ```
     *
     * @param {*} url
     * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
     *
     * @returns {Promise<*>} response
     */
    async sendDeleteRequest(url, headers = {}) {
      const request = {
        baseURL: this._url(url),
        method: 'DELETE',
        headers,
      }
  
      return this._executeRequest(request)
    }
  
    /**
     * Sends DELETE request to API with payload.
     *
     * ```js
     * I.sendDeleteRequestWithPayload('/api/users/1', { author: 'john' });
     * ```
     *
     * @param {*} url
     * @param {*} [payload={}] - the payload to be sent. By default it is sent as an empty object
     * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
     *
     * @returns {Promise<*>} response
     */
    async sendDeleteRequestWithPayload(url, payload = {}, headers = {}) {
      const request = {
        baseURL: this._url(url),
        method: 'DELETE',
        data: payload,
        headers,
      }
  
      return this._executeRequest(request)
    }
  }
  
  module.exports = REST
  
  function curlize(request) {
    if (request.data?.constructor.name.toLowerCase() === 'formdata') return 'cURL is not printed as the request body is not a JSON'
    let curl = `curl --location --request ${request.method ? request.method.toUpperCase() : 'GET'} ${request.baseURL} `.replace("'", '')
  
    if (request.headers) {
      Object.entries(request.headers).forEach(([key, value]) => {
        curl += `-H "${key}: ${value}" `
      })
    }
  
    if (!curl.toLowerCase().includes('content-type: application/json')) {
      curl += '-H "Content-Type: application/json" '
    }
  
    if (request.data) {
      curl += `-d '${JSON.stringify(request.data)}'`
    }
    return curl
  }
  