## 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 is parsed", flagEnvFilePath(@["--server", "x", "--env-file", "/a/b.env"]) == "/a/b.env" check "--env-file= 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."