├── errors.js ├── .gitignore ├── .npmignore ├── test ├── .eslintrc.json ├── fixtures │ └── server.js └── spec │ ├── request-next.js │ ├── request2.js │ └── plumbing.js ├── .travis.yml ├── .publishrc ├── .editorconfig ├── LICENSE ├── package.json ├── configure ├── request-next.js └── request2.js ├── lib ├── errors.js └── plumbing.js ├── gulpfile.js ├── .eslintrc.json └── README.md /errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/errors.js'); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /coverage/ 3 | /node_modules/ 4 | 5 | .DS_Store 6 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /coverage/ 3 | /test/ 4 | 5 | /.editorconfig 6 | /.eslintrc.json 7 | /.gitignore 8 | /.publishrc 9 | /.travis.yml 10 | /gulpfile.js 11 | 12 | .DS_Store 13 | npm-debug.log -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true 7 | }, 8 | "rules": { 9 | 10 | // Node.js and CommonJS 11 | 12 | "no-process-env": 0, 13 | "no-sync": 0 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | node_js: 5 | - "0.10" 6 | - "0.12" 7 | - "iojs" 8 | - "4" 9 | - "6" 10 | - "8" 11 | - "10" 12 | 13 | env: 14 | - V_REQUEST=2.34.0 15 | - V_REQUEST=latest 16 | 17 | before_script: 18 | - npm uninstall request 19 | - npm install request@$V_REQUEST 20 | -------------------------------------------------------------------------------- /.publishrc: -------------------------------------------------------------------------------- 1 | { 2 | "validations": { 3 | "vulnerableDependencies": false, 4 | "uncommittedChanges": true, 5 | "untrackedFiles": true, 6 | "sensitiveData": true, 7 | "branch": "master", 8 | "gitTag": true 9 | }, 10 | "confirm": true, 11 | "publishTag": "latest", 12 | "prePublishScript": "npm run test-publish" 13 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | 9 | [*.js] 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.{json,xml}] 16 | indent_style = space 17 | indent_size = 2 18 | trim_trailing_whitespace = true 19 | insert_final_newline = true 20 | 21 | [*.{md,txt}] 22 | indent_style = space 23 | indent_size = 4 24 | trim_trailing_whitespace = false 25 | insert_final_newline = false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Nicolai Kamenzky and contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /test/fixtures/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bodyParser = require('body-parser'); 4 | var http = require('http'); 5 | var url = require('url'); 6 | 7 | 8 | module.exports = function startServer(port, cb) { 9 | 10 | var server = http.createServer(function (req, res) { 11 | 12 | bodyParser.json()(req, res, function () { 13 | 14 | var path = url.parse(req.url).pathname; 15 | var status = Number(path.split('?')[0].split('/')[1]); 16 | 17 | switch (status) { 18 | case 301: 19 | res.writeHead(301, { Location: '/200' }); 20 | res.end(); 21 | break; 22 | default: 23 | res.writeHead(status, {'Content-Type': 'text/plain'}); 24 | var body = req.method === 'POST' ? ' - ' + JSON.stringify(req.body) : ''; 25 | res.end(req.method + ' ' + path + body); 26 | } 27 | 28 | }); 29 | 30 | }); 31 | 32 | server.listen(port, function () { 33 | 34 | cb(function stopServer(done) { 35 | // Wait for all requests to finish since they may produce unhandled errors for tests at the end that don't wait themselves. 36 | setTimeout(function () { 37 | server.close(); 38 | done(); 39 | }, 20); 40 | }); 41 | 42 | }); 43 | 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "request-promise-core", 3 | "version": "1.1.4", 4 | "description": "Core Promise support implementation for the simplified HTTP request client 'request'.", 5 | "keywords": [ 6 | "xhr", 7 | "http", 8 | "https", 9 | "promise", 10 | "request", 11 | "then", 12 | "thenable", 13 | "core" 14 | ], 15 | "main": "./lib/plumbing.js", 16 | "scripts": { 17 | "test": "./node_modules/.bin/gulp ci", 18 | "test-publish": "./node_modules/.bin/gulp ci-no-cov", 19 | "publish-please": "publish-please", 20 | "prepublish": "publish-please guard" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/request/promise-core.git" 25 | }, 26 | "author": "Nicolai Kamenzky (https://github.com/analog-nico)", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/request/promise-core/issues" 30 | }, 31 | "homepage": "https://github.com/request/promise-core#readme", 32 | "engines": { 33 | "node": ">=0.10.0" 34 | }, 35 | "dependencies": { 36 | "lodash": "^4.17.19" 37 | }, 38 | "peerDependencies": { 39 | "request": "^2.34" 40 | }, 41 | "devDependencies": { 42 | "@request/api": "^0.6.0", 43 | "@request/client": "^0.1.0", 44 | "bluebird": "~3.4.1", 45 | "body-parser": "~1.15.2", 46 | "chai": "~3.5.0", 47 | "chalk": "~1.1.3", 48 | "gulp": "~3.9.1", 49 | "gulp-coveralls": "~0.1.4", 50 | "gulp-eslint": "~2.1.0", 51 | "gulp-istanbul": "~1.0.0", 52 | "gulp-mocha": "~2.2.0", 53 | "node-version": "~1.0.0", 54 | "publish-please": "~2.4.1", 55 | "request": "^2.34.0", 56 | "rimraf": "~2.5.3", 57 | "run-sequence": "~1.2.2", 58 | "stealthy-require": "~1.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /configure/request-next.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var core = require('../'), 4 | isArray = require('lodash/isArray'), 5 | isFunction = require('lodash/isFunction'), 6 | isObjectLike = require('lodash/isObjectLike'); 7 | 8 | 9 | module.exports = function (options) { 10 | 11 | var errorText = 'Please verify options'; // For better minification because this string is repeating 12 | 13 | if (!isObjectLike(options)) { 14 | throw new TypeError(errorText); 15 | } 16 | 17 | if (!isFunction(options.client)) { 18 | throw new TypeError(errorText + '.client'); 19 | } 20 | 21 | if (!isArray(options.expose) || options.expose.length === 0) { 22 | throw new TypeError(errorText + '.expose'); 23 | } 24 | 25 | var thenExposed = false; 26 | for ( var i = 0; i < options.expose.length; i+=1 ) { 27 | if (options.expose[i] === 'then') { 28 | thenExposed = true; 29 | break; 30 | } 31 | } 32 | if (!thenExposed) { 33 | throw new Error('Please expose "then"'); 34 | } 35 | 36 | 37 | var plumbing = core({ 38 | PromiseImpl: options.PromiseImpl, 39 | constructorMixin: options.constructorMixin 40 | }); 41 | 42 | return function (requestOptions) { 43 | 44 | var self = {}; 45 | 46 | plumbing.init.call(self, requestOptions); 47 | 48 | var request = options.client(requestOptions); 49 | 50 | for ( var k = 0; k < options.expose.length; k+=1 ) { 51 | 52 | var method = options.expose[k]; 53 | 54 | plumbing[ method === 'promise' ? 'exposePromise' : 'exposePromiseMethod' ]( 55 | request, 56 | self, 57 | '_rp_promise', 58 | method 59 | ); 60 | 61 | } 62 | 63 | return request; 64 | 65 | }; 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | function RequestError(cause, options, response) { 5 | 6 | this.name = 'RequestError'; 7 | this.message = String(cause); 8 | this.cause = cause; 9 | this.error = cause; // legacy attribute 10 | this.options = options; 11 | this.response = response; 12 | 13 | if (Error.captureStackTrace) { // required for non-V8 environments 14 | Error.captureStackTrace(this); 15 | } 16 | 17 | } 18 | RequestError.prototype = Object.create(Error.prototype); 19 | RequestError.prototype.constructor = RequestError; 20 | 21 | 22 | function StatusCodeError(statusCode, body, options, response) { 23 | 24 | this.name = 'StatusCodeError'; 25 | this.statusCode = statusCode; 26 | this.message = statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body); 27 | this.error = body; // legacy attribute 28 | this.options = options; 29 | this.response = response; 30 | 31 | if (Error.captureStackTrace) { // required for non-V8 environments 32 | Error.captureStackTrace(this); 33 | } 34 | 35 | } 36 | StatusCodeError.prototype = Object.create(Error.prototype); 37 | StatusCodeError.prototype.constructor = StatusCodeError; 38 | 39 | 40 | function TransformError(cause, options, response) { 41 | 42 | this.name = 'TransformError'; 43 | this.message = String(cause); 44 | this.cause = cause; 45 | this.error = cause; // legacy attribute 46 | this.options = options; 47 | this.response = response; 48 | 49 | if (Error.captureStackTrace) { // required for non-V8 environments 50 | Error.captureStackTrace(this); 51 | } 52 | 53 | } 54 | TransformError.prototype = Object.create(Error.prototype); 55 | TransformError.prototype.constructor = TransformError; 56 | 57 | 58 | module.exports = { 59 | RequestError: RequestError, 60 | StatusCodeError: StatusCodeError, 61 | TransformError: TransformError 62 | }; 63 | -------------------------------------------------------------------------------- /configure/request2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var core = require('../'), 4 | isArray = require('lodash/isArray'), 5 | isFunction = require('lodash/isFunction'), 6 | isObjectLike = require('lodash/isObjectLike'); 7 | 8 | 9 | module.exports = function (options) { 10 | 11 | var errorText = 'Please verify options'; // For better minification because this string is repeating 12 | 13 | if (!isObjectLike(options)) { 14 | throw new TypeError(errorText); 15 | } 16 | 17 | if (!isFunction(options.request)) { 18 | throw new TypeError(errorText + '.request'); 19 | } 20 | 21 | if (!isArray(options.expose) || options.expose.length === 0) { 22 | throw new TypeError(errorText + '.expose'); 23 | } 24 | 25 | 26 | var plumbing = core({ 27 | PromiseImpl: options.PromiseImpl, 28 | constructorMixin: options.constructorMixin 29 | }); 30 | 31 | 32 | // Intercepting Request's init method 33 | 34 | var originalInit = options.request.Request.prototype.init; 35 | 36 | options.request.Request.prototype.init = function RP$initInterceptor(requestOptions) { 37 | 38 | // Init may be called again - currently in case of redirects 39 | if (isObjectLike(requestOptions) && !this._callback && !this._rp_promise) { 40 | 41 | plumbing.init.call(this, requestOptions); 42 | 43 | } 44 | 45 | return originalInit.apply(this, arguments); 46 | 47 | }; 48 | 49 | 50 | // Exposing the Promise capabilities 51 | 52 | var thenExposed = false; 53 | for ( var i = 0; i < options.expose.length; i+=1 ) { 54 | 55 | var method = options.expose[i]; 56 | 57 | plumbing[ method === 'promise' ? 'exposePromise' : 'exposePromiseMethod' ]( 58 | options.request.Request.prototype, 59 | null, 60 | '_rp_promise', 61 | method 62 | ); 63 | 64 | if (method === 'then') { 65 | thenExposed = true; 66 | } 67 | 68 | } 69 | 70 | if (!thenExposed) { 71 | throw new Error('Please expose "then"'); 72 | } 73 | 74 | }; 75 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var runSequence = require('run-sequence'); 5 | var istanbul = require('gulp-istanbul'); 6 | var mocha = require('gulp-mocha'); 7 | var chalk = require('chalk'); 8 | var rimraf = require('rimraf'); 9 | var coveralls = require('gulp-coveralls'); 10 | var eslint = require('gulp-eslint'); 11 | var _ = require('lodash'); 12 | 13 | var chai = require('chai'); 14 | global.expect = chai.expect; 15 | 16 | 17 | var paths = { 18 | libJsFiles: ['./configure/**/*.js', './lib/**/*.js', './errors.js'], 19 | specFiles: './test/spec/**/*.js', 20 | fixtureFiles: './test/fixtures/**/*.js', 21 | gulpfile: './gulpfile.js', 22 | eslintrc: './.eslintrc.json' 23 | }; 24 | 25 | 26 | gulp.task('dev', ['watch', 'validate']); 27 | 28 | gulp.task('watch', function () { 29 | 30 | gulp.watch(_.flatten([ 31 | paths.libJsFiles, 32 | paths.specFiles, 33 | paths.fixtureFiles, 34 | paths.gulpfile 35 | ]), [ 36 | 'validate' 37 | ]); 38 | 39 | gulp.watch(_.flatten([ 40 | paths.eslintrc 41 | ]), [ 42 | 'lint' 43 | ]); 44 | 45 | }); 46 | 47 | gulp.task('validate', function (done) { 48 | runSequence('lint', 'test', done); 49 | }); 50 | 51 | gulp.task('lint', function () { 52 | 53 | return gulp.src(_.flatten([ 54 | paths.libJsFiles, 55 | paths.gulpfile, 56 | paths.specFiles, 57 | paths.fixtureFiles, 58 | paths.gulpfile 59 | ])) 60 | .pipe(eslint()) 61 | .pipe(eslint.format()) 62 | .pipe(eslint.failAfterError()); 63 | 64 | }); 65 | 66 | gulp.task('test', ['clean'], function (done) { 67 | 68 | var coverageVariable = '$$cov_' + new Date().getTime() + '$$'; 69 | 70 | gulp.src(paths.libJsFiles) 71 | .pipe(istanbul({ 72 | coverageVariable: coverageVariable 73 | })) 74 | .pipe(istanbul.hookRequire()) 75 | .on('finish', function () { 76 | 77 | gulp.src(paths.specFiles) 78 | .pipe(mocha()) 79 | .on('error', function (err) { 80 | console.error(String(err)); 81 | console.error(chalk.bold.bgRed(' TESTS FAILED ')); 82 | done(new Error(' TESTS FAILED ')); 83 | }) 84 | .pipe(istanbul.writeReports({ 85 | reporters: ['lcov'], 86 | coverageVariable: coverageVariable 87 | })) 88 | .on('end', done); 89 | 90 | }); 91 | 92 | }); 93 | 94 | gulp.task('test-without-coverage', function () { 95 | 96 | return gulp.src(paths.specFiles) 97 | .pipe(mocha()) 98 | .on('error', function () { 99 | console.log(chalk.bold.bgRed(' TESTS FAILED ')); 100 | }); 101 | 102 | }); 103 | 104 | gulp.task('clean', ['clean-coverage']); 105 | 106 | gulp.task('clean-coverage', function (done) { 107 | rimraf('./coverage', done); 108 | }); 109 | 110 | gulp.task('ci', function (done) { 111 | runSequence('validate', 'coveralls', 'test-without-coverage', done); 112 | }); 113 | 114 | gulp.task('ci-no-cov', function (done) { 115 | runSequence('validate', 'test-without-coverage', done); 116 | }); 117 | 118 | gulp.task('coveralls', function () { 119 | return gulp.src('coverage/**/lcov.info') 120 | .pipe(coveralls()); 121 | }); 122 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "espree", 4 | "parserOptions": { 5 | "ecmaVersion": 5, 6 | "sourceType": "script", 7 | "ecmaFeatures": {} 8 | }, 9 | "plugins": [], 10 | "env": { 11 | "node": true 12 | }, 13 | "globals": {}, 14 | "rules": { 15 | 16 | // Possible Errors (fully reviewed 2016-07-05) 17 | 18 | "no-cond-assign": 2, 19 | "no-console": 0, 20 | "no-constant-condition": 2, 21 | "no-control-regex": 2, 22 | "no-debugger": 2, 23 | "no-dupe-args": 2, 24 | "no-dupe-keys": 2, 25 | "no-duplicate-case": 2, 26 | "no-empty": [2, { "allowEmptyCatch": false }], 27 | "no-empty-character-class": 2, 28 | "no-ex-assign": 2, 29 | "no-extra-boolean-cast": 2, 30 | "no-extra-parens": [2, "all"], 31 | "no-extra-semi": 2, 32 | "no-func-assign": 2, 33 | "no-inner-declarations": [2, "functions"], 34 | "no-invalid-regexp": 2, 35 | "no-irregular-whitespace": [2, { "skipComments": false }], 36 | "no-negated-in-lhs": 2, 37 | "no-obj-calls": 2, 38 | "no-prototype-builtins": 2, 39 | "no-regex-spaces": 2, 40 | "no-sparse-arrays": 2, 41 | "no-unexpected-multiline": 2, 42 | "no-unreachable": 2, 43 | "no-unsafe-finally": 2, 44 | "use-isnan": 2, 45 | "valid-jsdoc": 0, 46 | "valid-typeof": 2, 47 | 48 | // Best Practices 49 | 50 | "accessor-pairs": 2, 51 | "curly": [2, "multi-line"], 52 | "dot-location": [2, "property"], 53 | "eqeqeq": 2, 54 | "no-caller": 2, 55 | "no-empty-pattern": 0, // for ES6 destructuring 56 | "no-eval": 2, 57 | "no-extend-native": 2, 58 | "no-extra-bind": 2, 59 | "no-fallthrough": 2, 60 | "no-floating-decimal": 2, 61 | "no-implied-eval": 2, 62 | "no-iterator": 2, 63 | "no-labels": 2, 64 | "no-lone-blocks": 2, 65 | "no-magic-numbers": 0, 66 | "no-multi-spaces": 2, 67 | "no-multi-str": 2, 68 | "no-native-reassign": 2, 69 | "no-new": 2, 70 | "no-new-func": 2, 71 | "no-new-wrappers": 2, 72 | "no-octal": 2, 73 | "no-octal-escape": 2, 74 | "no-proto": 2, 75 | "no-redeclare": 2, 76 | "no-return-assign": [2, "except-parens"], 77 | "no-self-assign": 2, 78 | "no-self-compare": 2, 79 | "no-sequences": 2, 80 | "no-throw-literal": 2, 81 | "no-unmodified-loop-condition": 2, 82 | "no-useless-call": 2, 83 | "no-useless-escape": 2, 84 | "no-with": 2, 85 | "wrap-iife": [2, "inside"], 86 | "yoda": 2, 87 | 88 | // Strict Mode (fully reviewed 2016-07-05) 89 | 90 | "strict": [2, "safe"], 91 | 92 | // Variables (fully reviewed 2016-07-05) 93 | 94 | "init-declarations": [2, "always"], 95 | "no-catch-shadow": 0, 96 | "no-delete-var": 2, 97 | "no-label-var": 2, 98 | "no-restricted-globals": 0, 99 | "no-shadow": [2, { "builtinGlobals": false, "hoist": "all", "allow": [] }], 100 | "no-shadow-restricted-names": 2, 101 | "no-undef": [2, { "typeof": true }], 102 | "no-undef-init": 2, 103 | "no-undefined": 0, 104 | "no-unused-vars": [2, { "vars": "local", "args": "none", "caughtErrors": "none" }], 105 | "no-use-before-define": [2, { "functions": false, "classes": true }], 106 | 107 | // Node.js and CommonJS (fully reviewed 2016-07-05) 108 | 109 | "callback-return": 0, 110 | "global-require": 0, 111 | "handle-callback-err": [2, "^(err|error)$" ], 112 | "no-mixed-requires": 0, 113 | "no-new-require": 2, 114 | "no-path-concat": 2, 115 | "no-process-env": 2, 116 | "no-process-exit": 2, 117 | "no-restricted-modules": 0, 118 | "no-sync": 2, 119 | 120 | // Stylistic Issues 121 | 122 | "block-spacing": 2, 123 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 124 | "camelcase": [2, { "properties": "never" }], 125 | "comma-dangle": [2, "never"], 126 | "comma-spacing": 2, 127 | "comma-style": 2, 128 | "eol-last": 2, 129 | "indent": [2, 4, { "SwitchCase": 1 }], 130 | "jsx-quotes": 0, 131 | "key-spacing": 2, 132 | "keyword-spacing": 2, 133 | "new-cap": 0, 134 | "new-parens": 2, 135 | "no-array-constructor": 2, 136 | "no-mixed-spaces-and-tabs": 2, 137 | "no-multiple-empty-lines": [2, { "max": 2, "maxBOF": 0, "maxEOF": 1 }], 138 | "no-new-object": 2, 139 | "no-plusplus": [2, { "allowForLoopAfterthoughts": false }], 140 | "no-spaced-func": 2, 141 | "no-trailing-spaces": 2, 142 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 143 | "no-whitespace-before-property": 2, 144 | "one-var": 0, 145 | "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], 146 | "padded-blocks": 0, 147 | "quotes": [2, "single", "avoid-escape"], 148 | "semi": [2, "always"], 149 | "semi-spacing": 2, 150 | "space-before-blocks": 2, 151 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 152 | "space-in-parens": 0, 153 | "space-infix-ops": 0, 154 | "space-unary-ops": 2, 155 | "spaced-comment": 0 156 | 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/plumbing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var errors = require('./errors.js'), 4 | isFunction = require('lodash/isFunction'), 5 | isObjectLike = require('lodash/isObjectLike'), 6 | isString = require('lodash/isString'), 7 | isUndefined = require('lodash/isUndefined'); 8 | 9 | 10 | module.exports = function (options) { 11 | 12 | var errorText = 'Please verify options'; // For better minification because this string is repeating 13 | 14 | if (!isObjectLike(options)) { 15 | throw new TypeError(errorText); 16 | } 17 | 18 | if (!isFunction(options.PromiseImpl)) { 19 | throw new TypeError(errorText + '.PromiseImpl'); 20 | } 21 | 22 | if (!isUndefined(options.constructorMixin) && !isFunction(options.constructorMixin)) { 23 | throw new TypeError(errorText + '.PromiseImpl'); 24 | } 25 | 26 | var PromiseImpl = options.PromiseImpl; 27 | var constructorMixin = options.constructorMixin; 28 | 29 | 30 | var plumbing = {}; 31 | 32 | plumbing.init = function (requestOptions) { 33 | 34 | var self = this; 35 | 36 | self._rp_promise = new PromiseImpl(function (resolve, reject) { 37 | self._rp_resolve = resolve; 38 | self._rp_reject = reject; 39 | if (constructorMixin) { 40 | constructorMixin.apply(self, arguments); // Using arguments since specific Promise libraries may pass additional parameters 41 | } 42 | }); 43 | 44 | self._rp_callbackOrig = requestOptions.callback; 45 | requestOptions.callback = self.callback = function RP$callback(err, response, body) { 46 | plumbing.callback.call(self, err, response, body); 47 | }; 48 | 49 | if (isString(requestOptions.method)) { 50 | requestOptions.method = requestOptions.method.toUpperCase(); 51 | } 52 | 53 | requestOptions.transform = requestOptions.transform || plumbing.defaultTransformations[requestOptions.method]; 54 | 55 | self._rp_options = requestOptions; 56 | self._rp_options.simple = requestOptions.simple !== false; 57 | self._rp_options.resolveWithFullResponse = requestOptions.resolveWithFullResponse === true; 58 | self._rp_options.transform2xxOnly = requestOptions.transform2xxOnly === true; 59 | 60 | }; 61 | 62 | plumbing.defaultTransformations = { 63 | HEAD: function (body, response, resolveWithFullResponse) { 64 | return resolveWithFullResponse ? response : response.headers; 65 | } 66 | }; 67 | 68 | plumbing.callback = function (err, response, body) { 69 | 70 | var self = this; 71 | 72 | var origCallbackThrewException = false, thrownException = null; 73 | 74 | if (isFunction(self._rp_callbackOrig)) { 75 | try { 76 | self._rp_callbackOrig.apply(self, arguments); // TODO: Apply to self mimics behavior of request@2. Is that also right for request@next? 77 | } catch (e) { 78 | origCallbackThrewException = true; 79 | thrownException = e; 80 | } 81 | } 82 | 83 | var is2xx = !err && /^2/.test('' + response.statusCode); 84 | 85 | if (err) { 86 | 87 | self._rp_reject(new errors.RequestError(err, self._rp_options, response)); 88 | 89 | } else if (self._rp_options.simple && !is2xx) { 90 | 91 | if (isFunction(self._rp_options.transform) && self._rp_options.transform2xxOnly === false) { 92 | 93 | (new PromiseImpl(function (resolve) { 94 | resolve(self._rp_options.transform(body, response, self._rp_options.resolveWithFullResponse)); // transform may return a Promise 95 | })) 96 | .then(function (transformedResponse) { 97 | self._rp_reject(new errors.StatusCodeError(response.statusCode, body, self._rp_options, transformedResponse)); 98 | }) 99 | .catch(function (transformErr) { 100 | self._rp_reject(new errors.TransformError(transformErr, self._rp_options, response)); 101 | }); 102 | 103 | } else { 104 | self._rp_reject(new errors.StatusCodeError(response.statusCode, body, self._rp_options, response)); 105 | } 106 | 107 | } else { 108 | 109 | if (isFunction(self._rp_options.transform) && (is2xx || self._rp_options.transform2xxOnly === false)) { 110 | 111 | (new PromiseImpl(function (resolve) { 112 | resolve(self._rp_options.transform(body, response, self._rp_options.resolveWithFullResponse)); // transform may return a Promise 113 | })) 114 | .then(function (transformedResponse) { 115 | self._rp_resolve(transformedResponse); 116 | }) 117 | .catch(function (transformErr) { 118 | self._rp_reject(new errors.TransformError(transformErr, self._rp_options, response)); 119 | }); 120 | 121 | } else if (self._rp_options.resolveWithFullResponse) { 122 | self._rp_resolve(response); 123 | } else { 124 | self._rp_resolve(body); 125 | } 126 | 127 | } 128 | 129 | if (origCallbackThrewException) { 130 | throw thrownException; 131 | } 132 | 133 | }; 134 | 135 | plumbing.exposePromiseMethod = function (exposeTo, bindTo, promisePropertyKey, methodToExpose, exposeAs) { 136 | 137 | exposeAs = exposeAs || methodToExpose; 138 | 139 | if (exposeAs in exposeTo) { 140 | throw new Error('Unable to expose method "' + exposeAs + '"'); 141 | } 142 | 143 | exposeTo[exposeAs] = function RP$exposed() { 144 | var self = bindTo || this; 145 | return self[promisePropertyKey][methodToExpose].apply(self[promisePropertyKey], arguments); 146 | }; 147 | 148 | }; 149 | 150 | plumbing.exposePromise = function (exposeTo, bindTo, promisePropertyKey, exposeAs) { 151 | 152 | exposeAs = exposeAs || 'promise'; 153 | 154 | if (exposeAs in exposeTo) { 155 | throw new Error('Unable to expose method "' + exposeAs + '"'); 156 | } 157 | 158 | exposeTo[exposeAs] = function RP$promise() { 159 | var self = bindTo || this; 160 | return self[promisePropertyKey]; 161 | }; 162 | 163 | }; 164 | 165 | return plumbing; 166 | 167 | }; 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Promises/A+ logo 3 | 4 | 5 | # request-promise-core 6 | 7 | [![Gitter](https://img.shields.io/badge/gitter-join_chat-blue.svg?style=flat-square&maxAge=2592000)](https://gitter.im/request/request-promise?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | [![Build Status](https://img.shields.io/travis/request/promise-core/master.svg?style=flat-square&maxAge=2592000)](https://travis-ci.org/request/promise-core) 9 | [![Coverage Status](https://img.shields.io/coveralls/request/promise-core.svg?style=flat-square&maxAge=2592000)](https://coveralls.io/r/request/promise-core) 10 | [![Dependency Status](https://img.shields.io/david/request/promise-core.svg?style=flat-square&maxAge=2592000)](https://david-dm.org/request/promise-core) 11 | [![Known Vulnerabilities](https://snyk.io/test/npm/promise-core/badge.svg?style=flat-square&maxAge=2592000)](https://snyk.io/test/npm/promise-core) 12 | 13 | 14 | This package is the core for the following packages: 15 | 16 | - [`request-promise`](https://github.com/request/request-promise) 17 | - [`request-promise-any`](https://github.com/request/request-promise-any) 18 | - [`request-promise-bluebird`](https://github.com/request/request-promise-bluebird) 19 | - [`request-promise-native`](https://github.com/request/request-promise-native) 20 | 21 | `request-promise-core` contains the core logic to add Promise support to [`request`](https://github.com/request/request). 22 | 23 | Please use one of the libraries above. It is only recommended to use this library directly, if you have very specific requirements. 24 | 25 | ## Installation for `request@^2.34` 26 | 27 | This module is installed via npm: 28 | 29 | ``` 30 | npm install --save request 31 | npm install --save request-promise-core 32 | ``` 33 | 34 | `request` is defined as a peer-dependency and thus has to be installed separately. 35 | 36 | ## Usage for `request@^2.34` 37 | 38 | ``` js 39 | // 1. Load the request library 40 | 41 | // Only use a direct require if you are 100% sure that: 42 | // - Your project does not use request directly. That is without the Promise capabilities by calling require('request'). 43 | // - Any of the installed libraries use request. 44 | // ...because Request's prototype will be patched in step 2. 45 | /* var request = require('request'); */ 46 | 47 | // Instead use: 48 | var stealthyRequire = require('stealthy-require'); 49 | var request = stealthyRequire(require.cache, function () { 50 | return require('request'); 51 | }); 52 | 53 | 54 | // 2. Add Promise support to request 55 | 56 | var configure = require('request-promise-core/configure/request2'); 57 | 58 | configure({ 59 | request: request, 60 | // Pass your favorite ES6-compatible promise implementation 61 | PromiseImpl: Promise, 62 | // Expose all methods of the promise instance you want to call on the request(...) call 63 | expose: [ 64 | 'then', // Allows to use request(...).then(...) 65 | 'catch', // Allows to use request(...).catch(...) 66 | 'promise' // Allows to use request(...).promise() which returns the promise instance 67 | ], 68 | // Optional: Pass a callback that is called within the Promise constructor 69 | constructorMixin: function (resolve, reject) { 70 | // `this` is the request object 71 | // Additional arguments may be passed depending on the PromiseImpl used 72 | } 73 | }); 74 | 75 | 76 | // 3. Use request with its promise capabilities 77 | 78 | // E.g. crawl a web page: 79 | request('http://www.google.com') 80 | .then(function (htmlString) { 81 | // Process html... 82 | }) 83 | .catch(function (err) { 84 | // Crawling failed... 85 | }); 86 | ``` 87 | 88 | ## Installation and Usage for `request@next` 89 | 90 | [Request Next](https://github.com/request/request/issues/1982) is still in alpha. However, `request-promise-core` is already designed to be compatible and ships with a configuration helper – `require('request-promise-core/configure/request-next')` – that is [used by `request-promise`](https://github.com/request/request-promise/blob/next/lib/rp.js) in its "next" branch. 91 | 92 | ## Contributing 93 | 94 | To set up your development environment: 95 | 96 | 1. clone the repo to your desktop, 97 | 2. in the shell `cd` to the main folder, 98 | 3. hit `npm install`, 99 | 4. hit `npm install gulp -g` if you haven't installed gulp globally yet, and 100 | 5. run `gulp dev`. (Or run `node ./node_modules/.bin/gulp dev` if you don't want to install gulp globally.) 101 | 102 | `gulp dev` watches all source files and if you save some changes it will lint the code and execute all tests. The test coverage report can be viewed from `./coverage/lcov-report/index.html`. 103 | 104 | If you want to debug a test you should use `gulp test-without-coverage` to run all tests without obscuring the code by the test coverage instrumentation. 105 | 106 | ## Change History 107 | 108 | - 1.1.4 (2020-07-21) 109 | - Security fix: bumped `lodash` to `^4.17.19` following [this advisory](https://www.npmjs.com/advisories/1523). 110 | - 1.1.3 (2019-11-03) 111 | - Security fix: bumped `lodash` to `^4.17.15`. See [vulnerabilty reports](https://snyk.io/vuln/search?q=lodash&type=npm). 112 | *(Thanks to @daniel-nagy for pull request [#20](https://github.com/request/promise-core/pull/20) and thanks to @quetzaluz for reporting this in issue [#21](https://github.com/request/promise-core/issues/21).)* 113 | - 1.1.2 (2019-02-14) 114 | - Security fix: bumped `lodash` to `^4.17.11`. See [vulnerabilty reports](https://snyk.io/vuln/search?q=lodash&type=npm). 115 | *(Thanks to @lucaswillering and @sam-warren-finnair for reporting this in issues [#12](https://github.com/request/promise-core/issues/12) and [#13](https://github.com/request/promise-core/issues/13) and thanks to @Alec321 for pull request [#14](https://github.com/request/promise-core/pull/14).)* 116 | - 1.1.1 (2016-08-08) 117 | - Renamed package to `request-promise-core` because there were [too](https://github.com/request/request-promise/issues/137) [many](https://github.com/request/request-promise/issues/141) issues with the scoped package name `@request/promise-core` 118 | - 1.1.0 (2016-07-30) 119 | - Added `constructorMixin` option to enable [request/request-promise#123](https://github.com/request/request-promise/pull/123) 120 | - 1.0.0 (2016-07-15) 121 | - All tests green, ready for prime time 122 | - 1.0.0-rc.1 (2016-07-10) 123 | - Reimplementation of core logic based on `request-promise@3.0.0` 124 | - Plus `transform2xxOnly` option (fixes [request/request-promise#131](https://github.com/request/request-promise/issues/131)) 125 | 126 | ## License (ISC) 127 | 128 | In case you never heard about the [ISC license](http://en.wikipedia.org/wiki/ISC_license) it is functionally equivalent to the MIT license. 129 | 130 | See the [LICENSE file](LICENSE) for details. 131 | -------------------------------------------------------------------------------- /test/spec/request-next.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Bluebird = require('bluebird'), 4 | configure = require('../../configure/request-next.js'), 5 | errors = require('../../errors'), 6 | nodeVersion = require('node-version'), 7 | startServer = require('../fixtures/server.js'); 8 | 9 | 10 | describe('Promise-Core for Request@next', function () { 11 | 12 | if (Number(nodeVersion.major) < 4) { 13 | return; // request@next uses ES6 and thus wouldn't run on old node.js versions 14 | } 15 | 16 | var api = require('@request/api'), 17 | client = require('@request/client'); 18 | 19 | 20 | describe('during configuration', function () { 21 | 22 | it('should verify the options', function () { 23 | 24 | expect(function () { 25 | configure(); 26 | }).to.throw('Please verify options'); 27 | 28 | expect(function () { 29 | configure('not an object'); 30 | }).to.throw('Please verify options'); 31 | 32 | expect(function () { 33 | configure({}); 34 | }).to.throw('Please verify options.client'); 35 | 36 | expect(function () { 37 | configure({ 38 | client: 'not a function' 39 | }); 40 | }).to.throw('Please verify options.client'); 41 | 42 | expect(function () { 43 | configure({ 44 | client: client 45 | }); 46 | }).to.throw('Please verify options.expose'); 47 | 48 | expect(function () { 49 | configure({ 50 | client: client, 51 | expose: 'not an array' 52 | }); 53 | }).to.throw('Please verify options.expose'); 54 | 55 | expect(function () { 56 | configure({ 57 | client: client, 58 | expose: [] 59 | }); 60 | }).to.throw('Please verify options.expose'); 61 | 62 | expect(function () { 63 | configure({ 64 | client: client, 65 | expose: ['then', 'promise'] 66 | }); 67 | }).to.throw('Please verify options.PromiseImpl'); 68 | 69 | expect(function () { 70 | configure({ 71 | client: client, 72 | expose: ['then', 'promise'], 73 | PromiseImpl: 'not a function' 74 | }); 75 | }).to.throw('Please verify options.PromiseImpl'); 76 | 77 | expect(function () { 78 | configure({ 79 | client: client, 80 | expose: ['then', 'promise'], 81 | PromiseImpl: Bluebird 82 | }); 83 | }).not.to.throw(); 84 | 85 | expect(function () { 86 | configure({ 87 | client: client, 88 | expose: ['promise'], 89 | PromiseImpl: Bluebird 90 | }); 91 | }).to.throw('Please expose "then"'); 92 | 93 | }); 94 | 95 | it('should forward the constructorMixin', function () { 96 | 97 | var mixinCalled = false; // eslint-disable-line no-unused-vars 98 | 99 | var request = api({ 100 | type: 'basic', 101 | define: { 102 | request: configure({ 103 | client: client, 104 | PromiseImpl: Bluebird, 105 | expose: [ 106 | 'then', 107 | 'catch' 108 | ], 109 | constructorMixin: function () { 110 | mixinCalled = true; 111 | } 112 | }) 113 | } 114 | }); 115 | 116 | return request('http://localhost:4000') // not started yet so expecting ECONNREFUSED 117 | .catch(function () { 118 | expect(mixinCalled).to.eql(true); 119 | }); 120 | 121 | }); 122 | 123 | }); 124 | 125 | /** 126 | * The following tests are testing the correct integration of plumbing.init and plumbing.callback. 127 | * 128 | * That means their test coverage is not 100% (the plumbing unit tests already do that) 129 | * but instead focus on the following integration aspects: 130 | * - All input parameters are passed to the two functions as expected. 131 | * - All return values of the two functions are processed by Request as expected. 132 | * - All operations on the context (this) of the two functions have the expected effect. 133 | * - Plus 100% coverage of the configuration code. 134 | */ 135 | describe('doing requests', function () { 136 | 137 | var request = null, stopServer = null; 138 | 139 | before(function (done) { 140 | 141 | request = api({ 142 | type: 'basic', 143 | define: { 144 | request: configure({ 145 | client: client, 146 | PromiseImpl: Bluebird, 147 | expose: [ 148 | 'then', 149 | 'catch', 150 | 'finally', 151 | 'promise' 152 | ] 153 | }) 154 | } 155 | }); 156 | 157 | startServer(4000, function (stop) { 158 | stopServer = stop; 159 | done(); 160 | }); 161 | 162 | }); 163 | 164 | after(function (done) { 165 | 166 | stopServer(done); 167 | 168 | }); 169 | 170 | it('that is successful', function (done) { 171 | 172 | request('http://localhost:4000/200') 173 | .then(function (body) { 174 | expect(body).to.eql('GET /200'); 175 | done(); 176 | }) 177 | .catch(function (err) { 178 | done(err); 179 | }); 180 | 181 | }); 182 | 183 | // TODO: Check if request@next fixed passing the response to the callback 184 | xit('that is successful with non-default options', function (done) { 185 | 186 | request({ 187 | uri: 'http://localhost:4000/404', 188 | simple: false, 189 | resolveWithFullResponse: true, 190 | transform: function () { 191 | return 'must not be called'; 192 | }, 193 | transform2xxOnly: true 194 | }) 195 | .then(function (response) { 196 | expect(response.body).to.eql('GET /404'); 197 | done(); 198 | }) 199 | .catch(function (err) { 200 | done(err); 201 | }); 202 | 203 | }); 204 | 205 | it('with method "post" in lower case', function (done) { 206 | 207 | request({ 208 | method: 'post', 209 | uri: 'http://localhost:4000/200', 210 | body: { 211 | a: 'b' 212 | }, 213 | json: true 214 | }) 215 | .then(function (body) { 216 | // TODO: Check if request@next fixed sending the body 217 | // expect(body).to.eql('POST /200 - {"a":"b"}'); 218 | expect(body).to.eql('POST /200 - {}'); 219 | done(); 220 | }) 221 | .catch(function (err) { 222 | done(err); 223 | }); 224 | 225 | }); 226 | 227 | it('with a transform function', function (done) { 228 | 229 | request({ 230 | method: 'post', 231 | uri: 'http://localhost:4000/200', 232 | body: { 233 | a: 'b' 234 | }, 235 | json: true, 236 | transform: function (body, response, resolveWithFullResponse) { 237 | return body.split('').reverse().join(''); 238 | } 239 | }) 240 | .then(function (body) { 241 | // TODO: Check if request@next fixed sending the body 242 | // expect(body).to.eql('POST /200 - {"a":"b"}'.split('').reverse().join('')); 243 | expect(body).to.eql('POST /200 - {}'.split('').reverse().join('')); 244 | done(); 245 | }) 246 | .catch(function (err) { 247 | done(err); 248 | }); 249 | 250 | }); 251 | 252 | it('that fails', function (done) { 253 | 254 | request('http://localhost:1/200') 255 | .then(function () { 256 | done(new Error('Expected promise to be rejected.')); 257 | }) 258 | .catch(function (err) { 259 | expect(err instanceof errors.RequestError).to.eql(true); 260 | done(); 261 | }); 262 | 263 | }); 264 | 265 | it('that gets a 500 response', function (done) { 266 | 267 | request('http://localhost:4000/500') 268 | .then(function () { 269 | done(new Error('Expected promise to be rejected.')); 270 | }) 271 | .catch(function (err) { 272 | expect(err instanceof errors.StatusCodeError).to.eql(true); 273 | done(); 274 | }); 275 | 276 | }); 277 | 278 | it('calling the callback, too', function (done) { 279 | 280 | var callbackWasCalled = false; 281 | 282 | request('http://localhost:4000/200', function () { 283 | callbackWasCalled = true; 284 | }) 285 | .then(function (body) { 286 | expect(body).to.eql('GET /200'); 287 | expect(callbackWasCalled).to.eql(true); 288 | done(); 289 | }) 290 | .catch(function (err) { 291 | done(err); 292 | }); 293 | 294 | }); 295 | 296 | }); 297 | 298 | describe('should support Request\'s', function () { 299 | 300 | // TODO: Add tests for Request's non-promise related features 301 | 302 | }); 303 | 304 | }); 305 | -------------------------------------------------------------------------------- /test/spec/request2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Bluebird = require('bluebird'), 4 | configure = require('../../configure/request2.js'), 5 | errors = require('../../errors'), 6 | stealthyRequire = require('stealthy-require'), 7 | startServer = require('../fixtures/server.js'); 8 | 9 | 10 | describe('Promise-Core for Request@2', function () { 11 | 12 | describe('during configuration', function () { 13 | 14 | it('should verify the options', function () { 15 | 16 | var request = stealthyRequire(require.cache, function () { 17 | return require('request'); 18 | }); 19 | 20 | expect(function () { 21 | configure(); 22 | }).to.throw('Please verify options'); 23 | 24 | expect(function () { 25 | configure('not an object'); 26 | }).to.throw('Please verify options'); 27 | 28 | expect(function () { 29 | configure({}); 30 | }).to.throw('Please verify options.request'); 31 | 32 | expect(function () { 33 | configure({ 34 | request: 'not a function' 35 | }); 36 | }).to.throw('Please verify options.request'); 37 | 38 | expect(function () { 39 | configure({ 40 | request: request 41 | }); 42 | }).to.throw('Please verify options.expose'); 43 | 44 | expect(function () { 45 | configure({ 46 | request: request, 47 | expose: 'not an array' 48 | }); 49 | }).to.throw('Please verify options.expose'); 50 | 51 | expect(function () { 52 | configure({ 53 | request: request, 54 | expose: [] 55 | }); 56 | }).to.throw('Please verify options.expose'); 57 | 58 | expect(function () { 59 | configure({ 60 | request: request, 61 | expose: ['then', 'promise'] 62 | }); 63 | }).to.throw('Please verify options.PromiseImpl'); 64 | 65 | expect(function () { 66 | configure({ 67 | request: request, 68 | expose: ['then', 'promise'], 69 | PromiseImpl: 'not a function' 70 | }); 71 | }).to.throw('Please verify options.PromiseImpl'); 72 | 73 | expect(function () { 74 | configure({ 75 | request: request, 76 | expose: ['then', 'promise'], 77 | PromiseImpl: Bluebird 78 | }); 79 | }).not.to.throw(); 80 | 81 | request = stealthyRequire(require.cache, function () { 82 | return require('request'); 83 | }); 84 | 85 | expect(function () { 86 | configure({ 87 | request: request, 88 | expose: ['promise'], 89 | PromiseImpl: Bluebird 90 | }); 91 | }).to.throw('Please expose "then"'); 92 | 93 | }); 94 | 95 | it('should forward the constructorMixin', function () { 96 | 97 | var mixinCalled = false; // eslint-disable-line no-unused-vars 98 | 99 | var request = stealthyRequire(require.cache, function () { 100 | return require('request'); 101 | }); 102 | 103 | configure({ 104 | request: request, 105 | expose: ['then', 'catch'], 106 | PromiseImpl: Bluebird, 107 | constructorMixin: function () { 108 | mixinCalled = true; 109 | } 110 | }); 111 | 112 | return request('http://localhost:4000') // not started yet so expecting ECONNREFUSED 113 | .catch(function () { 114 | expect(mixinCalled).to.eql(true); 115 | }); 116 | 117 | }); 118 | 119 | }); 120 | 121 | /** 122 | * The following tests are testing the correct integration of plumbing.init and plumbing.callback. 123 | * 124 | * That means their test coverage is not 100% (the plumbing unit tests already do that) 125 | * but instead focus on the following integration aspects: 126 | * - All input parameters are passed to the two functions as expected. 127 | * - All return values of the two functions are processed by Request as expected. 128 | * - All operations on the context (this) of the two functions have the expected effect. 129 | * - Plus 100% coverage of the configuration code. 130 | */ 131 | describe('doing requests', function () { 132 | 133 | var request = null, stopServer = null; 134 | 135 | before(function (done) { 136 | 137 | request = stealthyRequire(require.cache, function () { 138 | return require('request'); 139 | }); 140 | 141 | configure({ 142 | request: request, 143 | PromiseImpl: Bluebird, 144 | expose: [ 145 | 'then', 146 | 'catch', 147 | 'finally', 148 | 'promise' 149 | ] 150 | }); 151 | 152 | startServer(4000, function (stop) { 153 | stopServer = stop; 154 | done(); 155 | }); 156 | 157 | }); 158 | 159 | after(function (done) { 160 | 161 | stopServer(done); 162 | 163 | }); 164 | 165 | it('that is successful', function (done) { 166 | 167 | request('http://localhost:4000/200') 168 | .then(function (body) { 169 | expect(body).to.eql('GET /200'); 170 | done(); 171 | }) 172 | .catch(function (err) { 173 | done(err); 174 | }); 175 | 176 | }); 177 | 178 | it('that is successful with non-default options', function (done) { 179 | 180 | request({ 181 | uri: 'http://localhost:4000/404', 182 | simple: false, 183 | resolveWithFullResponse: true, 184 | transform: function () { 185 | return 'must not be called'; 186 | }, 187 | transform2xxOnly: true 188 | }) 189 | .then(function (response) { 190 | expect(response.body).to.eql('GET /404'); 191 | done(); 192 | }) 193 | .catch(function (err) { 194 | done(err); 195 | }); 196 | 197 | }); 198 | 199 | it('with method "post" in lower case', function (done) { 200 | 201 | request({ 202 | method: 'post', 203 | uri: 'http://localhost:4000/200', 204 | body: { 205 | a: 'b' 206 | }, 207 | json: true 208 | }) 209 | .then(function (body) { 210 | expect(body).to.eql('POST /200 - {"a":"b"}'); 211 | done(); 212 | }) 213 | .catch(function (err) { 214 | done(err); 215 | }); 216 | 217 | }); 218 | 219 | it('with a transform function', function (done) { 220 | 221 | request({ 222 | method: 'post', 223 | uri: 'http://localhost:4000/200', 224 | body: { 225 | a: 'b' 226 | }, 227 | json: true, 228 | transform: function (body, response, resolveWithFullResponse) { 229 | return body.split('').reverse().join(''); 230 | } 231 | }) 232 | .then(function (body) { 233 | expect(body).to.eql('POST /200 - {"a":"b"}'.split('').reverse().join('')); 234 | done(); 235 | }) 236 | .catch(function (err) { 237 | done(err); 238 | }); 239 | 240 | }); 241 | 242 | it('that is successfully redirected', function (done) { 243 | 244 | request('http://localhost:4000/301') 245 | .then(function (body) { 246 | expect(body).to.eql('GET /200'); 247 | done(); 248 | }) 249 | .catch(function (err) { 250 | done(err); 251 | }); 252 | 253 | }); 254 | 255 | it('that fails', function (done) { 256 | 257 | request('http://localhost:1/200') 258 | .then(function () { 259 | done(new Error('Expected promise to be rejected.')); 260 | }) 261 | .catch(function (err) { 262 | expect(err instanceof errors.RequestError).to.eql(true); 263 | done(); 264 | }); 265 | 266 | }); 267 | 268 | it('that gets a 500 response', function (done) { 269 | 270 | request('http://localhost:4000/500') 271 | .then(function () { 272 | done(new Error('Expected promise to be rejected.')); 273 | }) 274 | .catch(function (err) { 275 | expect(err instanceof errors.StatusCodeError).to.eql(true); 276 | done(); 277 | }); 278 | 279 | }); 280 | 281 | it('calling the callback, too', function (done) { 282 | 283 | var callbackWasCalled = false; 284 | 285 | request('http://localhost:4000/200', function () { 286 | callbackWasCalled = true; 287 | }) 288 | .then(function (body) { 289 | expect(body).to.eql('GET /200'); 290 | expect(callbackWasCalled).to.eql(true); 291 | done(); 292 | }) 293 | .catch(function (err) { 294 | done(err); 295 | }); 296 | 297 | }); 298 | 299 | }); 300 | 301 | describe('should support Request\'s', function () { 302 | 303 | var request = null, stopServer = null; 304 | 305 | before(function (done) { 306 | 307 | request = stealthyRequire(require.cache, function () { 308 | return require('request'); 309 | }); 310 | 311 | configure({ 312 | request: request, 313 | PromiseImpl: Bluebird, 314 | expose: [ 315 | 'then', 316 | 'catch', 317 | 'finally', 318 | 'promise' 319 | ] 320 | }); 321 | 322 | startServer(4000, function (stop) { 323 | stopServer = stop; 324 | done(); 325 | }); 326 | 327 | }); 328 | 329 | after(function (done) { 330 | 331 | stopServer(done); 332 | 333 | }); 334 | 335 | it('method shortcuts', function (done) { 336 | 337 | request.post({ 338 | uri: 'http://localhost:4000/404', 339 | body: { 340 | a: 'b' 341 | }, 342 | json: true, 343 | simple: false // <-- ensures that parameter is forwarded 344 | }) 345 | .then(function (body) { 346 | expect(body).to.eql('POST /404 - {"a":"b"}'); 347 | done(); 348 | }) 349 | .catch(function (err) { 350 | done(err); 351 | }); 352 | 353 | }); 354 | 355 | it('.defaults(...) feature', function (done) { 356 | 357 | var rpSimpleOff = request.defaults({ simple: false }); 358 | 359 | rpSimpleOff({ 360 | uri: 'http://localhost:4000/404', 361 | resolveWithFullResponse: true 362 | }) 363 | .then(function (response) { 364 | expect(response.body).to.eql('GET /404'); 365 | done(); 366 | }) 367 | .catch(function (err) { 368 | done(err); 369 | }); 370 | 371 | }); 372 | 373 | if (process.env.V_REQUEST !== '2.34.0') { // Was never supported in this version so fixing it wouldn't make sense. 374 | 375 | it('.defaults(...) feature using it multiple times', function (done) { 376 | 377 | var rpSimpleOff = request.defaults({ simple: false }); 378 | var rpSimpleOffWithFullResp = rpSimpleOff.defaults({ resolveWithFullResponse: true }); 379 | 380 | rpSimpleOffWithFullResp('http://localhost:4000/404') 381 | .then(function (response) { 382 | expect(response.body).to.eql('GET /404'); 383 | done(); 384 | }) 385 | .catch(function (err) { 386 | done(err); 387 | }); 388 | 389 | }); 390 | 391 | } 392 | 393 | it('event emitter', function (done) { 394 | 395 | request('http://localhost:4000/200') 396 | .on('complete', function (httpResponse, body) { 397 | expect(httpResponse.statusCode).to.eql(200); 398 | expect(body).to.eql('GET /200'); 399 | done(); 400 | }); 401 | 402 | }); 403 | 404 | it('main function to take extra options as the second parameter', function (done) { 405 | 406 | request('http://localhost:4000/200', { method: 'POST', json: { foo: 'bar' } }) 407 | .then(function (body) { 408 | expect(body).to.eql('POST /200 - {"foo":"bar"}'); 409 | done(); 410 | }) 411 | .catch(function (err) { 412 | done(err); 413 | }); 414 | 415 | }); 416 | 417 | }); 418 | 419 | }); 420 | -------------------------------------------------------------------------------- /test/spec/plumbing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | Bluebird = require('bluebird'), 5 | errors = require('../../errors'), 6 | plumbing = require('../../'); 7 | 8 | 9 | describe('Promise-Core\'s Plumbing', function () { 10 | 11 | it('should verify the options', function () { 12 | 13 | expect(function () { 14 | plumbing(); 15 | }).to.throw('Please verify options'); 16 | 17 | expect(function () { 18 | plumbing('not an object'); 19 | }).to.throw('Please verify options'); 20 | 21 | expect(function () { 22 | plumbing({}); 23 | }).to.throw('Please verify options.PromiseImpl'); 24 | 25 | expect(function () { 26 | plumbing({ 27 | PromiseImpl: 'not a function' 28 | }); 29 | }).to.throw('Please verify options.PromiseImpl'); 30 | 31 | expect(function () { 32 | plumbing({ 33 | PromiseImpl: function () {}, 34 | constructorMixin: false 35 | }); 36 | }).to.throw('Please verify options.PromiseImpl'); 37 | 38 | expect(function () { 39 | plumbing({ 40 | PromiseImpl: function () {}, 41 | constructorMixin: function () {} 42 | }); 43 | }).not.to.throw(); 44 | 45 | }); 46 | 47 | describe('should provide .init(...)', function () { 48 | 49 | var pl = null; 50 | 51 | before(function () { 52 | 53 | pl = plumbing({ 54 | PromiseImpl: Bluebird 55 | }); 56 | 57 | }); 58 | 59 | it('that sets up the promise and the resolve function', function (done) { 60 | 61 | var context = {}; 62 | 63 | pl.init.call(context, {}); 64 | 65 | expect(_.isFunction(context._rp_promise.then)).to.eql(true); 66 | expect(_.isFunction(context._rp_resolve)).to.eql(true); 67 | 68 | context._rp_resolve(); 69 | 70 | context._rp_promise 71 | .then(function () { 72 | done(); 73 | }) 74 | .catch(function (err) { 75 | done(err); 76 | }); 77 | 78 | }); 79 | 80 | it('that sets up the promise and the reject function', function (done) { 81 | 82 | var context = {}; 83 | pl.init.call(context, {}); 84 | 85 | expect(_.isFunction(context._rp_promise.then)).to.eql(true); 86 | expect(_.isFunction(context._rp_reject)).to.eql(true); 87 | 88 | context._rp_reject(new Error('Rejected by test case')); 89 | 90 | context._rp_promise 91 | .then(function () { 92 | done(new Error('Expected promise to be rejected.')); 93 | }) 94 | .catch(function (err) { 95 | expect(err.message).to.eql('Rejected by test case'); 96 | done(); 97 | }); 98 | 99 | }); 100 | 101 | it('that invokes the constructorMixin', function (done) { 102 | 103 | var pl2 = plumbing({ 104 | PromiseImpl: Bluebird, 105 | constructorMixin: function (resolve, reject) { 106 | if (this._rp_reject === reject) { 107 | reject('mixin invoked and this binding correct'); 108 | } else { 109 | reject('mixin invoked but this binding not correct'); 110 | } 111 | } 112 | }); 113 | 114 | var context = {}; 115 | pl2.init.call(context, {}); 116 | 117 | context._rp_promise 118 | .then(function () { 119 | done(new Error('Expected rejected promise')); 120 | }) 121 | .catch(function (message) { 122 | try { 123 | expect(message).to.eql('mixin invoked and this binding correct'); 124 | done(); 125 | } catch (e) { 126 | done(e); 127 | } 128 | }); 129 | 130 | }); 131 | 132 | it('that sets up the default options', function () { 133 | 134 | var context = {}; 135 | pl.init.call(context, {}); 136 | 137 | expect(_.isFunction(context._rp_options.callback)).to.eql(true); 138 | delete context._rp_options.callback; 139 | 140 | expect(context._rp_options).to.eql({ 141 | simple: true, 142 | resolveWithFullResponse: false, 143 | transform: undefined, 144 | transform2xxOnly: false 145 | }); 146 | 147 | }); 148 | 149 | it('that forwards any custom options', function () { 150 | 151 | var context = {}; 152 | pl.init.call(context, { 153 | custom: 'test' 154 | }); 155 | 156 | delete context._rp_options.callback; 157 | 158 | expect(context._rp_options).to.eql({ 159 | custom: 'test', 160 | simple: true, 161 | resolveWithFullResponse: false, 162 | transform: undefined, 163 | transform2xxOnly: false 164 | }); 165 | 166 | }); 167 | 168 | it('that allows custom values for the Request-Promise options', function () { 169 | 170 | var customTransform = function () {}; 171 | 172 | var context = {}; 173 | pl.init.call(context, { 174 | simple: false, 175 | resolveWithFullResponse: true, 176 | transform: customTransform, 177 | transform2xxOnly: true 178 | }); 179 | 180 | delete context._rp_options.callback; 181 | 182 | expect(context._rp_options).to.eql({ 183 | simple: false, 184 | resolveWithFullResponse: true, 185 | transform: customTransform, 186 | transform2xxOnly: true 187 | }); 188 | 189 | }); 190 | 191 | it('that converts the method to upper case', function () { 192 | 193 | var context = {}; 194 | pl.init.call(context, { 195 | method: 'get' 196 | }); 197 | 198 | expect(context._rp_options.method).to.eql('GET'); 199 | 200 | }); 201 | 202 | it('that applies a default transform for HEAD requests', function () { 203 | 204 | var context = {}; 205 | pl.init.call(context, { 206 | method: 'head' 207 | }); 208 | 209 | expect(context._rp_options.transform).to.eql(pl.defaultTransformations.HEAD); 210 | 211 | }); 212 | 213 | it('that keeps the already existing callback', function () { 214 | 215 | var alreadyExistingCallback = function () {}; 216 | 217 | var context = {}; 218 | pl.init.call(context, { 219 | callback: alreadyExistingCallback 220 | }); 221 | 222 | expect(context._rp_callbackOrig).to.eql(alreadyExistingCallback); 223 | 224 | }); 225 | 226 | }); 227 | 228 | describe('should provide .callback(...)', function () { 229 | 230 | var pl = null; 231 | 232 | before(function () { 233 | 234 | pl = plumbing({ 235 | PromiseImpl: Bluebird 236 | }); 237 | 238 | }); 239 | 240 | it('that rejects if an error is passed', function (done) { 241 | 242 | var context = {}; 243 | pl.init.call(context, {}); 244 | 245 | var passedError = new Error('test error'); 246 | pl.callback.call(context, passedError, 'dummy response'); 247 | 248 | context._rp_promise 249 | .then(function () { 250 | done(new Error('Expected promise to be rejected.')); 251 | }) 252 | .catch(function (err) { 253 | expect(err instanceof errors.RequestError).to.eql(true); 254 | expect(err.name).to.eql('RequestError'); 255 | expect(err.message).to.eql(String(passedError)); 256 | expect(err.cause).to.eql(passedError); 257 | expect(err.error).to.eql(passedError); 258 | expect(err.options).to.eql(context._rp_options); 259 | expect(err.response).to.eql('dummy response'); 260 | done(); 261 | }); 262 | 263 | }); 264 | 265 | it('that resolves a 200 response', function (done) { 266 | 267 | var context = {}; 268 | pl.init.call(context, {}); 269 | 270 | var response = { 271 | statusCode: 200, 272 | body: { 273 | a: 'b' 274 | } 275 | }; 276 | pl.callback.call(context, null, response, response.body); 277 | 278 | context._rp_promise 279 | .then(function (body) { 280 | expect(body).to.eql(response.body); 281 | done(); 282 | }) 283 | .catch(function (err) { 284 | done(err); 285 | }); 286 | 287 | }); 288 | 289 | it('that resolves a 201 response', function (done) { 290 | 291 | var context = {}; 292 | pl.init.call(context, {}); 293 | 294 | var response = { 295 | statusCode: 201, 296 | body: { 297 | a: 'b' 298 | } 299 | }; 300 | pl.callback.call(context, null, response, response.body); 301 | 302 | context._rp_promise 303 | .then(function (body) { 304 | expect(body).to.eql(response.body); 305 | done(); 306 | }) 307 | .catch(function (err) { 308 | done(err); 309 | }); 310 | 311 | }); 312 | 313 | it('that resolves a 2xx response with full response', function (done) { 314 | 315 | var context = {}; 316 | pl.init.call(context, { 317 | resolveWithFullResponse: true 318 | }); 319 | 320 | var response = { 321 | statusCode: 201, 322 | body: { 323 | a: 'b' 324 | } 325 | }; 326 | pl.callback.call(context, null, response, response.body); 327 | 328 | context._rp_promise 329 | .then(function (fullResponse) { 330 | expect(fullResponse).to.eql(response); 331 | done(); 332 | }) 333 | .catch(function (err) { 334 | done(err); 335 | }); 336 | 337 | }); 338 | 339 | it('that rejects a non-2xx response in simple mode', function (done) { 340 | 341 | var context = {}; 342 | pl.init.call(context, {}); 343 | 344 | var response = { 345 | statusCode: 404, 346 | body: { 347 | a: 'b' 348 | } 349 | }; 350 | pl.callback.call(context, null, response, response.body); 351 | 352 | context._rp_promise 353 | .then(function () { 354 | done(new Error('Expected promise to be rejected.')); 355 | }) 356 | .catch(function (err) { 357 | expect(err instanceof errors.StatusCodeError).to.eql(true); 358 | expect(err.name).to.eql('StatusCodeError'); 359 | expect(err.statusCode).to.eql(404); 360 | expect(err.message).to.eql('404 - {"a":"b"}'); 361 | expect(err.error).to.eql(response.body); 362 | expect(err.options).to.eql(context._rp_options); 363 | expect(err.response).to.eql(response); 364 | done(); 365 | }); 366 | 367 | }); 368 | 369 | it('that resolves a non-2xx response in non-simple mode', function (done) { 370 | 371 | var context = {}; 372 | pl.init.call(context, { 373 | simple: false 374 | }); 375 | 376 | var response = { 377 | statusCode: 404, 378 | body: { 379 | a: 'b' 380 | } 381 | }; 382 | pl.callback.call(context, null, response, response.body); 383 | 384 | context._rp_promise 385 | .then(function (body) { 386 | expect(body).to.eql(response.body); 387 | done(); 388 | }) 389 | .catch(function (err) { 390 | done(err); 391 | }); 392 | 393 | }); 394 | 395 | it('that applies the transform function to 2xx responses', function (done) { 396 | 397 | var context = {}; 398 | pl.init.call(context, { 399 | transform: function (body, response, resolveWithFullResponse) { 400 | return JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse; 401 | } 402 | }); 403 | 404 | var res = { 405 | statusCode: 200, 406 | body: { 407 | a: 'b' 408 | } 409 | }; 410 | pl.callback.call(context, null, res, res.body); 411 | 412 | context._rp_promise 413 | .then(function (transformed) { 414 | expect(transformed).to.eql(JSON.stringify(res.body) + ' - ' + JSON.stringify(res) + ' - false'); 415 | done(); 416 | }) 417 | .catch(function (err) { 418 | done(err); 419 | }); 420 | 421 | }); 422 | 423 | it('that applies the transform function to 2xx responses which throws an error', function (done) { 424 | 425 | var cause = new Error('transform failed'); 426 | 427 | var context = {}; 428 | pl.init.call(context, { 429 | transform: function (body, response, resolveWithFullResponse) { 430 | throw cause; 431 | } 432 | }); 433 | 434 | var res = { 435 | statusCode: 200, 436 | body: { 437 | a: 'b' 438 | } 439 | }; 440 | pl.callback.call(context, null, res, res.body); 441 | 442 | context._rp_promise 443 | .then(function () { 444 | done(new Error('Expected promise to be rejected.')); 445 | }) 446 | .catch(function (err) { 447 | expect(err instanceof errors.TransformError).to.eql(true); 448 | expect(err.name).to.eql('TransformError'); 449 | expect(err.message).to.eql(String(cause)); 450 | expect(err.cause).to.eql(cause); 451 | expect(err.error).to.eql(cause); 452 | expect(err.options).to.eql(context._rp_options); 453 | expect(err.response).to.eql(res); 454 | done(); 455 | }); 456 | 457 | }); 458 | 459 | it('that applies the transform function to 2xx responses which returns a promise', function (done) { 460 | 461 | var context = {}; 462 | pl.init.call(context, { 463 | transform: function (body, response, resolveWithFullResponse) { 464 | return Bluebird.resolve(JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse); 465 | } 466 | }); 467 | 468 | var res = { 469 | statusCode: 200, 470 | body: { 471 | a: 'b' 472 | } 473 | }; 474 | pl.callback.call(context, null, res, res.body); 475 | 476 | context._rp_promise 477 | .then(function (transformed) { 478 | expect(transformed).to.eql(JSON.stringify(res.body) + ' - ' + JSON.stringify(res) + ' - false'); 479 | done(); 480 | }) 481 | .catch(function (err) { 482 | done(err); 483 | }); 484 | 485 | }); 486 | 487 | it('that applies the transform function to 2xx responses which returns a rejected promise', function (done) { 488 | 489 | var cause = new Error('transform failed'); 490 | 491 | var context = {}; 492 | pl.init.call(context, { 493 | transform: function (body, response, resolveWithFullResponse) { 494 | return Bluebird.reject(cause); 495 | } 496 | }); 497 | 498 | var res = { 499 | statusCode: 200, 500 | body: { 501 | a: 'b' 502 | } 503 | }; 504 | pl.callback.call(context, null, res, res.body); 505 | 506 | context._rp_promise 507 | .then(function () { 508 | done(new Error('Expected promise to be rejected.')); 509 | }) 510 | .catch(function (err) { 511 | expect(err instanceof errors.TransformError).to.eql(true); 512 | expect(err.name).to.eql('TransformError'); 513 | expect(err.message).to.eql(String(cause)); 514 | expect(err.cause).to.eql(cause); 515 | expect(err.error).to.eql(cause); 516 | expect(err.options).to.eql(context._rp_options); 517 | expect(err.response).to.eql(res); 518 | done(); 519 | }); 520 | 521 | }); 522 | 523 | it('that applies the transform function to 2xx responses for simple = true and transform2xxOnly = true', function (done) { 524 | 525 | var context = {}; 526 | pl.init.call(context, { 527 | transform: function (body, response, resolveWithFullResponse) { 528 | return JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse; 529 | }, 530 | transform2xxOnly: true 531 | }); 532 | 533 | var res = { 534 | statusCode: 200, 535 | body: { 536 | a: 'b' 537 | } 538 | }; 539 | pl.callback.call(context, null, res, res.body); 540 | 541 | context._rp_promise 542 | .then(function (transformed) { 543 | expect(transformed).to.eql(JSON.stringify(res.body) + ' - ' + JSON.stringify(res) + ' - false'); 544 | done(); 545 | }) 546 | .catch(function (err) { 547 | done(err); 548 | }); 549 | 550 | }); 551 | 552 | it('that applies the transform function to 2xx responses for simple = false and transform2xxOnly = true', function (done) { 553 | 554 | var context = {}; 555 | pl.init.call(context, { 556 | simple: false, 557 | transform: function (body, response, resolveWithFullResponse) { 558 | return JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse; 559 | }, 560 | transform2xxOnly: true 561 | }); 562 | 563 | var res = { 564 | statusCode: 200, 565 | body: { 566 | a: 'b' 567 | } 568 | }; 569 | pl.callback.call(context, null, res, res.body); 570 | 571 | context._rp_promise 572 | .then(function (transformed) { 573 | expect(transformed).to.eql(JSON.stringify(res.body) + ' - ' + JSON.stringify(res) + ' - false'); 574 | done(); 575 | }) 576 | .catch(function (err) { 577 | done(err); 578 | }); 579 | 580 | }); 581 | 582 | it('that applies the transform function to non-2xx responses in simple mode', function (done) { 583 | 584 | var context = {}; 585 | pl.init.call(context, { 586 | transform: function (body, response, resolveWithFullResponse) { 587 | return JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse; 588 | } 589 | }); 590 | 591 | var res = { 592 | statusCode: 404, 593 | body: { 594 | a: 'b' 595 | } 596 | }; 597 | pl.callback.call(context, null, res, res.body); 598 | 599 | context._rp_promise 600 | .then(function () { 601 | done(new Error('Expected promise to be rejected.')); 602 | }) 603 | .catch(function (err) { 604 | expect(err instanceof errors.StatusCodeError).to.eql(true); 605 | expect(err.name).to.eql('StatusCodeError'); 606 | expect(err.statusCode).to.eql(404); 607 | expect(err.message).to.eql('404 - {"a":"b"}'); 608 | expect(err.error).to.eql(res.body); 609 | expect(err.options).to.eql(context._rp_options); 610 | expect(err.response).to.eql(JSON.stringify(res.body) + ' - ' + JSON.stringify(res) + ' - false'); 611 | done(); 612 | }); 613 | 614 | }); 615 | 616 | it('that applies the transform function to non-2xx responses in simple mode which throws an error', function (done) { 617 | 618 | var cause = new Error('transform failed'); 619 | 620 | var context = {}; 621 | pl.init.call(context, { 622 | transform: function (body, response, resolveWithFullResponse) { 623 | throw cause; 624 | } 625 | }); 626 | 627 | var res = { 628 | statusCode: 404, 629 | body: { 630 | a: 'b' 631 | } 632 | }; 633 | pl.callback.call(context, null, res, res.body); 634 | 635 | context._rp_promise 636 | .then(function () { 637 | done(new Error('Expected promise to be rejected.')); 638 | }) 639 | .catch(function (err) { 640 | expect(err instanceof errors.TransformError).to.eql(true); 641 | expect(err.name).to.eql('TransformError'); 642 | expect(err.message).to.eql(String(cause)); 643 | expect(err.cause).to.eql(cause); 644 | expect(err.error).to.eql(cause); 645 | expect(err.options).to.eql(context._rp_options); 646 | expect(err.response).to.eql(res); 647 | done(); 648 | }); 649 | 650 | }); 651 | 652 | it('that applies the transform function to non-2xx responses in simple mode which returns a promise', function (done) { 653 | 654 | var context = {}; 655 | pl.init.call(context, { 656 | transform: function (body, response, resolveWithFullResponse) { 657 | return Bluebird.resolve(JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse); 658 | } 659 | }); 660 | 661 | var res = { 662 | statusCode: 404, 663 | body: { 664 | a: 'b' 665 | } 666 | }; 667 | pl.callback.call(context, null, res, res.body); 668 | 669 | context._rp_promise 670 | .then(function () { 671 | done(new Error('Expected promise to be rejected.')); 672 | }) 673 | .catch(function (err) { 674 | expect(err instanceof errors.StatusCodeError).to.eql(true); 675 | expect(err.name).to.eql('StatusCodeError'); 676 | expect(err.statusCode).to.eql(404); 677 | expect(err.message).to.eql('404 - {"a":"b"}'); 678 | expect(err.error).to.eql(res.body); 679 | expect(err.options).to.eql(context._rp_options); 680 | expect(err.response).to.eql(JSON.stringify(res.body) + ' - ' + JSON.stringify(res) + ' - false'); 681 | done(); 682 | }); 683 | 684 | }); 685 | 686 | it('that applies the transform function to non-2xx responses in simple mode which returns a rejected promise', function (done) { 687 | 688 | var cause = new Error('transform failed'); 689 | 690 | var context = {}; 691 | pl.init.call(context, { 692 | transform: function (body, response, resolveWithFullResponse) { 693 | return Bluebird.reject(cause); 694 | } 695 | }); 696 | 697 | var res = { 698 | statusCode: 404, 699 | body: { 700 | a: 'b' 701 | } 702 | }; 703 | pl.callback.call(context, null, res, res.body); 704 | 705 | context._rp_promise 706 | .then(function () { 707 | done(new Error('Expected promise to be rejected.')); 708 | }) 709 | .catch(function (err) { 710 | expect(err instanceof errors.TransformError).to.eql(true); 711 | expect(err.name).to.eql('TransformError'); 712 | expect(err.message).to.eql(String(cause)); 713 | expect(err.cause).to.eql(cause); 714 | expect(err.error).to.eql(cause); 715 | expect(err.options).to.eql(context._rp_options); 716 | expect(err.response).to.eql(res); 717 | done(); 718 | }); 719 | 720 | }); 721 | 722 | it('that applies the transform function to non-2xx responses for simple = false and transform2xxOnly = false', function (done) { 723 | 724 | var context = {}; 725 | pl.init.call(context, { 726 | simple: false, 727 | transform: function (body, response, resolveWithFullResponse) { 728 | return JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse; 729 | } 730 | }); 731 | 732 | var res = { 733 | statusCode: 404, 734 | body: { 735 | a: 'b' 736 | } 737 | }; 738 | pl.callback.call(context, null, res, res.body); 739 | 740 | context._rp_promise 741 | .then(function (transformed) { 742 | expect(transformed).to.eql(JSON.stringify(res.body) + ' - ' + JSON.stringify(res) + ' - false'); 743 | done(); 744 | }) 745 | .catch(function (err) { 746 | done(err); 747 | }); 748 | 749 | }); 750 | 751 | it('that does not apply the transform function to non-2xx responses for simple = true and transform2xxOnly = false', function (done) { 752 | 753 | var context = {}; 754 | pl.init.call(context, { 755 | transform: function (body, response, resolveWithFullResponse) { 756 | return JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse; 757 | }, 758 | transform2xxOnly: true 759 | }); 760 | 761 | var res = { 762 | statusCode: 404, 763 | body: { 764 | a: 'b' 765 | } 766 | }; 767 | pl.callback.call(context, null, res, res.body); 768 | 769 | context._rp_promise 770 | .then(function () { 771 | done(new Error('Expected promise to be rejected.')); 772 | }) 773 | .catch(function (err) { 774 | expect(err instanceof errors.StatusCodeError).to.eql(true); 775 | expect(err.name).to.eql('StatusCodeError'); 776 | expect(err.statusCode).to.eql(404); 777 | expect(err.message).to.eql('404 - {"a":"b"}'); 778 | expect(err.error).to.eql(res.body); 779 | expect(err.options).to.eql(context._rp_options); 780 | expect(err.response).to.eql(res); 781 | done(); 782 | }); 783 | 784 | }); 785 | 786 | it('that does not apply the transform function to non-2xx responses for simple = false and transform2xxOnly = false', function (done) { 787 | 788 | var context = {}; 789 | pl.init.call(context, { 790 | simple: false, 791 | transform: function (body, response, resolveWithFullResponse) { 792 | return JSON.stringify(body) + ' - ' + JSON.stringify(response) + ' - ' + resolveWithFullResponse; 793 | }, 794 | transform2xxOnly: true 795 | }); 796 | 797 | var res = { 798 | statusCode: 404, 799 | body: { 800 | a: 'b' 801 | } 802 | }; 803 | pl.callback.call(context, null, res, res.body); 804 | 805 | context._rp_promise 806 | .then(function (body) { 807 | expect(body).to.eql(res.body); 808 | done(); 809 | }) 810 | .catch(function (err) { 811 | done(err); 812 | }); 813 | 814 | }); 815 | 816 | it('that applies the default HEAD transform', function (done) { 817 | 818 | var context = {}; 819 | pl.init.call(context, { 820 | method: 'HEAD' 821 | }); 822 | 823 | var res = { 824 | statusCode: 200, 825 | headers: { 826 | a: 'b' 827 | } 828 | }; 829 | pl.callback.call(context, null, res, res.body); 830 | 831 | context._rp_promise 832 | .then(function (transformed) { 833 | expect(transformed).to.eql(res.headers); 834 | done(); 835 | }) 836 | .catch(function (err) { 837 | done(err); 838 | }); 839 | 840 | }); 841 | 842 | it('that applies the default HEAD transform doing nothing when resolving full response', function (done) { 843 | 844 | var context = {}; 845 | pl.init.call(context, { 846 | method: 'HEAD', 847 | resolveWithFullResponse: true 848 | }); 849 | 850 | var res = { 851 | statusCode: 200, 852 | headers: { 853 | a: 'b' 854 | } 855 | }; 856 | pl.callback.call(context, null, res, res.body); 857 | 858 | context._rp_promise 859 | .then(function (transformed) { 860 | expect(transformed).to.eql(res); 861 | done(); 862 | }) 863 | .catch(function (err) { 864 | done(err); 865 | }); 866 | 867 | }); 868 | 869 | it('that applies a custom HEAD transform', function (done) { 870 | 871 | var context = {}; 872 | pl.init.call(context, { 873 | method: 'HEAD', 874 | transform: function () { 875 | return 'custom'; 876 | } 877 | }); 878 | 879 | var res = { 880 | statusCode: 200, 881 | headers: { 882 | a: 'b' 883 | } 884 | }; 885 | pl.callback.call(context, null, res, res.body); 886 | 887 | context._rp_promise 888 | .then(function (transformed) { 889 | expect(transformed).to.eql('custom'); 890 | done(); 891 | }) 892 | .catch(function (err) { 893 | done(err); 894 | }); 895 | 896 | }); 897 | 898 | it('that ignores the transform option if it is not a function', function (done) { 899 | 900 | // IMHO input validation should reject this but behavior is kept this way for backwards compatibility. 901 | 902 | var context = {}; 903 | pl.init.call(context, { 904 | method: 'HEAD', 905 | transform: 'not a function', 906 | resolveWithFullResponse: true 907 | }); 908 | 909 | var res = { 910 | statusCode: 200, 911 | headers: { 912 | a: 'b' 913 | } 914 | }; 915 | pl.callback.call(context, null, res, res.body); 916 | 917 | context._rp_promise 918 | .then(function (response) { 919 | expect(response.headers).to.eql(res.headers); 920 | done(); 921 | }) 922 | .catch(function (err) { 923 | done(err); 924 | }); 925 | 926 | }); 927 | 928 | it('that ignores the transform option for non-2xx responses if it is not a function', function (done) { 929 | 930 | // IMHO input validation should reject this but behavior is kept this way for backwards compatibility. 931 | 932 | var context = {}; 933 | pl.init.call(context, { 934 | method: 'HEAD', 935 | transform: 'not a function', 936 | resolveWithFullResponse: true 937 | }); 938 | 939 | var res = { 940 | statusCode: 404, 941 | headers: { 942 | a: 'b' 943 | } 944 | }; 945 | pl.callback.call(context, null, res, res.body); 946 | 947 | context._rp_promise 948 | .then(function () { 949 | done(new Error('Expected promise to be rejected.')); 950 | }) 951 | .catch(function (err) { 952 | expect(err instanceof errors.StatusCodeError).to.eql(true); 953 | expect(err.name).to.eql('StatusCodeError'); 954 | expect(err.statusCode).to.eql(404); 955 | expect(err.message).to.eql('404 - undefined'); 956 | expect(err.error).to.eql(res.body); 957 | expect(err.options).to.eql(context._rp_options); 958 | expect(err.response).to.eql(res); 959 | done(); 960 | }); 961 | 962 | }); 963 | 964 | it('that also calls an already existing callback', function (done) { 965 | 966 | var callbackWasCalled = 0; 967 | var callbackArgs = {}; 968 | 969 | var context = {}; 970 | pl.init.call(context, { 971 | callback: function (err, response, body) { 972 | callbackWasCalled += 1; 973 | callbackArgs.err = err; 974 | callbackArgs.response = response; 975 | callbackArgs.body = body; 976 | } 977 | }); 978 | 979 | var res = { 980 | statusCode: 200, 981 | body: { 982 | a: 'b' 983 | } 984 | }; 985 | pl.callback.call(context, null, res, res.body); 986 | 987 | context._rp_promise 988 | .then(function (body) { 989 | 990 | expect(body).to.eql(res.body); 991 | 992 | expect(callbackWasCalled).to.eql(1); 993 | expect(callbackArgs).to.eql({ 994 | err: null, 995 | response: res, 996 | body: res.body 997 | }); 998 | 999 | done(); 1000 | 1001 | }) 1002 | .catch(function (err) { 1003 | done(err); 1004 | }); 1005 | 1006 | }); 1007 | 1008 | it('that also calls an already existing callback while handlings an error it throws', function (done) { 1009 | 1010 | var error = new Error('thrown by callback'); 1011 | 1012 | var context = {}; 1013 | pl.init.call(context, { 1014 | callback: function () { 1015 | throw error; 1016 | } 1017 | }); 1018 | 1019 | var res = { 1020 | statusCode: 200, 1021 | body: { 1022 | a: 'b' 1023 | } 1024 | }; 1025 | 1026 | expect(function () { 1027 | pl.callback.call(context, null, res, res.body); 1028 | }).to.throw('thrown by callback'); 1029 | 1030 | context._rp_promise 1031 | .then(function (body) { 1032 | expect(body).to.eql(res.body); 1033 | done(); 1034 | }) 1035 | .catch(function (err) { 1036 | done(err); 1037 | }); 1038 | 1039 | }); 1040 | 1041 | }); 1042 | 1043 | describe('should expose a method', function () { 1044 | 1045 | var exposePromiseMethod = plumbing({ PromiseImpl: Bluebird }).exposePromiseMethod; 1046 | 1047 | it('with default binding', function () { 1048 | 1049 | var target = { 1050 | fakePromise: { 1051 | attr: 5, 1052 | then: function () { 1053 | return this.attr; 1054 | } 1055 | } 1056 | }; 1057 | 1058 | exposePromiseMethod(target, null, 'fakePromise', 'then'); 1059 | 1060 | expect(target.then()).to.eql(5); 1061 | 1062 | }); 1063 | 1064 | it('with custom binding', function () { 1065 | 1066 | var target = {}; 1067 | var source = { 1068 | fakePromise: { 1069 | attr: 5, 1070 | then: function () { 1071 | return this.attr; 1072 | } 1073 | } 1074 | }; 1075 | 1076 | exposePromiseMethod(target, source, 'fakePromise', 'then'); 1077 | 1078 | expect(target.then()).to.eql(5); 1079 | 1080 | }); 1081 | 1082 | it('with a different name', function () { 1083 | 1084 | var target = { 1085 | fakePromise: { 1086 | attr: 5, 1087 | then: function () { 1088 | return this.attr; 1089 | } 1090 | } 1091 | }; 1092 | 1093 | exposePromiseMethod(target, null, 'fakePromise', 'then', 'next'); 1094 | 1095 | expect(target.next()).to.eql(5); 1096 | 1097 | }); 1098 | 1099 | it('and not overwrite existing method', function () { 1100 | 1101 | var target = { 1102 | fakePromise: { 1103 | attr: 5, 1104 | then: function () { 1105 | return this.attr; 1106 | } 1107 | } 1108 | }; 1109 | 1110 | expect(function () { 1111 | exposePromiseMethod(target, null, 'fakePromise', 'then', 'fakePromise'); 1112 | }).to.throw('Unable to expose method "fakePromise"'); 1113 | 1114 | }); 1115 | 1116 | it('and forward arguments when called', function () { 1117 | 1118 | var target = { 1119 | fakePromise: { 1120 | attr: 5, 1121 | then: function (a, b) { 1122 | return this.attr + a + b; 1123 | } 1124 | } 1125 | }; 1126 | 1127 | exposePromiseMethod(target, null, 'fakePromise', 'then'); 1128 | 1129 | expect(target.then(7, 11)).to.eql(5+7+11); 1130 | 1131 | }); 1132 | 1133 | }); 1134 | 1135 | describe('should expose the promise', function () { 1136 | 1137 | var exposePromise = plumbing({ PromiseImpl: Bluebird }).exposePromise; 1138 | 1139 | it('with default binding', function () { 1140 | 1141 | var target = { 1142 | fakePromise: { 1143 | attr: 5, 1144 | then: function () { 1145 | return this.attr; 1146 | } 1147 | } 1148 | }; 1149 | 1150 | exposePromise(target, null, 'fakePromise'); 1151 | 1152 | expect(target.promise().then()).to.eql(5); 1153 | 1154 | }); 1155 | 1156 | it('with custom binding', function () { 1157 | 1158 | var target = {}; 1159 | var source = { 1160 | fakePromise: { 1161 | attr: 5, 1162 | then: function () { 1163 | return this.attr; 1164 | } 1165 | } 1166 | }; 1167 | 1168 | exposePromise(target, source, 'fakePromise'); 1169 | 1170 | expect(target.promise().then()).to.eql(5); 1171 | 1172 | }); 1173 | 1174 | it('with a different name', function () { 1175 | 1176 | var target = { 1177 | fakePromise: { 1178 | attr: 5, 1179 | then: function () { 1180 | return this.attr; 1181 | } 1182 | } 1183 | }; 1184 | 1185 | exposePromise(target, null, 'fakePromise', 'promise2'); 1186 | 1187 | expect(target.promise2().then()).to.eql(5); 1188 | 1189 | }); 1190 | 1191 | it('and not overwrite existing method', function () { 1192 | 1193 | var target = { 1194 | fakePromise: { 1195 | attr: 5, 1196 | then: function () { 1197 | return this.attr; 1198 | } 1199 | } 1200 | }; 1201 | 1202 | expect(function () { 1203 | exposePromise(target, null, 'fakePromise', 'fakePromise'); 1204 | }).to.throw('Unable to expose method "fakePromise"'); 1205 | 1206 | }); 1207 | 1208 | }); 1209 | 1210 | }); 1211 | --------------------------------------------------------------------------------