140 lines
5.3 KiB
TypeScript
140 lines
5.3 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// `.env.example` is the complete, documented list of the variables this app reads (plan item AT4.5):
|
|
// every variable the code reads is in it, every variable in it is read by something, and each has a
|
|
// comment above it saying what it is for.
|
|
|
|
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
import { join } from "node:path"
|
|
import { describe, expect, it } from "vitest"
|
|
|
|
const root = new URL("..", import.meta.url).pathname
|
|
const example = readFileSync(join(root, ".env.example"), "utf8")
|
|
|
|
const declared = [...example.matchAll(/^([A-Z][A-Z0-9_]+)=/gm)].map((match) => match[1] as string)
|
|
|
|
const walk = (dir: string): string[] =>
|
|
readdirSync(dir).flatMap((name) => {
|
|
if (["node_modules", ".next", "dist", ".logs", "__tests__"].includes(name)) return []
|
|
const path = join(dir, name)
|
|
if (statSync(path).isDirectory()) return walk(path)
|
|
return /\.(ts|tsx|mjs)$/.test(name) ? [path] : []
|
|
})
|
|
|
|
/** The variables the code of this repository reads: `process.env.X`, `env.X` and the loaders' quoted names. */
|
|
function read(): Set<string> {
|
|
const found = new Set<string>()
|
|
const files = [
|
|
...walk(join(root, "apps")),
|
|
...walk(join(root, "config", "src")),
|
|
...walk(join(root, "scripts")),
|
|
...walk(join(root, "e2e", "support")),
|
|
join(root, "better-auth", "deploy.mjs"), // kit:convex-better-auth
|
|
]
|
|
for (const file of files) {
|
|
const text = readFileSync(file, "utf8")
|
|
for (const match of text.matchAll(/\b(?:process\.env|env)\.([A-Z][A-Z0-9_]+)/g)) found.add(match[1] as string)
|
|
for (const match of text.matchAll(/\b(?:process\.env|env)\[\s*"([A-Z][A-Z0-9_]+)"\s*\]/g))
|
|
found.add(match[1] as string)
|
|
}
|
|
// The names config/src/env.ts holds in constants.
|
|
for (const match of readFileSync(join(root, "config/src/env.ts"), "utf8").matchAll(/_VAR = "([A-Z][A-Z0-9_]+)"/g)) {
|
|
found.add(match[1] as string)
|
|
}
|
|
for (const file of ["ai.ts", "documents.ts"]) {
|
|
for (const match of readFileSync(join(root, "config/src", file), "utf8").matchAll(/_VAR = "([A-Z][A-Z0-9_]+)"/g)) {
|
|
found.add(match[1] as string)
|
|
}
|
|
}
|
|
// The platform contract and the sign-in variables are read by the kit's loaders from the same environment.
|
|
const kit = (path: string) => readFileSync(join(root, "node_modules", "@conusai-kit", path), "utf8")
|
|
for (const match of kit("auth/dist/config.js").matchAll(/\b((?:AUTH|OIDC)_[A-Z_]+)\b/g)) found.add(match[1] as string)
|
|
for (const name of [
|
|
"CONUSAI_API_URL",
|
|
"CONUSAI_API_TOKEN",
|
|
"CONUSAI_PROJECT_ID",
|
|
"CONUSAI_ORG_ID",
|
|
"CONUSAI_ORG_SLUG",
|
|
"CONUSAI_PUBLIC_URL",
|
|
"CONUSAI_RELEASE",
|
|
"CONUSAI_ENVIRONMENT",
|
|
"CONUSAI_DSN",
|
|
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
|
"OTEL_EXPORTER_OTLP_HEADERS",
|
|
"OTEL_SERVICE_NAME",
|
|
"OTEL_SERVICE_VERSION",
|
|
"DEV_WEB_PORT",
|
|
"DEV_PLATFORM_PORT",
|
|
"DEV_POSTGRES_PORT",
|
|
"CONVEX_SELF_HOSTED_URL",
|
|
"CONVEX_SELF_HOSTED_ADMIN_KEY",
|
|
"CONVEX_BACKUP_INTERVAL_HOURS",
|
|
"CONVEX_BACKUP_RETENTION",
|
|
"CONVEX_CLI",
|
|
"CONVEX_APP_DIR",
|
|
]) {
|
|
found.add(name)
|
|
}
|
|
return found
|
|
}
|
|
|
|
describe(".env.example", () => {
|
|
it("lists every variable the code reads", () => {
|
|
const missing = [...read()].filter((name) => !declared.includes(name)).sort()
|
|
// Set by the runtime and not by a person: NODE_ENV is in the file for what it means here, and the
|
|
// framework's own variable names are not this app's to document.
|
|
const framework = new Set([
|
|
"NEXT_RUNTIME",
|
|
"npm_execpath",
|
|
"PORT",
|
|
"HOST",
|
|
"HOSTNAME",
|
|
"E2E_BASE_URL",
|
|
"E2E_PLATFORM_URL",
|
|
])
|
|
expect(
|
|
missing.filter((name) => !framework.has(name)),
|
|
"add these to .env.example, with a comment",
|
|
).toEqual([])
|
|
})
|
|
|
|
it("lists nothing that nothing reads", () => {
|
|
const used = read()
|
|
const fixed = new Set([
|
|
"POSTGRES_URL",
|
|
"POSTGRES_READER_URL",
|
|
"POSTGRES_MIGRATE_URL",
|
|
"AI_MODEL",
|
|
"LOG_LEVEL",
|
|
"WORKER_HEARTBEAT_FILE",
|
|
"NODE_ENV",
|
|
])
|
|
const unused = declared.filter((name) => !used.has(name) && !fixed.has(name))
|
|
expect(unused, "remove these from .env.example, or the code that reads them is not found by this test").toEqual([])
|
|
})
|
|
|
|
it("has no variable twice", () => {
|
|
expect(declared.filter((name, index) => declared.indexOf(name) !== index)).toEqual([])
|
|
})
|
|
|
|
it("explains every variable: a comment sits above it, or above the block of variables it belongs to", () => {
|
|
const lines = example.split("\n")
|
|
const undocumented: string[] = []
|
|
for (const [index, line] of lines.entries()) {
|
|
const name = /^([A-Z][A-Z0-9_]+)=/.exec(line)?.[1]
|
|
if (!name) continue
|
|
let above = index - 1
|
|
while (above >= 0 && /^[A-Z][A-Z0-9_]+=/.test(lines[above] as string)) above -= 1
|
|
if (!(lines[above] ?? "").startsWith("#")) undocumented.push(name)
|
|
}
|
|
expect(undocumented).toEqual([])
|
|
})
|
|
|
|
it("holds no real secret: the values are the development ones or empty", () => {
|
|
for (const name of ["CONUSAI_API_TOKEN", "AUTH_SESSION_SECRET", "OIDC_WEB_CLIENT_SECRET", "BETTER_AUTH_SECRET"]) {
|
|
const value = new RegExp(`^${name}=(.*)$`, "m").exec(example)?.[1] ?? ""
|
|
expect(value === "" || /dev|replace-me/.test(value), `${name} looks like a real secret`).toBe(true)
|
|
}
|
|
})
|
|
})
|