89639a6d3e
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
402 lines
13 KiB
Python
Executable File
402 lines
13 KiB
Python
Executable File
"""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
|