"""InMemoryAssetRepository 单元测试.""" import pytest from packages.adapters.in_memory.asset_repository import InMemoryAssetRepository from packages.domain.entities import Asset, AssetStatus, ClassificationStatus @pytest.fixture def repo() -> InMemoryAssetRepository: return InMemoryAssetRepository() @pytest.fixture def sample_asset() -> Asset: return Asset.create( project_id="proj-1", library_id="lib-1", name="test.mp4", storage_key="storage/key1", mime_type="video/mp4", file_size=1024, file_hash="hash-abc", ) @pytest.fixture def asset2() -> Asset: return Asset.create( project_id="proj-1", library_id="lib-1", name="test2.jpg", storage_key="storage/key2", mime_type="image/jpeg", file_size=512, file_hash="hash-def", ) @pytest.fixture def asset_other_project() -> Asset: return Asset.create( project_id="proj-2", library_id="lib-2", name="other.mp3", storage_key="storage/key3", mime_type="audio/mpeg", file_size=256, file_hash="hash-ghi", ) class TestCreateAndGet: def test_create_returns_asset(self, repo, sample_asset): result = repo.create(sample_asset) assert result.id == sample_asset.id assert result.name == "test.mp4" def test_get_existing_asset(self, repo, sample_asset): repo.create(sample_asset) result = repo.get(sample_asset.id) assert result is not None assert result.id == sample_asset.id def test_get_nonexistent_returns_none(self, repo): assert repo.get("nonexistent") is None def test_find_by_id_same_as_get(self, repo, sample_asset): repo.create(sample_asset) assert repo.find_by_id(sample_asset.id).id == repo.get(sample_asset.id).id class TestListByProject: def test_list_by_project_filters_correctly(self, repo, sample_asset, asset2, asset_other_project): repo.create(sample_asset) repo.create(asset2) repo.create(asset_other_project) proj1 = repo.list_by_project("proj-1") assert len(proj1) == 2 assert all(a.project_id == "proj-1" for a in proj1) proj2 = repo.list_by_project("proj-2") assert len(proj2) == 1 assert proj2[0].id == asset_other_project.id def test_list_by_project_empty(self, repo): assert repo.list_by_project("nonexistent") == [] class TestListByLibrary: def test_list_by_library_filters_correctly(self, repo, sample_asset, asset2, asset_other_project): repo.create(sample_asset) repo.create(asset2) repo.create(asset_other_project) lib1 = repo.list_by_library("lib-1") assert len(lib1) == 2 lib2 = repo.list_by_library("lib-2") assert len(lib2) == 1 assert lib2[0].id == asset_other_project.id def test_find_by_library_is_alias(self, repo, sample_asset): repo.create(sample_asset) assert repo.find_by_library("lib-1") == repo.list_by_library("lib-1") def test_list_by_library_empty(self, repo): assert repo.list_by_library("nonexistent") == [] class TestFindByLibraryAndFileType: def test_filter_by_video(self, repo, sample_asset, asset2, asset_other_project): repo.create(sample_asset) repo.create(asset2) repo.create(asset_other_project) videos = repo.find_by_library_and_file_type("lib-1", "video") assert len(videos) == 1 assert videos[0].mime_type.startswith("video/") def test_filter_by_image(self, repo, sample_asset, asset2): repo.create(sample_asset) repo.create(asset2) images = repo.find_by_library_and_file_type("lib-1", "image") assert len(images) == 1 assert images[0].mime_type.startswith("image/") def test_filter_by_audio(self, repo, sample_asset, asset_other_project): repo.create(sample_asset) repo.create(asset_other_project) audio = repo.find_by_library_and_file_type("lib-2", "audio") assert len(audio) == 1 def test_empty_result(self, repo, sample_asset): repo.create(sample_asset) assert repo.find_by_library_and_file_type("lib-1", "audio") == [] class TestUpdate: def test_update_existing_asset(self, repo, sample_asset): repo.create(sample_asset) sample_asset.name = "updated.mp4" sample_asset.file_size = 2048 result = repo.update(sample_asset) assert result.name == "updated.mp4" assert result.file_size == 2048 fetched = repo.get(sample_asset.id) assert fetched.name == "updated.mp4" def test_update_nonexistent_creates(self, repo, sample_asset): """update 直接覆盖,不存在则相当于 create.""" result = repo.update(sample_asset) assert result.id == sample_asset.id assert repo.get(sample_asset.id) is not None class TestDelete: def test_delete_existing(self, repo, sample_asset): repo.create(sample_asset) assert repo.delete(sample_asset.id) is True assert repo.get(sample_asset.id) is None def test_delete_nonexistent(self, repo): assert repo.delete("nonexistent") is False class TestBatchDelete: def test_batch_delete_soft_delete(self, repo, sample_asset, asset2): repo.create(sample_asset) repo.create(asset2) count = repo.batch_delete([sample_asset.id, asset2.id]) assert count == 2 a1 = repo.get(sample_asset.id) a2 = repo.get(asset2.id) assert a1.status == AssetStatus.DELETED assert a2.status == AssetStatus.DELETED assert a1.updated_at is not None assert a2.updated_at is not None def test_batch_delete_skip_already_deleted(self, repo, sample_asset): repo.create(sample_asset) sample_asset.status = AssetStatus.DELETED repo.update(sample_asset) count = repo.batch_delete([sample_asset.id]) assert count == 0 def test_batch_delete_nonexistent(self, repo): count = repo.batch_delete(["nonexistent-1", "nonexistent-2"]) assert count == 0 def test_batch_delete_partial(self, repo, sample_asset): repo.create(sample_asset) count = repo.batch_delete([sample_asset.id, "nonexistent"]) assert count == 1 class TestBatchUpdateMetadata: def test_batch_update_metadata_merge(self, repo, sample_asset, asset2): sample_asset.metadata = {"key1": "val1"} repo.create(sample_asset) repo.create(asset2) count = repo.batch_update_metadata( [sample_asset.id, asset2.id], {"key2": "val2"}, ) assert count == 2 a1 = repo.get(sample_asset.id) a2 = repo.get(asset2.id) assert a1.metadata == {"key1": "val1", "key2": "val2"} assert a2.metadata == {"key2": "val2"} def test_batch_update_metadata_overwrite_existing_key(self, repo, sample_asset): sample_asset.metadata = {"key1": "old"} repo.create(sample_asset) count = repo.batch_update_metadata([sample_asset.id], {"key1": "new"}) assert count == 1 assert repo.get(sample_asset.id).metadata["key1"] == "new" def test_batch_update_metadata_nonexistent(self, repo): count = repo.batch_update_metadata(["nonexistent"], {"key": "val"}) assert count == 0 class TestBatchAddTags: def test_batch_add_tags_new_tags(self, repo, sample_asset, asset2): repo.create(sample_asset) repo.create(asset2) count = repo.batch_add_tags([sample_asset.id, asset2.id], ["tag1", "tag2"]) assert count == 2 a1 = repo.get(sample_asset.id) a2 = repo.get(asset2.id) assert set(a1.tag_ids) == {"tag1", "tag2"} assert set(a2.tag_ids) == {"tag1", "tag2"} def test_batch_add_tags_dedup(self, repo, sample_asset): sample_asset.tag_ids = ["tag1"] repo.create(sample_asset) count = repo.batch_add_tags([sample_asset.id], ["tag1", "tag2"]) assert count == 1 # tag1已存在,但tag2新增,所以有变化 tags = repo.get(sample_asset.id).tag_ids assert tags.count("tag1") == 1 assert "tag2" in tags def test_batch_add_tags_no_change_when_all_exist(self, repo, sample_asset): sample_asset.tag_ids = ["tag1", "tag2"] repo.create(sample_asset) count = repo.batch_add_tags([sample_asset.id], ["tag1", "tag2"]) assert count == 0 # 没有变化 def test_batch_add_tags_nonexistent_assets(self, repo): count = repo.batch_add_tags(["nonexistent"], ["tag1"]) assert count == 0 class TestBatchReplaceTags: def test_batch_replace_tags_override(self, repo, sample_asset): sample_asset.tag_ids = ["old1", "old2"] repo.create(sample_asset) count = repo.batch_replace_tags([sample_asset.id], ["new1", "new2", "new3"]) assert count == 1 tags = repo.get(sample_asset.id).tag_ids assert tags == ["new1", "new2", "new3"] def test_batch_replace_tags_empty(self, repo, sample_asset): sample_asset.tag_ids = ["tag1"] repo.create(sample_asset) count = repo.batch_replace_tags([sample_asset.id], []) assert count == 1 assert repo.get(sample_asset.id).tag_ids == [] def test_batch_replace_tags_nonexistent(self, repo): count = repo.batch_replace_tags(["nonexistent"], ["tag1"]) assert count == 0 class TestFindByProjectPagination: @pytest.fixture def five_assets(self, repo): assets = [] for i in range(5): a = Asset.create( project_id="proj-paged", library_id="lib-paged", name=f"asset-{i}.mp4", storage_key=f"key-{i}", mime_type="video/mp4", ) repo.create(a) assets.append(a) return assets def test_find_by_project_default_pagination(self, repo, five_assets): result = repo.find_by_project("proj-paged") assert len(result) == 5 def test_find_by_project_skip(self, repo, five_assets): result = repo.find_by_project("proj-paged", skip=2) assert len(result) == 3 def test_find_by_project_limit(self, repo, five_assets): result = repo.find_by_project("proj-paged", limit=2) assert len(result) == 2 def test_find_by_project_skip_and_limit(self, repo, five_assets): result = repo.find_by_project("proj-paged", skip=1, limit=2) assert len(result) == 2 def test_find_by_project_skip_past_end(self, repo, five_assets): result = repo.find_by_project("proj-paged", skip=10) assert result == [] def test_find_by_project_empty(self, repo): assert repo.find_by_project("nonexistent") == [] class TestFindByTagIds: def test_find_by_tag_ids_match_all(self, repo, sample_asset, asset2): sample_asset.tag_ids = ["tag1", "tag2", "tag3"] asset2.tag_ids = ["tag1", "tag2"] repo.create(sample_asset) repo.create(asset2) result = repo.find_by_tag_ids(["tag1", "tag2"]) assert len(result) == 2 def test_find_by_tag_ids_subset_match(self, repo, sample_asset, asset2): sample_asset.tag_ids = ["tag1", "tag2"] asset2.tag_ids = ["tag1"] repo.create(sample_asset) repo.create(asset2) result = repo.find_by_tag_ids(["tag1", "tag2"]) assert len(result) == 1 assert result[0].id == sample_asset.id def test_find_by_tag_ids_empty_tag_list(self, repo, sample_asset): sample_asset.tag_ids = ["tag1"] repo.create(sample_asset) assert repo.find_by_tag_ids([]) == [] def test_find_by_tag_ids_no_match(self, repo, sample_asset): sample_asset.tag_ids = ["tag1"] repo.create(sample_asset) assert repo.find_by_tag_ids(["tag999"]) == [] def test_find_by_tag_ids_pagination(self, repo): for i in range(5): a = Asset.create( project_id="p1", library_id="l1", name=f"a{i}.mp4", storage_key=f"k{i}", mime_type="video/mp4", ) a.tag_ids = ["shared-tag"] repo.create(a) result = repo.find_by_tag_ids(["shared-tag"], skip=1, limit=2) assert len(result) == 2 class TestFindByLibraryAndFileHash: def test_find_by_hash_match(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_hash_wrong_library(self, repo, sample_asset): repo.create(sample_asset) result = repo.find_by_library_and_file_hash("lib-2", "hash-abc") assert result is None def test_find_by_hash_wrong_hash(self, repo, sample_asset): repo.create(sample_asset) result = repo.find_by_library_and_file_hash("lib-1", "hash-wrong") assert result is None def test_find_by_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