"""P0-staging:OSS 上传崩溃修复测试. 测试: 1. oss_bucket() 传递 connect_timeout 参数 2. upload_to_oss() 小文件走 put_object_from_file,大文件走分片上传 3. upload_to_oss() 超时保护(超过总超时返回 None) 4. upload_to_oss() 异常时返回 None """ from __future__ import annotations import tempfile import time from pathlib import Path from unittest.mock import MagicMock, patch # ── oss_bucket connect_timeout 测试 ─────────────────────────────────────────── class TestOSSBucketConnectTimeout: """测试 oss_bucket() 传递 connect_timeout 参数.""" def test_oss_bucket_has_connect_timeout(self): """oss_bucket 应传递 connect_timeout=10s 参数.""" from video_processing.oss_helpers import oss_bucket mock_bucket_instance = MagicMock() with ( patch( "video_processing.oss_helpers.oss_settings", return_value=("test-key", "test-secret", "oss-cn-hangzhou.aliyuncs.com", "test-bucket"), ), patch("video_processing.oss_helpers.oss2.Auth"), patch("video_processing.oss_helpers.oss2.Bucket", return_value=mock_bucket_instance) as mock_bucket_cls, ): bucket = oss_bucket() assert bucket is mock_bucket_instance # 验证 connect_timeout 关键字参数 call_kwargs = mock_bucket_cls.call_args[1] assert "connect_timeout" in call_kwargs, "oss_bucket 应传递 connect_timeout 参数" assert ( call_kwargs["connect_timeout"] == 10 ), f"connect_timeout 应为 10,实际为 {call_kwargs['connect_timeout']}" def test_oss_bucket_no_config_returns_none(self): """OSS 配置缺失时返回 None.""" from video_processing.oss_helpers import oss_bucket with patch("video_processing.oss_helpers.oss_settings", return_value=None): bucket = oss_bucket() assert bucket is None # ── upload_to_oss 分片上传测试 ──────────────────────────────────────────────── class TestUploadToOSSMultipart: """测试 upload_to_oss() 根据文件大小选择上传方式.""" def _create_temp_file(self, size_bytes: int) -> Path: """创建指定大小的临时文件.""" tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") tmp.write(b"x" * size_bytes) tmp.close() return Path(tmp.name) def test_small_file_uses_put_object(self): """小文件(<100MB)走 put_object_from_file.""" from video_processing.oss_helpers import upload_to_oss small_file = self._create_temp_file(10 * 1024 * 1024) # 10MB try: mock_bucket = MagicMock() with ( patch( "video_processing.oss_helpers.oss_settings", return_value=("test-key", "test-secret", "oss-cn-hangzhou.aliyuncs.com", "test-bucket"), ), patch("video_processing.oss_helpers.oss2.Auth"), patch("video_processing.oss_helpers.oss2.Bucket", return_value=mock_bucket), patch("video_processing.oss_helpers.oss2.resumable_upload") as mock_resumable, ): url = upload_to_oss(small_file, "test/small.mp4") # 验证调用了 put_object_from_file mock_bucket.put_object_from_file.assert_called_once() # 验证没调用分片上传 mock_resumable.assert_not_called() # 验证返回 URL assert url == "https://test-bucket.oss-cn-hangzhou.aliyuncs.com/test/small.mp4" finally: small_file.unlink() def test_large_file_uses_resumable_upload(self): """大文件(>=100MB)走 resumable_upload 分片上传.""" from video_processing.oss_helpers import upload_to_oss large_file = self._create_temp_file(100 * 1024 * 1024) # 100MB try: mock_bucket = MagicMock() with ( patch( "video_processing.oss_helpers.oss_settings", return_value=("test-key", "test-secret", "oss-cn-hangzhou.aliyuncs.com", "test-bucket"), ), patch("video_processing.oss_helpers.oss2.Auth"), patch("video_processing.oss_helpers.oss2.Bucket", return_value=mock_bucket), patch("video_processing.oss_helpers.oss2.resumable_upload") as mock_resumable, ): url = upload_to_oss(large_file, "test/large.mp4") # 验证调用了分片上传 mock_resumable.assert_called_once() # 验证没调用 put_object_from_file mock_bucket.put_object_from_file.assert_not_called() # 验证分片参数 call_kwargs = mock_resumable.call_args[1] assert call_kwargs["multipart_threshold"] == 100 * 1024 * 1024 assert call_kwargs["part_size"] == 8 * 1024 * 1024 assert call_kwargs["num_threads"] == 3 # 验证返回 URL assert url == "https://test-bucket.oss-cn-hangzhou.aliyuncs.com/test/large.mp4" finally: large_file.unlink() # ── upload_to_oss 超时测试 ──────────────────────────────────────────────────── class TestUploadToOSSTimeout: """测试 upload_to_oss() 超时保护.""" def test_upload_timeout_returns_none(self): """上传超过总超时时返回 None.""" from video_processing.oss_helpers import upload_to_oss small_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") small_file.write(b"x" * 1024) # 1KB small_file.close() file_path = Path(small_file.name) def slow_upload(*args, **kwargs): """模拟慢速上传,超过超时时间.""" time.sleep(2) mock_bucket = MagicMock() mock_bucket.put_object_from_file.side_effect = slow_upload try: with ( patch( "video_processing.oss_helpers.oss_settings", return_value=("test-key", "test-secret", "oss-cn-hangzhou.aliyuncs.com", "test-bucket"), ), patch("video_processing.oss_helpers.oss2.Auth"), patch("video_processing.oss_helpers.oss2.Bucket", return_value=mock_bucket), patch("video_processing.oss_helpers.OSS_UPLOAD_TOTAL_TIMEOUT", 1), # 1秒超时 ): url = upload_to_oss(file_path, "test/slow.mp4") # 超时应返回 None assert url is None, "上传超时应返回 None" finally: file_path.unlink() def test_upload_exception_returns_none(self): """上传异常时返回 None.""" from video_processing.oss_helpers import upload_to_oss small_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") small_file.write(b"x" * 1024) small_file.close() file_path = Path(small_file.name) mock_bucket = MagicMock() mock_bucket.put_object_from_file.side_effect = RuntimeError("Network error") try: with ( patch( "video_processing.oss_helpers.oss_settings", return_value=("test-key", "test-secret", "oss-cn-hangzhou.aliyuncs.com", "test-bucket"), ), patch("video_processing.oss_helpers.oss2.Auth"), patch("video_processing.oss_helpers.oss2.Bucket", return_value=mock_bucket), ): url = upload_to_oss(file_path, "test/error.mp4") assert url is None, "上传异常应返回 None" finally: file_path.unlink() def test_upload_no_bucket_returns_none(self): """OSS 未配置时返回 None.""" from video_processing.oss_helpers import upload_to_oss small_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") small_file.write(b"x" * 1024) small_file.close() file_path = Path(small_file.name) try: with patch("video_processing.oss_helpers.oss_settings", return_value=None): url = upload_to_oss(file_path, "test/noconfig.mp4") assert url is None finally: file_path.unlink()