@@ -17,12 +17,22 @@ import {
1717 createExpressResponse ,
1818} from '@salesforce/mrt-utilities/streaming' ;
1919
20+ type WritableWithChunksAndMetadata = Writable &
21+ EventEmitter & {
22+ chunks : Buffer [ ] ;
23+ metadata : { statusCode : number ; headers : Record < string , any > ; cookies ?: string [ ] } ;
24+ getWrittenData : ( encoding ?: BufferEncoding ) => Buffer | string ;
25+ } ;
26+
2027// Mock awslambda global
2128const mockHttpResponseStream = {
2229 from : sinon
2330 . stub ( )
2431 . callsFake (
25- ( stream : Writable , _metadata : { statusCode : number ; headers : Record < string , any > ; cookies ?: string [ ] } ) => {
32+ (
33+ stream : WritableWithChunksAndMetadata ,
34+ _metadata : { statusCode : number ; headers : Record < string , any > ; cookies ?: string [ ] } ) => {
35+ stream . metadata = _metadata ;
2636 return stream ;
2737 } ,
2838 ) ,
@@ -35,7 +45,7 @@ const mockHttpResponseStream = {
3545} ;
3646
3747// Mock stream type with Sinon stubs so assertions (e.g. .called, .calledWith) type-check
38- type MockWritable = ( Writable & EventEmitter ) & {
48+ type MockWritable = WritableWithChunksAndMetadata & {
3949 write : sinon . SinonStub ;
4050 end : sinon . SinonStub ;
4151 destroy : sinon . SinonStub ;
@@ -53,6 +63,8 @@ function createMockWritable(): MockWritable {
5363 stream . writableEnded = false ;
5464 stream . writableFinished = false ;
5565 stream . destroyed = false ;
66+ stream . chunks = chunks ;
67+ stream . metadata = undefined ;
5668
5769 stream . write = sinon . stub ( ) . callsFake ( ( chunk : any ) => {
5870 if ( destroyed || ended ) return false ;
@@ -84,6 +96,11 @@ function createMockWritable(): MockWritable {
8496 // Mock flush method
8597 } ) ;
8698
99+ stream . getWrittenData = function ( encoding ?: BufferEncoding ) {
100+ const data = Buffer . concat ( this . chunks ) ;
101+ return encoding ? data . toString ( encoding ) : data ;
102+ } ;
103+
87104 return stream as MockWritable ;
88105}
89106
@@ -241,7 +258,6 @@ describe('create-lambda-adapter', () => {
241258 mockResponseStream = createMockWritable ( ) ;
242259 mockApp = express ( ) ;
243260 mockHttpResponseStream . from . resetHistory ( ) ;
244- mockHttpResponseStream . from . callsFake ( ( stream ) => stream ) ;
245261 } ) ;
246262
247263 afterEach ( ( ) => {
@@ -283,7 +299,7 @@ describe('create-lambda-adapter', () => {
283299
284300 await handler ( event , context ) ;
285301
286- expect ( mockResponseStream . write . firstCall . args [ 0 ] ) . to . include ( '500 Internal Server Error' ) ;
302+ expect ( mockResponseStream . write . firstCall . args [ 0 ] ) . to . include ( 'Error' ) ;
287303 expect ( mockResponseStream . end . called ) . to . be . true ;
288304 } ) ;
289305
@@ -298,7 +314,7 @@ describe('create-lambda-adapter', () => {
298314
299315 await handler ( event , context ) ;
300316
301- expect ( mockResponseStream . write . firstCall . args [ 0 ] ) . to . include ( '500 Internal Server Error' ) ;
317+ expect ( mockResponseStream . write . firstCall . args [ 0 ] ) . to . include ( 'Error' ) ;
302318 expect ( mockResponseStream . end . called ) . to . be . true ;
303319 } ) ;
304320
@@ -321,22 +337,80 @@ describe('create-lambda-adapter', () => {
321337 expect ( closedStream . write . called ) . to . be . false ;
322338 } ) ;
323339
324- it ( 'should handle stream without write method' , async ( ) => {
325- mockApp . get ( '/test' , ( ) => {
326- throw new Error ( 'Test error' ) ;
340+ it ( 'should return status code 400 response for requests to streaming route with invalid path' , async ( ) => {
341+ // We need a catch-all route to force the router to try to decode the path
342+ const dummyCatchAllRoute = ( _ : any , res : any ) => {
343+ res . status ( 200 ) . send ( 'dummy catch-all route response' ) ;
344+ } ;
345+ try {
346+ //express 4 style catch-all route, throws an error when installed
347+ // in express 5, see https://github.com/pillarjs/path-to-regexp#errors
348+ mockApp . get ( '/*' , dummyCatchAllRoute ) ;
349+ } catch ( error ) {
350+ //express 5 style catch-all route
351+ mockApp . get ( '/{*splat}' , dummyCatchAllRoute ) ;
352+ }
353+
354+
355+ const handler = createStreamingLambdaAdapter ( mockApp , mockResponseStream ) ;
356+ const event = createMockEvent ( { path : '/%80' } ) ;
357+ const context = createMockContext ( ) ;
358+
359+ await handler ( event , context ) ;
360+
361+ expect ( mockResponseStream . metadata . statusCode ) . to . equal ( 400 ) ;
362+ expect ( mockResponseStream . write . called ) . to . be . true ;
363+ expect ( mockResponseStream . end . called ) . to . be . true ;
364+
365+ const responseData = mockResponseStream . getWrittenData ( 'utf-8' ) ;
366+
367+ // The response body returned by finalhandler depends on the NODE_ENV environment variable, when it is set to "production", a brief error page with the message "Bad Request" is returned, otherwise it returns a more detailed error page with a stack trace for a URIError exception
368+ expect ( responseData ) . to . contain ( '<title>Error</title>' ) ;
369+ expect ( responseData ) . to . match ( / U R I E r r o r | B a d R e q u e s t / ) ;
370+ } ) ;
371+
372+ it ( 'should return status code 404 response for requests to streaming route that explicitly raises to return a 404' , async ( ) => {
373+ // A 404 implemented by throwing
374+ mockApp . get ( '/intentional404' , ( ) => {
375+ type ErrorWithStatus = Error & { status : number } ;
376+ const err = new Error ( 'Not Found' ) as ErrorWithStatus ;
377+ err . message = 'Not Found' ;
378+ err . status = 404 ;
379+ throw err ;
327380 } ) ;
328381
329- const streamWithoutWrite = createMockWritable ( ) ;
330- delete ( streamWithoutWrite as any ) . write ;
382+ const handler = createStreamingLambdaAdapter ( mockApp , mockResponseStream ) ;
383+ const event = createMockEvent ( { path : '/intentional404' } ) ;
384+ const context = createMockContext ( ) ;
385+
386+ await handler ( event , context ) ;
331387
332- const handler = createStreamingLambdaAdapter ( mockApp , streamWithoutWrite ) ;
333- const event = createMockEvent ( { path : '/test' } ) ;
388+ //expect(mockResponseStream.write).toHaveBeenCalled();
389+ expect ( mockResponseStream . end . called ) . to . be . true ;
390+ expect ( mockResponseStream . metadata . statusCode ) . to . equal ( 404 ) ;
391+
392+ expect ( mockResponseStream . getWrittenData ( 'utf-8' ) ) . to . contain ( 'Not Found' ) ;
393+ } ) ;
394+
395+ it ( 'should stream status code 200 response for requests to an finalhandler style route' , async ( ) => {
396+ mockApp . get ( '/simpleroute' , ( req , res ) => {
397+ // A hanlder that uses the response object in the style of https://github.com/pillarjs/finalhandler/blob/v2.1.0/index.js#L245-L280 which is used by express to handle the final response in case of an error
398+ res . statusCode = 200 ;
399+ res . statusMessage = 'OK' ;
400+ const body = 'hello world' ;
401+ res . setHeader ( 'Content-Type' , 'text/plain; charset=utf-8' ) ;
402+ res . setHeader ( 'Content-Length' , Buffer . byteLength ( body , 'utf8' ) ) ;
403+ res . end ( body ) ;
404+ } ) ;
405+
406+ const handler = createStreamingLambdaAdapter ( mockApp , mockResponseStream ) ;
407+ const event = createMockEvent ( { path : '/simpleroute' } ) ;
334408 const context = createMockContext ( ) ;
335409
336410 await handler ( event , context ) ;
337411
338- // Should not throw
339- expect ( streamWithoutWrite . end . called ) . to . be . true ;
412+ expect ( mockResponseStream . metadata . statusCode ) . to . equal ( 200 ) ;
413+ expect ( mockResponseStream . getWrittenData ( 'utf-8' ) ) . to . equal ( 'hello world' ) ;
340414 } ) ;
341415
342416 it ( 'should handle stream without end method in finally' , async ( ) => {
0 commit comments