Skip to content

Commit 8f487b9

Browse files
authored
test(maintenance): Expose ResponseStream object to simplify testing in event handler (#4753)
1 parent 5d3cf2d commit 8f487b9

File tree

5 files changed

+64
-55
lines changed

5 files changed

+64
-55
lines changed

packages/event-handler/src/rest/utils.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Duplex, Readable, Writable } from 'node:stream';
1+
import { Duplex, Readable } from 'node:stream';
22
import {
33
isRecord,
44
isRegExp,
@@ -308,17 +308,12 @@ export const resolvePrefixedPath = (path: Path, prefix?: Path): Path => {
308308

309309
export const HttpResponseStream =
310310
globalThis.awslambda?.HttpResponseStream ??
311-
class LocalHttpResponseStream extends Writable {
312-
#contentType: string | undefined;
313-
314-
setContentType(contentType: string) {
315-
this.#contentType = contentType;
316-
}
317-
311+
// biome-ignore lint/complexity/noStaticOnlyClass: This is how the Lambda RIC implements it
312+
class LocalHttpResponseStream {
318313
static from(
319314
underlyingStream: ResponseStream,
320315
prelude: Record<string, string>
321-
) {
316+
): ResponseStream {
322317
underlyingStream.setContentType(
323318
"'application/vnd.awslambda.http-integration-response'"
324319
);
@@ -401,22 +396,21 @@ const streamifyResponse =
401396
return (async (event, responseStream, context) => {
402397
await handler(event, responseStream, context);
403398

404-
/* v8 ignore else -- @preserve */
405-
if ('chunks' in responseStream && Array.isArray(responseStream.chunks)) {
406-
const output = Buffer.concat(responseStream.chunks as Buffer[]);
407-
const nullBytes = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]);
408-
const separatorIndex = output.indexOf(nullBytes);
409-
410-
const preludeBuffer = output.subarray(0, separatorIndex);
411-
const bodyBuffer = output.subarray(separatorIndex + 8);
412-
const prelude = JSON.parse(preludeBuffer.toString());
413-
414-
return {
415-
body: bodyBuffer.toString(),
416-
headers: prelude.headers,
417-
statusCode: prelude.statusCode,
418-
} as TResult;
419-
}
399+
/* v8 ignore next -- @preserve */
400+
const output: Buffer =
401+
(responseStream as ResponseStream).getBuffer?.() ?? Buffer.from([]);
402+
const nullBytes = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]);
403+
const separatorIndex = output.indexOf(nullBytes);
404+
405+
const preludeBuffer = output.subarray(0, separatorIndex);
406+
const bodyBuffer = output.subarray(separatorIndex + 8);
407+
const prelude = JSON.parse(preludeBuffer.toString());
408+
409+
return {
410+
body: bodyBuffer.toString(),
411+
headers: prelude.headers,
412+
statusCode: prelude.statusCode,
413+
} as TResult;
420414
}) as StreamifyHandler<TEvent, TResult>;
421415
});
422416

