This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user