diff --git a/backend/app/config.py b/backend/app/config.py index c9faa9a..ce54990 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,6 +1,7 @@ -import os +import base64 from pathlib import Path -from typing import Literal +from typing import Optional +from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -24,6 +25,22 @@ class Settings(BaseSettings): env_file_encoding='utf-8', ) + @field_validator('encrypt_key') + @classmethod + def validate_encrypt_key(cls, v: str) -> str: + """验证 encrypt_key 是否为有效的 base64 格式且长度足够.""" + # Check if valid base64 + try: + decoded = base64.b64decode(v, validate=True) + except Exception: + raise ValueError('encrypt_key must be valid base64 string') + + # Check length (AES-256 requires 32 bytes) + if len(decoded) < 32: + raise ValueError('encrypt_key must decode to at least 32 bytes for AES-256') + + return v + @property def db_path(self) -> Path: """SQLite 数据库路径.""" @@ -40,4 +57,13 @@ class Settings(BaseSettings): return self.data_dir / 'repos' -settings = Settings() +# Lazy initialization - settings is loaded on first access +_settings: Optional[Settings] = None + + +def get_settings() -> Settings: + """获取全局配置实例(懒加载).""" + global _settings + if _settings is None: + _settings = Settings() + return _settings diff --git a/backend/tests/test_config.py b/backend/tests/test_config.py index e140641..25e0fa8 100644 --- a/backend/tests/test_config.py +++ b/backend/tests/test_config.py @@ -1,8 +1,8 @@ -import os import pytest import base64 from pathlib import Path + def test_config_defaults(test_env_vars, monkeypatch): """测试配置默认值.""" # Clear GM_DATA_DIR to test default value @@ -16,6 +16,7 @@ def test_config_defaults(test_env_vars, monkeypatch): assert settings.host == '0.0.0.0' assert settings.port == 8000 + def test_config_from_env(monkeypatch): """测试从环境变量读取配置.""" # Set required security fields @@ -30,3 +31,68 @@ def test_config_from_env(monkeypatch): assert settings.data_dir == Path('/custom/data') assert settings.port == 9000 + + +def test_computed_properties(monkeypatch): + """测试计算属性(db_path, ssh_keys_dir, repos_dir).""" + # Set required env vars but not GM_DATA_DIR to test default + monkeypatch.setenv("GM_ENCRYPT_KEY", base64.b64encode(b'test-key-32-bytes-long-1234567890').decode()) + monkeypatch.setenv("GM_API_TOKEN", "test-token") + monkeypatch.delenv("GM_DATA_DIR", raising=False) + + from app.config import Settings + + # Test with default data_dir + settings = Settings() + assert settings.db_path == Path('./data/git_manager.db') + assert settings.ssh_keys_dir == Path('./data/ssh_keys') + assert settings.repos_dir == Path('./data/repos') + + # Test with custom data_dir + monkeypatch.setenv("GM_DATA_DIR", "/custom/data") + settings = Settings() + assert settings.db_path == Path('/custom/data/git_manager.db') + assert settings.ssh_keys_dir == Path('/custom/data/ssh_keys') + assert settings.repos_dir == Path('/custom/data/repos') + + +def test_encrypt_key_validation_invalid_base64(test_env_vars, monkeypatch): + """测试 encrypt_key 验证:无效的 base64 字符串.""" + from app.config import Settings + from pydantic import ValidationError + + monkeypatch.setenv("GM_ENCRYPT_KEY", "not-valid-base64!!!") + + with pytest.raises(ValidationError) as exc_info: + Settings() + + assert 'encrypt_key' in str(exc_info.value).lower() + assert 'base64' in str(exc_info.value).lower() + + +def test_encrypt_key_validation_too_short(test_env_vars, monkeypatch): + """测试 encrypt_key 验证:解码后长度不足.""" + from app.config import Settings + from pydantic import ValidationError + + # Only 10 bytes instead of required 32 + short_key = base64.b64encode(b'short-key-1').decode() + monkeypatch.setenv("GM_ENCRYPT_KEY", short_key) + + with pytest.raises(ValidationError) as exc_info: + Settings() + + assert 'encrypt_key' in str(exc_info.value).lower() + assert '32' in str(exc_info.value).lower() or 'byte' in str(exc_info.value).lower() + + +def test_encrypt_key_validation_valid(test_env_vars, monkeypatch): + """测试 encrypt_key 验证:有效的密钥.""" + from app.config import Settings + + # Valid 32-byte key + valid_key = base64.b64encode(b'test-key-32-bytes-long-1234567890').decode() + monkeypatch.setenv("GM_ENCRYPT_KEY", valid_key) + + settings = Settings() + assert settings.encrypt_key == valid_key