|
| 1 | +import fsp from "node:fs/promises"; |
| 2 | +import path from "node:path"; |
| 3 | +import { createRequestHandler as createReactRequestHandler } from "react-router"; |
| 4 | +import type { |
| 5 | + APIGatewayProxyEventV2, |
| 6 | + APIGatewayProxyStructuredResultV2, |
| 7 | +} from "aws-lambda"; |
| 8 | +import lambdaTester from "lambda-tester"; |
| 9 | + |
| 10 | +import { |
| 11 | + createRequestHandler, |
| 12 | + createReactRouterHeaders, |
| 13 | + createReactRouterRequest, |
| 14 | + sendReactRouterResponse, |
| 15 | +} from "../server"; |
| 16 | + |
| 17 | +// We don't want to test that the React Router server works here, |
| 18 | +// we just want to test the architect adapter |
| 19 | +jest.mock("react-router", () => { |
| 20 | + let original = jest.requireActual("react-router"); |
| 21 | + return { |
| 22 | + ...original, |
| 23 | + createRequestHandler: jest.fn(), |
| 24 | + }; |
| 25 | +}); |
| 26 | +let mockedCreateRequestHandler = |
| 27 | + createReactRequestHandler as jest.MockedFunction< |
| 28 | + typeof createReactRequestHandler |
| 29 | + >; |
| 30 | + |
| 31 | +function createMockEvent(event: Partial<APIGatewayProxyEventV2> = {}) { |
| 32 | + let now = new Date(); |
| 33 | + return { |
| 34 | + headers: { |
| 35 | + host: "localhost:3333", |
| 36 | + accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", |
| 37 | + "upgrade-insecure-requests": "1", |
| 38 | + "user-agent": |
| 39 | + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", |
| 40 | + "accept-language": "en-US,en;q=0.9", |
| 41 | + "accept-encoding": "gzip, deflate", |
| 42 | + ...event.headers, |
| 43 | + }, |
| 44 | + isBase64Encoded: false, |
| 45 | + rawPath: "/", |
| 46 | + rawQueryString: "", |
| 47 | + requestContext: { |
| 48 | + http: { |
| 49 | + method: "GET", |
| 50 | + path: "/", |
| 51 | + protocol: "HTTP/1.1", |
| 52 | + userAgent: |
| 53 | + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", |
| 54 | + sourceIp: "127.0.0.1", |
| 55 | + ...event.requestContext?.http, |
| 56 | + }, |
| 57 | + routeKey: "ANY /{proxy+}", |
| 58 | + accountId: "accountId", |
| 59 | + requestId: "requestId", |
| 60 | + apiId: "apiId", |
| 61 | + domainName: "id.execute-api.us-east-1.amazonaws.com", |
| 62 | + domainPrefix: "id", |
| 63 | + stage: "test", |
| 64 | + time: now.toISOString(), |
| 65 | + timeEpoch: now.getTime(), |
| 66 | + ...event.requestContext, |
| 67 | + }, |
| 68 | + routeKey: "foo", |
| 69 | + version: "2.0", |
| 70 | + ...event, |
| 71 | + }; |
| 72 | +} |
| 73 | + |
| 74 | +describe("architect createRequestHandler", () => { |
| 75 | + describe("basic requests", () => { |
| 76 | + afterEach(() => { |
| 77 | + mockedCreateRequestHandler.mockReset(); |
| 78 | + }); |
| 79 | + |
| 80 | + afterAll(() => { |
| 81 | + jest.restoreAllMocks(); |
| 82 | + }); |
| 83 | + |
| 84 | + it("handles requests", async () => { |
| 85 | + mockedCreateRequestHandler.mockImplementation(() => async (req) => { |
| 86 | + return new Response(`URL: ${new URL(req.url).pathname}`); |
| 87 | + }); |
| 88 | + |
| 89 | + // We don't have a real app to test, but it doesn't matter. We won't ever |
| 90 | + // call through to the real createRequestHandler |
| 91 | + // @ts-expect-error |
| 92 | + await lambdaTester(createRequestHandler({ build: undefined })) |
| 93 | + .event(createMockEvent({ rawPath: "/foo/bar" })) |
| 94 | + .expectResolve((res: any) => { |
| 95 | + expect(res.statusCode).toBe(200); |
| 96 | + expect(res.body).toBe("URL: /foo/bar"); |
| 97 | + }); |
| 98 | + }); |
| 99 | + |
| 100 | + it("handles root // requests", async () => { |
| 101 | + mockedCreateRequestHandler.mockImplementation(() => async (req) => { |
| 102 | + return new Response(`URL: ${new URL(req.url).pathname}`); |
| 103 | + }); |
| 104 | + |
| 105 | + // We don't have a real app to test, but it doesn't matter. We won't ever |
| 106 | + // call through to the real createRequestHandler |
| 107 | + // @ts-expect-error |
| 108 | + await lambdaTester(createRequestHandler({ build: undefined })) |
| 109 | + .event(createMockEvent({ rawPath: "//" })) |
| 110 | + .expectResolve((res: any) => { |
| 111 | + expect(res.statusCode).toBe(200); |
| 112 | + expect(res.body).toBe("URL: //"); |
| 113 | + }); |
| 114 | + }); |
| 115 | + |
| 116 | + it("handles nested // requests", async () => { |
| 117 | + mockedCreateRequestHandler.mockImplementation(() => async (req) => { |
| 118 | + return new Response(`URL: ${new URL(req.url).pathname}`); |
| 119 | + }); |
| 120 | + |
| 121 | + // We don't have a real app to test, but it doesn't matter. We won't ever |
| 122 | + // call through to the real createRequestHandler |
| 123 | + // @ts-expect-error |
| 124 | + await lambdaTester(createRequestHandler({ build: undefined })) |
| 125 | + .event(createMockEvent({ rawPath: "//foo//bar" })) |
| 126 | + .expectResolve((res: APIGatewayProxyStructuredResultV2) => { |
| 127 | + expect(res.statusCode).toBe(200); |
| 128 | + expect(res.body).toBe("URL: //foo//bar"); |
| 129 | + }); |
| 130 | + }); |
| 131 | + |
| 132 | + it("handles null body", async () => { |
| 133 | + mockedCreateRequestHandler.mockImplementation(() => async () => { |
| 134 | + return new Response(null, { status: 200 }); |
| 135 | + }); |
| 136 | + |
| 137 | + // We don't have a real app to test, but it doesn't matter. We won't ever |
| 138 | + // call through to the real createRequestHandler |
| 139 | + // @ts-expect-error |
| 140 | + await lambdaTester(createRequestHandler({ build: undefined })) |
| 141 | + .event(createMockEvent({ rawPath: "/foo/bar" })) |
| 142 | + .expectResolve((res: APIGatewayProxyStructuredResultV2) => { |
| 143 | + expect(res.statusCode).toBe(200); |
| 144 | + }); |
| 145 | + }); |
| 146 | + |
| 147 | + it("handles status codes", async () => { |
| 148 | + mockedCreateRequestHandler.mockImplementation(() => async () => { |
| 149 | + return new Response(null, { status: 204 }); |
| 150 | + }); |
| 151 | + |
| 152 | + // We don't have a real app to test, but it doesn't matter. We won't ever |
| 153 | + // call through to the real createRequestHandler |
| 154 | + // @ts-expect-error |
| 155 | + await lambdaTester(createRequestHandler({ build: undefined })) |
| 156 | + .event(createMockEvent({ rawPath: "/foo/bar" })) |
| 157 | + .expectResolve((res: APIGatewayProxyStructuredResultV2) => { |
| 158 | + expect(res.statusCode).toBe(204); |
| 159 | + }); |
| 160 | + }); |
| 161 | + |
| 162 | + it("sets headers", async () => { |
| 163 | + mockedCreateRequestHandler.mockImplementation(() => async () => { |
| 164 | + let headers = new Headers(); |
| 165 | + headers.append("X-Time-Of-Year", "most wonderful"); |
| 166 | + headers.append( |
| 167 | + "Set-Cookie", |
| 168 | + "first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax" |
| 169 | + ); |
| 170 | + headers.append( |
| 171 | + "Set-Cookie", |
| 172 | + "second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax" |
| 173 | + ); |
| 174 | + headers.append( |
| 175 | + "Set-Cookie", |
| 176 | + "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax" |
| 177 | + ); |
| 178 | + |
| 179 | + return new Response(null, { headers }); |
| 180 | + }); |
| 181 | + |
| 182 | + // We don't have a real app to test, but it doesn't matter. We won't ever |
| 183 | + // call through to the real createRequestHandler |
| 184 | + // @ts-expect-error |
| 185 | + await lambdaTester(createRequestHandler({ build: undefined })) |
| 186 | + .event(createMockEvent({ rawPath: "/" })) |
| 187 | + .expectResolve((res: APIGatewayProxyStructuredResultV2) => { |
| 188 | + expect(res.statusCode).toBe(200); |
| 189 | + expect(res.headers?.["x-time-of-year"]).toBe("most wonderful"); |
| 190 | + expect(res.cookies).toEqual([ |
| 191 | + "first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax", |
| 192 | + "second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax", |
| 193 | + "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax", |
| 194 | + ]); |
| 195 | + }); |
| 196 | + }); |
| 197 | + }); |
| 198 | +}); |
| 199 | + |
| 200 | +describe("architect createReactRouterHeaders", () => { |
| 201 | + describe("creates fetch headers from architect headers", () => { |
| 202 | + it("handles empty headers", () => { |
| 203 | + let headers = createReactRouterHeaders({}); |
| 204 | + expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`{}`); |
| 205 | + }); |
| 206 | + |
| 207 | + it("handles simple headers", () => { |
| 208 | + let headers = createReactRouterHeaders({ "x-foo": "bar" }); |
| 209 | + expect(headers.get("x-foo")).toBe("bar"); |
| 210 | + }); |
| 211 | + |
| 212 | + it("handles multiple headers", () => { |
| 213 | + let headers = createReactRouterHeaders({ |
| 214 | + "x-foo": "bar", |
| 215 | + "x-bar": "baz", |
| 216 | + }); |
| 217 | + expect(headers.get("x-foo")).toBe("bar"); |
| 218 | + expect(headers.get("x-bar")).toBe("baz"); |
| 219 | + }); |
| 220 | + |
| 221 | + it("handles headers with multiple values", () => { |
| 222 | + let headers = createReactRouterHeaders({ |
| 223 | + "x-foo": "bar, baz", |
| 224 | + "x-bar": "baz", |
| 225 | + }); |
| 226 | + expect(headers.get("x-foo")).toEqual("bar, baz"); |
| 227 | + expect(headers.get("x-bar")).toBe("baz"); |
| 228 | + }); |
| 229 | + |
| 230 | + it("handles multiple request cookies", () => { |
| 231 | + let headers = createReactRouterHeaders({}, [ |
| 232 | + "__session=some_value", |
| 233 | + "__other=some_other_value", |
| 234 | + ]); |
| 235 | + expect(headers.get("cookie")).toEqual( |
| 236 | + "__session=some_value; __other=some_other_value" |
| 237 | + ); |
| 238 | + }); |
| 239 | + }); |
| 240 | +}); |
| 241 | + |
| 242 | +describe("architect createReactRouterRequest", () => { |
| 243 | + it("creates a request with the correct headers", () => { |
| 244 | + let request = createReactRouterRequest( |
| 245 | + createMockEvent({ cookies: ["__session=value"] }) |
| 246 | + ); |
| 247 | + |
| 248 | + expect(request.method).toBe("GET"); |
| 249 | + expect(request.headers.get("cookie")).toBe("__session=value"); |
| 250 | + }); |
| 251 | +}); |
| 252 | + |
| 253 | +describe("sendReactRouterResponse", () => { |
| 254 | + it("handles regular responses", async () => { |
| 255 | + let response = new Response("anything"); |
| 256 | + let result = await sendReactRouterResponse(response); |
| 257 | + expect(result.body).toBe("anything"); |
| 258 | + }); |
| 259 | + |
| 260 | + it("handles resource routes with regular data", async () => { |
| 261 | + let json = JSON.stringify({ foo: "bar" }); |
| 262 | + let response = new Response(json, { |
| 263 | + headers: { |
| 264 | + "Content-Type": "application/json", |
| 265 | + "content-length": json.length.toString(), |
| 266 | + }, |
| 267 | + }); |
| 268 | + |
| 269 | + let result = await sendReactRouterResponse(response); |
| 270 | + |
| 271 | + expect(result.body).toMatch(json); |
| 272 | + }); |
| 273 | + |
| 274 | + it("handles resource routes with binary data", async () => { |
| 275 | + let image = await fsp.readFile(path.join(__dirname, "554828.jpeg")); |
| 276 | + |
| 277 | + let response = new Response(image, { |
| 278 | + headers: { |
| 279 | + "content-type": "image/jpeg", |
| 280 | + "content-length": image.length.toString(), |
| 281 | + }, |
| 282 | + }); |
| 283 | + |
| 284 | + let result = await sendReactRouterResponse(response); |
| 285 | + |
| 286 | + expect(result.body).toMatch(image.toString("base64")); |
| 287 | + }); |
| 288 | +}); |
0 commit comments