diff --git a/packages/open-next/src/converters/aws-apigw-v2.ts b/packages/open-next/src/converters/aws-apigw-v2.ts index 8a30d2ad5..8bc443cc7 100644 --- a/packages/open-next/src/converters/aws-apigw-v2.ts +++ b/packages/open-next/src/converters/aws-apigw-v2.ts @@ -91,6 +91,7 @@ async function convertToApiGatewayProxyResultV2( ): Promise { const headers: Record = {}; Object.entries(result.headers) + .map(([key, value]) => [key.toLowerCase(), value] as const) .filter( ([key]) => !CloudFrontBlacklistedHeaders.some((header) => diff --git a/packages/open-next/src/http/request.ts b/packages/open-next/src/http/request.ts index d33b13955..88c68049e 100644 --- a/packages/open-next/src/http/request.ts +++ b/packages/open-next/src/http/request.ts @@ -15,9 +15,9 @@ export class IncomingMessage extends http.IncomingMessage { }: { method: string; url: string; - headers: Record; + headers: Record; body?: Buffer; - remoteAddress: string; + remoteAddress?: string; }) { super({ encrypted: true, diff --git a/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts b/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts new file mode 100644 index 000000000..03234bb3f --- /dev/null +++ b/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts @@ -0,0 +1,264 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import converter from "@opennextjs/aws/converters/aws-apigw-v1.js"; +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import { Readable } from "stream"; + +describe("convertTo", () => { + describe("AWS API Gateway v2 Result", () => { + it("Should parse the headers", async () => { + const response = (await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + "content-type": "application/json", + test: "test", + }, + isBase64Encoded: false, + statusCode: 200, + })) as APIGatewayProxyResult; + + expect(response.headers).toStrictEqual({ + "content-type": "application/json", + test: "test", + }); + }); + + it("Should parse the headers with arrays", async () => { + const response = (await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + test: ["test1", "test2"], + }, + isBase64Encoded: false, + statusCode: 200, + })) as APIGatewayProxyResult; + + expect(response.multiValueHeaders).toStrictEqual({ + test: ["test1", "test2"], + }); + }); + + it("Should parse single and array headers", async () => { + const response = (await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + single: "test", + multi: ["test1", "test2"], + }, + isBase64Encoded: false, + statusCode: 200, + })) as APIGatewayProxyResult; + + expect(response.headers).toStrictEqual({ + single: "test", + }); + expect(response.multiValueHeaders).toStrictEqual({ + multi: ["test1", "test2"], + }); + }); + }); +}); + +describe("convertFrom", () => { + it("Should convert a simple APIGatewayProxyEvent", async () => { + const event: APIGatewayProxyEvent = { + body: JSON.stringify({ message: "Hello, world!" }), + headers: { + "content-type": "application/json", + }, + multiValueHeaders: {}, + httpMethod: "POST", + isBase64Encoded: false, + path: "/", + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: { + identity: { + sourceIp: "::1", + }, + } as any, + resource: "", + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + }, + remoteAddress: "::1", + query: {}, + cookies: {}, + }); + }); + + it("Should handle multiValueHeaders", async () => { + const event: APIGatewayProxyEvent = { + body: JSON.stringify({ message: "Hello, world!" }), + headers: {}, + multiValueHeaders: { + test: ["test1", "test2"], + }, + httpMethod: "POST", + isBase64Encoded: false, + path: "/", + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: { + identity: { + sourceIp: "::1", + }, + } as any, + resource: "", + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + test: "test1,test2", + }, + remoteAddress: "::1", + query: {}, + cookies: {}, + }); + }); + + it("Should handle queryStringParameters and multiValueQueryStringParameters", async () => { + const event: APIGatewayProxyEvent = { + body: JSON.stringify({ message: "Hello, world!" }), + headers: {}, + multiValueHeaders: {}, + httpMethod: "POST", + isBase64Encoded: false, + path: "/", + pathParameters: null, + queryStringParameters: { + test: "test", + }, + multiValueQueryStringParameters: { + test: ["test"], + }, + stageVariables: null, + requestContext: { + identity: { + sourceIp: "::1", + }, + } as any, + resource: "", + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/?test=test", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: {}, + remoteAddress: "::1", + query: { + test: ["test"], + }, + cookies: {}, + }); + }); + + it("Should handle cookies", async () => { + const event: APIGatewayProxyEvent = { + body: JSON.stringify({ message: "Hello, world!" }), + headers: { + "content-type": "application/json", + }, + multiValueHeaders: { + cookie: ["test1=1", "test2=2"], + }, + httpMethod: "POST", + isBase64Encoded: false, + path: "/", + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: { + identity: { + sourceIp: "::1", + }, + } as any, + resource: "", + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + cookie: "test1=1,test2=2", + }, + remoteAddress: "::1", + query: {}, + cookies: { + test1: "1", + test2: "2", + }, + }); + }); + + it("Should handle base64 encoded body", async () => { + const event: APIGatewayProxyEvent = { + body: Buffer.from("Hello, world!").toString("base64"), + headers: { + "content-type": "application/json", + }, + multiValueHeaders: {}, + httpMethod: "GET", + isBase64Encoded: true, + path: "/", + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: { + identity: { + sourceIp: "::1", + }, + } as any, + resource: "", + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "GET", + rawPath: "/", + url: "/", + body: Buffer.from("Hello, world!"), + headers: { + "content-type": "application/json", + }, + remoteAddress: "::1", + query: {}, + cookies: {}, + }); + }); +}); diff --git a/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts b/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts new file mode 100644 index 000000000..ab9e4f505 --- /dev/null +++ b/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts @@ -0,0 +1,259 @@ +import converter from "@opennextjs/aws/converters/aws-apigw-v2.js"; +import { APIGatewayProxyEventV2 } from "aws-lambda"; +import { Readable } from "stream"; +import { vi } from "vitest"; + +vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({})); + +describe("convertTo", () => { + it("Should parse the headers", async () => { + const response = await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + "content-type": "application/json", + test: "test", + }, + isBase64Encoded: false, + statusCode: 200, + }); + + expect(response.headers).toStrictEqual({ + "content-type": "application/json", + test: "test", + }); + }); + + it("Should parse the headers with arrays", async () => { + const response = await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + test: ["test1", "test2"], + }, + isBase64Encoded: false, + statusCode: 200, + }); + + expect(response.headers).toStrictEqual({ + test: "test1, test2", + }); + }); + + describe("blacklisted headers", () => { + it("should remove all blacklisted headers from the response", async () => { + const response = await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + Connection: "keep-alive", + expect: "100-continue", + "keep-Alive": "timeout=5, max=100", + "Proxy-Authenticate": "Basic", + "proxy-authorization": "Basic", + "proxy-connection": "keep-alive", + trailer: "Max-Forwards", + Upgrade: "HTTP/2.0", + "X-accel-buffering": "no", + "X-accel-charset": "UTF-8", + "x-accel-limit-rate": "1000", + "X-accel-redirect": "http://example.com", + "X-amz-cf-id": "example", + "x-amzn-auth": "example", + "x-Amzn-cf-billing": "example", + "x-Amzn-cf-id": "example", + "x-Amzn-Cf-xff": "example", + "x-amzn-Errortype": "example", + "x-amzn-fle-Profile": "example", + "x-amzn-header-Count": "example", + "x-amzn-Header-order": "example", + "X-Amzn-Lambda-Integration-tag": "example", + "x-amzn-Requestid": "example", + "x-edge-Location": "example", + "X-Cache": "Hit from cloudfront", + "X-Forwarded-proto": "https", + "x-Real-ip": "example", + "Accept-encoding": "gzip", + "content-length": "100", + "if-modified-Since": "example", + "if-none-match": "example", + "if-range": "example", + "if-unmodified-since": "example", + "transfer-encoding": "example", + via: "1.1 abc123.cloudfront.net (CloudFront)", + "x-powered-by": "Next.js", + }, + isBase64Encoded: false, + statusCode: 200, + type: "cf", + }); + + expect(response.headers).toStrictEqual({ + "accept-encoding": "gzip", + "content-length": "100", + "if-modified-since": "example", + "if-none-match": "example", + "if-range": "example", + "if-unmodified-since": "example", + "transfer-encoding": "example", + "x-powered-by": "Next.js", + }); + }); + }); +}); + +describe("convertFrom", () => { + it("Should parse the headers", async () => { + const event: APIGatewayProxyEventV2 = { + rawPath: "/", + rawQueryString: "", + cookies: undefined, + headers: { + "content-type": "application/json", + }, + version: "2.0", + routeKey: "", + body: JSON.stringify({ message: "Hello, world!" }), + isBase64Encoded: false, + requestContext: { + http: { + method: "POST", + sourceIp: "::1", + }, + } as any, + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + }, + cookies: {}, + query: {}, + remoteAddress: "::1", + }); + }); + + it("Should parse cookies", async () => { + const event: APIGatewayProxyEventV2 = { + rawPath: "/", + rawQueryString: "", + cookies: ["foo=bar", "hello=world"], + headers: { + "content-type": "application/json", + }, + version: "2.0", + routeKey: "", + body: JSON.stringify({ message: "Hello, world!" }), + isBase64Encoded: false, + requestContext: { + http: { + method: "POST", + sourceIp: "::1", + }, + } as any, + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + cookie: "foo=bar; hello=world", + }, + cookies: { + foo: "bar", + hello: "world", + }, + query: {}, + remoteAddress: "::1", + }); + }); + + it("Should parse query string", async () => { + const event: APIGatewayProxyEventV2 = { + rawPath: "/", + rawQueryString: "hello=world&foo=1&foo=2", + cookies: undefined, + headers: { + "content-type": "application/json", + }, + version: "2.0", + routeKey: "", + body: JSON.stringify({ message: "Hello, world!" }), + isBase64Encoded: false, + requestContext: { + http: { + method: "POST", + sourceIp: "::1", + }, + } as any, + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/?hello=world&foo=1&foo=2", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + }, + cookies: {}, + query: { + hello: "world", + foo: ["1", "2"], + }, + remoteAddress: "::1", + }); + }); + + it("Should handle base64 encoded body", async () => { + const event: APIGatewayProxyEventV2 = { + rawPath: "/", + rawQueryString: "", + cookies: undefined, + headers: { + "content-type": "application/json", + }, + version: "2.0", + routeKey: "", + body: Buffer.from(JSON.stringify({ message: "Hello, world!" })).toString( + "base64", + ), + isBase64Encoded: true, + requestContext: { + http: { + method: "POST", + sourceIp: "::1", + }, + } as any, + }; + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + }, + cookies: {}, + query: {}, + remoteAddress: "::1", + }); + }); +}); diff --git a/packages/tests-unit/tests/converters/aws-cloudfront.test.ts b/packages/tests-unit/tests/converters/aws-cloudfront.test.ts index 740f86b28..bed3ed744 100644 --- a/packages/tests-unit/tests/converters/aws-cloudfront.test.ts +++ b/packages/tests-unit/tests/converters/aws-cloudfront.test.ts @@ -1,45 +1,159 @@ import converter from "@opennextjs/aws/converters/aws-cloudfront.js"; -import { CloudFrontRequestResult } from "aws-lambda"; +import { CloudFrontRequestEvent, CloudFrontRequestResult } from "aws-lambda"; import { Readable } from "stream"; import { vi } from "vitest"; vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({})); describe("convertTo", () => { - describe("CloudFront Result", () => { - it("Should parse the headers", async () => { - const response = (await converter.convertTo({ - body: Readable.toWeb(Readable.from(Buffer.from(""))), - headers: { - "content-type": "application/json", - test: "test", + it("Should parse the headers", async () => { + const response = (await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + "content-type": "application/json", + test: "test", + }, + isBase64Encoded: false, + statusCode: 200, + type: "cf", + })) as CloudFrontRequestResult; + + expect(response?.headers).toStrictEqual({ + "content-type": [ + { + key: "content-type", + value: "application/json", }, - isBase64Encoded: false, - statusCode: 200, - type: "cf", - })) as CloudFrontRequestResult; + ], + test: [ + { + key: "test", + value: "test", + }, + ], + }); + }); - expect(response?.headers).toStrictEqual({ - "content-type": [ - { - key: "content-type", - value: "application/json", - }, - ], - test: [ - { - key: "test", - value: "test", - }, - ], - }); + it("Should parse the headers with arrays", async () => { + const response = (await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + test: ["test1", "test2"], + }, + isBase64Encoded: false, + statusCode: 200, + type: "cf", + })) as CloudFrontRequestResult; + + expect(response?.headers).toStrictEqual({ + test: [ + { + key: "test", + value: "test1", + }, + { + key: "test", + value: "test2", + }, + ], + }); + }); + + it("Should parse the headers with cookies", async () => { + const response = (await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + "set-cookie": + "test=1; Path=/; HttpOnly; Secure; SameSite=None, test=2; Path=/; HttpOnly; Secure; SameSite=None", + }, + isBase64Encoded: false, + statusCode: 200, + type: "cf", + })) as CloudFrontRequestResult; + + expect(response?.headers).toStrictEqual({ + "set-cookie": [ + { + key: "set-cookie", + value: "test=1; Path=/; HttpOnly; Secure; SameSite=None", + }, + { + key: "set-cookie", + value: "test=2; Path=/; HttpOnly; Secure; SameSite=None", + }, + ], + }); + }); + + it("Should parse the headers with cookies + expires", async () => { + const response = (await converter.convertTo({ + body: Readable.toWeb(Readable.from(Buffer.from(""))), + headers: { + "set-cookie": + "test=1; Path=/; Expires=Sun, 14 Apr 2024 22:19:07 GMT; HttpOnly; Secure; SameSite=None, test=2; Path=/; Expires=Sun, 14 Apr 2024 22:19:07 GMT; HttpOnly; Secure; SameSite=None", + }, + isBase64Encoded: false, + statusCode: 200, + type: "cf", + })) as CloudFrontRequestResult; + + expect(response?.headers).toStrictEqual({ + "set-cookie": [ + { + key: "set-cookie", + value: + "test=1; Path=/; Expires=Sun, 14 Apr 2024 22:19:07 GMT; HttpOnly; Secure; SameSite=None", + }, + { + key: "set-cookie", + value: + "test=2; Path=/; Expires=Sun, 14 Apr 2024 22:19:07 GMT; HttpOnly; Secure; SameSite=None", + }, + ], }); + }); - it("Should parse the headers with arrays", async () => { + describe("blacklisted headers", () => { + it("should remove all blacklisted or read-only headers from the response", async () => { const response = (await converter.convertTo({ body: Readable.toWeb(Readable.from(Buffer.from(""))), headers: { - test: ["test1", "test2"], + Connection: "keep-alive", + expect: "100-continue", + "keep-Alive": "timeout=5, max=100", + "Proxy-Authenticate": "Basic", + "proxy-authorization": "Basic", + "proxy-connection": "keep-alive", + trailer: "Max-Forwards", + Upgrade: "HTTP/2.0", + "X-accel-buffering": "no", + "X-accel-charset": "UTF-8", + "x-accel-limit-rate": "1000", + "X-accel-redirect": "http://example.com", + "X-amz-cf-id": "example", + "x-amzn-auth": "example", + "x-Amzn-cf-billing": "example", + "x-Amzn-cf-id": "example", + "x-Amzn-Cf-xff": "example", + "x-amzn-Errortype": "example", + "x-amzn-fle-Profile": "example", + "x-amzn-header-Count": "example", + "x-amzn-Header-order": "example", + "X-Amzn-Lambda-Integration-tag": "example", + "x-amzn-Requestid": "example", + "x-edge-Location": "example", + "X-Cache": "Hit from cloudfront", + "X-Forwarded-proto": "https", + "x-Real-ip": "example", + "Accept-encoding": "gzip", + "content-length": "100", + "if-modified-Since": "example", + "if-none-match": "example", + "if-range": "example", + "if-unmodified-since": "example", + "transfer-encoding": "example", + via: "1.1 abc123.cloudfront.net (CloudFront)", + "x-powered-by": "Next.js", }, isBase64Encoded: false, statusCode: 200, @@ -47,129 +161,166 @@ describe("convertTo", () => { })) as CloudFrontRequestResult; expect(response?.headers).toStrictEqual({ - test: [ + "x-powered-by": [ { - key: "test", - value: "test1", - }, - { - key: "test", - value: "test2", + key: "x-powered-by", + value: "Next.js", }, ], }); }); + }); +}); - it("Should parse the headers with cookies", async () => { - const response = (await converter.convertTo({ - body: Readable.toWeb(Readable.from(Buffer.from(""))), - headers: { - "set-cookie": - "test=1; Path=/; HttpOnly; Secure; SameSite=None, test=2; Path=/; HttpOnly; Secure; SameSite=None", - }, - isBase64Encoded: false, - statusCode: 200, - type: "cf", - })) as CloudFrontRequestResult; +describe("convertFrom", () => { + type CloudFrontRequest = + CloudFrontRequestEvent["Records"][number]["cf"]["request"]; + type CloudFrontConfig = + CloudFrontRequestEvent["Records"][number]["cf"]["config"]; - expect(response?.headers).toStrictEqual({ - "set-cookie": [ - { - key: "set-cookie", - value: "test=1; Path=/; HttpOnly; Secure; SameSite=None", + function createEvent( + request: CloudFrontRequest, + config: CloudFrontConfig = { + distributionDomainName: "d123.cloudfront.net", + distributionId: "EDFDVBD6EXAMPLE", + eventType: "origin-request", + requestId: "EXAMPLE", + }, + ): CloudFrontRequestEvent { + return { + Records: [ + { + cf: { + request, + config, }, + }, + ], + }; + } + + it("Should parse the headers", async () => { + const event = createEvent({ + clientIp: "::1", + headers: { + "content-type": [ { - key: "set-cookie", - value: "test=2; Path=/; HttpOnly; Secure; SameSite=None", + key: "content-type", + value: "application/json", }, ], - }); + }, + method: "POST", + querystring: "", + uri: "/", + body: { + action: "read-only", + data: JSON.stringify({ message: "Hello, world!" }), + encoding: "text", + inputTruncated: false, + }, + origin: {} as any, }); - it("Should parse the headers with cookies + expires", async () => { - const response = (await converter.convertTo({ - body: Readable.toWeb(Readable.from(Buffer.from(""))), - headers: { - "set-cookie": - "test=1; Path=/; Expires=Sun, 14 Apr 2024 22:19:07 GMT; HttpOnly; Secure; SameSite=None, test=2; Path=/; Expires=Sun, 14 Apr 2024 22:19:07 GMT; HttpOnly; Secure; SameSite=None", - }, - isBase64Encoded: false, - statusCode: 200, - type: "cf", - })) as CloudFrontRequestResult; + const response = await converter.convertFrom(event); - expect(response?.headers).toStrictEqual({ - "set-cookie": [ + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + }, + remoteAddress: "::1", + query: {}, + cookies: {}, + }); + }); + + it("Should parse query string", async () => { + const event = createEvent({ + clientIp: "::1", + headers: { + "content-type": [ { - key: "set-cookie", - value: - "test=1; Path=/; Expires=Sun, 14 Apr 2024 22:19:07 GMT; HttpOnly; Secure; SameSite=None", + key: "content-type", + value: "application/json", }, + ], + }, + method: "POST", + querystring: "hello=world&foo=1&foo=2", + uri: "/", + body: { + action: "read-only", + data: JSON.stringify({ message: "Hello, world!" }), + encoding: "text", + inputTruncated: false, + }, + origin: {} as any, + }); + + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/?hello=world&foo=1&foo=2", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + }, + remoteAddress: "::1", + query: { + hello: "world", + foo: ["1", "2"], + }, + cookies: {}, + }); + }); + + it("Should parse base64 encoded body", async () => { + const event = createEvent({ + clientIp: "::1", + headers: { + "content-type": [ { - key: "set-cookie", - value: - "test=2; Path=/; Expires=Sun, 14 Apr 2024 22:19:07 GMT; HttpOnly; Secure; SameSite=None", + key: "content-type", + value: "application/json", }, ], - }); + }, + method: "POST", + querystring: "", + uri: "/", + body: { + action: "read-only", + data: Buffer.from( + JSON.stringify({ message: "Hello, world!" }), + ).toString("base64"), + encoding: "base64", + inputTruncated: false, + }, + origin: {} as any, }); - describe("blacklisted headers", () => { - it("should remove all blacklisted or read-only headers from the response", async () => { - const response = (await converter.convertTo({ - body: Readable.toWeb(Readable.from(Buffer.from(""))), - headers: { - Connection: "keep-alive", - expect: "100-continue", - "keep-Alive": "timeout=5, max=100", - "Proxy-Authenticate": "Basic", - "proxy-authorization": "Basic", - "proxy-connection": "keep-alive", - trailer: "Max-Forwards", - Upgrade: "HTTP/2.0", - "X-accel-buffering": "no", - "X-accel-charset": "UTF-8", - "x-accel-limit-rate": "1000", - "X-accel-redirect": "http://example.com", - "X-amz-cf-id": "example", - "x-amzn-auth": "example", - "x-Amzn-cf-billing": "example", - "x-Amzn-cf-id": "example", - "x-Amzn-Cf-xff": "example", - "x-amzn-Errortype": "example", - "x-amzn-fle-Profile": "example", - "x-amzn-header-Count": "example", - "x-amzn-Header-order": "example", - "X-Amzn-Lambda-Integration-tag": "example", - "x-amzn-Requestid": "example", - "x-edge-Location": "example", - "X-Cache": "Hit from cloudfront", - "X-Forwarded-proto": "https", - "x-Real-ip": "example", - "Accept-encoding": "gzip", - "content-length": "100", - "if-modified-Since": "example", - "if-none-match": "example", - "if-range": "example", - "if-unmodified-since": "example", - "transfer-encoding": "example", - via: "1.1 abc123.cloudfront.net (CloudFront)", - "x-powered-by": "Next.js", - }, - isBase64Encoded: false, - statusCode: 200, - type: "cf", - })) as CloudFrontRequestResult; - - expect(response?.headers).toStrictEqual({ - "x-powered-by": [ - { - key: "x-powered-by", - value: "Next.js", - }, - ], - }); - }); + const response = await converter.convertFrom(event); + + expect(response).toEqual({ + type: "core", + method: "POST", + rawPath: "/", + url: "/", + body: Buffer.from('{"message":"Hello, world!"}'), + headers: { + "content-type": "application/json", + }, + remoteAddress: "::1", + query: {}, + cookies: {}, }); }); }); diff --git a/packages/tests-unit/tests/converters/node.test.ts b/packages/tests-unit/tests/converters/node.test.ts new file mode 100644 index 000000000..da0c07757 --- /dev/null +++ b/packages/tests-unit/tests/converters/node.test.ts @@ -0,0 +1,215 @@ +import converter from "@opennextjs/aws/converters/node.js"; +import { IncomingMessage } from "@opennextjs/aws/http/request.js"; + +describe("convertFrom", () => { + it("should convert GET request", async () => { + const result = await converter.convertFrom( + new IncomingMessage({ + url: "/", + method: "GET", + headers: { + "content-length": "0", + }, + remoteAddress: "::1", + }), + ); + + expect(result).toEqual({ + type: "core", + url: "/", + rawPath: "/", + method: "GET", + headers: { + "content-length": "0", + }, + remoteAddress: "::1", + body: Buffer.from(""), + cookies: {}, + query: {}, + }); + }); + + it("should convert GET request with host header", async () => { + const result = await converter.convertFrom( + new IncomingMessage({ + url: "/path", + method: "GET", + headers: { + "content-length": "0", + host: "localhost", + }, + remoteAddress: "127.0.0.1", + }), + ); + + expect(result).toEqual({ + type: "core", + url: "/path", + rawPath: "/path", + method: "GET", + headers: { + "content-length": "0", + host: "localhost", + }, + remoteAddress: "127.0.0.1", + body: Buffer.from(""), + cookies: {}, + query: {}, + }); + }); + + it("should convert GET request with default remoteAddress", async () => { + const result = await converter.convertFrom( + new IncomingMessage({ + url: "/path", + method: "GET", + headers: { + "content-length": "0", + }, + }), + ); + + expect(result).toEqual({ + type: "core", + url: "/path", + rawPath: "/path", + method: "GET", + headers: { + "content-length": "0", + }, + remoteAddress: "::1", + body: Buffer.from(""), + cookies: {}, + query: {}, + }); + }); + + it("should convert GET request with remoteAddress from x-forwarded-for header", async () => { + const result = await converter.convertFrom( + new IncomingMessage({ + url: "/path", + method: "GET", + headers: { + "content-length": "0", + "x-forwarded-for": "127.0.0.2", + }, + remoteAddress: "127.0.0.1", + }), + ); + + expect(result).toEqual({ + type: "core", + url: "/path", + rawPath: "/path", + method: "GET", + headers: { + "content-length": "0", + "x-forwarded-for": "127.0.0.2", + }, + remoteAddress: "127.0.0.2", + body: Buffer.from(""), + cookies: {}, + query: {}, + }); + }); + + it("should convert GET request with query string", async () => { + const result = await converter.convertFrom( + new IncomingMessage({ + url: "/path?search=1", + method: "GET", + headers: { + "content-length": "0", + host: "localhost", + }, + remoteAddress: "::1", + }), + ); + + expect(result).toEqual({ + type: "core", + url: "/path?search=1", + rawPath: "/path", + method: "GET", + headers: { + "content-length": "0", + host: "localhost", + }, + remoteAddress: "::1", + body: Buffer.from(""), + cookies: {}, + query: { + search: "1", + }, + }); + }); + + it("should convert POST request with single cookie", async () => { + const result = await converter.convertFrom( + new IncomingMessage({ + url: "/path", + method: "POST", + headers: { + "content-length": "2", + "content-type": "application/json", + cookie: "foo=bar", + }, + remoteAddress: "::1", + body: Buffer.from("{}"), + }), + ); + + expect(result).toEqual({ + type: "core", + url: "/path", + rawPath: "/path", + method: "POST", + headers: { + "content-length": "2", + "content-type": "application/json", + cookie: "foo=bar", + }, + remoteAddress: "::1", + body: Buffer.from("{}"), + cookies: { + foo: "bar", + }, + query: {}, + }); + }); + + it("should convert PUT request with multiple cookie headers", async () => { + const result = await converter.convertFrom( + new IncomingMessage({ + url: "/path", + method: "PUT", + headers: { + "content-length": "2", + "content-type": "application/json", + cookie: ["foo=bar", "hello=world"], + }, + remoteAddress: "::1", + body: Buffer.from("{}"), + }), + ); + + expect(result).toEqual({ + type: "core", + url: "/path", + rawPath: "/path", + method: "PUT", + headers: { + "content-length": "2", + "content-type": "application/json", + cookie: "foo=bar,hello=world", + }, + remoteAddress: "::1", + body: Buffer.from("{}"), + cookies: { + foo: "bar", + hello: "world", + }, + query: {}, + }); + }); +}); diff --git a/packages/tests-unit/tests/converters/utils.test.ts b/packages/tests-unit/tests/converters/utils.test.ts new file mode 100644 index 000000000..796fc6d31 --- /dev/null +++ b/packages/tests-unit/tests/converters/utils.test.ts @@ -0,0 +1,31 @@ +import { removeUndefinedFromQuery } from "@opennextjs/aws/converters/utils.js"; + +describe("removeUndefinedFromQuery", () => { + it("should remove undefined from query", () => { + const result = removeUndefinedFromQuery({ + a: "1", + b: ["2", "3"], + c: undefined, + }); + + expect(result).toEqual({ + a: "1", + b: ["2", "3"], + }); + }); + + it("should return empty object if input is empty", () => { + const result = removeUndefinedFromQuery({}); + + expect(result).toEqual({}); + }); + + it("should return empty object if all values are undefined", () => { + const result = removeUndefinedFromQuery({ + a: undefined, + b: undefined, + }); + + expect(result).toEqual({}); + }); +});