18bd0de3fa
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 1m42s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m44s
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 / Build Staging Web Image (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m26s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m22s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 4m29s
CI/CD Pipeline / Integration Tests (push) Successful in 2m16s
CI/CD Pipeline / Unit Tests (push) Failing after 5m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m58s
368 lines
11 KiB
Python
Executable File
368 lines
11 KiB
Python
Executable File
"""InMemoryAssetRepository 单测 — 素材仓储内存实现."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from packages.adapters.in_memory.asset_repository import InMemoryAssetRepository
|
|
from packages.domain import Asset, AssetStatus
|
|
|
|
# ── Fixtures ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture
|
|
def repo():
|
|
return InMemoryAssetRepository()
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_asset():
|
|
return Asset.create(
|
|
project_id="proj-1",
|
|
library_id="lib-1",
|
|
name="test.mp4",
|
|
storage_key="assets/test.mp4",
|
|
mime_type="video/mp4",
|
|
file_size=1024,
|
|
file_hash="hash-abc",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_assets(repo):
|
|
"""创建几个测试素材."""
|
|
assets = []
|
|
for i in range(5):
|
|
asset = Asset.create(
|
|
project_id="proj-1",
|
|
library_id="lib-1",
|
|
name=f"video_{i}.mp4",
|
|
storage_key=f"assets/video_{i}.mp4",
|
|
mime_type="video/mp4",
|
|
file_size=1000 + i,
|
|
file_hash=f"hash-{i}",
|
|
)
|
|
assets.append(repo.create(asset))
|
|
return assets
|
|
|
|
|
|
# ── CRUD 基本操作 ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestAssetRepoCRUD:
|
|
"""基本 CRUD 操作."""
|
|
|
|
def test_create_and_get(self, repo, sample_asset):
|
|
created = repo.create(sample_asset)
|
|
assert created.id == sample_asset.id
|
|
|
|
fetched = repo.get(sample_asset.id)
|
|
assert fetched is not None
|
|
assert fetched.id == sample_asset.id
|
|
assert fetched.name == "test.mp4"
|
|
|
|
def test_get_not_found(self, repo):
|
|
assert repo.get("nonexistent") is None
|
|
|
|
def test_find_by_id_alias(self, repo, sample_asset):
|
|
repo.create(sample_asset)
|
|
assert repo.find_by_id(sample_asset.id).id == sample_asset.id
|
|
|
|
def test_update(self, repo, sample_asset):
|
|
repo.create(sample_asset)
|
|
sample_asset.name = "renamed.mp4"
|
|
updated = repo.update(sample_asset)
|
|
assert updated.name == "renamed.mp4"
|
|
|
|
fetched = repo.get(sample_asset.id)
|
|
assert fetched.name == "renamed.mp4"
|
|
|
|
def test_delete_existing(self, repo, sample_asset):
|
|
repo.create(sample_asset)
|
|
result = repo.delete(sample_asset.id)
|
|
assert result is True
|
|
assert repo.get(sample_asset.id) is None
|
|
|
|
def test_delete_nonexistent(self, repo):
|
|
result = repo.delete("nonexistent")
|
|
assert result is False
|
|
|
|
|
|
# ── 查询方法 ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestAssetRepoQueries:
|
|
"""查询类方法."""
|
|
|
|
def test_list_by_project(self, repo, sample_assets):
|
|
result = repo.list_by_project("proj-1")
|
|
assert len(result) == 5
|
|
|
|
def test_list_by_project_empty(self, repo):
|
|
result = repo.list_by_project("nonexistent")
|
|
assert result == []
|
|
|
|
def test_list_by_library(self, repo, sample_assets):
|
|
result = repo.list_by_library("lib-1")
|
|
assert len(result) == 5
|
|
|
|
def test_find_by_library_alias(self, repo, sample_assets):
|
|
result = repo.find_by_library("lib-1")
|
|
assert len(result) == 5
|
|
|
|
def test_find_by_library_and_file_type_video(self, repo):
|
|
video = Asset.create(
|
|
project_id="p1",
|
|
library_id="lib-1",
|
|
name="v.mp4",
|
|
storage_key="v.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
audio = Asset.create(
|
|
project_id="p1",
|
|
library_id="lib-1",
|
|
name="a.mp3",
|
|
storage_key="a.mp3",
|
|
mime_type="audio/mp3",
|
|
)
|
|
image = Asset.create(
|
|
project_id="p1",
|
|
library_id="lib-1",
|
|
name="i.jpg",
|
|
storage_key="i.jpg",
|
|
mime_type="image/jpeg",
|
|
)
|
|
repo.create(video)
|
|
repo.create(audio)
|
|
repo.create(image)
|
|
|
|
videos = repo.find_by_library_and_file_type("lib-1", "video")
|
|
assert len(videos) == 1
|
|
assert videos[0].id == video.id
|
|
|
|
audios = repo.find_by_library_and_file_type("lib-1", "audio")
|
|
assert len(audios) == 1
|
|
assert audios[0].id == audio.id
|
|
|
|
def test_find_by_project_with_pagination(self, repo, sample_assets):
|
|
result = repo.find_by_project("proj-1", skip=0, limit=3)
|
|
assert len(result) == 3
|
|
|
|
result2 = repo.find_by_project("proj-1", skip=3, limit=10)
|
|
assert len(result2) == 2
|
|
|
|
def test_find_by_tag_ids_single_tag(self, repo):
|
|
a1 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a1.mp4",
|
|
storage_key="a1.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a1.tag_ids = ["tag1", "tag2"]
|
|
a2 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a2.mp4",
|
|
storage_key="a2.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a2.tag_ids = ["tag1"]
|
|
a3 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a3.mp4",
|
|
storage_key="a3.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a3.tag_ids = ["tag3"]
|
|
repo.create(a1)
|
|
repo.create(a2)
|
|
repo.create(a3)
|
|
|
|
result = repo.find_by_tag_ids(["tag1"])
|
|
assert len(result) == 2
|
|
|
|
def test_find_by_tag_ids_multiple_tags_all_match(self, repo):
|
|
"""必须包含所有指定标签(AND 逻辑)."""
|
|
a1 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a1.mp4",
|
|
storage_key="a1.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a1.tag_ids = ["tag1", "tag2"]
|
|
a2 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a2.mp4",
|
|
storage_key="a2.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a2.tag_ids = ["tag1"]
|
|
repo.create(a1)
|
|
repo.create(a2)
|
|
|
|
result = repo.find_by_tag_ids(["tag1", "tag2"])
|
|
assert len(result) == 1
|
|
assert result[0].id == a1.id
|
|
|
|
def test_find_by_tag_ids_empty_list(self, repo, sample_assets):
|
|
result = repo.find_by_tag_ids([])
|
|
assert result == []
|
|
|
|
def test_find_by_library_and_file_hash(self, repo, sample_asset):
|
|
repo.create(sample_asset)
|
|
result = repo.find_by_library_and_file_hash("lib-1", "hash-abc")
|
|
assert result is not None
|
|
assert result.id == sample_asset.id
|
|
|
|
def test_find_by_library_and_file_hash_not_found(self, repo):
|
|
result = repo.find_by_library_and_file_hash("lib-1", "nonexistent")
|
|
assert result is None
|
|
|
|
def test_find_by_library_and_file_hash_empty_hash(self, repo, sample_asset):
|
|
repo.create(sample_asset)
|
|
result = repo.find_by_library_and_file_hash("lib-1", "")
|
|
assert result is None
|
|
|
|
|
|
# ── 批量操作 ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestAssetRepoBatchOperations:
|
|
"""批量操作方法."""
|
|
|
|
def test_batch_delete_marks_deleted(self, repo):
|
|
a1 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a1.mp4",
|
|
storage_key="a1.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a2 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a2.mp4",
|
|
storage_key="a2.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
repo.create(a1)
|
|
repo.create(a2)
|
|
|
|
count = repo.batch_delete([a1.id, a2.id])
|
|
assert count == 2
|
|
|
|
# 状态变为 deleted
|
|
assert repo.get(a1.id).status == AssetStatus.DELETED
|
|
assert repo.get(a2.id).status == AssetStatus.DELETED
|
|
|
|
def test_batch_delete_skip_already_deleted(self, repo):
|
|
a1 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a1.mp4",
|
|
storage_key="a1.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a1.status = AssetStatus.DELETED
|
|
repo.create(a1)
|
|
a2 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a2.mp4",
|
|
storage_key="a2.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
repo.create(a2)
|
|
|
|
count = repo.batch_delete([a1.id, a2.id])
|
|
assert count == 1 # 只有a2被标记
|
|
|
|
def test_batch_delete_nonexistent(self, repo):
|
|
count = repo.batch_delete(["nonexistent"])
|
|
assert count == 0
|
|
|
|
def test_batch_update_metadata(self, repo):
|
|
a1 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a1.mp4",
|
|
storage_key="a1.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a1.metadata = {"key1": "val1"}
|
|
a2 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a2.mp4",
|
|
storage_key="a2.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
repo.create(a1)
|
|
repo.create(a2)
|
|
|
|
count = repo.batch_update_metadata([a1.id, a2.id], {"key2": "val2"})
|
|
assert count == 2
|
|
|
|
# 合并而非覆盖
|
|
assert repo.get(a1.id).metadata["key1"] == "val1"
|
|
assert repo.get(a1.id).metadata["key2"] == "val2"
|
|
assert repo.get(a2.id).metadata["key2"] == "val2"
|
|
|
|
def test_batch_add_tags(self, repo):
|
|
a1 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a1.mp4",
|
|
storage_key="a1.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a1.tag_ids = ["existing"]
|
|
repo.create(a1)
|
|
|
|
count = repo.batch_add_tags([a1.id], ["tag1", "tag2"])
|
|
assert count == 1
|
|
|
|
tags = repo.get(a1.id).tag_ids
|
|
assert "existing" in tags
|
|
assert "tag1" in tags
|
|
assert "tag2" in tags
|
|
|
|
def test_batch_add_tags_dedup(self, repo):
|
|
"""添加已存在的标签不会重复."""
|
|
a1 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a1.mp4",
|
|
storage_key="a1.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a1.tag_ids = ["tag1"]
|
|
repo.create(a1)
|
|
|
|
before_count = len(a1.tag_ids)
|
|
repo.batch_add_tags([a1.id], ["tag1", "tag1"])
|
|
# 没有变化,count 应该是0?不对,tag_ids去重后还是["tag1"],但原先是["tag1"]
|
|
# 添加tag1时发现已存在,changed=False,所以count=0
|
|
assert repo.get(a1.id).tag_ids.count("tag1") == 1
|
|
|
|
def test_batch_replace_tags(self, repo):
|
|
a1 = Asset.create(
|
|
project_id="p1",
|
|
library_id="l1",
|
|
name="a1.mp4",
|
|
storage_key="a1.mp4",
|
|
mime_type="video/mp4",
|
|
)
|
|
a1.tag_ids = ["old1", "old2"]
|
|
repo.create(a1)
|
|
|
|
count = repo.batch_replace_tags([a1.id], ["new1", "new2"])
|
|
assert count == 1
|
|
|
|
tags = repo.get(a1.id).tag_ids
|
|
assert tags == ["new1", "new2"]
|