"""OSS助手纯逻辑测试 — normalize_storage_key / resolve_asset_path 输入校验.""" from __future__ import annotations import hashlib import os from pathlib import Path from unittest.mock import patch import pytest from video_processing.oss_helpers import normalize_storage_key, resolve_asset_path class TestNormalizeStorageKey: """normalize_storage_key 存储键标准化测试.""" def test_plain_key_passthrough(self): """普通路径原样返回.""" assert normalize_storage_key("path/to/file.mp4") == "path/to/file.mp4" def test_https_url_extracts_path(self): """HTTPS URL提取path部分.""" result = normalize_storage_key("https://bucket.oss-cn-hangzhou.aliyuncs.com/path/to/file.mp4") assert result == "path/to/file.mp4" def test_http_url_extracts_path(self): """HTTP URL提取path部分.""" result = normalize_storage_key("http://example.com/assets/video.mp4") assert result == "assets/video.mp4" def test_url_with_query_params(self): """带query参数的URL只取path.""" result = normalize_storage_key("https://bucket.oss-cn-hangzhou.aliyuncs.com/file.mp4?token=abc&expires=123") assert result == "file.mp4" def test_leading_slash_stripped(self): """开头斜杠被去掉.""" assert normalize_storage_key("/path/to/file.mp4") == "path/to/file.mp4" def test_url_without_path(self): """URL没有path部分返回空字符串.""" result = normalize_storage_key("https://example.com") assert result == "" def test_nested_path(self): """多层嵌套路径.""" assert normalize_storage_key("a/b/c/d/file.mp4") == "a/b/c/d/file.mp4" def test_empty_string(self): """空字符串.""" assert normalize_storage_key("") == "" def test_url_with_port(self): """带端口的URL.""" result = normalize_storage_key("http://localhost:9000/bucket/file.mp4") assert result == "bucket/file.mp4" class TestResolveAssetPathInputValidation: """resolve_asset_path 输入校验测试(不涉及真实下载).""" def test_empty_string_returns_none(self, tmp_path): """空字符串返回None.""" assert resolve_asset_path("", tmp_path) is None def test_none_returns_none(self, tmp_path): """None返回None(类型检查).""" assert resolve_asset_path(None, tmp_path) is None # type: ignore def test_non_string_returns_none(self, tmp_path): """非字符串返回None.""" assert resolve_asset_path(123, tmp_path) is None # type: ignore def test_null_byte_rejected(self, tmp_path): """包含空字节的asset_id被拒绝.""" assert resolve_asset_path("file\x00.mp4", tmp_path) is None def test_path_traversal_rejected(self, tmp_path): """包含../的路径遍历攻击被拒绝(第3步下载前检查).""" # mock download_asset不被调用,因为路径包含..会直接返回None with patch("video_processing.oss_helpers.download_asset") as mock_dl: result = resolve_asset_path("../etc/passwd", tmp_path) assert result is None mock_dl.assert_not_called() def test_absolute_path_key_rejected(self, tmp_path): """以/开头的存储键在下载前检查被拒.""" with patch("video_processing.oss_helpers.download_asset") as mock_dl: result = resolve_asset_path("/etc/passwd", tmp_path) assert result is None mock_dl.assert_not_called() def test_cache_hit_returns_cached_path(self, tmp_path): """缓存命中返回缓存路径.""" asset_id = "test-asset-123" cache_hash = hashlib.sha256(asset_id.encode()).hexdigest()[:16] cached_file = tmp_path / f"{cache_hash}.mp4" cached_file.write_bytes(b"fake video data") result = resolve_asset_path(asset_id, tmp_path) assert result == cached_file assert result.exists() def test_cache_empty_file_not_considered_hit(self, tmp_path): """空文件不算缓存命中.""" asset_id = "empty-cache-file" cache_hash = hashlib.sha256(asset_id.encode()).hexdigest()[:16] cached_file = tmp_path / f"{cache_hash}.mp4" cached_file.touch() # 空文件 with patch("video_processing.oss_helpers.download_asset", return_value=False): result = resolve_asset_path(asset_id, tmp_path) # 空文件不命中缓存,走下载,下载失败返回None assert result is None def test_download_success_returns_path(self, tmp_path): """下载成功返回本地路径.""" asset_id = "remote-asset" cache_hash = hashlib.sha256(asset_id.encode()).hexdigest()[:16] expected_path = tmp_path / f"{cache_hash}.mp4" def fake_download(storage_key, local_path): Path(local_path).write_bytes(b"downloaded data") return True with patch("video_processing.oss_helpers.download_asset", side_effect=fake_download): result = resolve_asset_path(asset_id, tmp_path) assert result == expected_path assert result.exists() assert result.stat().st_size > 0 def test_download_failure_returns_none(self, tmp_path): """下载失败返回None.""" with patch("video_processing.oss_helpers.download_asset", return_value=False): result = resolve_asset_path("nonexistent-asset", tmp_path) assert result is None def test_work_dir_not_exists_creates_on_demand(self, tmp_path): """work_dir不存在时也能处理.""" asset_id = "new-dir-asset" new_dir = tmp_path / "subdir" / "nested" with patch("video_processing.oss_helpers.download_asset", return_value=False): # 不存在的work_dir,缓存检查也不会命中 result = resolve_asset_path(asset_id, new_dir) assert result is None