Files
YG_FT/backend/tests/test_config.py
2026-07-21 16:15:01 +08:00

197 lines
5.7 KiB
Python

from __future__ import annotations
from pathlib import Path
import pytest
from app.core.config import ConfigurationError, load_settings
CONFIG_ENV_NAMES = (
"APP_NAME",
"APP_ENV",
"APP_MODE",
"MODELTF_ROUTE_PREFIX",
"FRONTEND_PORT",
"BACKEND_PORT",
"DATABASE_URL",
"DATABASE_BASE_URL",
"DATABASE_USERNAME",
"DATABASE_PASSWORD",
"CORS_ALLOW_ORIGINS",
"COMPUTE_MODE",
"COMPUTE_STATUS_SYNC_MODE",
"COMPUTE_POLL_INTERVAL_SECONDS",
"LOG_LEVEL",
"LOG_DIR",
"LOG_FILE_PREFIX",
"LOG_ERROR_FILE_PREFIX",
"LOG_MAX_BYTES",
"LOG_RETENTION_DAYS",
)
def clear_config_env(monkeypatch: pytest.MonkeyPatch) -> None:
for env_name in CONFIG_ENV_NAMES:
monkeypatch.delenv(env_name, raising=False)
def write_config(path: Path) -> None:
path.write_text(
"""
app:
name: YAML API
route_prefix: /yaml-api
cors_allow_origins:
- http://yaml.example
server:
frontend_port: 18001
backend_port: 18002
database:
url: postgresql+psycopg://db:5432/yaml
username: yaml-user
password: yaml-secret
compute:
mode: simulated
poll_interval_seconds: 9
logging:
directory: ./yaml-logs
max_bytes: 1024
retention_days: 2
""".strip(),
encoding="utf-8",
)
def test_load_settings_from_yaml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
clear_config_env(monkeypatch)
config_path = tmp_path / "config.yaml"
write_config(config_path)
settings = load_settings(config_path)
assert settings.app_name == "YAML API"
assert settings.route_prefix == "/yaml-api"
assert settings.frontend_port == 18001
assert settings.backend_port == 18002
assert settings.database_url == "postgresql+psycopg://yaml-user:yaml-secret@db:5432/yaml"
assert settings.cors_allow_origins == ["http://yaml.example"]
assert settings.compute_mode == "simulated"
assert settings.compute_poll_interval_seconds == 9
assert settings.log_dir == str(tmp_path / "yaml-logs")
assert settings.log_max_bytes == 1024
assert settings.log_retention_days == 2
def test_environment_overrides_yaml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
clear_config_env(monkeypatch)
config_path = tmp_path / "config.yaml"
write_config(config_path)
monkeypatch.setenv("DATABASE_URL", "postgresql+psycopg://env:secret@db:5432/env")
monkeypatch.setenv("CORS_ALLOW_ORIGINS", "http://one.example,http://two.example")
monkeypatch.setenv("COMPUTE_POLL_INTERVAL_SECONDS", "15")
monkeypatch.setenv("FRONTEND_PORT", "19001")
monkeypatch.setenv("BACKEND_PORT", "19002")
settings = load_settings(config_path)
assert settings.database_url == "postgresql+psycopg://env:secret@db:5432/env"
assert settings.cors_allow_origins == ["http://one.example", "http://two.example"]
assert settings.compute_poll_interval_seconds == 15
assert settings.frontend_port == 19001
assert settings.backend_port == 19002
def test_separate_database_credentials_are_encoded(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
clear_config_env(monkeypatch)
config_path = tmp_path / "config.yaml"
write_config(config_path)
monkeypatch.setenv("DATABASE_USERNAME", "user@example.com")
monkeypatch.setenv("DATABASE_PASSWORD", "secret:/?#[]@")
settings = load_settings(config_path)
assert settings.database_url == (
"postgresql+psycopg://user%40example.com:secret%3A%2F%3F%23%5B%5D%40@db:5432/yaml"
)
def test_explicit_missing_config_is_rejected(tmp_path: Path) -> None:
with pytest.raises(ConfigurationError, match="does not exist"):
load_settings(tmp_path / "missing.yaml")
def test_default_cors_follows_frontend_port(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
clear_config_env(monkeypatch)
config_path = tmp_path / "config.yaml"
config_path.write_text(
"server:\n frontend_port: 28001\n backend_port: 28002\n",
encoding="utf-8",
)
settings = load_settings(config_path)
assert settings.cors_allow_origins == [
"http://localhost:28001",
"http://127.0.0.1:28001",
]
@pytest.mark.parametrize("frontend_port", [0, 65536, "invalid", True])
def test_invalid_server_port_is_rejected(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
frontend_port: object,
) -> None:
clear_config_env(monkeypatch)
config_path = tmp_path / "config.yaml"
config_path.write_text(
"server:\n"
f" frontend_port: {str(frontend_port).lower()}\n"
" backend_port: 28002\n",
encoding="utf-8",
)
with pytest.raises(ConfigurationError, match="server.frontend_port"):
load_settings(config_path)
def test_frontend_and_backend_ports_must_differ(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
clear_config_env(monkeypatch)
config_path = tmp_path / "config.yaml"
config_path.write_text(
"server:\n frontend_port: 28001\n backend_port: 28001\n",
encoding="utf-8",
)
with pytest.raises(ConfigurationError, match="must be different"):
load_settings(config_path)
@pytest.mark.parametrize(
("database_base_url", "message"),
[
("mysql://localhost:3306/yg_ft", "must use postgresql"),
("postgresql+psycopg://localhost:5432", "must include a database name"),
],
)
def test_invalid_database_url_is_rejected(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
database_base_url: str,
message: str,
) -> None:
clear_config_env(monkeypatch)
config_path = tmp_path / "config.yaml"
write_config(config_path)
monkeypatch.setenv("DATABASE_BASE_URL", database_base_url)
with pytest.raises(ConfigurationError, match=message):
load_settings(config_path)