packages/event-handler/src/types/rest.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Readable } from 'node:stream';
1+
import type { Readable, Writable } from 'node:stream';
22
import type {
33
GenericLogger,
44
JSONValue,
@@ -12,7 +12,6 @@ import type {
1212
} from 'aws-lambda';
1313
import type { HttpStatusCodes, HttpVerbs } from '../rest/constants.js';
1414
import type { Route } from '../rest/Route.js';
15-
import type { HttpResponseStream } from '../rest/utils.js';
1615
import type { ResolveOptions } from './common.js';
1716

1817
type ResponseType = 'ApiGatewayV1' | 'ApiGatewayV2';
@@ -140,9 +139,11 @@ type ValidationResult = {
140139
issues: string[];
141140
};
142141

143-
type ResponseStream = InstanceType<typeof HttpResponseStream> & {
144-
_onBeforeFirstWrite?: (write: (data: Uint8Array | string) => void) => void;
145-
};
142+
interface ResponseStream extends Writable {
143+
setContentType(contentType: string): void;
144+
_onBeforeFirstWrite?: (writeFn: (chunk: unknown) => void) => void;
145+
getBuffer?: () => Buffer;
146+
}
146147

147148
type V1Headers = {
148149
headers: Record<string, string>;

packages/event-handler/tests/unit/rest/Router/decorators.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
createTestEventV2,
1818
createTestLambdaClass,
1919
createTrackingMiddleware,
20-
MockResponseStream,
20+
ResponseStream,
2121
} from '../helpers.js';
2222

2323
describe.each([
@@ -498,7 +498,7 @@ describe.each([
498498
}
499499

500500
const lambda = new Lambda();
501-
const responseStream = new MockResponseStream();
501+
const responseStream = new ResponseStream();
502502
const handler = lambda.handler.bind(lambda);
503503

504504
// Act
@@ -540,7 +540,7 @@ describe.each([
540540
}
541541

542542
const lambda = new Lambda();
543-
const responseStream = new MockResponseStream();
543+
const responseStream = new ResponseStream();
544544
const handler = lambda.handler.bind(lambda);
545545

546546
// Act

packages/event-handler/tests/unit/rest/Router/streaming.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import {
1010
createTestEvent,
1111
createTestEventV2,
12-
MockResponseStream,
12+
ResponseStream,
1313
} from '../helpers.js';
1414

1515
describe.each([
@@ -22,7 +22,7 @@ describe.each([
2222
app.get('/test', async () => ({ message: 'Hello, World!' }));
2323

2424
const handler = streamify(app);
25-
const responseStream = new MockResponseStream();
25+
const responseStream = new ResponseStream();
2626

2727
// Act
2828
const result = await handler(
@@ -47,7 +47,7 @@ describe.each([
4747
});
4848

4949
const handler = streamify(app);
50-
const responseStream = new MockResponseStream();
50+
const responseStream = new ResponseStream();
5151

5252
// Act
5353
const result = await handler(
@@ -65,7 +65,7 @@ describe.each([
6565
// Prepare
6666
const app = new Router();
6767
const handler = streamify(app);
68-
const responseStream = new MockResponseStream();
68+
const responseStream = new ResponseStream();
6969

7070
// Act
7171
const result = await handler(
@@ -93,7 +93,7 @@ describe.each([
9393
app.get('/test', () => ({ message: 'middleware test' }));
9494

9595
const handler = streamify(app);
96-
const responseStream = new MockResponseStream();
96+
const responseStream = new ResponseStream();
9797

9898
// Act
9999
const result = await handler(
@@ -116,7 +116,7 @@ describe.each([
116116
});
117117

118118
const handler = streamify(app);
119-
const responseStream = new MockResponseStream();
119+
const responseStream = new ResponseStream();
120120

121121
// Act
122122
const result = await handler(
@@ -145,7 +145,7 @@ describe.each([
145145
});
146146

147147
const handler = streamify(app);
148-
const responseStream = new MockResponseStream();
148+
const responseStream = new ResponseStream();
149149

150150
// Act
151151
const result = await handler(
@@ -193,7 +193,7 @@ describe.each([
193193
const app = new Router();
194194
app.get('/test', handlerFn);
195195
const handler = streamify(app);
196-
const responseStream = new MockResponseStream();
196+
const responseStream = new ResponseStream();
197197

198198
// Act
199199
const result = await handler(
@@ -212,7 +212,7 @@ describe.each([
212212
const app = new Router();
213213
app.get('/test', () => new Response(null, { status: 204 }));
214214
const handler = streamify(app);
215-
const responseStream = new MockResponseStream();
215+
const responseStream = new ResponseStream();
216216

217217
// Act
218218
const result = await handler(
@@ -231,7 +231,7 @@ describe.each([
231231
const app = new Router();
232232
app.get('/test', () => new Response(undefined, { status: 200 }));
233233
const handler = streamify(app);
234-
const responseStream = new MockResponseStream();
234+
const responseStream = new ResponseStream();
235235

236236
// Act
237237
const result = await handler(
@@ -256,7 +256,7 @@ describe.each([
256256

257257
app.get('/test', () => new Response(errorStream, { status: 200 }));
258258
const handler = streamify(app);
259-
const responseStream = new MockResponseStream();
259+
const responseStream = new ResponseStream();
260260

261261
// Act & Assess
262262
await expect(
@@ -275,7 +275,7 @@ describe.each([
275275
});
276276

277277
const handler = streamify(app);
278-
const responseStream = new MockResponseStream();
278+
const responseStream = new ResponseStream();
279279

280280
// Act
281281
const result = await handler(
@@ -299,7 +299,7 @@ describe.each([
299299
});
300300

301301
const handler = streamify(app);
302-
const responseStream = new MockResponseStream();
302+
const responseStream = new ResponseStream();
303303

304304
// Act
305305
const result = await handler(
@@ -323,7 +323,7 @@ describe.each([
323323
const app = new Router();
324324
const handler = streamify(app);
325325
const invalidEvent = { invalid: 'event' };
326-
const responseStream = new MockResponseStream();
326+
const responseStream = new ResponseStream();
327327

328328
// Act & Assess
329329
await expect(
@@ -344,7 +344,7 @@ describe.each([
344344
}));
345345

346346
const handler = streamify(app);
347-
const responseStream = new MockResponseStream();
347+
const responseStream = new ResponseStream();
348348

349349
// Act
350350
const result = await handler(

packages/event-handler/tests/unit/rest/helpers.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Writable } from 'node:stream';
12
import type {
23
APIGatewayProxyEvent,
34
APIGatewayProxyEventV2,
@@ -6,8 +7,11 @@ import type {
67
Context,
78
} from 'aws-lambda';
89
import type { Router } from '../../../src/rest/Router.js';
9-
import { HttpResponseStream } from '../../../src/rest/utils.js';
10-
import type { HandlerResponse, Middleware } from '../../../src/types/rest.js';
10+
import type {
11+
HandlerResponse,
12+
ResponseStream as IResponseStream,
13+
Middleware,
14+
} from '../../../src/types/rest.js';
1115

1216
export const createTestEvent = (
1317
path: string,
@@ -129,24 +133,34 @@ export const createHeaderCheckMiddleware = (headers: {
129133
};
130134
};
131135

132-
// Mock ResponseStream that extends the actual ResponseStream class
133-
export class MockResponseStream extends HttpResponseStream {
134-
public chunks: Buffer[] = [];
136+
export class ResponseStream extends Writable implements IResponseStream {
137+
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: This is how the Lambda RIC implements it
138+
#contentType: string | undefined;
139+
readonly #chunks: Buffer[] = [];
135140
public _onBeforeFirstWrite?: (
136141
write: (data: Uint8Array | string) => void
137142
) => void;
138143
#firstWrite = true;
139144

145+
setContentType(contentType: string) {
146+
this.#contentType = contentType;
147+
}
148+
140149
_write(chunk: Buffer, _encoding: string, callback: () => void): void {
150+
/* v8 ignore else -- @preserve */
141151
if (this.#firstWrite && this._onBeforeFirstWrite) {
142152
this._onBeforeFirstWrite((data: Uint8Array | string) => {
143-
this.chunks.push(Buffer.from(data));
153+
this.#chunks.push(Buffer.from(data));
144154
});
145155
this.#firstWrite = false;
146156
}
147-
this.chunks.push(chunk);
157+
this.#chunks.push(chunk);
148158
callback();
149159
}
160+
161+
public getBuffer(): Buffer {
162+
return Buffer.concat(this.#chunks);
163+
}
150164
}
151165

152166
// Create a handler function from the Router instance

0 commit comments

Comments
 (0)