"""Tests for YAML configuration module.""" from __future__ import annotations import os from unittest.mock import patch import pytest from gitea_dashboard.config import load_config, merge_config, resolve_env_vars class TestResolveEnvVars: """Test resolve_env_vars function.""" def test_resolve_env_vars_simple(self): """${VAR} is replaced by the environment variable value.""" with patch.dict(os.environ, {"GITEA_TOKEN": "abc123"}): result = resolve_env_vars("${GITEA_TOKEN}") assert result == "abc123" def test_resolve_env_vars_undefined(self): """${UNDEFINED} is left as-is when the variable is not set.""" with patch.dict(os.environ, {}, clear=True): result = resolve_env_vars("${UNDEFINED_VAR}") assert result == "${UNDEFINED_VAR}" def test_resolve_env_vars_in_list(self): """resolve_env_vars works on individual string elements.""" with patch.dict(os.environ, {"MY_VAR": "resolved"}): result = resolve_env_vars("prefix-${MY_VAR}-suffix") assert result == "prefix-resolved-suffix" class TestLoadConfig: """Test load_config function.""" def test_load_config_valid_yaml(self, tmp_path): """Valid YAML file is loaded as a dict with all keys.""" config_file = tmp_path / "config.yml" config_file.write_text( "url: http://localhost:3000\ntoken: ${GITEA_TOKEN}\nsort: activity\n" ) with patch.dict(os.environ, {"GITEA_TOKEN": "secret123"}): result = load_config(str(config_file)) assert result["url"] == "http://localhost:3000" assert result["token"] == "secret123" assert result["sort"] == "activity" def test_load_config_partial_yaml(self, tmp_path): """YAML with only some keys returns a partial dict.""" config_file = tmp_path / "config.yml" config_file.write_text("url: http://localhost:3000\nsort: name\n") result = load_config(str(config_file)) assert result["url"] == "http://localhost:3000" assert result["sort"] == "name" assert "token" not in result def test_load_config_empty_file(self, tmp_path): """Empty YAML file returns an empty dict.""" config_file = tmp_path / "config.yml" config_file.write_text("") result = load_config(str(config_file)) assert result == {} def test_load_config_invalid_yaml(self, tmp_path): """Syntactically invalid YAML raises a clear error.""" config_file = tmp_path / "config.yml" config_file.write_text("invalid: yaml: content: [unclosed") with pytest.raises(ValueError, match="[Ii]nvalid"): load_config(str(config_file)) def test_load_config_custom_path(self, tmp_path): """--config /path/to/custom.yml loads the specified file.""" config_file = tmp_path / "custom.yml" config_file.write_text("sort: issues\n") result = load_config(str(config_file)) assert result["sort"] == "issues" def test_load_config_missing_custom_path(self): """--config with a nonexistent path raises FileNotFoundError.""" with pytest.raises(FileNotFoundError): load_config("/nonexistent/path/config.yml") def test_load_config_default_paths(self, tmp_path, monkeypatch): """Config file in current directory is auto-discovered.""" config_file = tmp_path / ".gitea-dashboard.yml" config_file.write_text("sort: activity\n") monkeypatch.chdir(tmp_path) result = load_config() assert result["sort"] == "activity" class TestMergeConfig: """Test merge_config function.""" def test_merge_config_priority(self): """CLI > env > config > defaults — CLI wins.""" cli = {"sort": "name", "url": None} env = {"sort": "issues", "url": "http://env:3000"} config = {"sort": "activity", "url": "http://config:3000", "exclude": ["old"]} defaults = {"sort": "name", "url": "http://default:3000", "exclude": None} result = merge_config(cli, env, config, defaults) assert result["sort"] == "name" # CLI wins assert result["url"] == "http://env:3000" # CLI is None, env wins assert result["exclude"] == ["old"] # env has no exclude, config wins def test_merge_config_none_does_not_override(self): """None in a higher-priority source does not mask a lower-priority value.""" cli = {"token": None} env = {"token": None} config = {"token": "from-config"} defaults = {"token": "default-token"} result = merge_config(cli, env, config, defaults) assert result["token"] == "from-config"