"""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"]