Skip to content

Commit a9d49e3

Browse files
Move constants & env to central config (#107)
* Move constants & process.env to central config * Update jest test mocking to match config change
1 parent 5e09100 commit a9d49e3

25 files changed

+213
-133
lines changed

.eslintrc.cjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module.exports = {
2727
"arrow-body-style": ["error", "always"],
2828
curly: ["error", "all"],
2929
"no-lonely-if": ["error"],
30-
"no-magic-numbers": ["error", { ignoreClassFieldInitialValues: true }],
30+
"no-magic-numbers": ["error", { ignoreClassFieldInitialValues: true, ignore: [0] }],
3131
"no-multi-assign": ["error"],
3232
"no-nested-ternary": ["error"],
3333
"no-var": ["error"],
@@ -43,5 +43,11 @@ module.exports = {
4343
"@typescript-eslint/no-var-requires": "off", // Required for jest
4444
},
4545
},
46+
{
47+
files: ["src/config.ts"], // Disable specific rules for config
48+
rules: {
49+
"no-magic-numbers": "off",
50+
},
51+
},
4652
],
4753
};

.test.env

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# IMPORTANT: DO NOT PUT REAL CREDENTIALS HERE!!!!
22

3+
# Regex for CORS policies
4+
PROD_REGEX=".*"
5+
DEPLOY_REGEX=".*"
6+
37
# ----- GENERAL CREDENTIALS -----
48
DB_USERNAME=test-username
59
DB_PASSWORD=test-password
@@ -14,3 +18,6 @@ GOOGLE_OAUTH_ID="123456789.apps.googleusercontent.com"
1418
GOOGLE_OAUTH_SECRET="123456789"
1519

1620
JWT_SECRET="123456789"
21+
22+
# System administrators
23+
SYSTEM_ADMINS=""

jest.presetup.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { configDotenv } from "dotenv";
1+
import dotenv from "dotenv";
22
import path from "path";
33
import { jest } from "@jest/globals";
4+
import { readFileSync } from "fs";
45

56
// Mock the env loading to load from .test.env instead
67
jest.mock("./src/env.js", () => {
7-
configDotenv({
8-
path: path.join(__dirname, ".test.env"),
9-
});
8+
const rawEnv = readFileSync(path.join(__dirname, ".test.env"));
9+
const env = dotenv.parse(rawEnv);
1010

1111
return {
12-
TEST: true,
12+
default: env,
13+
__esModule: true,
1314
};
1415
});
1516

jest.setup.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import { beforeEach, afterEach, expect, jest } from "@jest/globals";
22
import { MatcherState } from "expect";
33
import { MongoMemoryServer } from "mongodb-memory-server";
4+
import * as Config from "./src/config.js";
5+
6+
function mockConfig(dbUrl: string) {
7+
jest.mock("./src/config.js", () => {
8+
const actual = jest.requireActual("./src/config.js") as typeof Config;
9+
10+
const newConfig: typeof Config.default = {
11+
...actual.default,
12+
TEST: true,
13+
DB_URL: dbUrl,
14+
};
15+
16+
return {
17+
...actual,
18+
default: newConfig,
19+
__esModule: true,
20+
};
21+
});
22+
}
423

