2025-09-26 10:03:36 +00:00
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import pytest
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
2026-01-30 09:49:50 +00:00
|
|
|
from src.env import read_env, read_int_env, read_bool_env, read_str_env, read_float_env
|
2025-09-26 10:03:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReadEnv:
|
|
|
|
|
def test_returns_direct_env_var_when_exists(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR": "direct_value"}):
|
|
|
|
|
result = read_env("TEST_VAR")
|
|
|
|
|
assert result == "direct_value"
|
|
|
|
|
|
|
|
|
|
def test_returns_none_when_no_env_var(self):
|
|
|
|
|
with patch.dict(os.environ, clear=True):
|
|
|
|
|
result = read_env("NONEXISTENT_VAR")
|
|
|
|
|
assert result is None
|
|
|
|
|
|
|
|
|
|
def test_reads_from_file_when_file_env_var_exists(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write("file_value\n")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR_FILE": f.name}):
|
|
|
|
|
result = read_env("TEST_VAR")
|
|
|
|
|
assert result == "file_value"
|
|
|
|
|
|
|
|
|
|
def test_strips_whitespace_from_file_content(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write(" value_with_spaces \n\n")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR_FILE": f.name}):
|
|
|
|
|
result = read_env("TEST_VAR")
|
|
|
|
|
assert result == "value_with_spaces"
|
|
|
|
|
|
|
|
|
|
def test_direct_env_var_takes_precedence_over_file(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write("file_value")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(
|
|
|
|
|
os.environ,
|
|
|
|
|
{"TEST_VAR": "direct_value", "TEST_VAR_FILE": f.name},
|
|
|
|
|
):
|
|
|
|
|
result = read_env("TEST_VAR")
|
|
|
|
|
assert result == "direct_value"
|
|
|
|
|
|
|
|
|
|
def test_raises_error_when_file_not_found(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR_FILE": "/nonexistent/file.txt"}):
|
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
|
|
|
read_env("TEST_VAR")
|
|
|
|
|
assert "Failed to read TEST_VAR_FILE from file" in str(exc_info.value)
|
|
|
|
|
|
|
|
|
|
def test_handles_empty_file(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write("")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR_FILE": f.name}):
|
|
|
|
|
result = read_env("TEST_VAR")
|
|
|
|
|
assert result == ""
|
|
|
|
|
|
|
|
|
|
def test_handles_multiline_file_content(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write("line1\nline2\nline3")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR_FILE": f.name}):
|
|
|
|
|
result = read_env("TEST_VAR")
|
|
|
|
|
assert result == "line1\nline2\nline3"
|
|
|
|
|
|
|
|
|
|
def test_handles_unicode_content(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=True) as f:
|
|
|
|
|
f.write("unicode: 你好世界 🌍")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR_FILE": f.name}):
|
|
|
|
|
result = read_env("TEST_VAR")
|
|
|
|
|
assert result == "unicode: 你好世界 🌍"
|
|
|
|
|
|
|
|
|
|
def test_raises_error_with_permission_denied(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
|
|
|
|
|
f.write("secret_value")
|
|
|
|
|
temp_file_path = f.name
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
Path(temp_file_path).chmod(0o000) # Make file unreadable
|
|
|
|
|
with patch.dict(os.environ, {"TEST_VAR_FILE": temp_file_path}):
|
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
|
|
|
read_env("TEST_VAR")
|
|
|
|
|
assert "Failed to read TEST_VAR_FILE from file" in str(exc_info.value)
|
|
|
|
|
finally:
|
|
|
|
|
Path(temp_file_path).chmod(0o644)
|
|
|
|
|
Path(temp_file_path).unlink()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReadStrEnv:
|
|
|
|
|
def test_returns_string_from_direct_env(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_STR": "hello world"}):
|
|
|
|
|
result = read_str_env("TEST_STR", default="default")
|
|
|
|
|
assert result == "hello world"
|
|
|
|
|
|
|
|
|
|
def test_returns_string_from_file(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write("file content")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_STR_FILE": f.name}):
|
|
|
|
|
result = read_str_env("TEST_STR", default="default")
|
|
|
|
|
assert result == "file content"
|
|
|
|
|
|
|
|
|
|
def test_returns_default_when_not_set(self):
|
|
|
|
|
with patch.dict(os.environ, clear=True):
|
|
|
|
|
result = read_str_env("TEST_STR", default="fallback")
|
|
|
|
|
assert result == "fallback"
|
|
|
|
|
|
|
|
|
|
def test_handles_empty_string_from_env(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_STR": ""}):
|
|
|
|
|
result = read_str_env("TEST_STR", default="default")
|
|
|
|
|
assert result == ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReadIntEnv:
|
|
|
|
|
def test_returns_int_from_direct_env(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_INT": "42"}):
|
|
|
|
|
result = read_int_env("TEST_INT", default=0)
|
|
|
|
|
assert result == 42
|
|
|
|
|
|
|
|
|
|
def test_returns_int_from_file(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write("123")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_INT_FILE": f.name}):
|
|
|
|
|
result = read_int_env("TEST_INT", default=0)
|
|
|
|
|
assert result == 123
|
|
|
|
|
|
|
|
|
|
def test_returns_default_when_not_set(self):
|
|
|
|
|
with patch.dict(os.environ, clear=True):
|
|
|
|
|
result = read_int_env("TEST_INT", default=999)
|
|
|
|
|
assert result == 999
|
|
|
|
|
|
|
|
|
|
def test_raises_error_for_invalid_int(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_INT": "not_a_number"}):
|
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
|
|
|
read_int_env("TEST_INT", default=0)
|
|
|
|
|
assert "must be an integer" in str(exc_info.value)
|
|
|
|
|
|
|
|
|
|
def test_handles_negative_numbers(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_INT": "-42"}):
|
|
|
|
|
result = read_int_env("TEST_INT", default=0)
|
|
|
|
|
assert result == -42
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReadBoolEnv:
|
|
|
|
|
def test_returns_true_for_true_string(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_BOOL": "true"}):
|
|
|
|
|
result = read_bool_env("TEST_BOOL", default=False)
|
|
|
|
|
assert result is True
|
|
|
|
|
|
|
|
|
|
def test_returns_false_for_false_string(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_BOOL": "false"}):
|
|
|
|
|
result = read_bool_env("TEST_BOOL", default=True)
|
|
|
|
|
assert result is False
|
|
|
|
|
|
|
|
|
|
def test_returns_true_from_file(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write("true")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_BOOL_FILE": f.name}):
|
|
|
|
|
result = read_bool_env("TEST_BOOL", default=False)
|
|
|
|
|
assert result is True
|
|
|
|
|
|
|
|
|
|
def test_returns_default_when_not_set(self):
|
|
|
|
|
with patch.dict(os.environ, clear=True):
|
|
|
|
|
result = read_bool_env("TEST_BOOL", default=True)
|
|
|
|
|
assert result is True
|
2026-01-30 09:49:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReadFloatEnv:
|
|
|
|
|
def test_returns_float_from_direct_env(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_FLOAT": "3.14"}):
|
|
|
|
|
result = read_float_env("TEST_FLOAT", default=0.0)
|
|
|
|
|
assert result == 3.14
|
|
|
|
|
|
|
|
|
|
def test_returns_float_from_file(self):
|
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=True) as f:
|
|
|
|
|
f.write("2.718")
|
|
|
|
|
f.flush()
|
|
|
|
|
|
|
|
|
|
with patch.dict(os.environ, {"TEST_FLOAT_FILE": f.name}):
|
|
|
|
|
result = read_float_env("TEST_FLOAT", default=0.0)
|
|
|
|
|
assert result == 2.718
|
|
|
|
|
|
|
|
|
|
def test_returns_default_when_not_set(self):
|
|
|
|
|
with patch.dict(os.environ, clear=True):
|
|
|
|
|
result = read_float_env("TEST_FLOAT", default=9.99)
|
|
|
|
|
assert result == 9.99
|
|
|
|
|
|
|
|
|
|
def test_raises_error_for_invalid_float(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_FLOAT": "not_a_number"}):
|
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
|
|
|
read_float_env("TEST_FLOAT", default=0.0)
|
|
|
|
|
assert "must be a float" in str(exc_info.value)
|
|
|
|
|
|
|
|
|
|
def test_handles_negative_numbers(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_FLOAT": "-42.5"}):
|
|
|
|
|
result = read_float_env("TEST_FLOAT", default=0.0)
|
|
|
|
|
assert result == -42.5
|
|
|
|
|
|
|
|
|
|
def test_handles_integer_values(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_FLOAT": "42"}):
|
|
|
|
|
result = read_float_env("TEST_FLOAT", default=0.0)
|
|
|
|
|
assert result == 42.0
|
|
|
|
|
|
|
|
|
|
def test_handles_zero(self):
|
|
|
|
|
with patch.dict(os.environ, {"TEST_FLOAT": "0"}):
|
|
|
|
|
result = read_float_env("TEST_FLOAT", default=1.0)
|
|
|
|
|
assert result == 0.0
|