koa-sham
Version:
To send fake request to a Koa application without starting a http server. So that, you can require a Koa app into your code.
163 lines (133 loc) • 4.41 kB
JavaScript
const http = require( 'http' );
const url = require( 'url' );
const querystring = require( 'querystring' );
const Stream = require( 'stream' );
const preuse = require( 'koa-preuse' );
const is = require( '@lvchengbin/is' );
const HAS_USED_SHAM = Symbol( 'has#used#sham' );
const jsonTypes = [
'application/json',
'application/json-patch+json',
'application/vnd.api+json',
'application/csp-report'
];
const formTypes = [
'application/x-www-form-urlencoded'
];
module.exports = ( app, uri, options = {}, callback ) => {
if( is.function( uri ) ) {
callback = uri;
options = {};
}
if( is.function( options ) ) {
callback = options;
options = {};
}
if( !is.string( uri ) ) {
options = uri || {};
uri = options.uri || '/';
}
const socket = new Stream.Duplex();
socket.remoteAddress = options.remoteAddress || '127.0.0.1';
socket._write = () => {};
const req = new http.IncomingMessage( socket );
req.httpVersion = 'sham';
const request = Object.create( app.request );
request.req = req;
const host = options.host || '127.0.0.1';
const port = options.port || 80;
const protocol = options.https ? 'https' : 'http';
if( uri.charAt( 0 ) === '/' ) {
uri = url.resolve( `${protocol}://${host}:${port}`, uri );
} else if( uri.indexOf( '://' ) === -1 ) {
uri = `${protocol}://${uri}`;
}
req.socket.encrypted = !uri.indexOf( 'https:' );
request.url = uri;
request.method = options.method ? options.method.toUpperCase() : 'GET';
if( options.qs ) {
if( request.query ) {
request.query = Object.assign( request.query, options.qs );
} else {
request.query = options.qs;
}
}
const headers = {};
if( options.headers ) {
for( const name of Object.keys( options.headers ) ) {
headers[ name.toLowerCase() ] = options.headers[ name ];
}
}
if( options.cookies ) {
const cookies = [];
for( const name of Object.keys( options.cookies ) ) {
cookies.push( name + '=' + encodeURIComponent( options.cookies[ name ] ) );
}
if( headers.cookie ) {
headers.cookie = headers.cookie.replace( /;\s*$/, '' ) + cookies.join( '; ' );
} else {
headers.cookie = cookies.join( '; ' );
}
}
// to set Host in headers
if( !headers.host ) {
headers.host = url.parse( request.url ).host;
}
request.headers = headers;
if( [ 'POST', 'PUT' ].indexOf( req.method ) > -1 ) {
if( !headers[ 'content-type' ] ) {
headers[ 'content-type' ] = 'application/json';
}
headers[ 'transfer-encoding' ] = 'chunked';
delete headers[ 'content-length' ];
if( request.is( jsonTypes ) ) {
options.body && req.push( JSON.stringify( options.body ) );
} else if( request.is( formTypes ) ) {
options.body && req.push( querystring.stringify( options.body ) );
}
req.push( null );
}
const res = new http.ServerResponse( req );
const response = Object.create( app.response );
response.res = res;
res.assignSocket( socket );
if( !app[ HAS_USED_SHAM ] ) {
app[ HAS_USED_SHAM ] = true;
preuse( app, ( ctx, next ) => {
ctx.res.ctx = ctx;
return next();
} );
}
if( options.promise ) {
try {
return app.callback()( req, res ).then( () => {
if( options.resolveWithFullResponse ) return res;
if( ( '' + res.statusCode ).charAt( 0 ) === '2' ) {
return res.ctx.body;
}
throw res;
} );
} catch( e ) {
return Promise.reject( e );
}
}
const readable = new Stream.Readable();
readable._read = () => {};
socket._write = chunk => {
readable.push( chunk );
};
if( is.function( callback ) ) {
try {
app.callback()( req, res ).then( () => {
callback( null, res, res.ctx.body );
} ).catch( e => {
callback( e );
} );
} catch( e ) {
callback( e );
}
} else {
app.callback()( req, res );
}
return readable;
};