a3967c6829
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 6s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m6s
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Failing after 28m22s
1. Remove unused aiosmtplib dependency from requirements.txt
2. Remove 5 unused config items (API_PREFIX, REDIS_MAX_CONNECTION,
JWT_SECRET_KEY_OLD, SECRET_ROTATION_DAYS, LOG_LEVEL) from config.py,
.env.production.example, and init_production_env.sh.
Kept ENABLE_REDIS_SESSIONS (actively used in dependencies.py).
3. Fix project permission check TODOs in test_error_scenarios.py —
permissions are implemented, updated assertions to expect 403.
4. Merge two Storage implementations into shared package:
- packages/shared/storage.py: merged API features (HTTPS endpoint
fix, diagnose(), detailed logging) into SharedStorageService
- apps/api/app/core/storage.py: thin re-export wrapper for
backward compatibility
- Updated test mocks to target packages.shared.storage
159 lines
6.7 KiB
Python
159 lines
6.7 KiB
Python
"""P0-2 修复:OSS 凭证验证 + 启动诊断。
|
|
|
|
验证:
|
|
1. 非开发环境 OSS_ACCESS_KEY_ID/SECRET 为空时启动失败
|
|
2. 开发环境允许空凭证
|
|
3. diagnose() 方法正确输出配置状态
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
def _fresh_settings(env: str):
|
|
"""清除 config 模块缓存,以指定 APP_ENV 重新导入 Settings。
|
|
|
|
为非开发环境预设 OSS 环境变量,确保模块级 get_settings() 能成功完成导入。
|
|
测试方法内可根据需要清除这些变量来测试验证器。
|
|
"""
|
|
for mod_name in [m for m in list(sys.modules) if "app.config" in m]:
|
|
del sys.modules[mod_name]
|
|
os.environ["APP_ENV"] = env
|
|
# 非开发环境下,为模块级导入提供有效凭证(避免导入时验证失败)
|
|
if env != "development":
|
|
os.environ.setdefault("OSS_ACCESS_KEY_ID", "test-key-for-import")
|
|
os.environ.setdefault("OSS_ACCESS_KEY_SECRET", "test-secret-for-import")
|
|
# 重置单例,让测试方法自行控制实例化
|
|
from apps.api.app import config as _cfg
|
|
from apps.api.app.config import Settings
|
|
|
|
_cfg._settings = None
|
|
return Settings
|
|
|
|
|
|
class TestOSSCredentialValidation:
|
|
"""测试 OSS 凭证验证器(直接调用验证器类方法)。"""
|
|
|
|
def test_empty_oss_key_id_rejected_in_staging(self):
|
|
"""非开发环境 OSS_ACCESS_KEY_ID 为空应报错。"""
|
|
Settings = _fresh_settings("staging")
|
|
with pytest.raises(Exception, match="OSS_ACCESS_KEY_ID"):
|
|
Settings.validate_oss_access_key_id("")
|
|
|
|
def test_empty_oss_key_secret_rejected_in_staging(self):
|
|
"""非开发环境 OSS_ACCESS_KEY_SECRET 为空应报错。"""
|
|
Settings = _fresh_settings("staging")
|
|
with pytest.raises(Exception, match="OSS_ACCESS_KEY_SECRET"):
|
|
Settings.validate_oss_access_key_secret("")
|
|
|
|
def test_empty_oss_credentials_allowed_in_development(self):
|
|
"""开发环境允许空 OSS 凭证。"""
|
|
Settings = _fresh_settings("development")
|
|
assert Settings.validate_oss_access_key_id("") == ""
|
|
assert Settings.validate_oss_access_key_secret("") == ""
|
|
|
|
def test_valid_credentials_pass_validation(self):
|
|
"""有效凭证应通过验证。"""
|
|
Settings = _fresh_settings("staging")
|
|
assert Settings.validate_oss_access_key_id("test-key-id") == "test-key-id"
|
|
assert Settings.validate_oss_access_key_secret("test-key-secret") == "test-key-secret"
|
|
|
|
def test_valid_credentials_instantiation_succeeds(self):
|
|
"""有效凭证应能成功创建 Settings 实例。"""
|
|
os.environ.pop("OSS_ACCESS_KEY_ID", None)
|
|
os.environ.pop("OSS_ACCESS_KEY_SECRET", None)
|
|
Settings = _fresh_settings("staging")
|
|
os.environ["OSS_ACCESS_KEY_ID"] = "test-key-id"
|
|
os.environ["OSS_ACCESS_KEY_SECRET"] = "test-key-secret"
|
|
s = Settings(_env_file=None)
|
|
assert s.OSS_ACCESS_KEY_ID == "test-key-id"
|
|
assert s.OSS_ACCESS_KEY_SECRET == "test-key-secret"
|
|
|
|
|
|
class TestOSSDiagnose:
|
|
"""测试 SharedStorageService.diagnose() 方法。"""
|
|
|
|
@patch("packages.shared.storage.oss2", None)
|
|
@patch("packages.shared.storage.get_shared_settings")
|
|
def test_diagnose_logs_error_when_bucket_none(self, mock_settings, caplog):
|
|
"""bucket=None 时 diagnose 应输出 ERROR 日志。"""
|
|
from packages.shared.storage import SharedStorageService
|
|
|
|
mock_settings.return_value.oss_bucket_name = "test-bucket"
|
|
mock_settings.return_value.oss_endpoint = "oss-cn-test.com"
|
|
mock_settings.return_value.oss_access_key_id = ""
|
|
mock_settings.return_value.oss_access_key_secret = ""
|
|
|
|
service = SharedStorageService()
|
|
assert service.bucket is None
|
|
|
|
with caplog.at_level(logging.ERROR, logger="packages.shared.storage"):
|
|
service.diagnose()
|
|
|
|
assert any("❌" in record.message for record in caplog.records)
|
|
|
|
@patch("packages.shared.storage.oss2")
|
|
@patch("packages.shared.storage.get_shared_settings")
|
|
def test_diagnose_logs_success_when_bucket_configured(self, mock_settings, mock_oss2, caplog):
|
|
"""bucket 已配置时 diagnose 应输出成功日志。"""
|
|
from packages.shared.storage import SharedStorageService
|
|
|
|
mock_settings.return_value.oss_bucket_name = "test-bucket"
|
|
mock_settings.return_value.oss_endpoint = "oss-cn-test.com"
|
|
mock_settings.return_value.oss_access_key_id = "test-key-id"
|
|
mock_settings.return_value.oss_access_key_secret = "test-key-secret"
|
|
mock_oss2.Bucket.return_value = MagicMock()
|
|
|
|
service = SharedStorageService()
|
|
assert service.bucket is not None
|
|
|
|
with caplog.at_level(logging.INFO, logger="packages.shared.storage"):
|
|
service.diagnose()
|
|
|
|
assert any("OSS诊断" in record.message for record in caplog.records)
|
|
|
|
|
|
class TestOSSHTTPSEndpoint:
|
|
"""测试 P0-2 真正根因:sign_url 必须返回 HTTPS URL。"""
|
|
|
|
@patch("packages.shared.storage.oss2")
|
|
@patch("packages.shared.storage.get_shared_settings")
|
|
def test_endpoint_without_scheme_gets_https_prefix(self, mock_settings, mock_oss2):
|
|
"""endpoint 无 scheme 时应自动加 https://,确保 sign_url 生成 HTTPS URL。"""
|
|
from packages.shared.storage import SharedStorageService
|
|
|
|
mock_settings.return_value.oss_bucket_name = "test-bucket"
|
|
mock_settings.return_value.oss_endpoint = "oss-cn-hangzhou.aliyuncs.com"
|
|
mock_settings.return_value.oss_access_key_id = "test-key-id"
|
|
mock_settings.return_value.oss_access_key_secret = "test-key-secret"
|
|
mock_oss2.Bucket.return_value = MagicMock()
|
|
|
|
SharedStorageService()
|
|
|
|
# 验证传给 oss2.Bucket 的 endpoint 带了 https://
|
|
call_args = mock_oss2.Bucket.call_args
|
|
endpoint_passed = call_args[0][1] # 第二个位置参数
|
|
assert endpoint_passed == "https://oss-cn-hangzhou.aliyuncs.com"
|
|
|
|
@patch("packages.shared.storage.oss2")
|
|
@patch("packages.shared.storage.get_shared_settings")
|
|
def test_endpoint_with_existing_https_not_doubled(self, mock_settings, mock_oss2):
|
|
"""endpoint 已有 https:// 时不应重复添加。"""
|
|
from packages.shared.storage import SharedStorageService
|
|
|
|
mock_settings.return_value.oss_bucket_name = "test-bucket"
|
|
mock_settings.return_value.oss_endpoint = "https://oss-cn-hangzhou.aliyuncs.com"
|
|
mock_settings.return_value.oss_access_key_id = "test-key-id"
|
|
mock_settings.return_value.oss_access_key_secret = "test-key-secret"
|
|
mock_oss2.Bucket.return_value = MagicMock()
|
|
|
|
SharedStorageService()
|
|
|
|
call_args = mock_oss2.Bucket.call_args
|
|
endpoint_passed = call_args[0][1]
|
|
assert endpoint_passed == "https://oss-cn-hangzhou.aliyuncs.com"
|