Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@
- pacexy
- pcattori
- penspinner
- penx
- phishy
- plastic041
- princerajroy
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"packages/remix-dev",
"packages/remix-eslint-config",
"packages/remix-express",
"packages/remix-google-cloud-functions",
"packages/remix-netlify",
"packages/remix-node",
"packages/remix-react",
Expand Down Expand Up @@ -82,7 +83,8 @@
"eslint-plugin-markdown": "^2.2.1",
"eslint-plugin-prefer-let": "^3.0.1",
"express": "^4.17.1",
"jest": "^27.5.1",
"jest": "^28.1.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate why this is necessary?

I don't see any indication in @google-cloud/functions-framework why jest@^28.0.0 is necessary.

Copy link
Contributor Author

@penx penx Jun 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { getTestServer } from "@google-cloud/functions-framework/testing";

This import makes use of package exports:

https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/759aac18ad297363c4651efcf19ab2bf8fa9625a/package.json#L13

Full support for this was added in Jest 28.

In Jest 27, server-test.ts will fail saying it cannot find @google-cloud/functions-framework/testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll open up a PR for the Jest upgrade shortly, which will need to be merged before this one. I can remove the commits from this branch too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this in a separate PR instead, so we keep this PR focussed.

I think the Jest 28 change is also causing some tests to fail, so we could solve them in isolation in the new PR.

"jest-environment-jsdom": "^28.1.0",
"jest-watch-select-projects": "^2.0.0",
"jest-watch-typeahead": "^0.6.5",
"jsonfile": "^6.0.1",
Expand Down
13 changes: 13 additions & 0 deletions packages/remix-google-cloud-functions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Welcome to Remix!

