Add node dapi HTTP implementation
* src/dapi/http/HttpError.js: Add error subtype. * src/dapi/http/NodeHttpImpl.js: Add node-based HTTP impl. * test/dapi/http/HttpErrorTest.js: Add test. * test/dapi/http/NodeHttpImplTest.js: Add test.master
parent
3be28a7858
commit
a3e359a050
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* Error representing non-200 HTTP status code
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework.
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Class } = require( 'easejs' );
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents error in performing HTTP request
|
||||||
|
*/
|
||||||
|
module.exports = Class( 'HttpError' )
|
||||||
|
.extend( Error,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* HTTP status code
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
'public statuscode': 500,
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set error message and HTTP status code
|
||||||
|
*
|
||||||
|
* The HTTP status code defaults to 500 if not set. No check is
|
||||||
|
* performed to determine whether the given status code is a valid error
|
||||||
|
* code.
|
||||||
|
*
|
||||||
|
* The mesage is _not_ automatically set from the status code.
|
||||||
|
*
|
||||||
|
* @param {string} message error message
|
||||||
|
* @param {number=} statuscode HTTP status code
|
||||||
|
*/
|
||||||
|
__construct( message, statuscode )
|
||||||
|
{
|
||||||
|
this.statuscode = statuscode || 500;
|
||||||
|
},
|
||||||
|
} );
|
|
@ -0,0 +1,217 @@
|
||||||
|
/**
|
||||||
|
* HTTP over Node.js-compatible API
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework.
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Class } = require( 'easejs' );
|
||||||
|
const HttpImpl = require( './HttpImpl' );
|
||||||
|
const HttpError = require( './HttpError' );
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP adapter using Node.js-compatible objects (e.g. its `http` modules)
|
||||||
|
*/
|
||||||
|
module.exports = Class( 'NodeHttpImpl' )
|
||||||
|
.implement( HttpImpl )
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Clients for desired protocols (e.g. HTTP(s))
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _protoHandlers': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL parser
|
||||||
|
* @type {url}
|
||||||
|
*/
|
||||||
|
'private _urlParser': '',
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize with protocol handlers and URL parser
|
||||||
|
*
|
||||||
|
* `proto_handlers` must be a key-value mapping of the protocol string
|
||||||
|
* to a handler object conforming to Node's http(s) APIs---that is, it
|
||||||
|
* should provide a `#request` method.
|
||||||
|
*
|
||||||
|
* @param {Object} proto_handlers protocol handler key-value map
|
||||||
|
* @param {Object} url_parser URL parser
|
||||||
|
*/
|
||||||
|
constructor( proto_handlers, url_parser )
|
||||||
|
{
|
||||||
|
this._protoHandlers = proto_handlers;
|
||||||
|
this._urlParser = url_parser;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform HTTP request
|
||||||
|
*
|
||||||
|
* If the request is synchronous, it must still return the data via the
|
||||||
|
* provided callback. The provided data is expected to be key-value if an
|
||||||
|
* object is given, otherwise a string of binary data.
|
||||||
|
*
|
||||||
|
* @param {string} url destination URL
|
||||||
|
* @param {string} method RFC-2616-compliant HTTP method
|
||||||
|
* @param {Object|string} data request params
|
||||||
|
* @param {function(?Error, ?string)} callback server response callback
|
||||||
|
*
|
||||||
|
* @return {HttpImpl} self
|
||||||
|
*/
|
||||||
|
'public requestData'( url, method, data, callback )
|
||||||
|
{
|
||||||
|
const options = this._urlParser.parse( url );
|
||||||
|
const protocol = options.protocol.replace( /:$/, '' );
|
||||||
|
const handler = this._protoHandlers[ protocol ];
|
||||||
|
|
||||||
|
if ( !handler )
|
||||||
|
{
|
||||||
|
throw Error( `No handler for ${protocol}` );
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setOptions( options, method, data );
|
||||||
|
|
||||||
|
let forbid_end = false;
|
||||||
|
|
||||||
|
const req = handler.request( options, res =>
|
||||||
|
{
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
res.on( 'data', chunk => data += chunk );
|
||||||
|
res.on( 'end', () =>
|
||||||
|
!forbid_end && this.requestEnd( res, data, callback )
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
req.on( 'error', e =>
|
||||||
|
{
|
||||||
|
this.serveError( e, null, null, callback );
|
||||||
|
|
||||||
|
// guarantee that the callback will not be invoked a second time
|
||||||
|
// if something tries to end the request
|
||||||
|
forbid_end = true;
|
||||||
|
} );
|
||||||
|
|
||||||
|
if ( method === 'POST' )
|
||||||
|
{
|
||||||
|
req.write( data );
|
||||||
|
}
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set request options
|
||||||
|
*
|
||||||
|
* @param {Object} options request options
|
||||||
|
* @param {string} method HTTP method
|
||||||
|
* @param {string} data request data
|
||||||
|
*
|
||||||
|
* @return {Object} request headers
|
||||||
|
*/
|
||||||
|
'private _setOptions'( options, method, data )
|
||||||
|
{
|
||||||
|
const { headers = {} } = options;
|
||||||
|
|
||||||
|
options.method = method;
|
||||||
|
|
||||||
|
if ( method === 'POST' )
|
||||||
|
{
|
||||||
|
headers[ 'Content-Type' ] = 'application/x-www-form-urlencoded';
|
||||||
|
|
||||||
|
options.headers = headers;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( data )
|
||||||
|
{
|
||||||
|
options.path += '?' + data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when a request is completed
|
||||||
|
*
|
||||||
|
* Subtypes may override this method to handle their own request
|
||||||
|
* processing before the continuation `callback` is invoked with the
|
||||||
|
* final data.
|
||||||
|
*
|
||||||
|
* To override only error situations, see `#serveError`.
|
||||||
|
*
|
||||||
|
* @param {Object} res Node http.ServerResponse
|
||||||
|
* @param {string} data raw response data
|
||||||
|
* @param {function(?Error,?string)} callback completion continuation
|
||||||
|
*
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
'virtual protected requestEnd'( res, data, callback )
|
||||||
|
{
|
||||||
|
if ( !this.isSuccessful( res ) )
|
||||||
|
{
|
||||||
|
this.serveError(
|
||||||
|
HttpError( res.statusMessage, res.statusCode ),
|
||||||
|
res,
|
||||||
|
data,
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback( null, data );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predicate to determine whether HTTP request was successful
|
||||||
|
*
|
||||||
|
* Non-2xx status codes represent failures.
|
||||||
|
*
|
||||||
|
* @param {Object} res Node http.ServerResponse
|
||||||
|
*
|
||||||
|
* @return {boolean} whether HTTP status code represents a success
|
||||||
|
*/
|
||||||
|
'virtual protected isSuccessful'( res )
|
||||||
|
{
|
||||||
|
return ( +res.statusCode >= 200 ) && ( +res.statusCode < 300 );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke continuation `callback` with an error `e`
|
||||||
|
*
|
||||||
|
* @param {Error} e error
|
||||||
|
* @param {Object} res Node http.ServerResponse
|
||||||
|
* @param {string} data raw response data
|
||||||
|
* @param {function(?Error,?data)} callback continuation
|
||||||
|
*
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
'virtual protected serveError'( e, res, data, callback )
|
||||||
|
{
|
||||||
|
callback( e, data );
|
||||||
|
},
|
||||||
|
} );
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Tests error representing non-200 HTTP status code
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework.
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { expect } = require( 'chai' );
|
||||||
|
const Sut = require( '../../../' ).dapi.http.HttpError;
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
describe( "HttpError", () =>
|
||||||
|
{
|
||||||
|
it( "provides HTTP status code", () =>
|
||||||
|
{
|
||||||
|
const code = 418;
|
||||||
|
|
||||||
|
expect( Sut( 'message', code ).statuscode )
|
||||||
|
.to.equal( code );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
// just make sure overriding ctor calls parent
|
||||||
|
it( "sets message", () =>
|
||||||
|
{
|
||||||
|
const message = 'foobar';
|
||||||
|
|
||||||
|
expect( Sut( message ).message )
|
||||||
|
.to.equal( message );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,413 @@
|
||||||
|
/**
|
||||||
|
* Test HTTP using Node.js-compatible API
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework.
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { expect } = require( 'chai' );
|
||||||
|
const { Class } = require( 'easejs' );
|
||||||
|
|
||||||
|
const {
|
||||||
|
HttpImpl,
|
||||||
|
NodeHttpImpl: Sut,
|
||||||
|
HttpError,
|
||||||
|
} = require( '../../../' ).dapi.http;
|
||||||
|
|
||||||
|
|
||||||
|
describe( "NodeHttpImpl", () =>
|
||||||
|
{
|
||||||
|
it( 'is an HttpImpl', function()
|
||||||
|
{
|
||||||
|
var sut = Sut( function() {} );
|
||||||
|
expect( Class.isA( HttpImpl, sut ) ).to.be.ok;
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: "uses http for plain HTTP requests",
|
||||||
|
protocol: 'http:',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "uses http for plain HTTP requests",
|
||||||
|
protocol: 'https:',
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
].forEach( ( { label, protocol, method } ) =>
|
||||||
|
{
|
||||||
|
it( label, done =>
|
||||||
|
{
|
||||||
|
const url_result = {
|
||||||
|
protocol: protocol,
|
||||||
|
hostname: 'host',
|
||||||
|
port: 8888,
|
||||||
|
path: 'foo',
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = _createMockUrl( given_url => url_result );
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
const callback_expected = {};
|
||||||
|
const callback = () => callback_expected;
|
||||||
|
|
||||||
|
const check = proto => ( opts, given_callback ) =>
|
||||||
|
{
|
||||||
|
expect( opts.protocol ).to.equal( proto );
|
||||||
|
expect( opts.hostname ).to.equal( url_result.hostname );
|
||||||
|
expect( opts.port ).to.equal( url_result.port );
|
||||||
|
expect( opts.path ).to.equal( url_result.path );
|
||||||
|
expect( opts.method ).to.equal( method );
|
||||||
|
|
||||||
|
given_callback( _createMockResp() );
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
const http = _createMockHttp( check( 'http:' ) );
|
||||||
|
const https = _createMockHttp( check( 'https:' ) );
|
||||||
|
|
||||||
|
Sut( { http: http, https: https }, url )
|
||||||
|
.requestData( '', method, data, callback );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "returns response when no error", done =>
|
||||||
|
{
|
||||||
|
const res = _createMockResp();
|
||||||
|
const chunks = [ 'a', 'b', 'c', 'd' ];
|
||||||
|
|
||||||
|
const http = _createMockHttp( ( _, callback ) =>
|
||||||
|
{
|
||||||
|
callback( res );
|
||||||
|
|
||||||
|
chunks.forEach( chunk => res.trigger( 'data', chunk ) );
|
||||||
|
res.trigger( 'end' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
Sut( { http: http }, _createMockUrl() )
|
||||||
|
.requestData( "", 'GET', '', ( e, data ) =>
|
||||||
|
{
|
||||||
|
expect( e ).to.equal( null );
|
||||||
|
expect( data ).to.equal( chunks.join( '' ) );
|
||||||
|
|
||||||
|
done();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "adds data to query string on GET", done =>
|
||||||
|
{
|
||||||
|
const given_path = '/path';
|
||||||
|
const expected_query = 'write data';
|
||||||
|
|
||||||
|
const res = _createMockResp();
|
||||||
|
const url = _createMockUrl( given_url => ( {
|
||||||
|
protocol: 'http:',
|
||||||
|
path: given_path,
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
const http = _createMockHttp( ( options, callback ) =>
|
||||||
|
{
|
||||||
|
expect( options.path )
|
||||||
|
.to.equal( given_path + '?' + expected_query );
|
||||||
|
|
||||||
|
callback( res );
|
||||||
|
res.trigger( 'end' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
Sut( { http: http }, url )
|
||||||
|
.requestData( "", 'GET', expected_query, done );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "writes form data on POST", done =>
|
||||||
|
{
|
||||||
|
const expected_data = 'expected';
|
||||||
|
const expected_write = 'write data';
|
||||||
|
|
||||||
|
const res = _createMockResp();
|
||||||
|
|
||||||
|
const http = _createMockHttp( ( options, callback ) =>
|
||||||
|
{
|
||||||
|
expect( http.req.written ).to.equal( expected_write );
|
||||||
|
|
||||||
|
expect( options.headers[ 'Content-Type' ] )
|
||||||
|
.to.equal( 'application/x-www-form-urlencoded' );
|
||||||
|
|
||||||
|
callback( res );
|
||||||
|
|
||||||
|
// make sure we're still handling the response as well
|
||||||
|
res.trigger( 'data', expected_data );
|
||||||
|
res.trigger( 'end' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
Sut( { http: http }, _createMockUrl() )
|
||||||
|
.requestData( "", 'POST', expected_write, ( e, data ) =>
|
||||||
|
{
|
||||||
|
expect( e ).to.equal( null );
|
||||||
|
expect( data ).to.equal( expected_data );
|
||||||
|
|
||||||
|
done();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "returns error and response given non-200 status code", done =>
|
||||||
|
{
|
||||||
|
const res = _createMockResp();
|
||||||
|
const http = _createMockHttp( ( _, callback ) =>
|
||||||
|
{
|
||||||
|
callback( res )
|
||||||
|
|
||||||
|
res.statusCode = 418;
|
||||||
|
res.statusMessage = "I'm a teapot";
|
||||||
|
|
||||||
|
res.trigger( 'end' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
Sut( { http: http }, _createMockUrl() )
|
||||||
|
.requestData( "", 'GET', '', ( e, data ) =>
|
||||||
|
{
|
||||||
|
expect( e ).to.be.instanceOf( HttpError );
|
||||||
|
expect( e.message ).to.equal( res.statusMessage );
|
||||||
|
expect( e.statuscode ).to.equal( res.statusCode );
|
||||||
|
|
||||||
|
done();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
describe( "given a request error", () =>
|
||||||
|
{
|
||||||
|
it( "returns error with no response on request error", done =>
|
||||||
|
{
|
||||||
|
const error = Error( 'test error' );
|
||||||
|
const http = _createMockHttp( () => {} );
|
||||||
|
|
||||||
|
Sut( { http: http }, _createMockUrl() )
|
||||||
|
.requestData( "", 'GET', '', ( e, data ) =>
|
||||||
|
{
|
||||||
|
expect( data ).to.equal( null );
|
||||||
|
expect( e ).to.equal( error );
|
||||||
|
|
||||||
|
done();
|
||||||
|
} );
|
||||||
|
|
||||||
|
// request will be hanging at this point since we didn't call
|
||||||
|
// the callback, so we can fail the request
|
||||||
|
http.req.trigger( 'error', error );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// this should never happen in practice, but we want to defend
|
||||||
|
// against it to make sure the callback is not invoked twice
|
||||||
|
it( "will not complete request on end", () =>
|
||||||
|
{
|
||||||
|
let res = _createMockResp();
|
||||||
|
|
||||||
|
const http = _createMockHttp( ( _, callback ) =>
|
||||||
|
{
|
||||||
|
// allow hooking `end'
|
||||||
|
callback( res );
|
||||||
|
} );
|
||||||
|
|
||||||
|
Sut( { http: http }, _createMockUrl() )
|
||||||
|
.requestData( "", 'GET', '', ( e, data ) =>
|
||||||
|
{
|
||||||
|
// will fail on successful callback
|
||||||
|
expect( data ).to.equal( null );
|
||||||
|
} );
|
||||||
|
|
||||||
|
http.req.trigger( 'error', Error() );
|
||||||
|
|
||||||
|
// do not invoke a second time (should do nothing)
|
||||||
|
res.trigger( 'end' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
describe( "protected API", () =>
|
||||||
|
{
|
||||||
|
it( "allows overriding request end behavior", done =>
|
||||||
|
{
|
||||||
|
const expected_data = "expected";
|
||||||
|
const e = Error( "test e" );
|
||||||
|
const value = "resp data";
|
||||||
|
const res = _createMockResp();
|
||||||
|
|
||||||
|
const http = _createMockHttp( ( _, callback ) =>
|
||||||
|
{
|
||||||
|
callback( res );
|
||||||
|
|
||||||
|
res.trigger( 'data', expected_data );
|
||||||
|
res.trigger( 'end' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
Sut.extend(
|
||||||
|
{
|
||||||
|
'override requestEnd'( given_res, data, callback )
|
||||||
|
{
|
||||||
|
expect( given_res ).to.equal( res );
|
||||||
|
expect( data ).to.equal( expected_data );
|
||||||
|
|
||||||
|
callback( e, value );
|
||||||
|
},
|
||||||
|
} )( { http: http }, _createMockUrl() )
|
||||||
|
.requestData( "", 'GET', '', ( given_e, given_data ) =>
|
||||||
|
{
|
||||||
|
expect( given_e ).to.equal( e );
|
||||||
|
expect( given_data ).to.equal( value );
|
||||||
|
|
||||||
|
done();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "allows overriding concept of success", done =>
|
||||||
|
{
|
||||||
|
const res = _createMockResp();
|
||||||
|
|
||||||
|
const http = _createMockHttp( ( _, callback ) =>
|
||||||
|
{
|
||||||
|
callback( res );
|
||||||
|
res.trigger( 'end' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// would normally be a failure
|
||||||
|
res.statusCode = 500;
|
||||||
|
|
||||||
|
Sut.extend(
|
||||||
|
{
|
||||||
|
'override isSuccessful': ( given_res ) => true,
|
||||||
|
} )( { http: http }, _createMockUrl() )
|
||||||
|
.requestData( "", 'GET', '', ( e ) =>
|
||||||
|
{
|
||||||
|
expect( e ).to.equal( null );
|
||||||
|
done();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( "allows overriding error handling", done =>
|
||||||
|
{
|
||||||
|
const expected_e = Error( 'expected' );
|
||||||
|
const error = {};
|
||||||
|
const value = 'error data';
|
||||||
|
|
||||||
|
const http = _createMockHttp( ( _, callback ) =>
|
||||||
|
{
|
||||||
|
callback( _createMockResp() );
|
||||||
|
} );
|
||||||
|
|
||||||
|
Sut.extend(
|
||||||
|
{
|
||||||
|
'override serveError'(
|
||||||
|
given_e, given_res, given_data, callback
|
||||||
|
)
|
||||||
|
{
|
||||||
|
expect( given_e ).to.equal( expected_e );
|
||||||
|
expect( given_res ).to.equal( null );
|
||||||
|
expect( given_data ).to.equal( null );
|
||||||
|
|
||||||
|
error.e = given_e;
|
||||||
|
callback( error, value );
|
||||||
|
},
|
||||||
|
} )( { http: http }, _createMockUrl() )
|
||||||
|
.requestData( "", 'GET', '', ( e, given_value ) =>
|
||||||
|
{
|
||||||
|
expect( e ).to.equal( error );
|
||||||
|
expect( e.e ).to.equal( expected_e );
|
||||||
|
expect( given_value ).to.equal( value );
|
||||||
|
|
||||||
|
done();
|
||||||
|
} );
|
||||||
|
|
||||||
|
// we're still hanging the request since we haven't called the
|
||||||
|
// callback in http
|
||||||
|
http.req.trigger( 'error', expected_e );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
const _createMockHttp = req_callback =>
|
||||||
|
{
|
||||||
|
const events = {};
|
||||||
|
|
||||||
|
return Object.create( {
|
||||||
|
req: Object.create( {
|
||||||
|
written: '',
|
||||||
|
|
||||||
|
on( event, hook )
|
||||||
|
{
|
||||||
|
events[ event ] = hook;
|
||||||
|
},
|
||||||
|
|
||||||
|
trigger( event, data )
|
||||||
|
{
|
||||||
|
events[ event ]( data );
|
||||||
|
},
|
||||||
|
|
||||||
|
end()
|
||||||
|
{
|
||||||
|
// thunk defined by #request below
|
||||||
|
events.onend();
|
||||||
|
},
|
||||||
|
|
||||||
|
write( data )
|
||||||
|
{
|
||||||
|
this.written = data;
|
||||||
|
},
|
||||||
|
} ),
|
||||||
|
|
||||||
|
request( options, callback )
|
||||||
|
{
|
||||||
|
// not a real event; just for convenience
|
||||||
|
events.onend = () => req_callback( options, callback );
|
||||||
|
|
||||||
|
return this.req;
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const _createMockUrl = callback => ( {
|
||||||
|
parse: callback || ( () => ( { protocol: 'http:' } ) ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
const _createMockResp = () => Object.create( {
|
||||||
|
event: {
|
||||||
|
data() {},
|
||||||
|
end() {},
|
||||||
|
},
|
||||||
|
|
||||||
|
statusCode: 200,
|
||||||
|
|
||||||
|
on( ev, hook )
|
||||||
|
{
|
||||||
|
this.event[ ev ] = hook;
|
||||||
|
},
|
||||||
|
|
||||||
|
trigger( ev, data )
|
||||||
|
{
|
||||||
|
this.event[ ev ]( data );
|
||||||
|
}
|
||||||
|
} );
|
Loading…
Reference in New Issue