feat: 增加 YAML 配置与一键启动脚本
This commit is contained in:
196
backend/tests/test_config.py
Normal file
196
backend/tests/test_config.py
Normal file
@@ -0,0 +1,196 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user