7a72cfd709
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m9s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m7s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m17s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Unit Tests (push) Failing after 4m59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m25s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 27m2s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 30m15s
147 lines
5.9 KiB
Python
Executable File
147 lines
5.9 KiB
Python
Executable File
"""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
|