TOML-urile din configs/ rămân 100% calibrare — safe to commit. Secretele (ATM_DISCORD_URL, ATM_TG_TOKEN, ATM_TG_CHAT) trăiesc în .env la rădăcină (ignored), cu loader stdlib (shell wins peste file). Validare fail-fast pentru env lipsă, placeholder REPLACE_ME, chat_id non-numeric. Include .env.example + secţiune README §Secrets. Tests: 19 noi (env loader + missing-env + placeholder + chat_id + regression post-migrate snapshot). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.1 KiB
Python
105 lines
3.1 KiB
Python
"""Tests for the minimal .env loader (stdlib, no python-dotenv)."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from atm.config import _find_env_file, _load_env_file
|
|
|
|
|
|
def test_no_file_returns_none(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
assert _find_env_file() is None
|
|
|
|
|
|
def test_finds_env_in_root(tmp_path: Path) -> None:
|
|
(tmp_path / "pyproject.toml").write_text("", encoding="utf-8")
|
|
(tmp_path / ".env").write_text("X=1\n", encoding="utf-8")
|
|
sub = tmp_path / "sub" / "deeper"
|
|
sub.mkdir(parents=True)
|
|
found = _find_env_file(sub)
|
|
assert found == (tmp_path / ".env").resolve()
|
|
|
|
|
|
def test_pyproject_sentinel_stops_walk(tmp_path: Path) -> None:
|
|
(tmp_path / "pyproject.toml").write_text("", encoding="utf-8")
|
|
sub = tmp_path / "sub"
|
|
sub.mkdir()
|
|
assert _find_env_file(sub) is None
|
|
|
|
|
|
def test_parses_simple_kv(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
for k in ("A", "B", "C"):
|
|
monkeypatch.delenv(k, raising=False)
|
|
env = tmp_path / ".env"
|
|
env.write_text(
|
|
"# comment\n"
|
|
"\n"
|
|
"A=1\n"
|
|
"B=hello world\n"
|
|
" # indented comment\n"
|
|
"C=with=equals=in=value\n",
|
|
encoding="utf-8",
|
|
)
|
|
loaded, overridden = _load_env_file(env)
|
|
assert loaded == 3
|
|
assert overridden == 0
|
|
import os
|
|
assert os.environ["A"] == "1"
|
|
assert os.environ["B"] == "hello world"
|
|
assert os.environ["C"] == "with=equals=in=value"
|
|
|
|
|
|
def test_parses_quoted_values(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
for k in ("SQ", "DQ"):
|
|
monkeypatch.delenv(k, raising=False)
|
|
env = tmp_path / ".env"
|
|
env.write_text("SQ='abc'\nDQ=\"def\"\n", encoding="utf-8")
|
|
_load_env_file(env)
|
|
import os
|
|
assert os.environ["SQ"] == "abc"
|
|
assert os.environ["DQ"] == "def"
|
|
|
|
|
|
def test_handles_crlf_and_whitespace(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
for k in ("K1", "K2"):
|
|
monkeypatch.delenv(k, raising=False)
|
|
env = tmp_path / ".env"
|
|
env.write_bytes(b"K1=v1\r\n K2 = v2 \r\n")
|
|
_load_env_file(env)
|
|
import os
|
|
assert os.environ["K1"] == "v1"
|
|
assert os.environ["K2"] == "v2"
|
|
|
|
|
|
def test_shell_env_wins(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("SHELLWINS", "from_shell")
|
|
env = tmp_path / ".env"
|
|
env.write_text("SHELLWINS=from_file\nOTHER=x\n", encoding="utf-8")
|
|
monkeypatch.delenv("OTHER", raising=False)
|
|
loaded, overridden = _load_env_file(env)
|
|
import os
|
|
assert os.environ["SHELLWINS"] == "from_shell"
|
|
assert os.environ["OTHER"] == "x"
|
|
assert loaded == 1
|
|
assert overridden == 1
|
|
|
|
|
|
def test_malformed_line_raises_with_lineno(tmp_path: Path) -> None:
|
|
env = tmp_path / ".env"
|
|
env.write_text("A=1\nOOPSNOEQUALS\n", encoding="utf-8")
|
|
with pytest.raises(ValueError, match=":2:"):
|
|
_load_env_file(env)
|
|
|
|
|
|
def test_missing_path_is_noop() -> None:
|
|
assert _load_env_file(None) == (0, 0)
|
|
assert _load_env_file(Path("/nonexistent/does-not-exist-xyz")) == (0, 0)
|