UNPKG

tartare-chai

Version:

A set of Chai plugins to help writing acceptance tests

676 lines (615 loc) 27.8 kB
/* Copyright 2015-2016 Telefonica Investigación y Desarrollo, S.A.U This file is part of Tartare. Tartare is free software: you can redistribute it and/or modify it under the terms of the Apache License as published by the Apache Software Foundation, either version 2.0 of the License, or (at your option) any later version. Tartare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Apache License for more details. You should have received a copy of the Apache License along with Tartare. If not, see http://www.apache.org/licenses/LICENSE-2.0 For those usages not covered by the Apache License please contact with: joseantonio.rodriguezfernandez@telefonica.com */ 'use strict'; /* eslint-disable no-unused-expressions */ var tartareHttp = require('tartare-util').http; var jsonValidator = require('is-my-json-valid'); var codependency = require('codependency'); var requireLazy = codependency.register(module.parent, {index: ['lazyDependencies']}); var schemas = require('./schemas'); module.exports = function(chai, utils) { var Assertion = chai.Assertion; // This is an alias for wellFormedJsonApiResponse Assertion.addProperty('wellFormedApiResponse', function assertWellFormedApiResponse() { var res = this._obj; new Assertion(res).to.be.a.wellFormedJsonApiResponse; }); Assertion.addProperty('wellFormedJsonApiResponse', function assertWellFormedJsonApiResponse() { var negated = utils.flag(this, 'negate'); var res = this._obj; new Assertion(res).to.have.httpHeaders('content-type'); var actualContentType = tartareHttp.lowerCaseHeaders(res.headers)['content-type'].split(';')[0]; var expectedContentType = 'application/json'; this.assert( actualContentType === expectedContentType, 'expected Content-Type header to be #{exp} but is #{act}', 'expected Content-Type header to not be #{act}', negated ? undefined : expectedContentType, actualContentType ); // If HTTP Body can be parsed as JSON, it is a JSON document var isJson = null; try { JSON.parse(res.body); isJson = true; } catch (err) { isJson = false; } this.assert( isJson === true, 'expected Response Body to be a valid JSON document but is #{act}', 'expected Response Body to not be a valid JSON document', undefined, res.body ); }); Assertion.addProperty('wellFormedXmlApiResponse', function assertWellFormedXmlApiResponse() { var negated = utils.flag(this, 'negate'); var res = this._obj; new Assertion(res).to.have.httpHeaders('content-type'); var actualContentType = tartareHttp.lowerCaseHeaders(res.headers)['content-type'].split(';')[0]; var expectedContentTypes = ['application/xml', 'text/xml']; this.assert( expectedContentTypes.indexOf(actualContentType) !== -1, 'expected Content-Type header to be one of #{exp} but is #{act}', 'expected Content-Type header to not be #{act}', negated ? undefined : expectedContentTypes, actualContentType ); // If HTTP Body can be parsed as XML, it is an XML document var xml = requireLazy('libxmljs'); var isXml = null; try { xml.parseXmlString(res.body); isXml = true; } catch (err) { isXml = false; } this.assert( isXml === true, 'expected Response Body to be a valid XML document but is #{act}', 'expected Response Body to not be a valid XML document', undefined, res.body ); }); Assertion.addProperty('wellFormedSoap11ApiResponse', function assertWellFormedSoap11ApiResponse() { var negated = utils.flag(this, 'negate'); var res = this._obj; new Assertion(res).to.have.httpHeaders('content-type'); var actualContentType = tartareHttp.lowerCaseHeaders(res.headers)['content-type'].split(';')[0]; var expectedContentType = 'text/xml'; this.assert( actualContentType === expectedContentType, 'expected Content-Type header to be #{exp} but is #{act}', 'expected Content-Type header to not be #{act}', negated ? undefined : expectedContentType, actualContentType ); var isXml = null; var xmlDoc = null; // If HTTP Body can be parsed as XML, it is the first step to be a SOAP message var xml = requireLazy('libxmljs'); try { xmlDoc = xml.parseXmlString(res.body); isXml = true; } catch (err) { isXml = false; } this.assert( isXml === true, 'expected Response Body to be a valid XML document but is #{act}', 'expected Response Body to not be a valid XML document', undefined, res.body ); // Check that the XML document validates against the SOAP 1.1 schema if (isXml) { var isSoap11 = xmlDoc.validate(schemas.soap11.get()); this.assert( isSoap11 === true, 'expected Response Body to be a valid SOAP 1.1 document but is #{act}', 'expected Response Body to not be a valid SOAP 1.1 document', undefined, res.body ); } }); // This is an alias for jsonApiError (backwards compatibility) Assertion.addMethod('apiError', function assertApiError(expectedStatusCode, expectedErrorId) { var res = this._obj; new Assertion(res).to.be.a.jsonApiError(expectedStatusCode, expectedErrorId, 'v2'); }); Assertion.addMethod('jsonApiError', function assertJsonApiError( expectedStatusCode, expectedErrorId, expectedVersion, hasPlaceholders) { var negated = utils.flag(this, 'negate'); var res = this._obj; expectedVersion = expectedVersion || 'v2'; // Defaults to UNICA v2 specification if (expectedVersion !== 'v1' && expectedVersion !== 'v2') { throw new Error('Unsupported UNICA API version: ' + expectedVersion); } this.assert( res.statusCode === expectedStatusCode, 'expected HTTP Status Code to be #{exp} but is #{act}', 'expected HTTP Status Code to not be #{act}', negated ? undefined : expectedStatusCode, res.statusCode ); this.assert( res.statusCode >= 400 && res.statusCode < 600, 'expected HTTP Status Code to be between 400 and 599 but is #{act}', 'expected HTTP Status Code to not not be between 400 and 599 but is #{act}', undefined, res.statusCode ); // Check some mandatory headers that must always be present depending on the Status Code var mandatoryHeaders = { 401: ['www-authenticate'], 405: ['allow'] }; var expectedHeaders = mandatoryHeaders[res.statusCode] || []; var actualHeaders = tartareHttp.lowerCaseHeaders(res.headers); expectedHeaders.forEach(function(expectedHeader) { this.assert( expectedHeader in actualHeaders, 'expected HTTP header #{exp} to exist when HTTP Status Code is ' + res.statusCode, 'expected HTTP header #{exp} to not exist when HTTP Status Code is ' + res.statusCode, expectedHeader, res.headers ); }, this); // Check that, above all, it is a well formed JSON API response new Assertion(res).to.be.a.wellFormedJsonApiResponse; // Parse JSON body and validate it against the JSON UNICA schema (depending on the expected version) var jsonBody = JSON.parse(res.body); var jsonValidate = jsonValidator(schemas.unicaApiErrors.json[expectedVersion].get(), {verbose: true}); this.assert( jsonValidate(jsonBody), 'expected HTTP Body to validate against the UNICA API schema\nErrors: ' + JSON.stringify(jsonValidate.errors), 'expected HTTP Body to not validate against the UNICA API schema', undefined, res.body ); // Message structure is different depending on the version switch (expectedVersion) { case 'v1': var actualExceptionCategory = null; var actualExceptionId = null; var actualExceptionText = null; var actualExceptionVariables = null; var expectedExceptionCategory = expectedErrorId.slice(0, 3); var expectedExceptionId = expectedErrorId.slice(3); var expectedFirstLevelElement = null; var placeholders = null; // Are we expecting a ClientException or a ServerException? if (expectedExceptionCategory.match(/^SVC|POL|SEC/)) { // ClientException expectedFirstLevelElement = 'ClientException'; } else if (expectedExceptionCategory.match(/^SVR/)) { // ServerException expectedFirstLevelElement = 'ServerException'; } else { throw new Error('Unsupported Exception Category: ' + expectedExceptionCategory); } // Check that the first element of the document matches the expected type of exception // and depending on it, get exceptionCategory and exceptionId this.assert( jsonBody[expectedFirstLevelElement], 'expected JSON UNICA v1 Exception to have a #{exp} element', 'expected JSON UNICA v1 Exception to not have a #{exp} element', negated ? undefined : expectedFirstLevelElement, null ); actualExceptionCategory = jsonBody[expectedFirstLevelElement].exceptionCategory; actualExceptionId = jsonBody[expectedFirstLevelElement].exceptionId; this.assert( actualExceptionCategory === expectedExceptionCategory, 'expected Exception Category to be #{exp} but is #{act}', 'expected Exception Category to not be #{act}', negated ? undefined : expectedExceptionCategory, actualExceptionCategory ); this.assert( actualExceptionId == expectedExceptionId, // eslint-disable-line eqeqeq 'expected Exception Id to be #{exp} but is #{act}', 'expected Exception Id to not be #{act}', negated ? undefined : expectedExceptionId, actualExceptionId ); actualExceptionText = jsonBody[expectedFirstLevelElement].text; actualExceptionVariables = jsonBody[expectedFirstLevelElement].variables; placeholders = actualExceptionText.match(/%\d+/g) || []; if (hasPlaceholders === true) { var highestPlaceholder = placeholders.map(function(placeholder) { return parseInt(placeholder.slice(1), 10); }).sort().pop(); if (highestPlaceholder) { this.assert( actualExceptionVariables && actualExceptionVariables.length === highestPlaceholder, 'expect Exception Variables to have #{exp} elements but it has #{act}', 'expect Exception Variables not have elements but it has #{act}', highestPlaceholder, actualExceptionVariables.length ); } else { this.assert( actualExceptionVariables === undefined, 'expected Exception Variables to not exist but it has #{act} elements', 'expected Exception Variables to exist but it does not', negated ? undefined : null, actualExceptionVariables ); } } else if (hasPlaceholders === false) { // There is not any placeholder in the exception Text this.assert( placeholders.length === 0, 'expected Exception Text to not have placeholders but it has #{act}', 'expected Exception Text to have placeholders but it has #{act}', negated ? undefined : null, placeholders.length ); // There is no variables field this.assert( actualExceptionVariables === undefined, 'expected Exception Variables to not exist but it has #{act} elements', 'expected Exception Variables field to exist', negated ? undefined : null, actualExceptionVariables ); } break; case 'v2': // This is straightforward in v2 since there is an only type of Exception, // and exceptionCategory and exceptionId have been merged this.assert( jsonBody.exceptionId === expectedErrorId, 'expected Exception Id to be #{exp} but is #{act}', 'expected Exception Id to not be #{act}', negated ? undefined : expectedErrorId, jsonBody.exceptionId ); break; default: throw new Error('Unsupported version'); } }); Assertion.addMethod('xmlApiError', function assertXmlApiError( expectedStatusCode, expectedErrorId, expectedVersion, hasPlaceholders) { var negated = utils.flag(this, 'negate'); var res = this._obj; expectedVersion = expectedVersion || 'v2'; // Defaults to UNICA v2 specification if (expectedVersion !== 'v1' && expectedVersion !== 'v2') { throw new Error('Unsupported UNICA API version: ' + expectedVersion); } this.assert( res.statusCode === expectedStatusCode, 'expected HTTP Status Code to be #{exp} but is #{act}', 'expected HTTP Status Code to not be #{act}', negated ? undefined : expectedStatusCode, res.statusCode ); this.assert( res.statusCode >= 400 && res.statusCode < 600, 'expected HTTP Status Code to be between 400 and 599 but is #{act}', 'expected HTTP Status Code to not not be between 400 and 599 but is #{act}', undefined, res.statusCode ); // Check some mandatory headers that must always be present depending on the Status Code var mandatoryHeaders = { 401: ['www-authenticate'], 405: ['allow'] }; var expectedHeaders = mandatoryHeaders[res.statusCode] || []; var actualHeaders = tartareHttp.lowerCaseHeaders(res.headers); expectedHeaders.forEach(function(expectedHeader) { this.assert( expectedHeader in actualHeaders, 'expected HTTP header #{exp} to exist when HTTP Status Code is ' + res.statusCode, 'expected HTTP header #{exp} to not exist when HTTP Status Code is ' + res.statusCode, expectedHeader, res.headers ); }, this); // Check that, above all, it is a well formed XML API response new Assertion(res).to.be.a.wellFormedXmlApiResponse; // Parse XML body and validates it against the XML UNICA schema (depending on the expected version) var xml = requireLazy('libxmljs'); var xmlBody = xml.parseXmlString(res.body); this.assert( xmlBody.validate(schemas.unicaApiErrors.xml[expectedVersion].get()), 'expected HTTP Body to validate against the UNICA API schema\n' + xmlBody.validationErrors, 'expected HTTP Body to not validate against the UNICA API schema', undefined, res.body ); // Message structure is different depending on the version switch (expectedVersion) { case 'v1': var actualExceptionCategory = null; var actualExceptionId = null; var actualExceptionText; var actualExceptionVariables; var expectedExceptionCategory = expectedErrorId.slice(0, 3); var expectedExceptionId = expectedErrorId.slice(3); var expectedFirstLevelElement = null; var placeholders; // Are we expected a ClientException or a ServerException? if (expectedExceptionCategory.match(/^SVC|POL|SEC/)) { // ClientException expectedFirstLevelElement = 'ClientException'; } else if (expectedExceptionCategory.match(/^SVR/)) { // ServerException expectedFirstLevelElement = 'ServerException'; } else { throw new Error('Unsupported Exception Category: ' + expectedExceptionCategory); } // Check that the first element of the document matches the expected type of exception // and depending on it, get exceptionCategory and exceptionId this.assert( xmlBody.find( '/uct:' + expectedFirstLevelElement, {uct: 'http://www.telefonica.com/schemas/UNICA/REST/common/v1'} ).length === 1, 'expected XML UNICA v1 Exception to have a #{exp} element', 'expected XML UNICA v1 Exception to not have a #{exp} element', negated ? undefined : expectedFirstLevelElement, null ); actualExceptionCategory = xmlBody.get( '/uct:' + expectedFirstLevelElement + '/uct:exceptionCategory', {uct: 'http://www.telefonica.com/schemas/UNICA/REST/common/v1'} ).text(); actualExceptionId = xmlBody.get( '/uct:' + expectedFirstLevelElement + '/uct:exceptionId', {uct: 'http://www.telefonica.com/schemas/UNICA/REST/common/v1'} ).text(); this.assert( actualExceptionCategory === expectedExceptionCategory, 'expected Exception Category to be #{exp} but is #{act}', 'expected Exception Category to not be #{act}', negated ? undefined : expectedExceptionCategory, actualExceptionCategory ); this.assert( actualExceptionId == expectedExceptionId, // eslint-disable-line eqeqeq 'expected Exception Id to be #{exp} but is #{act}', 'expected Exception Id to not be #{act}', negated ? undefined : expectedExceptionId, actualExceptionId ); actualExceptionText = xmlBody.get( '/uct:' + expectedFirstLevelElement + '/uct:text', {uct: 'http://www.telefonica.com/schemas/UNICA/REST/common/v1'} ).text(); actualExceptionVariables = xmlBody.find( '/uct:' + expectedFirstLevelElement + '/uct:variables', {uct: 'http://www.telefonica.com/schemas/UNICA/REST/common/v1'} ); placeholders = actualExceptionText.match(/%\d+/g) || []; if (hasPlaceholders === true) { var highestPlaceholder = placeholders.map(function(placeholder) { return parseInt(placeholder.slice(1), 10); }).sort().pop(); if (highestPlaceholder) { this.assert( actualExceptionVariables.length === highestPlaceholder, 'expect Exception Variables to have #{exp} elements but it has #{act}', 'expect Exception Variables not have be less than #{exp} but is #{act}', highestPlaceholder, actualExceptionVariables.length ); } else { this.assert( actualExceptionVariables.length === 0, 'expect Exception Variables to not exist it has #{act} elements', 'expect Exception Variables to exist but it does not', negated ? undefined : 0, actualExceptionVariables.length ); } } else if (hasPlaceholders === false) { // There is not any placeholder in the exception Text this.assert( placeholders.length === 0, 'expected Exception Text to not have placeholders but it has #{act}', 'expected Exception Text to have placeholders but it has #{act}', negated ? undefined : null, placeholders.length ); // There is not variables field this.assert( actualExceptionVariables.length === 0, 'expect Exception Variables to not exist but it has #{act} elements', 'expected Exception Variables to exist but it does not', negated ? undefined : null, actualExceptionVariables.length ); } break; case 'v2': // This is straightforward in v2 since there is only one type of Exception, // and exceptionCategory and exceptionId have been merged var actualExceptionId = xmlBody.get( // eslint-disable-line no-redeclare '/uct:exception/uct:exceptionId', {uct: 'http://www.telefonica.com/schemas/UNICA/REST/common/v2'} ).text(); this.assert( actualExceptionId === expectedErrorId, 'expected Exception Id to be #{exp} but is #{act}', 'expected Exception Id to not be #{act}', negated ? undefined : expectedErrorId, actualExceptionId ); break; default: throw new Error('Unsupported version'); } }); Assertion.addMethod('soap11ApiError', function assertSoap11ApiError( expectedErrorId, expectedVersion, hasPlaceholders) { var negated = utils.flag(this, 'negate'); var res = this._obj; expectedVersion = expectedVersion || 'v2'; // Defaults to UNICA v2 specification if (expectedVersion !== 'v1' && expectedVersion !== 'v2') { throw new Error('Unsupported UNICA API version: ' + expectedVersion); } // SOAP faults are always returned using 500 as HTTP Status Code var expectedStatusCode = 500; this.assert( res.statusCode === expectedStatusCode, 'expected HTTP Status Code to be #{exp} but is #{act}', 'expected HTTP Status Code to not be #{act}', negated ? undefined : expectedStatusCode, res.statusCode ); // Check that, above all, it is a well formed SOAP 1.1 API response new Assertion(res).to.be.a.wellFormedSoap11ApiResponse; // Parse XML body and validates it against the SOAP 1.1 UNICA schema (depending on the expected version) var xml = requireLazy('libxmljs'); var soapBody = xml.parseXmlString(res.body); this.assert( soapBody.validate(schemas.unicaApiErrors.soap11[expectedVersion].get()), 'expected HTTP Body to validate against the UNICA API schema', 'expected HTTP Body to not validate against the UNICA API schema', undefined, res.body ); // Message structure is the same both for v1 and v2, except for the namespaces var actualExceptionCategory; var actualExceptionId; var actualExceptionText; var actualExceptionVariables; var expectedExceptionCategory = expectedErrorId.slice(0, 3); var expectedExceptionId = expectedErrorId.slice(3); var expectedFirstLevelElement = null; var placeholders; // Are we expected a ClientException or a ServerException? if (expectedExceptionCategory.match(/^SVC|POL|SEC/)) { // ClientException expectedFirstLevelElement = 'ClientException'; } else if (expectedExceptionCategory.match(/^SVR/)) { // ServerException expectedFirstLevelElement = 'ServerException'; } else { throw new Error('Unsupported Exception Category: ' + expectedExceptionCategory); } // Check that the first element of the document inside the fault's detail element // matches the expected type of exception and depending on it, get exceptionCategory and exceptionId this.assert( soapBody.find( '/soap:Envelope/soap:Body/soap:Fault/detail/faults:' + expectedFirstLevelElement, { soap: 'http://schemas.xmlsoap.org/soap/envelope/', faults: 'http://www.telefonica.com/wsdl/UNICA/SOAP/common/' + expectedVersion + '/faults' } ).length === 1, 'expected SOAP 1.1 UNICA ' + expectedVersion + ' Exception to have a #{exp} element', 'expected SOAP 1.1 UNICA ' + expectedVersion + ' Exception to not have a #{exp} element', negated ? undefined : expectedFirstLevelElement, null ); actualExceptionCategory = soapBody.get( '/soap:Envelope/soap:Body/soap:Fault/detail/faults:' + expectedFirstLevelElement + '/uct:exceptionCategory', { soap: 'http://schemas.xmlsoap.org/soap/envelope/', faults: 'http://www.telefonica.com/wsdl/UNICA/SOAP/common/' + expectedVersion + '/faults', uct: 'http://www.telefonica.com/schemas/UNICA/SOAP/common/' + expectedVersion } ).text(); actualExceptionId = soapBody.get( '/soap:Envelope/soap:Body/soap:Fault/detail/faults:' + expectedFirstLevelElement + '/uct:exceptionId', { soap: 'http://schemas.xmlsoap.org/soap/envelope/', faults: 'http://www.telefonica.com/wsdl/UNICA/SOAP/common/' + expectedVersion + '/faults', uct: 'http://www.telefonica.com/schemas/UNICA/SOAP/common/' + expectedVersion } ).text(); this.assert( actualExceptionCategory === expectedExceptionCategory, 'expected Exception Category to be #{exp} but is #{act}', 'expected Exception Category to not be #{act}', negated ? undefined : expectedExceptionCategory, actualExceptionCategory ); this.assert( actualExceptionId == expectedExceptionId, // eslint-disable-line eqeqeq 'expected Exception Id to be #{exp} but is #{act}', 'expected Exception Id to not be #{act}', negated ? undefined : expectedExceptionId, actualExceptionId ); actualExceptionText = soapBody.get( '/soap:Envelope/soap:Body/soap:Fault/detail/faults:' + expectedFirstLevelElement + '/uct:text', { soap: 'http://schemas.xmlsoap.org/soap/envelope/', faults: 'http://www.telefonica.com/wsdl/UNICA/SOAP/common/' + expectedVersion + '/faults', uct: 'http://www.telefonica.com/schemas/UNICA/SOAP/common/' + expectedVersion } ).text(); actualExceptionVariables = soapBody.find( '/soap:Envelope/soap:Body/soap:Fault/detail/faults:' + expectedFirstLevelElement + '/uct:variables', { soap: 'http://schemas.xmlsoap.org/soap/envelope/', faults: 'http://www.telefonica.com/wsdl/UNICA/SOAP/common/' + expectedVersion + '/faults', uct: 'http://www.telefonica.com/schemas/UNICA/SOAP/common/' + expectedVersion } ); placeholders = actualExceptionText.match(/%\d+/g) || []; if (hasPlaceholders === true) { var highestPlaceholder = placeholders.map(function(placeholder) { return parseInt(placeholder.slice(1), 10); }).sort().pop(); if (highestPlaceholder) { this.assert( actualExceptionVariables.length === highestPlaceholder, 'expect Exception Variables to have #{exp} elements but it has #{act}', 'expect Exception Variables to not have #{exp} elements', highestPlaceholder, actualExceptionVariables.length ); } else { this.assert( actualExceptionVariables.length === 0, 'expect Exception Variables to not exist but it has #{act} elements', 'expect Exception Variables to exist but it does not', negated ? undefined : 0, actualExceptionVariables.length ); } } else if (hasPlaceholders === false) { // There is not any placeholder in the exception Text this.assert( placeholders.length === 0, 'expected Exception Text to not have placeholders but it has #{act}', 'expected Exception Text to have placeholders but it has #{act}', negated ? undefined : null, placeholders.length ); // There is no variables field this.assert( actualExceptionVariables.length === 0, 'expected Exception Variables to not exist but it has #{act} elements', 'expected Exception Variables to exist but it does not', negated ? undefined : null, actualExceptionVariables.length ); } }); };