197 lines
5.7 KiB
Python
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)
|