Skip to content

Commit

Permalink
Rationalise env var usage
Browse files Browse the repository at this point in the history
  • Loading branch information
textbook committed Jul 27, 2023
1 parent f5b5332 commit cdd6bb7
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 46 deletions.
26 changes: 20 additions & 6 deletions .devcontainer/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
# .devcontainer

Allows the project to be started in a [dev container] for GitHub Codespaces\* or VS Code.
Allows the project to be started in a [dev container] for [GitHub Codespaces] or VS Code.

- `devcontainer.json`: Defines the development environment.
- `docker-compose.yml`: Defines the services (including the dev container) - extends the root `docker-compose.yml`.
- `Dockerfile`: Defines the app's dev container (Debian, Node 18, PostgreSQL client).

\* _OAuth login doesn't currently work in the remote browser env._

## Setup

Follow the same instructions in the root README, except that:

- you can skip step 3 (as the environment variables are set for you);
- you can skip step 4 (as the services are started for you); and
- if `npx cypress verify` says Cypress isn't installed inside the container run `npx cypress install`.

If you want to connect directly to the DB, you can use `psql $DATABASE_URL`.

The environment is set up for the tests, to allow visiting the site and logging in from a local browser you need to
set the env var `OAUTH_AUTHORIZE_ENDPOINT=http://localhost:4212/login/oauth/authorize` before `npm run dev` or
`npm run serve`.
The environment is set up for the tests; to allow visiting the site and logging in from a local browser you need to
set the env var `OAUTH_AUTHORIZE_ENDPOINT=http://localhost:4212/login/oauth/authorize` (e.g. in a `.env` file) before
`npm run dev` or `npm run serve`.

### Codespaces login

Because of the way Codespaces work, it's even more awkward to get the login working. As above you can override the
`/authorize` endpoint, but you need to use the app's public URL (this port will be automatically forwarded):

```
OAUTH_AUTHORIZE_ENDPOINT="https://$CODESPACE_NAME-4212.preview.app.github.dev/login/oauth/authorize"
```

Now the "Log in" link will take you to right place (you may need to explicitly [forward] `gh:4212` to see the login
screen). However, this is going to try to redirect you _back_ to `http://localhost:4201`. Copy the query parameter and
visit `https://$CODESPACE_NAME-4201.preview.app.github.dev/api/auth/callback?code=<copied>` yourself.

[dev container]: https://code.visualstudio.com/docs/devcontainers/containers
[forward]: https://docs.github.com/en/codespaces/developing-in-codespaces/forwarding-ports-in-your-codespace
[github codespaces]: https://docs.github.com/en/codespaces
12 changes: 10 additions & 2 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@ services:
environment:
DATABASE_URL: postgres://postgres:@db:5432/postgres?sslmode=disable
OAUTH_BASE_URL: http://gh:4212/login/oauth
OAUTH_CLIENT_ID: fake-client-id
OAUTH_CLIENT_SECRET: fake-client-secret
GH_API_BASE_URL: http://gh:4212/api
OAUTH_CLIENT_ID: fakecb2e25b0b3a44f9b
OAUTH_CLIENT_SECRET: fake1761003c4f22016bb715ae01545f84474b3e
SESSION_SECRET: 6d3d8c476e06166175c6
SUDO_TOKEN: fake2f92040527fcba40
network_mode: service:db
volumes:
- ..:/workspaces:cached
db:
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
gh:
environment:
FAUXAUTH_CALLBACK_URL: http://localhost:4201/api/auth/callback
FAUXAUTH_CLIENT_ID: fakecb2e25b0b3a44f9b
FAUXAUTH_CLIENT_SECRET: fake1761003c4f22016bb715ae01545f84474b3e

volumes:
postgres-data:
9 changes: 7 additions & 2 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ on:

