extensions/http_api/definitions.js

'use strict'

const { expect } = require('chai')
const _ = require('lodash')

const Cast = require('../../cast')
const Helper = require('../../helper')

const { STATUS_CODES } = require('http')
const STATUS_MESSAGES = _.values(STATUS_CODES).map(_.lowerCase)

/**
 * Ensures there's a response available and returns it.
 *
 * @param {Object} client
 */
const mustGetResponse = client => {
    const response = client.getResponse()
    expect(response, 'No response available').to.not.be.empty

    return response
}

module.exports = ({ baseUrl = '' } = {}) => ({ Given, When, Then }) => {
    /**
     * Setting http headers
     */
    Given(/^(?:I )?set request headers$/, function(step) {
        this.httpApiClient.setHeaders(Cast.object(this.state.populateObject(step.rowsHash())))
    })

    /**
     * Setting a single http header
     */
    Given(/^(?:I )?set ([a-zA-Z0-9-]+) request header to (.+)$/, function(key, value) {
        this.httpApiClient.setHeader(key, Cast.value(this.state.populate(value)))
    })

    /**
     * Clearing headers
     */
    Given(/^(?:I )?clear request headers/, function() {
        this.httpApiClient.clearHeaders()
    })

    /**
     * Setting json payload
     */
    Given(/^(?:I )?set request json body$/, function(step) {
        this.httpApiClient.setJsonBody(Cast.object(this.state.populateObject(step.rowsHash())))
    })

    /**
     * Setting json payload from fixture file
     */
    Given(/^(?:I )?set request json body from (.+)$/, function(fixture) {
        return this.fixtures.load(fixture).then(data => {
            this.httpApiClient.setJsonBody(data)
        })
    })

    /**
     * Setting form data
     */
    Given(/^(?:I )?set request form body$/, function(step) {
        this.httpApiClient.setFormBody(Cast.object(this.state.populateObject(step.rowsHash())))
    })

    /**
     * Setting form data from fixture file
     */
    Given(/^(?:I )?set request form body from (.+)$/, function(fixture) {
        return this.fixtures.load(fixture).then(data => {
            this.httpApiClient.setFormBody(data)
        })
    })

    /**
     * Clearing body
     */
    Given(/^(?:I )?clear request body$/, function() {
        this.httpApiClient.clearBody()
    })

    /**
     * Setting query parameters
     */
    Given(/^(?:I )?set request query$/, function(step) {
        this.httpApiClient.setQuery(Cast.object(this.state.populateObject(step.rowsHash())))
    })

    Given(/^(?:I )?pick response json (.+) as (.+)$/, function(path, key) {
        const response = this.httpApiClient.getResponse()
        const body = response.body

        this.state.set(key, _.get(body, path))
    })

    /**
     * Enabling cookies
     */
    Given(/^(?:I )?enable cookies$/, function() {
        this.httpApiClient.enableCookies()
    })

    /**
     * Disabling cookies
     */
    Given(/^(?:I )?disable cookies$/, function() {
        this.httpApiClient.disableCookies()
    })

    /**
     * Setting a cookie from fixture file
     */
    Given(/^(?:I )?set cookie from (.+)$/, function(fixture) {
        return this.fixtures.load(fixture).then(cookie => {
            this.httpApiClient.setCookie(cookie)
        })
    })

    /**
     * Clearing client request cookies
     */
    Given(/^(?:I )?clear request cookies$/, function() {
        this.httpApiClient.clearRequestCookies()
    })

    /**
     * Resetting the client's state
     */
    When(/^(?:I )?reset http client$/, function() {
        this.httpApiClient.reset()
    })

    /**
     * Performing a request
     */
    When(/^(?:I )?(GET|POST|PUT|DELETE) (.+)$/, function(method, path) {
        return this.httpApiClient.makeRequest(method, this.state.populate(path), baseUrl)
    })

    /**
     * Dumping response body
     */
    When(/^(?:I )?dump response body$/, function() {
        const response = mustGetResponse(this.httpApiClient)
        console.log(response.body) // eslint-disable-line no-console
    })

    /**
     * Dumping response headers
     */
    When(/^(?:I )?dump response headers$/, function() {
        const response = mustGetResponse(this.httpApiClient)
        console.log(response.headers) // eslint-disable-line no-console
    })

    /**
     * Dumping response cookies
     */
    When(/^(?:I )?dump response cookies$/, function() {
        mustGetResponse(this.httpApiClient)
        console.log(this.httpApiClient.getCookies()) // eslint-disable-line no-console
    })

    /**
     * Checking response status code
     */
    Then(/^response status code should be ([1-5][0-9][0-9])$/, function(statusCode) {
        const response = mustGetResponse(this.httpApiClient)
        expect(
            response.statusCode,
            `Expected status code to be: ${statusCode}, but found: ${response.statusCode}`
        ).to.equal(Number(statusCode))
    })

    /**
     * Checking response status by message
     */
    Then(/^response status should be (.+)$/, function(statusMessage) {
        if (!STATUS_MESSAGES.includes(_.lowerCase(statusMessage))) {
            throw new TypeError(`'${statusMessage}' is not a valid status message`)
        }

        const response = mustGetResponse(this.httpApiClient)
        const statusCode = _.findKey(STATUS_CODES, msg => _.lowerCase(msg) === statusMessage)
        const currentStatusMessage = STATUS_CODES[`${response.statusCode}`] || response.statusCode

        expect(
            response.statusCode,
            `Expected status to be: '${statusMessage}', but found: '${_.lowerCase(
                currentStatusMessage
            )}'`
        ).to.equal(Number(statusCode))
    })

    /**
     * Checking response cookie is present|absent
     */
    Then(/^response should (not )?have an? (.+) cookie$/, function(flag, key) {
        const cookie = this.httpApiClient.getCookie(key)

        if (flag === undefined) {
            expect(cookie, `No cookie found for key '${key}'`).to.not.be.null
        } else {
            expect(cookie, `A cookie exists for key '${key}'`).to.be.null
        }
    })

    /**
     * Checking response cookie is|isn't secure
     */
    Then(/^response (.+) cookie should (not )?be secure$/, function(key, flag) {
        const cookie = this.httpApiClient.getCookie(key)
        expect(cookie, `No cookie found for key '${key}'`).to.not.be.null

        if (flag === undefined) {
            expect(cookie.secure, `Cookie '${key}' is not secure`).to.be.true
        } else {
            expect(cookie.secure, `Cookie '${key}' is secure`).to.be.false
        }
    })

    /**
     * Checking response cookie httpOnly
     */
    Then(/^response (.+) cookie should (not )?be http only$/, function(key, flag) {
        const cookie = this.httpApiClient.getCookie(key)
        expect(cookie, `No cookie found for key '${key}'`).to.not.be.null

        if (flag === undefined) {
            expect(cookie.httpOnly, `Cookie '${key}' is not http only`).to.be.true
        } else {
            expect(cookie.httpOnly, `Cookie '${key}' is http only`).to.be.false
        }
    })

    /**
     * Checking response cookie domain
     */
    Then(/^response (.+) cookie domain should (not )?be (.+)$/, function(key, flag, domain) {
        const cookie = this.httpApiClient.getCookie(key)
        expect(cookie, `No cookie found for key '${key}'`).to.not.be.null

        if (flag === undefined) {
            expect(
                cookie.domain,
                `Expected cookie '${key}' domain to be '${domain}', found '${cookie.domain}'`
            ).to.equal(domain)
        } else {
            expect(cookie.domain, `Cookie '${key}' domain is '${domain}'`).to.not.equal(domain)
        }
    })

    /**
     * This definition can be used for checking an object response.
     * It check that the properties of this object match with the expected properties
     * The columns header are | field | matcher | value |
     * You can define severals matchers :
     * - equals
     * - contains
     */
    Then(/^(?:I )?json response should (fully )?match$/, function(fully, table) {
        const response = mustGetResponse(this.httpApiClient)
        const { body } = response

        const expectedProperties = table.hashes()

        // We check the response has json content-type
        expect(response.headers['content-type']).to.contain('application/json')

        // We check response properties correspond to the expected response
        expectedProperties.forEach(({ field, matcher, value }) => {
            const currentValue = _.get(body, field)
            const expectedValue = Cast.value(this.state.populate(value))

            switch (matcher) {
                case 'match':
                case 'matches':
                    expect(
                        currentValue,
                        `Property '${field}' (${currentValue}) does not match '${expectedValue}'`
                    ).to.match(new RegExp(expectedValue))
                    break

                case 'contain':
                case 'contains':
                    expect(
                        currentValue,
                        `Property '${field}' (${currentValue}) does not contain '${expectedValue}'`
                    ).to.contain(expectedValue)
                    break

                case 'defined':
                case 'present':
                    expect(currentValue, `Property '${field}' is undefined`).to.not.be.undefined
                    break

                case 'equal':
                case 'equals':
                default:
                    expect(
                        currentValue,
                        `Expected property '${field}' to equal '${value}', but found '${currentValue}'`
                    ).to.be.deep.equal(expectedValue)
            }
        })

        // We check we have exactly the same number of properties as expected
        if (fully) {
            const propertiesCount = Helper.countNestedProperties(body)
            expect(
                propertiesCount,
                'Expected json response to fully match spec, but it does not'
            ).to.be.equal(table.hashes().length)
        }
    })

    /**
     * This definition verify that an array for a given path has the expected length
     */
    Then(/^(?:I )?should receive a collection of ([0-9]+) items?(?: for path )?(.+)?$/, function(
        size,
        path
    ) {
        const response = mustGetResponse(this.httpApiClient)
        const { body } = response

        const array = path !== undefined ? _.get(body, path) : body

        expect(array.length).to.be.equal(Number(size))
    })

    /**
     * Verifies that response matches a fixture.
     **/
    Then(/^response should match fixture (.+)$/, function(fixtureId) {
        const response = mustGetResponse(this.httpApiClient)

        return this.fixtures.load(fixtureId).then(snapshot => {
            expect(response.body).to.deep.equal(snapshot)
        })
    })

    /**
     * Checking response header.
     */
    Then(/^response header (.+) should (not )?(equal|contain|match) (.+)$/, function(
        key,
        flag,
        comparator,
        expectedValue
    ) {
        const response = mustGetResponse(this.httpApiClient)
        const header = response.headers[key.toLowerCase()]

        expect(header, `Header '${key}' does not exist`).to.not.be.undefined

        let expectFn = expect(
            header,
            `Expected header '${key}' to ${flag
                ? flag
                : ''}${comparator} '${expectedValue}', but found '${header}' which does${flag
                ? ''
                : ' not'}`
        ).to
        if (flag !== undefined) {
            expectFn = expectFn.not
        }
        expectFn[comparator](comparator === 'match' ? new RegExp(expectedValue) : expectedValue)
    })
}