524
function getIdForState(state: MatcherState): string {
625
return `${state.testPath}: ${state.currentTestName}`;
@@ -9,8 +28,6 @@ function getIdForState(state: MatcherState): string {
928
const servers = new Map<string, MongoMemoryServer>();
1029

1130
beforeEach(async () => {
12-
const baseUrl = require("./src/database/base-url.js");
13-
1431
const id = getIdForState(expect.getState());
1532

1633
if (servers.has(id)) {
@@ -21,7 +38,7 @@ beforeEach(async () => {
2138

2239
servers.set(id, mongod);
2340

24-
jest.spyOn(baseUrl, "getBaseURL").mockReturnValue(mongod.getUri());
41+
mockConfig(mongod.getUri());
2542
});
2643

2744
afterEach(async () => {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"format": "yarn run prettier --write '**/*.{ts,js,cjs,json}'",
1515
"lint:check": "yarn run eslint src --ext .js,.jsx,.ts,.tsx",
1616
"lint": "yarn run eslint src --ext .js,.jsx,.ts,.tsx --fix",
17-
"test": "jest",
17+
"test": "jest --verbose",
1818
"build": "tsc",
1919
"verify": "yarn build && yarn lint:check && yarn format:check",
2020
"serve": "node scripts/serve.mjs",

src/app.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { TEST } from "./env.js";
2-
31
import morgan from "morgan";
42
import express, { Application, Request, Response } from "express";
53

@@ -15,6 +13,7 @@ import admissionRouter from "./services/admission/admission-router.js";
1513
import { InitializeConfigReader } from "./middleware/config-reader.js";
1614
import Models from "./database/models.js";
1715
import { StatusCode } from "status-code-enum";
16+
import Config from "./config.js";
1817

1918
const app: Application = express();
2019

@@ -24,7 +23,7 @@ const app: Application = express();
2423
app.use(InitializeConfigReader);
2524

2625
// Enable request output when not a test
27-
if (!TEST) {
26+
if (!Config.TEST) {
2827
app.use(morgan("dev"));
2928
}
3029

@@ -58,7 +57,7 @@ export function setupServer(): void {
5857

5958
export function startServer(): Promise<Express.Application> {
6059
// eslint-disable-next-line no-magic-numbers
61-
const port = process.env.PORT || 3000;
60+
const port = Config.PORT;
6261

6362
return new Promise((resolve) => {
6463
// Setup server

src/config.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* This file defines all config used anywhere in the api. These values need to be defined on import.
3+
*
4+
* By moving all env variable usage to one place, we also make managing their usage much easier, and
5+
* can error if they are not defined.
6+
*/
7+
8+
import env from "./env.js";
9+
10+
export enum Device {
11+
ADMIN = "admin",
12+
DEV = "dev",
13+
WEB = "web",
14+
IOS = "ios",
15+
ANDROID = "android",
16+
}
17+
18+
function requireEnv(name: string): string {
19+
const value = env[name];
20+
21+
if (value === undefined) {
22+
throw new Error(`Env variable ${name} is not defined!`);
23+
}
24+
25+
return value;
26+
}
27+
28+
const Config = {
29+
/* Jest */
30+
TEST: false, // False by default, will be mocked over
31+
32+
/* URLs */
33+
PORT: env.PORT ? parseInt(env.PORT) : 3000,
34+
35+
DEFAULT_DEVICE: Device.WEB,
36+
37+
REDIRECT_URLS: new Map([
38+
[Device.ADMIN, "https://admin.hackillinois.org/auth/"],
39+
[Device.DEV, "https://adonix.hackillinois.org/auth/dev/"],
40+
[Device.WEB, "https://www.hackillinois.org/auth/"],
41+
[Device.IOS, "hackillinois://login/"],
42+
[Device.ANDROID, "hackillinois://login/"],
43+
]) as Map<string, string>,
44+
45+
CALLBACK_URLS: {
46+
GITHUB: "https://adonix.hackillinois.org/auth/github/callback/",
47+
// GITHUB: "http://localhost:3000/auth/github/callback/",
48+
GOOGLE: "https://adonix.hackillinois.org/auth/google/callback/",
49+
// GOOGLE: "http://127.0.0.1:3000/auth/google/callback/",
50+
},
51+
52+
METADATA_URL: "https://hackillinois.github.io/adonix-metadata/config.json",
53+
54+
/* OAuth, Keys, & Permissions */
55+
DB_URL: `mongodb+srv://${requireEnv("DB_USERNAME")}:${requireEnv("DB_PASSWORD")}@${requireEnv("DB_SERVER")}/`,
56+
57+
GITHUB_OAUTH_ID: requireEnv("GITHUB_OAUTH_ID"),
58+
GITHUB_OAUTH_SECRET: requireEnv("GITHUB_OAUTH_SECRET"),
59+
60+
GOOGLE_OAUTH_ID: requireEnv("GOOGLE_OAUTH_ID"),
61+
GOOGLE_OAUTH_SECRET: requireEnv("GOOGLE_OAUTH_SECRET"),
62+
63+
JWT_SECRET: requireEnv("JWT_SECRET"),
64+
65+
NEWSLETTER_CORS: {
66+
PROD_REGEX: requireEnv("PROD_REGEX"),
67+
DEPLOY_REGEX: requireEnv("DEPLOY_REGEX"),
68+
},
69+
70+
SYSTEM_ADMIN_LIST: requireEnv("SYSTEM_ADMINS").split(","),
71+
72+
/* Timings */
73+
MILLISECONDS_PER_SECOND: 1000,
74+
DEFAULT_JWT_EXPIRY_TIME: "24h",
75+
QR_EXPIRY_TIME: "20s",
76+
77+
/* Defaults */
78+
DEFAULT_POINT_VALUE: 0,
79+
DEFAULT_FOOD_WAVE: 0,
80+
81+
/* Limits */
82+
LEADERBOARD_QUERY_LIMIT: 25,
83+
84+
/* Misc */
85+
EVENT_ID_LENGTH: 32,
86+
EVENT_BYTES_GEN: 16,
87+
};
88+
89+
export default Config;

src/constants.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/database.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { IModelOptions } from "@typegoose/typegoose/lib/types.js";
2-
import { getBaseURL } from "./database/base-url.js";
2+
import Config from "./config.js";
33
import mongoose from "mongoose";
44

55
const params: string = "?retryWrites=true&w=majority";
66
const existingConnections: Map<string, mongoose.Connection> = new Map();
77

88
export function connectToMongoose(dbName: string): mongoose.Connection {
9-
const url: string = `${getBaseURL()}${dbName}${params}`;
9+
const url: string = `${Config.DB_URL}${dbName}${params}`;
1010

1111
let database: mongoose.Connection | undefined = existingConnections.get(dbName);
1212

src/database/base-url.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/database/event-db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { modelOptions, prop } from "@typegoose/typegoose";
22

3-
import Constants from "../constants.js";
3+
import Config from "../config.js";
44
import { GenericEventFormat } from "../services/event/event-formats.js";
55

66
// Interface for the location of the event
@@ -102,7 +102,7 @@ export class PublicEvent extends BaseEvent {
102102
this.isPrivate = baseEvent.isPrivate ?? false;
103103
this.displayOnStaffCheckIn = baseEvent.displayOnStaffCheckIn ?? false;
104104
this.sponsor = baseEvent.sponsor ?? "";
105-
this.points = baseEvent.points ?? Constants.DEFAULT_POINT_VALUE;
105+
this.points = baseEvent.points ?? Config.DEFAULT_POINT_VALUE;
106106
}
107107
}
108108

src/env.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
1-
import { configDotenv } from "dotenv";
1+
/*
2+
* This file loads the env variables we will use at runtime. Instead of relying on system envs dynamically,
3+
* we instead parse the .env file, and overwrite any existing variables with system variables.
4+
* Basically, .env file vars can be overwritten by system level env vars.
5+
*
6+
* The .env is also optional so that env vars can be entirely defined with system vars if needed, like for vercel.
7+
*/
28

3-
export const TEST = false;
9+
import dotenv from "dotenv";
10+
import { existsSync, readFileSync } from "fs";
11+
import path from "path";
412

5-
configDotenv();
13+
const envFilePath = path.join(process.cwd(), ".env");
14+
const rawEnv = existsSync(envFilePath) ? readFileSync(envFilePath) : "";
15+
const env = dotenv.parse(rawEnv);
16+
17+
for (const key in process.env) {
18+
const value = process.env[key];
19+
20+
if (value === undefined) {
21+
continue;
22+
}
23+
24+
env[key] = value;
25+
}
26+
27+
export default env;

src/middleware/config-reader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextFunction, Request, Response } from "express";
2-
import Constants from "../constants.js";
2+
import Config from "../config.js";
33
import axios, { AxiosResponse } from "axios";
44

55
interface ConfigFormat {
@@ -20,7 +20,7 @@ export class ConfigReader {
2020
static androidVersion: string;
2121

2222
async initialize(): Promise<void> {
23-
const url: string = Constants.METADATA_URL;
23+
const url: string = Config.METADATA_URL;
2424

2525
const response: AxiosResponse = await axios.get(url);
2626
const configData: ConfigFormat = response.data as ConfigFormat;

0 commit comments

Comments
 (0)