jobs:
build:
env:
DATABASE_URL: postgres://postgres:@localhost:4211/postgres
runs-on: ubuntu-22.04
steps:
- name: Check out source code
Expand All @@ -23,8 +25,6 @@ jobs:
run: docker compose up --detach
- name: Install dependencies
run: npm ci
- name: Set up environment
run: cp .env.example .env
- name: Apply database migrations
run: npm run migration -- up
- name: Check code style
Expand All @@ -33,6 +33,11 @@ jobs:
run: npm run test -- --coverage
env:
FORCE_COLOR: true
LOG_LEVEL: debug
OAUTH_CLIENT_ID: unused
OAUTH_CLIENT_SECRET: unused
SESSION_SECRET: correct-horse-battery-staple
SUDO_TOKEN: make-me-a-sandwich
- name: Run end-to-end tests
run: npm run e2e
env:
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ e2e/videos/
node_modules/
reports/
.DS_Store
.env
/.env
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ The prerequisites are:
- Node and NPM (see `engines` field in `package.json` for compatible versions)
- Docker Compose

If you're using devcontainers, see `.devcontainer/README.md`. Otherwise:
If you're using [dev containers], see `.devcontainer/README.md`. Otherwise:

1. Clone the repository
2. Run `npm ci` to install the dependencies
3. Copy `.env.example` to `.env` and update if needed
3. Copy `e2e/.env` to `.env` and update as needed
4. Run `npm run services:start` to start the services
5. Run `npm run migration -- up` to migrate the database
6. Run `npm run ship` to ensure that the tests pass
Expand All @@ -27,3 +27,5 @@ If you're using devcontainers, see `.devcontainer/README.md`. Otherwise:
- 4202: Express server (dev mode only)
- 4211: Postgres
- 4212: GitStub (mock GitHub OAuth/API)

[dev containers]: https://code.visualstudio.com/docs/devcontainers/containers
File renamed without changes.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dev": "concurrently --kill-others \"npm:dev:*\"",
"dev:client": "webpack serve --config client/webpack/dev.config.js",
"dev:server": "cross-env LOG_LEVEL=debug PORT=4202 nodemon --config .config/nodemon.json --inspect server/server.js",
"e2e": "concurrently --kill-others --success first --names \"app,e2e\" \"npm:serve\" \"npm:e2e:run\"",
"e2e": "cross-env DOTENV_CONFIG_PATH=e2e/.env concurrently --kill-others --success first --names \"app,e2e\" \"npm:serve\" \"npm:e2e:run\"",
"pree2e:run": "wait-on --log --timeout 60000 http-get://localhost:4201",
"e2e:run": "cypress run",
"lint": "npm run lint:eslint . && npm run lint:prettier -- --check .",
Expand Down
10 changes: 5 additions & 5 deletions server/resources/resources.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
import request from "supertest";

import app from "../app";
import { patterns } from "../setupTests";
import { patterns, sudoToken } from "../setupTests";

