fix(ci): auto-fix lint/format issues
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 36s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (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 / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 3m40s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 18s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 16m24s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 33m34s
PR Automation / Auto Approve on CI Green (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Failing after 0s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 36s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (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 / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 3m40s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 18s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 16m24s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 33m34s
PR Automation / Auto Approve on CI Green (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Failing after 0s
This commit is contained in:
@@ -10,12 +10,11 @@ FeatureFlagStore 单元测试
|
||||
import pytest
|
||||
|
||||
from packages.adapters.redis.feature_flag_store import (
|
||||
FEATURE_FLAG_REDIS_PREFIX,
|
||||
FeatureFlagConfig,
|
||||
InMemoryFeatureFlagStore,
|
||||
FEATURE_FLAG_REDIS_PREFIX,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 常量
|
||||
# ============================================================
|
||||
@@ -414,11 +413,13 @@ class TestInMemoryStoreIsActive:
|
||||
|
||||
def test_is_active_with_identifier_whitelist(self):
|
||||
store = InMemoryFeatureFlagStore()
|
||||
store.set(FeatureFlagConfig(
|
||||
name="beta",
|
||||
enabled=True,
|
||||
percentage=0,
|
||||
whitelist={"tester1"},
|
||||
))
|
||||
store.set(
|
||||
FeatureFlagConfig(
|
||||
name="beta",
|
||||
enabled=True,
|
||||
percentage=0,
|
||||
whitelist={"tester1"},
|
||||
)
|
||||
)
|
||||
assert store.is_active("beta", identifier="tester1") is True
|
||||
assert store.is_active("beta", identifier="other_user") is False
|
||||
|
||||
@@ -11,15 +11,14 @@ Module Registry 模块注册中心单元测试
|
||||
import pytest
|
||||
|
||||
from packages.infrastructure.module_registry import (
|
||||
Module,
|
||||
ModuleCapability,
|
||||
ModuleRegistry,
|
||||
ModuleStatus,
|
||||
QuotaRule,
|
||||
ModuleCapability,
|
||||
Module,
|
||||
ModuleRegistry,
|
||||
module_registry,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ModuleStatus
|
||||
# ============================================================
|
||||
@@ -413,18 +412,22 @@ class TestModuleRegistryCapabilities:
|
||||
|
||||
def test_has_capability_true(self):
|
||||
registry = ModuleRegistry()
|
||||
registry.register(Module(
|
||||
name="voice_module",
|
||||
capabilities=[ModuleCapability(name="generate_voice")],
|
||||
))
|
||||
registry.register(
|
||||
Module(
|
||||
name="voice_module",
|
||||
capabilities=[ModuleCapability(name="generate_voice")],
|
||||
)
|
||||
)
|
||||
assert registry.has_capability("generate_voice") is True
|
||||
|
||||
def test_has_capability_false(self):
|
||||
registry = ModuleRegistry()
|
||||
registry.register(Module(
|
||||
name="voice_module",
|
||||
capabilities=[ModuleCapability(name="generate_voice")],
|
||||
))
|
||||
registry.register(
|
||||
Module(
|
||||
name="voice_module",
|
||||
capabilities=[ModuleCapability(name="generate_voice")],
|
||||
)
|
||||
)
|
||||
assert registry.has_capability("generate_video") is False
|
||||
|
||||
def test_has_capability_inactive_module_not_counted(self):
|
||||
@@ -460,10 +463,12 @@ class TestModuleRegistryCapabilities:
|
||||
QuotaRule(dimension="storage", per_operation=0.5),
|
||||
]
|
||||
registry = ModuleRegistry()
|
||||
registry.register(Module(
|
||||
name="voice_mod",
|
||||
capabilities=[ModuleCapability(name="gen", quota_rules=rules)],
|
||||
))
|
||||
registry.register(
|
||||
Module(
|
||||
name="voice_mod",
|
||||
capabilities=[ModuleCapability(name="gen", quota_rules=rules)],
|
||||
)
|
||||
)
|
||||
result = registry.get_quota_rules("gen")
|
||||
assert len(result) == 2
|
||||
assert result[0].dimension == "credits"
|
||||
@@ -488,23 +493,27 @@ class TestModuleRegistryActiveCapabilities:
|
||||
|
||||
def test_single_module_with_caps(self):
|
||||
registry = ModuleRegistry()
|
||||
registry.register(Module(
|
||||
name="voice_mod",
|
||||
capabilities=[
|
||||
ModuleCapability(name="generate_voice"),
|
||||
ModuleCapability(name="clone_voice"),
|
||||
],
|
||||
))
|
||||
registry.register(
|
||||
Module(
|
||||
name="voice_mod",
|
||||
capabilities=[
|
||||
ModuleCapability(name="generate_voice"),
|
||||
ModuleCapability(name="clone_voice"),
|
||||
],
|
||||
)
|
||||
)
|
||||
result = registry.get_active_capabilities()
|
||||
assert "voice_mod" in result
|
||||
assert set(result["voice_mod"]) == {"generate_voice", "clone_voice"}
|
||||
|
||||
def test_skips_inactive_modules(self):
|
||||
registry = ModuleRegistry()
|
||||
registry.register(Module(
|
||||
name="active_mod",
|
||||
capabilities=[ModuleCapability(name="active_cap")],
|
||||
))
|
||||
registry.register(
|
||||
Module(
|
||||
name="active_mod",
|
||||
capabilities=[ModuleCapability(name="active_cap")],
|
||||
)
|
||||
)
|
||||
inactive = Module(
|
||||
name="inactive_mod",
|
||||
capabilities=[ModuleCapability(name="inactive_cap")],
|
||||
@@ -524,14 +533,18 @@ class TestModuleRegistryActiveCapabilities:
|
||||
|
||||
def test_multiple_modules(self):
|
||||
registry = ModuleRegistry()
|
||||
registry.register(Module(
|
||||
name="mod1",
|
||||
capabilities=[ModuleCapability(name="cap_a")],
|
||||
))
|
||||
registry.register(Module(
|
||||
name="mod2",
|
||||
capabilities=[ModuleCapability(name="cap_b"), ModuleCapability(name="cap_c")],
|
||||
))
|
||||
registry.register(
|
||||
Module(
|
||||
name="mod1",
|
||||
capabilities=[ModuleCapability(name="cap_a")],
|
||||
)
|
||||
)
|
||||
registry.register(
|
||||
Module(
|
||||
name="mod2",
|
||||
capabilities=[ModuleCapability(name="cap_b"), ModuleCapability(name="cap_c")],
|
||||
)
|
||||
)
|
||||
result = registry.get_active_capabilities()
|
||||
assert len(result) == 2
|
||||
assert result["mod1"] == ["cap_a"]
|
||||
@@ -552,4 +565,5 @@ class TestGlobalSingleton:
|
||||
|
||||
def test_singleton_is_same_instance(self):
|
||||
from packages.infrastructure.module_registry import module_registry as mr2
|
||||
|
||||
assert module_registry is mr2
|
||||
|
||||
@@ -12,13 +12,12 @@ import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from packages.application.common.pagination import (
|
||||
PaginationParams,
|
||||
PaginationMeta,
|
||||
PaginatedResponse,
|
||||
PaginationMeta,
|
||||
PaginationParams,
|
||||
paginate,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# PaginationParams
|
||||
# ============================================================
|
||||
|
||||
@@ -26,7 +26,6 @@ from packages.shared.storage import (
|
||||
get_storage_service,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Fixtures
|
||||
# ============================================================
|
||||
@@ -49,9 +48,10 @@ def _make_service(
|
||||
|
||||
mock_bucket = MagicMock() if with_bucket else None
|
||||
|
||||
with patch("packages.shared.storage.get_shared_settings", return_value=mock_settings), \
|
||||
patch.dict(os.environ, {"GENERATED_FILES_URL_PREFIX": local_url_prefix}, clear=False):
|
||||
|
||||
with (
|
||||
patch("packages.shared.storage.get_shared_settings", return_value=mock_settings),
|
||||
patch.dict(os.environ, {"GENERATED_FILES_URL_PREFIX": local_url_prefix}, clear=False),
|
||||
):
|
||||
if with_bucket:
|
||||
with patch("packages.shared.storage.oss2") as mock_oss2:
|
||||
mock_oss2.Auth.return_value = MagicMock()
|
||||
@@ -85,51 +85,37 @@ class TestNormalizeStorageKey:
|
||||
|
||||
def test_https_url_extracts_path(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"https://test-bucket.oss-cn-hangzhou.aliyuncs.com/videos/clip.mp4"
|
||||
)
|
||||
result = service._normalize_storage_key("https://test-bucket.oss-cn-hangzhou.aliyuncs.com/videos/clip.mp4")
|
||||
assert result == "videos/clip.mp4"
|
||||
|
||||
def test_http_url_extracts_path(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"http://test-bucket.oss-cn-hangzhou.aliyuncs.com/audio/voice.mp3"
|
||||
)
|
||||
result = service._normalize_storage_key("http://test-bucket.oss-cn-hangzhou.aliyuncs.com/audio/voice.mp3")
|
||||
assert result == "audio/voice.mp3"
|
||||
|
||||
def test_url_with_query_strips_query(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"https://bucket.oss-cn.com/file.mp4?signature=abc&expires=123"
|
||||
)
|
||||
result = service._normalize_storage_key("https://bucket.oss-cn.com/file.mp4?signature=abc&expires=123")
|
||||
assert result == "file.mp4"
|
||||
|
||||
def test_url_with_leading_slash_in_path(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"https://bucket.oss.com//double/slash.jpg"
|
||||
)
|
||||
result = service._normalize_storage_key("https://bucket.oss.com//double/slash.jpg")
|
||||
assert result == "double/slash.jpg"
|
||||
|
||||
def test_url_decodes_percent_encoded_spaces(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"https://bucket.oss.com/my%20video.mp4"
|
||||
)
|
||||
result = service._normalize_storage_key("https://bucket.oss.com/my%20video.mp4")
|
||||
assert result == "my video.mp4"
|
||||
|
||||
def test_url_decodes_percent_encoded_chinese(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"https://bucket.oss.com/%E4%B8%AD%E6%96%87.mp4"
|
||||
)
|
||||
result = service._normalize_storage_key("https://bucket.oss.com/%E4%B8%AD%E6%96%87.mp4")
|
||||
assert result == "中文.mp4"
|
||||
|
||||
def test_url_with_special_chars_decoded(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"https://bucket.oss.com/file%281%29.jpg"
|
||||
)
|
||||
result = service._normalize_storage_key("https://bucket.oss.com/file%281%29.jpg")
|
||||
assert result == "file(1).jpg"
|
||||
|
||||
def test_plain_key_with_percent_not_decoded(self):
|
||||
@@ -151,16 +137,12 @@ class TestNormalizeStorageKey:
|
||||
|
||||
def test_nested_path_url(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"https://bucket.oss.com/a/b/c/d/file.txt"
|
||||
)
|
||||
result = service._normalize_storage_key("https://bucket.oss.com/a/b/c/d/file.txt")
|
||||
assert result == "a/b/c/d/file.txt"
|
||||
|
||||
def test_url_with_port(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service._normalize_storage_key(
|
||||
"https://bucket.oss.com:443/file.txt"
|
||||
)
|
||||
result = service._normalize_storage_key("https://bucket.oss.com:443/file.txt")
|
||||
assert result == "file.txt"
|
||||
|
||||
|
||||
@@ -183,9 +165,7 @@ class TestIsLocalGeneratedUrl:
|
||||
|
||||
def test_full_url_with_local_path_returns_true(self):
|
||||
service, _, _ = _make_service()
|
||||
assert service._is_local_generated_url(
|
||||
"https://example.com/generated-files/abc.mp4"
|
||||
) is True
|
||||
assert service._is_local_generated_url("https://example.com/generated-files/abc.mp4") is True
|
||||
|
||||
def test_other_path_returns_false(self):
|
||||
service, _, _ = _make_service()
|
||||
@@ -252,31 +232,23 @@ class TestCreateDirectUploadPost:
|
||||
|
||||
def test_method_is_post(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service.create_direct_upload_post(
|
||||
"uploads/a.jpg", "image/jpeg", 1024, 3600
|
||||
)
|
||||
result = service.create_direct_upload_post("uploads/a.jpg", "image/jpeg", 1024, 3600)
|
||||
assert result["method"] == "POST"
|
||||
|
||||
def test_url_is_public_url(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service.create_direct_upload_post(
|
||||
"uploads/a.jpg", "image/jpeg", 1024, 3600
|
||||
)
|
||||
result = service.create_direct_upload_post("uploads/a.jpg", "image/jpeg", 1024, 3600)
|
||||
assert result["url"] == "https://test-bucket.oss-cn-hangzhou.aliyuncs.com"
|
||||
|
||||
def test_storage_key_normalized(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service.create_direct_upload_post(
|
||||
"/uploads/test.jpg", "image/jpeg", 1024, 3600
|
||||
)
|
||||
result = service.create_direct_upload_post("/uploads/test.jpg", "image/jpeg", 1024, 3600)
|
||||
assert result["storage_key"] == "uploads/test.jpg"
|
||||
assert result["fields"]["key"] == "uploads/test.jpg"
|
||||
|
||||
def test_fields_contain_required_keys(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service.create_direct_upload_post(
|
||||
"uploads/a.jpg", "image/jpeg", 1024, 3600
|
||||
)
|
||||
result = service.create_direct_upload_post("uploads/a.jpg", "image/jpeg", 1024, 3600)
|
||||
fields = result["fields"]
|
||||
assert fields["key"] == "uploads/a.jpg"
|
||||
assert fields["OSSAccessKeyId"] == "test-key-id"
|
||||
@@ -289,9 +261,7 @@ class TestCreateDirectUploadPost:
|
||||
"""验证 HMAC-SHA1 签名是否正确"""
|
||||
secret = "my-secret-key-123"
|
||||
service, _, _ = _make_service(access_key_secret=secret)
|
||||
result = service.create_direct_upload_post(
|
||||
"uploads/a.jpg", "image/jpeg", 1024, 3600
|
||||
)
|
||||
result = service.create_direct_upload_post("uploads/a.jpg", "image/jpeg", 1024, 3600)
|
||||
policy = result["fields"]["policy"]
|
||||
signature = result["fields"]["Signature"]
|
||||
|
||||
@@ -303,9 +273,7 @@ class TestCreateDirectUploadPost:
|
||||
|
||||
def test_policy_contains_bucket_and_key(self):
|
||||
service, _, _ = _make_service(bucket_name="my-bucket")
|
||||
result = service.create_direct_upload_post(
|
||||
"uploads/photo.png", "image/png", 2048, 1800
|
||||
)
|
||||
result = service.create_direct_upload_post("uploads/photo.png", "image/png", 2048, 1800)
|
||||
policy = json.loads(base64.b64decode(result["fields"]["policy"]))
|
||||
conditions = policy["conditions"]
|
||||
|
||||
@@ -314,9 +282,7 @@ class TestCreateDirectUploadPost:
|
||||
|
||||
def test_policy_contains_content_length_range(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service.create_direct_upload_post(
|
||||
"uploads/a.jpg", "image/jpeg", 5242880, 3600
|
||||
)
|
||||
result = service.create_direct_upload_post("uploads/a.jpg", "image/jpeg", 5242880, 3600)
|
||||
policy = json.loads(base64.b64decode(result["fields"]["policy"]))
|
||||
conditions = policy["conditions"]
|
||||
|
||||
@@ -327,9 +293,7 @@ class TestCreateDirectUploadPost:
|
||||
|
||||
def test_policy_content_type_starts_with(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service.create_direct_upload_post(
|
||||
"uploads/a.jpg", "image/jpeg", 1024, 3600
|
||||
)
|
||||
result = service.create_direct_upload_post("uploads/a.jpg", "image/jpeg", 1024, 3600)
|
||||
policy = json.loads(base64.b64decode(result["fields"]["policy"]))
|
||||
conditions = policy["conditions"]
|
||||
|
||||
@@ -340,9 +304,7 @@ class TestCreateDirectUploadPost:
|
||||
|
||||
def test_policy_has_expiration(self):
|
||||
service, _, _ = _make_service()
|
||||
result = service.create_direct_upload_post(
|
||||
"uploads/a.jpg", "image/jpeg", 1024, 3600
|
||||
)
|
||||
result = service.create_direct_upload_post("uploads/a.jpg", "image/jpeg", 1024, 3600)
|
||||
policy = json.loads(base64.b64decode(result["fields"]["policy"]))
|
||||
assert "expiration" in policy
|
||||
# ISO 8601 格式
|
||||
@@ -351,16 +313,12 @@ class TestCreateDirectUploadPost:
|
||||
def test_non_uploads_key_raises_value_error(self):
|
||||
service, _, _ = _make_service()
|
||||
with pytest.raises(ValueError, match="uploads/"):
|
||||
service.create_direct_upload_post(
|
||||
"videos/a.mp4", "video/mp4", 1024, 3600
|
||||
)
|
||||
service.create_direct_upload_post("videos/a.mp4", "video/mp4", 1024, 3600)
|
||||
|
||||
def test_no_credentials_raises_runtime_error(self):
|
||||
service, _, _ = _make_service(access_key_id="", access_key_secret="", with_bucket=False)
|
||||
with pytest.raises(RuntimeError, match="not configured"):
|
||||
service.create_direct_upload_post(
|
||||
"uploads/a.jpg", "image/jpeg", 1024, 3600
|
||||
)
|
||||
service.create_direct_upload_post("uploads/a.jpg", "image/jpeg", 1024, 3600)
|
||||
|
||||
def test_url_normalized_key_in_uploads(self):
|
||||
service, _, _ = _make_service()
|
||||
@@ -394,9 +352,7 @@ class TestGetDownloadUrlFallback:
|
||||
|
||||
def test_no_bucket_url_input_normalized(self):
|
||||
service, _, _ = _make_service(with_bucket=False)
|
||||
result = service.get_download_url(
|
||||
"https://test-bucket.oss-cn-hangzhou.aliyuncs.com/videos/test.mp4"
|
||||
)
|
||||
result = service.get_download_url("https://test-bucket.oss-cn-hangzhou.aliyuncs.com/videos/test.mp4")
|
||||
assert result == "https://test-bucket.oss-cn-hangzhou.aliyuncs.com/videos/test.mp4"
|
||||
|
||||
def test_with_bucket_calls_sign_url(self):
|
||||
@@ -534,8 +490,10 @@ class TestInitEndpointHandling:
|
||||
mock_settings.oss_access_key_id = "key-id"
|
||||
mock_settings.oss_access_key_secret = "key-secret"
|
||||
|
||||
with patch("packages.shared.storage.get_shared_settings", return_value=mock_settings), \
|
||||
patch("packages.shared.storage.oss2") as mock_oss2:
|
||||
with (
|
||||
patch("packages.shared.storage.get_shared_settings", return_value=mock_settings),
|
||||
patch("packages.shared.storage.oss2") as mock_oss2,
|
||||
):
|
||||
mock_oss2.Bucket.return_value = MagicMock()
|
||||
|
||||
service = SharedStorageService()
|
||||
@@ -551,8 +509,10 @@ class TestInitEndpointHandling:
|
||||
mock_settings.oss_access_key_id = "key-id"
|
||||
mock_settings.oss_access_key_secret = "key-secret"
|
||||
|
||||
with patch("packages.shared.storage.get_shared_settings", return_value=mock_settings), \
|
||||
patch("packages.shared.storage.oss2") as mock_oss2:
|
||||
with (
|
||||
patch("packages.shared.storage.get_shared_settings", return_value=mock_settings),
|
||||
patch("packages.shared.storage.oss2") as mock_oss2,
|
||||
):
|
||||
mock_oss2.Bucket.return_value = MagicMock()
|
||||
|
||||
service = SharedStorageService()
|
||||
@@ -567,8 +527,10 @@ class TestInitEndpointHandling:
|
||||
mock_settings.oss_access_key_id = "key-id"
|
||||
mock_settings.oss_access_key_secret = "key-secret"
|
||||
|
||||
with patch("packages.shared.storage.get_shared_settings", return_value=mock_settings), \
|
||||
patch("packages.shared.storage.oss2") as mock_oss2:
|
||||
with (
|
||||
patch("packages.shared.storage.get_shared_settings", return_value=mock_settings),
|
||||
patch("packages.shared.storage.oss2") as mock_oss2,
|
||||
):
|
||||
mock_oss2.Bucket.return_value = MagicMock()
|
||||
|
||||
service = SharedStorageService()
|
||||
|
||||
@@ -14,7 +14,6 @@ import pytest
|
||||
|
||||
from packages.application.tts_job.text_splitter import split_text
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 基础场景
|
||||
# ============================================================
|
||||
@@ -154,6 +153,7 @@ class TestLongSentenceHardCut:
|
||||
def test_all_segments_within_limit(self):
|
||||
"""所有段都不超过 max_chars"""
|
||||
import random
|
||||
|
||||
random.seed(42)
|
||||
# 生成随机长度的文本
|
||||
text = "".join(random.choices("字字字字。!?;\n", k=5000))
|
||||
|
||||
@@ -11,28 +11,27 @@ url_security URL安全校验单元测试
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from packages.shared.url_security import (
|
||||
ALLOWED_SCHEMES,
|
||||
ALLOWED_PORTS,
|
||||
MAX_URL_LENGTH,
|
||||
UrlSecurityError,
|
||||
NoRedirectHandler,
|
||||
validate_url_safety,
|
||||
is_url_safe,
|
||||
_validate_magic_number,
|
||||
_check_internal_hostnames,
|
||||
_is_trusted_domain,
|
||||
safe_download_file,
|
||||
safe_download_bytes,
|
||||
ALLOWED_AUDIO_MIME_TYPES,
|
||||
ALLOWED_IMAGE_MIME_TYPES,
|
||||
ALLOWED_PORTS,
|
||||
ALLOWED_SCHEMES,
|
||||
MAX_URL_LENGTH,
|
||||
NoRedirectHandler,
|
||||
UrlSecurityError,
|
||||
_check_internal_hostnames,
|
||||
_is_trusted_domain,
|
||||
_validate_magic_number,
|
||||
is_url_safe,
|
||||
safe_download_bytes,
|
||||
safe_download_file,
|
||||
validate_url_safety,
|
||||
)
|
||||
|
||||
|
||||
# ── validate_url_safety 基础校验 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -203,9 +202,10 @@ class TestTrustedDomains:
|
||||
def test_is_trusted_domain_exact_match(self):
|
||||
with patch("packages.shared.url_security.TRUSTED_DOMAINS", {"example.com", "cdn.example.org"}):
|
||||
# 重新加载模块以应用环境变量不太现实,直接测函数
|
||||
from packages.shared.url_security import _is_trusted_domain
|
||||
# 直接改全局状态再还原
|
||||
import packages.shared.url_security as mod
|
||||
from packages.shared.url_security import _is_trusted_domain
|
||||
|
||||
original = mod.TRUSTED_DOMAINS
|
||||
mod.TRUSTED_DOMAINS = {"example.com", "cdn.example.org"}
|
||||
try:
|
||||
@@ -216,6 +216,7 @@ class TestTrustedDomains:
|
||||
|
||||
def test_is_trusted_domain_subdomain(self):
|
||||
import packages.shared.url_security as mod
|
||||
|
||||
original = mod.TRUSTED_DOMAINS
|
||||
mod.TRUSTED_DOMAINS = {"example.com"}
|
||||
try:
|
||||
@@ -226,6 +227,7 @@ class TestTrustedDomains:
|
||||
|
||||
def test_is_trusted_domain_no_match(self):
|
||||
import packages.shared.url_security as mod
|
||||
|
||||
original = mod.TRUSTED_DOMAINS
|
||||
mod.TRUSTED_DOMAINS = {"example.com"}
|
||||
try:
|
||||
@@ -237,6 +239,7 @@ class TestTrustedDomains:
|
||||
def test_validate_with_trusted_domains_restricted(self):
|
||||
"""白名单非空时,不在白名单中的域名被拒"""
|
||||
import packages.shared.url_security as mod
|
||||
|
||||
original = mod.TRUSTED_DOMAINS
|
||||
mod.TRUSTED_DOMAINS = {"trusted.com"}
|
||||
try:
|
||||
@@ -297,8 +300,12 @@ class TestNoRedirectHandler:
|
||||
def test_redirect_request_returns_none(self):
|
||||
handler = NoRedirectHandler()
|
||||
result = handler.redirect_request(
|
||||
MagicMock(), MagicMock(), 302, "Found",
|
||||
{"Location": "http://other.com"}, "http://other.com",
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
302,
|
||||
"Found",
|
||||
{"Location": "http://other.com"},
|
||||
"http://other.com",
|
||||
)
|
||||
assert result is None
|
||||
|
||||
@@ -312,80 +319,80 @@ class TestMagicNumberValidation:
|
||||
def test_valid_png(self, tmp_path):
|
||||
f = tmp_path / "test.png"
|
||||
# PNG 文件头: 89 50 4E 47 0D 0A 1A 0A
|
||||
f.write_bytes(b'\x89PNG\r\n\x1a\n' + b'\x00' * 100)
|
||||
f.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
||||
# 不抛异常 = 通过
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_jpeg(self, tmp_path):
|
||||
f = tmp_path / "test.jpg"
|
||||
# JPEG 文件头: FF D8 FF
|
||||
f.write_bytes(b'\xff\xd8\xff\xe0' + b'\x00' * 100)
|
||||
f.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_gif87a(self, tmp_path):
|
||||
f = tmp_path / "test.gif"
|
||||
f.write_bytes(b'GIF87a' + b'\x00' * 100)
|
||||
f.write_bytes(b"GIF87a" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_gif89a(self, tmp_path):
|
||||
f = tmp_path / "test.gif"
|
||||
f.write_bytes(b'GIF89a' + b'\x00' * 100)
|
||||
f.write_bytes(b"GIF89a" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_webp(self, tmp_path):
|
||||
f = tmp_path / "test.webp"
|
||||
# RIFF....WEBP
|
||||
data = bytearray(b'RIFF')
|
||||
data += b'\x00\x00\x00\x00' # size placeholder
|
||||
data += b'WEBP'
|
||||
data += b'\x00' * 100
|
||||
data = bytearray(b"RIFF")
|
||||
data += b"\x00\x00\x00\x00" # size placeholder
|
||||
data += b"WEBP"
|
||||
data += b"\x00" * 100
|
||||
f.write_bytes(bytes(data))
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_bmp(self, tmp_path):
|
||||
f = tmp_path / "test.bmp"
|
||||
f.write_bytes(b'BM' + b'\x00' * 100)
|
||||
f.write_bytes(b"BM" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_wav(self, tmp_path):
|
||||
f = tmp_path / "test.wav"
|
||||
# RIFF....WAVE
|
||||
data = bytearray(b'RIFF')
|
||||
data += b'\x00\x00\x00\x00'
|
||||
data += b'WAVE'
|
||||
data += b'\x00' * 100
|
||||
data = bytearray(b"RIFF")
|
||||
data += b"\x00\x00\x00\x00"
|
||||
data += b"WAVE"
|
||||
data += b"\x00" * 100
|
||||
f.write_bytes(bytes(data))
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_valid_mp3_id3(self, tmp_path):
|
||||
f = tmp_path / "test.mp3"
|
||||
f.write_bytes(b'ID3\x03\x00\x00\x00\x00\x00\x00' + b'\x00' * 100)
|
||||
f.write_bytes(b"ID3\x03\x00\x00\x00\x00\x00\x00" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_valid_mp3_adts(self, tmp_path):
|
||||
f = tmp_path / "test.mp3"
|
||||
f.write_bytes(b'\xff\xfb\x90\x00' + b'\x00' * 100)
|
||||
f.write_bytes(b"\xff\xfb\x90\x00" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_valid_ogg(self, tmp_path):
|
||||
f = tmp_path / "test.ogg"
|
||||
f.write_bytes(b'OggS\x00\x02\x00\x00' + b'\x00' * 100)
|
||||
f.write_bytes(b"OggS\x00\x02\x00\x00" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_valid_flac(self, tmp_path):
|
||||
f = tmp_path / "test.flac"
|
||||
f.write_bytes(b'fLaC' + b'\x00' * 100)
|
||||
f.write_bytes(b"fLaC" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_invalid_file_content_raises(self, tmp_path):
|
||||
f = tmp_path / "test.bin"
|
||||
f.write_bytes(b'this is not an image file at all')
|
||||
f.write_bytes(b"this is not an image file at all")
|
||||
with pytest.raises(UrlSecurityError, match="魔数"):
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_empty_file_raises(self, tmp_path):
|
||||
f = tmp_path / "empty.bin"
|
||||
f.write_bytes(b'')
|
||||
f.write_bytes(b"")
|
||||
with pytest.raises(UrlSecurityError, match="为空"):
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
@@ -396,14 +403,14 @@ class TestMagicNumberValidation:
|
||||
def test_no_allowed_mime_types_skips(self, tmp_path):
|
||||
"""allowed_mime_types 为空时跳过校验"""
|
||||
f = tmp_path / "test.bin"
|
||||
f.write_bytes(b'random data here')
|
||||
f.write_bytes(b"random data here")
|
||||
# 不抛异常
|
||||
_validate_magic_number(str(f), set())
|
||||
|
||||
def test_unknown_mime_types_skips(self, tmp_path):
|
||||
"""没有已知魔数的 MIME 类型跳过校验"""
|
||||
f = tmp_path / "test.bin"
|
||||
f.write_bytes(b'random data')
|
||||
f.write_bytes(b"random data")
|
||||
_validate_magic_number(str(f), {"application/x-unknown-type"})
|
||||
|
||||
|
||||
@@ -441,7 +448,7 @@ class TestSafeDownloadFile:
|
||||
|
||||
def test_download_with_mime_check_passes(self, tmp_path):
|
||||
# PNG 文件
|
||||
test_content = b'\x89PNG\r\n\x1a\n' + b'\x00' * 200
|
||||
test_content = b"\x89PNG\r\n\x1a\n" + b"\x00" * 200
|
||||
dest = str(tmp_path / "test.png")
|
||||
|
||||
mock_resp = MagicMock()
|
||||
@@ -463,7 +470,7 @@ class TestSafeDownloadFile:
|
||||
assert size == len(test_content)
|
||||
|
||||
def test_download_mime_type_rejected(self, tmp_path):
|
||||
test_content = b'GIF89a' + b'\x00' * 50
|
||||
test_content = b"GIF89a" + b"\x00" * 50
|
||||
dest = str(tmp_path / "test.gif")
|
||||
|
||||
mock_resp = MagicMock()
|
||||
|
||||
@@ -8,17 +8,16 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.application.auth.verification_code_service import (
|
||||
validate_phone,
|
||||
normalize_phone,
|
||||
validate_email,
|
||||
)
|
||||
from packages.adapters.sqlalchemy_impl.schema_guard import (
|
||||
BLOCKED_AUTO_CREATE_ENVIRONMENTS,
|
||||
normalize_environment,
|
||||
assert_auto_create_schema_allowed,
|
||||
normalize_environment,
|
||||
)
|
||||
from packages.application.auth.verification_code_service import (
|
||||
normalize_phone,
|
||||
validate_email,
|
||||
validate_phone,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# validate_phone
|
||||
|
||||
Reference in New Issue
Block a user