Source: controllers/imposterController.js

'use strict';

const exceptions = require('../util/errors.js'),
    helpers = require('../util/helpers.js'),
    compatibility = require('../models/compatibility.js'),
    dryRunValidator = require('../models/dryRunValidator.js');

/**
 * The controller that gets and deletes single imposters
 * @module
 */

/**
 * Creates the imposter controller
 * @param {Object} protocols - the protocol implementations supported by mountebank
 * @param {Object} imposters - The map of ports to imposters
 * @param {Object} logger - The logger
 * @param {Boolean} allowInjection - Whether injection is allowed or not
 * @returns {{get, del}}
 */
function create (protocols, imposters, logger, allowInjection) {
    function isFlagSet (query, key) {
        if (!helpers.defined(query[key])) {
            return false;
        }
        return query[key].toLowerCase() === 'true';
    }

    /**
     * The function responding to GET /imposters/:id
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} - the promise
     */
    async function get (request, response) {
        const options = {
                replayable: isFlagSet(request.query, 'replayable'),
                removeProxies: isFlagSet(request.query, 'removeProxies')
            },
            imposter = await imposters.get(request.params.id),
            json = await imposter.toJSON(options);

        response.format({
            json: () => response.send(json),
            html: () => response.render('imposter', { imposter: json })
        });
    }

    /**
     * Corresponds to DELETE /imposters/:id/savedProxyResponses
     * Removes all saved proxy responses
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} A promise for testing
     */
    async function resetProxies (request, response) {
        const options = { replayable: false, removeProxies: false },
            imposter = await imposters.get(request.params.id);

        await imposters.stubsFor(request.params.id).deleteSavedProxyResponses();
        const json = await imposter.toJSON(options);

        response.format({
            json: () => response.send(json),
            html: () => response.render('imposter', { imposter: json })
        });
    }

    /**
     * Corresponds to DELETE /imposters/:id/savedRequests
     * Removes all saved requests
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} A promise for testing
     */
    async function resetRequests (request, response) {
        const imposter = await imposters.get(request.params.id);
        await imposter.resetRequests();
        const json = await imposter.toJSON();

        response.format({
            json: () => response.send(json),
            html: () => response.render('imposter', { imposter: json })
        });
    }

    /**
     * The function responding to DELETE /imposters/:id
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} A promise for testing
     */
    async function del (request, response) {
        const options = {
                replayable: isFlagSet(request.query, 'replayable'),
                removeProxies: isFlagSet(request.query, 'removeProxies')
            },
            imposter = await imposters.get(request.params.id);

        if (imposter) {
            const json = await imposter.toJSON(options);
            await imposters.del(request.params.id);
            response.send(json);
        }
        else {
            response.send({});
        }
    }

    /**
     * The function responding to POST /imposters/:id/_requests
     * This is what protocol implementations call to send the JSON request
     * structure to mountebank, which responds with the JSON response structure
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} - the promise
     */
    async function postRequest (request, response) {
        const imposter = await imposters.get(request.params.id),
            jsonResponse = await imposter.getResponseFor(request.body.request);

        response.send(jsonResponse);
    }

    /**
     * The function responding to POST /imposters/:id/_requests/:proxyResolutionKey
     * This is what protocol implementations call after proxying a request so
     * mountebank can record the response and add behaviors to
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} - the promise
     */
    async function postProxyResponse (request, response) {
        const proxyResolutionKey = request.params.proxyResolutionKey,
            proxyResponse = request.body.proxyResponse,
            imposter = await imposters.get(request.params.id),
            json = await imposter.getProxyResponseFor(proxyResponse, proxyResolutionKey);

        response.send(json);
    }

    async function validateStubs (imposter, newStubs) {
        const errors = [];

        if (!helpers.defined(newStubs)) {
            errors.push(exceptions.ValidationError("'stubs' is a required field"));
        }
        else if (!Array.isArray(newStubs)) {
            errors.push(exceptions.ValidationError("'stubs' must be an array"));
        }

        if (errors.length > 0) {
            return Promise.resolve({ isValid: false, errors });
        }

        const request = await imposter.toJSON(),
            Protocol = protocols[request.protocol],
            validator = dryRunValidator.create({
                testRequest: Protocol.testRequest,
                testProxyResponse: Protocol.testProxyResponse,
                additionalValidation: Protocol.validate,
                allowInjection: allowInjection
            });

        request.stubs = newStubs;
        compatibility.upcast(request);
        return validator.validate(request, logger);
    }

    function respondWithValidationErrors (response, validationErrors, statusCode = 400) {
        logger.error(`error changing stubs: ${JSON.stringify(exceptions.details(validationErrors))}`);
        response.statusCode = statusCode;
        response.send({ errors: validationErrors });
    }

    /**
     * The function responding to PUT /imposters/:id/stubs
     * Overwrites the stubs list without restarting the imposter
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} - promise for testing
     */
    async function putStubs (request, response) {
        const imposter = await imposters.get(request.params.id),
            stubs = imposters.stubsFor(request.params.id),
            newStubs = request.body.stubs,
            result = await validateStubs(imposter, newStubs);

        if (result.isValid) {
            await stubs.overwriteAll(newStubs);
            const json = await imposter.toJSON();
            response.send(json);
        }
        else {
            respondWithValidationErrors(response, result.errors);
        }
    }

    async function validateStubIndex (stubs, index) {
        const allStubs = await stubs.toJSON();
        const errors = [];
        if (typeof allStubs[index] === 'undefined') {
            errors.push(exceptions.ValidationError("'stubIndex' must be a valid integer, representing the array index position of the stub to replace"));
        }
        return { isValid: errors.length === 0, errors };
    }

    /**
     * The function responding to PUT /imposters/:id/stubs/:stubIndex
     * Overwrites a single stub without restarting the imposter
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} - promise for testing
     */
    async function putStub (request, response) {
        const imposter = await imposters.get(request.params.id),
            stubs = imposters.stubsFor(request.params.id),
            validation = await validateStubIndex(stubs, request.params.stubIndex);

        if (validation.isValid) {
            const newStub = request.body,
                result = await validateStubs(imposter, [newStub]);

            if (result.isValid) {
                await stubs.overwriteAtIndex(newStub, request.params.stubIndex);
                const json = await imposter.toJSON();
                response.send(json);
            }
            else {
                respondWithValidationErrors(response, result.errors);
            }
        }
        else {
            respondWithValidationErrors(response, validation.errors, 404);
        }
    }

    function validateNewStub (index, allStubs, newStub) {
        const errors = [];

        if (typeof index !== 'number' || index < 0 || index > allStubs.length) {
            errors.push(exceptions.ValidationError("'index' must be between 0 and the length of the stubs array"));
        }
        if (typeof newStub === 'undefined') {
            errors.push(exceptions.ValidationError("must contain 'stub' field"));
        }

        return { isValid: errors.length === 0, errors };
    }

    /**
     * The function responding to POST /imposters/:port/stubs
     * Creates a single stub without restarting the imposter
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} - promise for testing
     */
    async function postStub (request, response) {
        const imposter = await imposters.get(request.params.id),
            stubs = imposters.stubsFor(request.params.id),
            allStubs = await stubs.toJSON(),
            newStub = request.body.stub,
            index = typeof request.body.index === 'undefined' ? allStubs.length : request.body.index,
            validation = validateNewStub(index, allStubs, newStub);

        if (validation.isValid) {
            const result = await validateStubs(imposter, [newStub]);
            if (result.isValid) {
                await stubs.insertAtIndex(newStub, index);
                const json = await imposter.toJSON();
                response.send(json);
            }
            else {
                respondWithValidationErrors(response, result.errors);
            }
        }
        else {
            respondWithValidationErrors(response, validation.errors);
        }
    }

    /**
     * The function responding to DELETE /imposters/:port/stubs/:stubIndex
     * Removes a single stub without restarting the imposter
     * @memberOf module:controllers/imposterController#
     * @param {Object} request - the HTTP request
     * @param {Object} response - the HTTP response
     * @returns {Object} - promise for testing
     */
    async function deleteStub (request, response) {
        const imposter = await imposters.get(request.params.id),
            stubs = imposters.stubsFor(request.params.id),
            validation = await validateStubIndex(stubs, request.params.stubIndex);

        if (validation.isValid) {
            await stubs.deleteAtIndex(request.params.stubIndex);
            const json = await imposter.toJSON();
            response.send(json);
        }
        else {
            respondWithValidationErrors(response, validation.errors, 404);
        }
    }

    return {
        get,
        del,
        resetProxies,
        resetRequests,
        postRequest,
        postProxyResponse,
        putStubs,
        putStub,
        postStub,
        deleteStub
    };
}

module.exports = { create };