fix: improve config module with lazy init and validation
Fixes from code review: Critical: - Replace module-level `settings = Settings()` with lazy initialization via `get_settings()` function to avoid import failures when env vars not set Important: - Remove unused `import os` from test_config.py - Add tests for computed properties (db_path, ssh_keys_dir, repos_dir) - Add field validation for encrypt_key: * Validates base64 format * Ensures decoded key is at least 32 bytes for AES-256 - Fix Python 3.8 compatibility (use Optional[Settings] instead of | union) All tests pass (6/6). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
from typing import Optional
|
||||||
|
from pydantic import field_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +25,22 @@ class Settings(BaseSettings):
|
|||||||
env_file_encoding='utf-8',
|
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
|
@property
|
||||||
def db_path(self) -> Path:
|
def db_path(self) -> Path:
|
||||||
"""SQLite 数据库路径."""
|
"""SQLite 数据库路径."""
|
||||||
@@ -40,4 +57,13 @@ class Settings(BaseSettings):
|
|||||||
return self.data_dir / 'repos'
|
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
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import os
|
|
||||||
import pytest
|
import pytest
|
||||||
import base64
|
import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def test_config_defaults(test_env_vars, monkeypatch):
|
def test_config_defaults(test_env_vars, monkeypatch):
|
||||||
"""测试配置默认值."""
|
"""测试配置默认值."""
|
||||||
# Clear GM_DATA_DIR to test default value
|
# 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.host == '0.0.0.0'
|
||||||
assert settings.port == 8000
|
assert settings.port == 8000
|
||||||
|
|
||||||
|
|
||||||
def test_config_from_env(monkeypatch):
|
def test_config_from_env(monkeypatch):
|
||||||
"""测试从环境变量读取配置."""
|
"""测试从环境变量读取配置."""
|
||||||
# Set required security fields
|
# Set required security fields
|
||||||
@@ -30,3 +31,68 @@ def test_config_from_env(monkeypatch):
|
|||||||
|
|
||||||
assert settings.data_dir == Path('/custom/data')
|
assert settings.data_dir == Path('/custom/data')
|
||||||
assert settings.port == 9000
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user