[Remix](https://remix.run) is a web framework that helps you build better websites with React.

To get started, open a new shell and run:

```sh
npx create-remix@latest
```

Then follow the prompts you see in your terminal.

For more information about Remix, [visit remix.run](https://remix.run)!
272 changes: 272 additions & 0 deletions packages/remix-google-cloud-functions/__tests__/server-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import supertest from "supertest";
import { createRequest } from "node-mocks-http";
import {
createRequestHandler as createRemixRequestHandler,
Response as NodeResponse,
} from "@remix-run/node";
import { Readable } from "stream";
import { http } from "@google-cloud/functions-framework";
// @ts-ignore
import { getTestServer } from "@google-cloud/functions-framework/testing";

import {
createRemixHeaders,
createRemixRequest,
createRequestHandler,
} from "../server";

// We don't want to test that the remix server works here (that's what the
// puppetteer tests do), we just want to test the express adapter
jest.mock("@remix-run/node", () => {
let original = jest.requireActual("@remix-run/node");
return {
...original,
createRequestHandler: jest.fn(),
};
});
let mockedCreateRequestHandler =
createRemixRequestHandler as jest.MockedFunction<
typeof createRemixRequestHandler
>;

function createApp() {
http(
"remixServer",
createRequestHandler({
// We don't have a real app to test, but it doesn't matter. We
// won't ever call through to the real createRequestHandler
build: undefined,
})
);
return getTestServer("remixServer");
}

describe("express createRequestHandler", () => {
describe("basic requests", () => {
afterEach(() => {
mockedCreateRequestHandler.mockReset();
});

afterAll(() => {
jest.restoreAllMocks();
});

it("handles requests", async () => {
mockedCreateRequestHandler.mockImplementation(() => async (req) => {
return new Response(`URL: ${new URL(req.url).pathname}`);
});

let request = supertest(createApp());
let res = await request.get("/foo/bar");

expect(res.status).toBe(200);
expect(res.text).toBe("URL: /foo/bar");
expect(res.headers["x-powered-by"]).toBe(undefined);
});

it("handles null body", async () => {
mockedCreateRequestHandler.mockImplementation(() => async () => {
return new Response(null, { status: 200 });
});

let request = supertest(createApp());
let res = await request.get("/");

expect(res.status).toBe(200);
});

// https://github.com/node-fetch/node-fetch/blob/4ae35388b078bddda238277142bf091898ce6fda/test/response.js#L142-L148
it("handles body as stream", async () => {
mockedCreateRequestHandler.mockImplementation(() => async () => {
let stream = Readable.from("hello world");
return new NodeResponse(stream, { status: 200 }) as unknown as Response;
});

let request = supertest(createApp());
// note: vercel's createServerWithHelpers requires a x-now-bridge-request-id
let res = await request.get("/").set({ "x-now-bridge-request-id": "2" });

expect(res.status).toBe(200);
expect(res.text).toBe("hello world");
});

it("handles status codes", async () => {
mockedCreateRequestHandler.mockImplementation(() => async () => {
return new Response(null, { status: 204 });
});

let request = supertest(createApp());
let res = await request.get("/");

expect(res.status).toBe(204);
});

it("sets headers", async () => {
mockedCreateRequestHandler.mockImplementation(() => async () => {
let headers = new Headers({ "X-Time-Of-Year": "most wonderful" });
headers.append(
"Set-Cookie",
"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax"
);
headers.append(
"Set-Cookie",
"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax"
);
headers.append(
"Set-Cookie",
"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax"
);
return new Response(null, { headers });
});

let request = supertest(createApp());
let res = await request.get("/");

expect(res.headers["x-time-of-year"]).toBe("most wonderful");
expect(res.headers["set-cookie"]).toEqual([
"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax",
"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax",
"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax",
]);
});
});
});

describe("express createRemixHeaders", () => {
describe("creates fetch headers from express headers", () => {
it("handles empty headers", () => {
expect(createRemixHeaders({})).toMatchInlineSnapshot(`
Headers {
Symbol(query): Array [],
Symbol(context): null,
}
`);
});

it("handles simple headers", () => {
expect(createRemixHeaders({ "x-foo": "bar" })).toMatchInlineSnapshot(`
Headers {
Symbol(query): Array [
"x-foo",
"bar",
],
Symbol(context): null,
}
`);
});

it("handles multiple headers", () => {
expect(createRemixHeaders({ "x-foo": "bar", "x-bar": "baz" }))
.toMatchInlineSnapshot(`
Headers {
Symbol(query): Array [
"x-foo",
"bar",
"x-bar",
"baz",
],
Symbol(context): null,
}
`);
});

it("handles headers with multiple values", () => {
expect(createRemixHeaders({ "x-foo": "bar, baz" }))
.toMatchInlineSnapshot(`
Headers {
Symbol(query): Array [
"x-foo",
"bar, baz",
],
Symbol(context): null,
}
`);
});

it("handles headers with multiple values and multiple headers", () => {
expect(createRemixHeaders({ "x-foo": "bar, baz", "x-bar": "baz" }))
.toMatchInlineSnapshot(`
Headers {
Symbol(query): Array [
"x-foo",
"bar, baz",
"x-bar",
"baz",
],
Symbol(context): null,
}
`);
});

it("handles multiple set-cookie headers", () => {
expect(
createRemixHeaders({
"set-cookie": [
"__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
"__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax",
],
})
).toMatchInlineSnapshot(`
Headers {
Symbol(query): Array [
"set-cookie",
"__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax",
"set-cookie",
"__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax",
],
Symbol(context): null,
}
`);
});
});
});

describe("express createRemixRequest", () => {
it("creates a request with the correct headers", async () => {
let expressRequest = createRequest({
url: "/foo/bar",
method: "GET",
protocol: "http",
hostname: "localhost",
headers: {
"Cache-Control": "max-age=300, s-maxage=3600",
Host: "localhost:3000",
},
});

expect(createRemixRequest(expressRequest)).toMatchInlineSnapshot(`
NodeRequest {
"agent": undefined,
"compress": true,
"counter": 0,
"follow": 20,
"highWaterMark": 16384,
"insecureHTTPParser": false,
"size": 0,
Symbol(Body internals): Object {
"body": null,
"boundary": null,
"disturbed": false,
"error": null,
"size": 0,
"type": null,
},
Symbol(Request internals): Object {
"headers": Headers {
Symbol(query): Array [
"cache-control",
"max-age=300, s-maxage=3600",
"host",
"localhost:3000",
],
Symbol(context): null,
},
"method": "GET",
"parsedURL": "http://localhost:3000/foo/bar",
"redirect": "follow",
"signal": AbortSignal {},
},
}
`);
});
});
2 changes: 2 additions & 0 deletions packages/remix-google-cloud-functions/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { installGlobals } from "@remix-run/node";
installGlobals();
2 changes: 2 additions & 0 deletions packages/remix-google-cloud-functions/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { installGlobals } from "@remix-run/node";
installGlobals();
4 changes: 4 additions & 0 deletions packages/remix-google-cloud-functions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import "./globals";

export type { GetLoadContextFunction, RequestHandler } from "./server";
export { createRequestHandler } from "./server";
5 changes: 5 additions & 0 deletions packages/remix-google-cloud-functions/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
...require("../../jest/jest.config.shared"),
displayName: "google-cloud-functions",
};
23 changes: 23 additions & 0 deletions packages/remix-google-cloud-functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@remix-run/google-cloud-functions",
"description": "Google Cloud functions request handler for Remix",
"version": "1.5.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/remix-run/remix",
"directory": "packages/remix-google-cloud-functions"
},
"bugs": {
"url": "https://github.com/remix-run/remix/issues"
},
"dependencies": {
"@google-cloud/functions-framework": "^3.1.1",
"@remix-run/node": "1.5.1"
},
"devDependencies": {
"@types/supertest": "^2.0.10",
"node-mocks-http": "^1.10.1",
"supertest": "^6.0.1"
}
}
Loading