feat: YAML config support (~/.config/redmine-reporter/config.yml)
- Add AppConfig/SmtpConfig/EmailConfig dataclasses with from_yaml()/from_env()
- Add yaml_config.py: ${VAR} resolver, 0700/0600 permission helpers
- Config.get_*() methods gain YAML fallback in priority chain
- Priority: CLI > env > .env > YAML > code defaults
- CLI --init-config generates YAML from current environment
- --force flag allows overwriting existing config
- Secrets default to ${VAR} references, plaintext allowed
- Full backward compatibility with existing .env setups
Closes #46
This commit is contained in:
@@ -18,9 +18,11 @@ def _reset_config_overrides():
|
||||
|
||||
Config.set_redmine_url("")
|
||||
Config.set_redmine_api_key("")
|
||||
Config._app = None
|
||||
yield
|
||||
Config.set_redmine_url("")
|
||||
Config.set_redmine_api_key("")
|
||||
Config._app = None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -301,3 +303,93 @@ def test_cli_rejects_multiple_user_flags():
|
||||
]
|
||||
)
|
||||
assert code == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --init-config tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestInitConfig:
|
||||
"""Tests for --init-config flag."""
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
def test_init_config_creates_yaml_file(self, tmp_path):
|
||||
"""--init-config создаёт YAML файл с правильной структурой."""
|
||||
import yaml
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
code = main(["--init-config", "--config-path", str(config_path)])
|
||||
assert code == 0
|
||||
assert config_path.exists()
|
||||
|
||||
with open(config_path) as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
|
||||
assert "redmine" in data
|
||||
assert data["redmine"]["url"] == "https://red.eltex.loc"
|
||||
assert data["redmine"]["api_key"] == "${REDMINE_API_KEY}"
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
def test_init_config_existing_file_no_force(self, tmp_path, capsys):
|
||||
"""Без --force существующий файл → предупреждение и выход 1."""
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text("existing: true")
|
||||
|
||||
code = main(["--init-config", "--config-path", str(config_path)])
|
||||
assert code == 1
|
||||
captured = capsys.readouterr()
|
||||
assert "already exists" in captured.err or "already exists" in captured.out
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
def test_init_config_force_overwrites(self, tmp_path):
|
||||
"""--init-config --force перезаписывает существующий файл."""
|
||||
import yaml
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text("existing: true")
|
||||
|
||||
code = main(["--init-config", "--force", "--config-path", str(config_path)])
|
||||
assert code == 0
|
||||
|
||||
with open(config_path) as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
|
||||
assert "redmine" in data
|
||||
assert "existing" not in data
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
def test_init_config_with_date_is_error(self, capsys):
|
||||
"""--init-config с --date → ошибка, взаимоисключающие."""
|
||||
code = main(["--init-config", "--date", "2026-01-01--2026-01-31"])
|
||||
assert code == 1
|
||||
|
||||
@mock.patch.dict(os.environ, VALID_ENV, clear=True)
|
||||
def test_init_config_writes_secrets_as_env_ref(self, tmp_path):
|
||||
"""Секреты пишутся как ${VAR} когда переменная окружения существует."""
|
||||
import yaml
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
code = main(["--init-config", "--config-path", str(config_path)])
|
||||
assert code == 0
|
||||
|
||||
with open(config_path) as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
|
||||
assert data["redmine"]["api_key"] == "${REDMINE_API_KEY}"
|
||||
|
||||
@mock.patch.dict(os.environ, {}, clear=True)
|
||||
def test_init_config_no_env_vars(self, tmp_path):
|
||||
"""Без переменных окружения --init-config создаёт скелет."""
|
||||
import yaml
|
||||
|
||||
config_path = tmp_path / "config.yml"
|
||||
code = main(["--init-config", "--config-path", str(config_path)])
|
||||
assert code == 0
|
||||
|
||||
with open(config_path) as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
|
||||
assert "redmine" in data
|
||||
assert data["redmine"]["url"] == ""
|
||||
assert data["redmine"]["api_key"] == ""
|
||||
|
||||
Reference in New Issue
Block a user