181 lines
7.3 KiB
Nim
181 lines
7.3 KiB
Nim
## Guard test for the .env loader (the module the bot uses to read its
|
|
## settings from a `.env` file).
|
|
##
|
|
## NO battle, NO Java, NO server. Run with:
|
|
## nim c -r --path:common_libs common_libs/tests/test_env_dotenv.nim
|
|
##
|
|
## It pins the pieces that must not silently drift:
|
|
## * the parser (comments, `export `, quotes, empty value, CRLF, and a
|
|
## malformed line that MUST raise with the file+line);
|
|
## * resolution precedence (flag > TR_ENV_FILE > ./.env > .env next to the
|
|
## executable > none);
|
|
## * the FILE WINS over the real environment, and the conflict is reported;
|
|
## * an explicitly requested missing file raises (the caller then exits
|
|
## nonzero), while a missing DEFAULT file is a silent no-op.
|
|
|
|
import std/[os, strutils]
|
|
import "../../ModularBot_garage/src/env_dotenv"
|
|
|
|
var failures = 0
|
|
var checks = 0
|
|
|
|
proc check(name: string, ok: bool) =
|
|
inc checks
|
|
if ok: echo "PASS: ", name
|
|
else: echo "FAIL: ", name; inc failures
|
|
|
|
let tmpDir = getTempDir() / "test_env_dotenv_guard"
|
|
createDir(tmpDir)
|
|
|
|
proc tmpPath(name: string): string = tmpDir / name
|
|
|
|
proc writeTmp(name, content: string): string =
|
|
result = tmpPath(name)
|
|
writeFile(result, content)
|
|
|
|
# ── 1. the parser ────────────────────────────────────────────────────────────
|
|
|
|
proc testParser() =
|
|
let entries = parseEnvFileContent("""# a comment, ignored
|
|
|
|
TR_MOVEMENT=tfil
|
|
export TR_RACK_TMHORIZON=both
|
|
QUOTED="hello world"
|
|
SINGLE='x y'
|
|
EMPTY=
|
|
SPACED = trimmed
|
|
""", "sample.env")
|
|
check "comment + blank lines are skipped (6 entries)", entries.len == 6
|
|
if entries.len == 6:
|
|
check "plain KEY=VALUE", entries[0].key == "TR_MOVEMENT" and entries[0].value == "tfil"
|
|
check "leading `export ` is tolerated",
|
|
entries[1].key == "TR_RACK_TMHORIZON" and entries[1].value == "both"
|
|
check "double quotes are stripped",
|
|
entries[2].key == "QUOTED" and entries[2].value == "hello world"
|
|
check "single quotes are stripped",
|
|
entries[3].key == "SINGLE" and entries[3].value == "x y"
|
|
check "an empty value is allowed",
|
|
entries[4].key == "EMPTY" and entries[4].value == ""
|
|
check "whitespace around key and value is trimmed",
|
|
entries[5].key == "SPACED" and entries[5].value == "trimmed"
|
|
|
|
let crlf = parseEnvFileContent("A=1\r\nB=two\r\n", "crlf.env")
|
|
check "CRLF line endings are stripped from the value",
|
|
crlf.len == 2 and crlf[0].value == "1" and crlf[1].value == "two"
|
|
|
|
var raised = false
|
|
try:
|
|
discard parseEnvFileContent("this line has no equals sign\n", "bad.env")
|
|
except ValueError as e:
|
|
raised = true
|
|
check "malformed line error names the file", "bad.env" in e.msg
|
|
check "malformed line error names the line number", ":1:" in e.msg
|
|
check "malformed line RAISES (never silently ignored)", raised
|
|
|
|
var emptyKeyRaised = false
|
|
try:
|
|
discard parseEnvFileContent("=value\n", "bad2.env")
|
|
except ValueError:
|
|
emptyKeyRaised = true
|
|
check "an empty key raises", emptyKeyRaised
|
|
|
|
# ── 2. resolution precedence ─────────────────────────────────────────────────
|
|
|
|
proc testResolution() =
|
|
let flagChoice = chooseEnvFile("/x/flag.env", "/x/tr.env",
|
|
"/cwd/.env", "/exe/.env")
|
|
check "flag beats TR_ENV_FILE and the defaults",
|
|
flagChoice.source == "flag" and flagChoice.path == "/x/flag.env" and
|
|
flagChoice.explicit
|
|
|
|
let envChoice = chooseEnvFile("", "/x/tr.env", "/cwd/.env", "/exe/.env")
|
|
check "TR_ENV_FILE beats the default paths",
|
|
envChoice.source == EnvFileEnvVar and envChoice.path == "/x/tr.env" and
|
|
envChoice.explicit
|
|
|
|
let cwdFile = writeTmp("cwd.env", "A=1\n")
|
|
let exeFile = writeTmp("exe.env", "A=2\n")
|
|
let cwdChoice = chooseEnvFile("", "", cwdFile, exeFile)
|
|
check "./.env (cwd) is chosen when it exists and is not explicit",
|
|
cwdChoice.source == "cwd" and cwdChoice.path == cwdFile and
|
|
not cwdChoice.explicit
|
|
|
|
let exeChoice = chooseEnvFile("", "", tmpPath("missing_cwd.env"), exeFile)
|
|
check ".env next to the executable is the fallback",
|
|
exeChoice.source == "exe" and exeChoice.path == exeFile
|
|
|
|
let noneChoice = chooseEnvFile("", "", tmpPath("missing_cwd.env"),
|
|
tmpPath("missing_exe.env"))
|
|
check "no candidate exists -> none (silent)",
|
|
noneChoice.path == "" and noneChoice.source == "none" and
|
|
not noneChoice.explicit
|
|
|
|
check "--env-file <path> is parsed",
|
|
flagEnvFilePath(@["--server", "x", "--env-file", "/a/b.env"]) == "/a/b.env"
|
|
check "--env-file=<path> is parsed",
|
|
flagEnvFilePath(@["--env-file=/a/b.env"]) == "/a/b.env"
|
|
check "no --env-file yields empty", flagEnvFilePath(@["--server", "x"]) == ""
|
|
var raised = false
|
|
try:
|
|
discard flagEnvFilePath(@["--env-file"])
|
|
except EnvFileError:
|
|
raised = true
|
|
check "--env-file with no argument raises", raised
|
|
|
|
# ── 3. the file wins over the real environment ───────────────────────────────
|
|
|
|
proc testFileWins() =
|
|
let f = writeTmp("wins.env", "TR_TEST_J126=from-file\nTR_TEST_SAME_J126=same\n")
|
|
putEnv("TR_TEST_J126", "from-shell")
|
|
putEnv("TR_TEST_SAME_J126", "same")
|
|
let conflicts = applyEnvFile(f)
|
|
check "the file value WINS over the exported value",
|
|
getEnv("TR_TEST_J126") == "from-file"
|
|
check "the conflict is reported with key, file value and shell value",
|
|
conflicts.len == 1 and conflicts[0].key == "TR_TEST_J126" and
|
|
conflicts[0].fileValue == "from-file" and
|
|
conflicts[0].shellValue == "from-shell"
|
|
check "an identical value is NOT a conflict",
|
|
conflicts.len == 1 # TR_TEST_SAME_J126 is absent from conflicts
|
|
check "keys from the file are tracked for the boot report",
|
|
isFromEnvFile("TR_TEST_J126") and isFromEnvFile("TR_TEST_SAME_J126")
|
|
delEnv("TR_TEST_J126")
|
|
delEnv("TR_TEST_SAME_J126")
|
|
|
|
# ── 4. missing explicit file vs missing default file ─────────────────────────
|
|
|
|
proc testMissingFiles() =
|
|
var raised = false
|
|
try:
|
|
discard loadEnvFile(EnvFileChoice(path: tmpPath("nope.env"),
|
|
source: "flag", explicit: true))
|
|
except EnvFileError as e:
|
|
raised = true
|
|
check "missing explicit file error names the path", "nope.env" in e.msg
|
|
check "missing explicit file RAISES (caller exits nonzero)", raised
|
|
|
|
var quiet = true
|
|
var n = 0
|
|
try:
|
|
n = loadEnvFile(EnvFileChoice(path: tmpPath("missing-default.env"),
|
|
source: "cwd", explicit: false)).len
|
|
except CatchableError:
|
|
quiet = false
|
|
check "missing default file is a silent no-op", quiet and n == 0
|
|
|
|
check "no file at all is a silent no-op",
|
|
loadEnvFile(EnvFileChoice(path: "", source: "none", explicit: false)).len == 0
|
|
|
|
# ── driver ───────────────────────────────────────────────────────────────────
|
|
|
|
testParser()
|
|
testResolution()
|
|
testFileWins()
|
|
testMissingFiles()
|
|
|
|
echo "\n", checks, " checks, ", failures, " failure(s)"
|
|
if failures > 0:
|
|
echo checks, " check(s) run, ", failures, " FAILED"
|
|
quit(1)
|
|
echo "All env-dotenv checks passed."
|