Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 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
3 changes: 2 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,7 @@
"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-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