import * as thrift from '@creditkarma/thrift-server-core' ; import * as Hapi from '@hapi/hapi' ; import * as http from 'http' ; import * as request from 'request' ; import { CoreOptions } from 'request' ; import { HttpConnection , IRequest , IRequestResponse , IThriftRequest , NextFunction , } from '@creditkarma/thrift-client' ; import { ThriftClientTTwitterFilter , TTwitter , } from '@creditkarma/thrift-client-ttwitter-filter' ; import { HAPI_CALC_SERVER_CONFIG } from '../../config' ; import { expect } from '@hapi/code' ; import * as Lab from '@hapi/lab' ; import { createServer as addService } from '../../hapi-add-service' ; import { createServer as calculatorService } from '../../hapi-calculator-service' ; import { createServer as mockCollector , IMockCollector , } from '../tracing/mock-collector' ; import { Calculator , ICommonStruct } from '../../generated/calculator-service' ; import { ISharedUnion } from '../../generated/shared' ; export const lab = Lab . script ( ) ; const describe = lab . describe ; const it = lab . it ; const before = lab . before ; const after = lab . after ; describe ( 'HttpConnection' , ( ) => { let calcServer ; let addServer ; before ( async ( ) => { calcServer = await calculatorService ( ) ; addServer = await addService ( ) ; return Promise . all ( [ calcServer . start ( ) , addServer . start ( ) ] ) . then ( ( err ) => { console . log ( 'Thrift server running' ) ; } ) ; } ) ; after ( async ( ) => { return Promise . all ( [ calcServer . stop ( ) , addServer . stop ( ) ] ) . then ( ( err ) => { console . log ( 'Thrift server stopped' ) ; } ) ; } ) ; describe ( 'Basic Usage' , ( ) => { let connection ; let client ; before ( async ( ) => { connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; client = new Calculator . Client ( connection ) ; } ) ; it ( 'should corrently handle a service client request' , async ( ) => { return client . add ( 5 , 7 ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } , ( err ) => { console . log ( 'err: ' , err ) ; throw err ; } ) ; } ) ; it ( 'should corrently handle a void service client request' , async ( ) => { return client . ping ( ) . then ( ( response ) => { expect ( response ) . to . equal ( undefined ) ; } ) ; } ) ; it ( 'should corrently handle a service client request that returns a struct' , async ( ) => { return client . getStruct ( 5 ) . then ( ( response ) => { expect ( response ) . to . equal ( { code : { status : new thrift . Int64 ( 0 ) } , value : 'test' , } ) ; } ) ; } ) ; it ( 'should corrently handle a service client request that returns a union' , async ( ) => { return client . getUnion ( 1 ) . then ( ( response ) => { expect ( response ) . to . equal ( { option1 : 'foo' } ) ; } ) ; } ) ; it ( 'should allow passing of a request context' , async ( ) => { return client . addWithContext ( 5 , 7 , { headers : { 'x-fake-token' : 'fake-token' } , } ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } ) ; } ) ; it ( 'should reject auth request without context' , async ( ) => { return client . addWithContext ( 5 , 7 ) . then ( ( response ) => { expect ( false ) . to . equal ( true ) ; } , ( err ) => { expect ( err . message ) . to . equal ( 'Unauthorized' ) ; } ) ; } ) ; it ( 'should reject for a 500 server response' , async ( ) => { const badConnection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : '/return500' , } ) ; const badClient = new Calculator . Client ( badConnection ) ; return badClient . add ( 5 , 7 ) . then ( ( response ) => { throw new Error ( 'Should reject with status 500' ) ; } , ( err ) => { expect ( err . statusCode ) . to . equal ( 500 ) ; } ) ; } ) ; it ( 'should reject for a 400 server response' , async ( ) => { const badConnection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : '/return400' , } ) ; const badClient = new Calculator . Client ( badConnection ) ; return badClient . add ( 5 , 7 ) . then ( ( response ) => { throw new Error ( 'Should reject with status 400' ) ; } , ( err ) => { expect ( err . statusCode ) . to . equal ( 400 ) ; } ) ; } ) ; it ( 'should reject for a request to a missing service' , async ( ) => { const badConnection = new HttpConnection ( { serviceName : 'Calculator' , hostName : 'fakehost' , port : 8080 , requestOptions : { timeout : 5000 , } , } ) ; const badClient = new Calculator . Client ( badConnection ) ; return badClient . add ( 5 , 7 ) . then ( ( response ) => { throw new Error ( 'Should reject with host not found' ) ; } , ( err ) => { expect ( err ) . to . exist ( ) ; } ) ; } ) ; it ( 'should use request implementation if provided' , async ( ) => { let ourRequestWasCalled = false ; const mockRequest = request . defaults ( { auth : { bearer : ( ) => { ourRequestWasCalled = true ; return 'token' ; } , } , } ) ; const mockedConnection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , requestImpl : mockRequest , } ) ; const mockedClient = new Calculator . Client ( mockedConnection ) ; return mockedClient . add ( 5 , 7 ) . then ( ( ) => expect ( ourRequestWasCalled ) . to . be . true ( ) ) ; } ) ; } ) ; describe ( 'With endpoint per method' , ( ) => { let connection ; let client ; before ( async ( ) => { connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , withEndpointPerMethod : true , } ) ; client = new Calculator . Client ( connection ) ; } ) ; it ( 'should corrently handle a service client request' , async ( ) => { return client . add ( 5 , 7 ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } , ( err ) => { console . log ( 'err: ' , err ) ; throw err ; } ) ; } ) ; it ( 'should corrently handle a void service client request' , async ( ) => { return client . ping ( ) . then ( ( response ) => { expect ( response ) . to . equal ( undefined ) ; } ) ; } ) ; it ( 'should corrently handle a service client request that returns a struct' , async ( ) => { return client . getStruct ( 5 ) . then ( ( response ) => { expect ( response ) . to . equal ( { code : { status : new thrift . Int64 ( 0 ) } , value : 'test' , } ) ; } ) ; } ) ; it ( 'should corrently handle a service client request that returns a union' , async ( ) => { return client . getUnion ( 1 ) . then ( ( response ) => { expect ( response ) . to . equal ( { option1 : 'foo' } ) ; } ) ; } ) ; it ( 'should allow passing of a request context' , async ( ) => { return client . addWithContext ( 5 , 7 , { headers : { 'x-fake-token' : 'fake-token' } , } ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } ) ; } ) ; it ( 'should reject auth request without context' , async ( ) => { return client . addWithContext ( 5 , 7 ) . then ( ( response ) => { expect ( false ) . to . equal ( true ) ; } , ( err ) => { expect ( err . message ) . to . equal ( 'Unauthorized' ) ; } ) ; } ) ; it ( 'should reject for a request to a missing service' , async ( ) => { const badConnection = new HttpConnection ( { serviceName : 'Calculator' , hostName : 'fakehost' , port : 8080 , requestOptions : { timeout : 5000 , } , withEndpointPerMethod : true , } ) ; const badClient = new Calculator . Client ( badConnection ) ; return badClient . add ( 5 , 7 ) . then ( ( response ) => { throw new Error ( 'Should reject with host not found' ) ; } , ( err ) => { expect ( err ) . to . exist ( ) ; } ) ; } ) ; } ) ; describe ( 'Incoming Middleware' , ( ) => { it ( 'should resolve when middleware allows' , async ( ) => { const connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; const client = new Calculator . Client < IRequest > ( connection ) ; connection . register ( { handler ( req , next ) { if ( thrift . readThriftMethod ( req . data ) === 'add' ) { return next ( ) ; } else { return Promise . reject ( new Error ( `Unrecognized method name: ${ thrift . readThriftMethod ( req . data ) }` ) ) ; } } , } ) ; return client . add ( 5 , 7 ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } ) ; } ) ; it ( 'should resolve when middleware passes method filter' , async ( ) => { const connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; const client = new Calculator . Client < IRequest > ( connection ) ; connection . register ( { methods : [ 'add' ] , handler ( req , next ) { if ( thrift . readThriftMethod ( req . data ) === 'add' ) { return next ( ) ; } else { return Promise . reject ( new Error ( `Unrecognized method name: ${ thrift . readThriftMethod ( req . data ) }` ) ) ; } } , } ) ; return client . add ( 5 , 7 ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } ) ; } ) ; it ( 'should reject when middleware rejects' , async ( ) => { const connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; const client = new Calculator . Client < IRequest > ( connection ) ; connection . register ( { handler ( req , next ) { if ( thrift . readThriftMethod ( req . data ) === 'nope' ) { return next ( ) ; } else { return Promise . reject ( new Error ( `Unrecognized method name: ${ thrift . readThriftMethod ( req . data ) }` ) ) ; } } , } ) ; return client . add ( 5 , 7 ) . then ( ( response ) => { throw new Error ( `Mehtods should fail when middleware rejects` ) ; } , ( err ) => { expect ( err . message ) . to . equal ( 'Unrecognized method name: add' ) ; } ) ; } ) ; it ( 'should skip handler when middleware fails method filter' , async ( ) => { const connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; const client = new Calculator . Client < IRequest > ( connection ) ; connection . register ( { methods : [ 'nope' ] , handler ( req , next ) { return Promise . reject ( new Error ( `Unrecognized method name: ${ thrift . readThriftMethod ( req . data ) }` ) ) ; } , } ) ; return client . add ( 5 , 7 ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } ) ; } ) ; } ) ; describe ( 'Outgoing Middleware' , ( ) => { it ( 'should resolve when middleware adds auth token' , async ( ) => { const connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; const client = new Calculator . Client < IRequest > ( connection ) ; connection . register ( { handler ( req , next ) { return next ( req . data , { headers : { 'x-fake-token' : 'fake-token' , } , } ) ; } , } ) ; return client . addWithContext ( 5 , 7 ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } ) ; } ) ; it ( 'should resolve when middleware passes method filter' , async ( ) => { const connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; const client = new Calculator . Client < IRequest > ( connection ) ; connection . register ( { methods : [ 'addWithContext' ] , handler ( req , next ) { return next ( req . data , { headers : { 'x-fake-token' : 'fake-token' , } , } ) ; } , } ) ; return client . addWithContext ( 5 , 7 ) . then ( ( response ) => { expect ( response ) . to . equal ( 12 ) ; } ) ; } ) ; it ( 'should reject when middleware does not add auth token' , async ( ) => { const connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; const client = new Calculator . Client < IRequest > ( connection ) ; return client . addWithContext ( 5 , 7 ) . then ( ( response ) => { throw new Error ( `Mehtods should fail when middleware rejects` ) ; } , ( err ) => { expect ( err . message ) . to . equal ( 'Unauthorized' ) ; } ) ; } ) ; it ( 'should reject when middleware fails method filter' , async ( ) => { const connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : HAPI_CALC_SERVER_CONFIG . hostName , port : HAPI_CALC_SERVER_CONFIG . port , path : HAPI_CALC_SERVER_CONFIG . path , } ) ; const client = new Calculator . Client < IRequest > ( connection ) ; connection . register ( { methods : [ 'add' ] , handler ( req , next ) { return next ( req . data , { headers : { 'x-fake-token' : 'fake-token' , } , } ) ; } , } ) ; return client . addWithContext ( 5 , 7 ) . then ( ( response ) => { throw new Error ( `Mehtods should fail when middleware rejects` ) ; } , ( err ) => { expect ( err . message ) . to . equal ( 'Unauthorized' ) ; } ) ; } ) ; } ) ; describe ( 'ThriftClientTTwitterFilter' , ( ) => { const PORT = 9010 ; let connection ; let client ; let collectServer ; let mockServer ; before ( async ( ) => { connection = new HttpConnection ( { serviceName : 'Calculator' , hostName : 'localhost' , port : PORT , } ) ; connection . register ( ThriftClientTTwitterFilter ( { localServiceName : 'http-calculator-client' , remoteServiceName : 'calculator-service' , tracerConfig : { endpoint : 'http://localhost:9411/api/v1/spans' , sampleRate : 1 , httpInterval : 0 , } , } ) ) ; client = new Calculator . Client ( connection ) ; return mockCollector ( ) . then ( ( collector ) => { collectServer = collector ; } ) ; } ) ; after ( async ( ) => { return new Promise ( ( resolve , reject ) => { collectServer . close ( ) . then ( ( ) => { mockServer . close ( ( ) => { console . log ( 'HTTP server closed' ) ; mockServer . unref ( ) ; resolve ( ) ; } ) ; } ) ; } ) ; } ) ; it ( 'should handle appending data to payload' , async ( ) => { return new Promise ( ( resolve , reject ) => { let count = 0 ; collectServer . reset ( ) ; mockServer = http . createServer ( ( req , res ) : void => { if ( count < 1 ) { count += 1 ; const upgradeResponse = { } ; const writer = new thrift . BufferedTransport ( ) ; const output = new thrift . BinaryProtocol ( writer ) ; output . writeMessageBegin ( 'add' , thrift . MessageType . CALL , 1 ) ; TTwitter . UpgradeReplyCodec . encode ( upgradeResponse , output ) ; output . writeMessageEnd ( ) ; res . writeHead ( 200 ) ; res . end ( writer . flush ( ) ) ; } else { const responseHeader = { } ; const writer = new thrift . BufferedTransport ( ) ; const output = new thrift . BinaryProtocol ( writer ) ; output . writeMessageBegin ( 'add' , thrift . MessageType . CALL , 1 ) ; const result = new Calculator . Add__Result ( { success : 61 , } ) ; result . write ( output ) ; output . writeMessageEnd ( ) ; const data = writer . flush ( ) ; thrift . appendThriftObject ( responseHeader , data , TTwitter . ResponseHeaderCodec ) . then ( ( extended ) => { res . writeHead ( 200 ) ; res . end ( extended ) ; } ) ; } } ) ; mockServer . listen ( PORT , ( ) => { console . log ( `HTTP server listening on port: ${ PORT }` ) ; client . add ( 2 , 3 ) . then ( ( response ) => { expect ( response ) . to . equal ( 61 ) ; resolve ( ) ; } ) . catch ( ( err ) => { console . log ( 'err: ' , err ) ; reject ( err ) ; } ) ; } ) ; } ) ; } ) ; } ) ; } ) ;