Files
SirRoboGarage/common_libs/tests/test_env_dotenv.nim
T

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."