describe("/api/resources", () => {
describe("POST /", () => {
Expand Down Expand Up @@ -58,7 +58,7 @@ describe("/api/resources", () => {
const { body } = await request(app)
.get("/api/resources")
.query({ drafts: true })
.set("Authorization", `Bearer ${process.env.SUDO_TOKEN}`)
.set("Authorization", `Bearer ${sudoToken}`)
.set("User-Agent", "supertest")
.expect(200);

Expand Down Expand Up @@ -96,7 +96,7 @@ describe("/api/resources", () => {
const { body: updated } = await request(app)
.patch(`/api/resources/${resource.id}`)
.send({ draft: false })
.set("Authorization", `Bearer ${process.env.SUDO_TOKEN}`)
.set("Authorization", `Bearer ${sudoToken}`)
.set("User-Agent", "supertest")
.expect(200);

Expand Down Expand Up @@ -125,7 +125,7 @@ describe("/api/resources", () => {
await request(app)
.patch(`/api/resources/${resource.id}`)
.send({ draft: true, title: "Something else" })
.set("Authorization", `Bearer ${process.env.SUDO_TOKEN}`)
.set("Authorization", `Bearer ${sudoToken}`)
.set("User-Agent", "supertest")
.expect(400);
});
Expand All @@ -134,7 +134,7 @@ describe("/api/resources", () => {
await request(app)
.patch(`/api/resources/${randomUUID()}`)
.send({ draft: false })
.set("Authorization", `Bearer ${process.env.SUDO_TOKEN}`)
.set("Authorization", `Bearer ${sudoToken}`)
.set("User-Agent", "supertest")
.expect(404);
});
Expand Down
48 changes: 29 additions & 19 deletions server/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const patterns = {
UUID: /[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}/,
};
export const server = setupServer();
export const { sudoToken } = config;

beforeAll(() => {
server.listen({
Expand Down Expand Up @@ -43,25 +44,34 @@ afterAll(async () => {
export const authenticateAs = async (user, email) => {
const agent = request.agent(app);
server.use(
rest.post(config.oauth.tokenURL, (req, res, ctx) => {
return res(
ctx.json({
access_token: "my-cool-token",
scope: "read:user,user:email",
token_type: "bearer",
})
);
}),
rest.get(config.oauth.userProfileURL, (req, res, ctx) => {
return res(ctx.json(user));
}),
rest.get(config.oauth.userEmailURL, (req, res, ctx) => {
return res(
ctx.json([
{ email, primary: true, verified: true, visibility: "public" },
])
);
})
rest.post(
config.oauth.tokenURL ?? "https://github.com/login/oauth/access_token",
(req, res, ctx) => {
return res(
ctx.json({
access_token: "my-cool-token",
scope: "read:user,user:email",
token_type: "bearer",
})
);
}
),
rest.get(
config.oauth.userProfileURL ?? "https://api.github.com/user",
(req, res, ctx) => {
return res(ctx.json(user));
}
),
rest.get(
config.oauth.userEmailURL ?? "https://api.github.com/user/emails",
(req, res, ctx) => {
return res(
ctx.json([
{ email, primary: true, verified: true, visibility: "public" },
])
);
}
)
);
await agent
.get("/api/auth/callback")
Expand Down
23 changes: 15 additions & 8 deletions server/utils/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@ export default {
callbackURL: process.env.OAUTH_CALLBACK_URL,
tokenURL: oauthUrl("OAUTH_ACCESS_TOKEN_ENDPOINT", "/access_token"),
userAgent: "CodeYourFuture/tech-products-demo",
userEmailURL:
process.env.GH_API_BASE_URL &&
`${process.env.GH_API_BASE_URL}/user/emails`,
userProfileURL:
process.env.GH_API_BASE_URL && `${process.env.GH_API_BASE_URL}/user`,
userEmailURL: apiUrl("GH_API_BASE_URL", "/user/emails"),
userProfileURL: apiUrl("GH_API_BASE_URL", "/user"),
},
port: parseInt(process.env.PORT ?? "4201", 10),
production: process.env.NODE_ENV?.toLowerCase() === "production",
Expand All @@ -31,6 +28,16 @@ export default {
sudoToken: process.env.SUDO_TOKEN,
};

/**
* Determine URLs for API endpoints
* @param {string} envVar - environment variable to use if available
* @param {string} endpoint - endpoint to add
* @returns {string}
*/
function apiUrl(envVar, endpoint) {
return process.env[envVar] && `${process.env[envVar]}${endpoint}`;
}

/**
* Throws an error if any required env vars are missing
* @param {string[]} required - names of required env vars
Expand All @@ -55,7 +62,7 @@ function oauthUrl(envVar, endpoint) {
if (process.env[envVar]) {
return process.env[envVar];
}
if (process.env.OAUTH_BASE_URL) {
return `${process.env.OAUTH_BASE_URL}${endpoint}`;
}
return (
process.env.OAUTH_BASE_URL && `${process.env.OAUTH_BASE_URL}${endpoint}`
);
}

0 comments on commit cdd6bb7

Please sign in to comment.