test(wave207): InMemory小型仓储单测补全 +41测
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 58s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m22s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m23s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m36s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1167h18m28s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1167h18m29s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1167h18m29s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1167h18m31s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1167h18m32s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1167h18m33s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1167h51m34s

- asset_library_repository: 13测(CRUD/按项目查询/kind过滤/计数增减)
- tag_repository: 8测(CRUD/按名称查找/分页/排序/计数)
- project_repository: 8测(保存/查找/共享访问/计数/删除)
- ingest_job_repository: 4测(创建/获取/更新状态/更新结果)
- classification_job_repository: 4测(创建/获取/更新结果/更新失败)

总计: 41个单测,pytest全绿
This commit is contained in:
xiaoxia
2026-07-30 00:08:36 +08:00
parent c2ec722644
commit 540df908f5
+408
View File
@@ -0,0 +1,408 @@
"""InMemory 小型仓储模块单元测试(asset_library/tag/project/ingest_job/classification_job)."""
import pytest
from packages.domain.entities import AssetLibrary, IngestJob, Project
from packages.domain.tag import Tag
from packages.domain.classification import (
AssetLibraryKind,
ClassificationJob,
ClassificationJobStatus,
IngestJobStatus,
)
from packages.adapters.in_memory.asset_library_repository import InMemoryAssetLibraryRepository
from packages.adapters.in_memory.tag_repository import InMemoryTagRepository
from packages.adapters.in_memory.project_repository import InMemoryProjectRepository
from packages.adapters.in_memory.ingest_job_repository import InMemoryIngestJobRepository
from packages.adapters.in_memory.classification_job_repository import InMemoryClassificationJobRepository
# ==================== AssetLibrary ====================
class TestInMemoryAssetLibraryRepository:
@pytest.fixture
def repo(self):
return InMemoryAssetLibraryRepository()
@pytest.fixture
def lib_video(self):
return AssetLibrary.create(project_id="p1", name="视频库", kind=AssetLibraryKind.VIDEO)
@pytest.fixture
def lib_image(self):
return AssetLibrary.create(project_id="p1", name="图片库", kind=AssetLibraryKind.IMAGE)
@pytest.fixture
def lib_other_project(self):
return AssetLibrary.create(project_id="p2", name="其他项目库", kind=AssetLibraryKind.VIDEO)
def test_create_and_get(self, repo, lib_video):
result = repo.create(lib_video)
assert result.id == lib_video.id
assert result.name == "视频库"
fetched = repo.get(lib_video.id)
assert fetched is not None
assert fetched.id == lib_video.id
def test_get_nonexistent(self, repo):
assert repo.get("nonexistent") is None
def test_find_by_id_alias(self, repo, lib_video):
repo.create(lib_video)
assert repo.find_by_id(lib_video.id).id == repo.get(lib_video.id).id
def test_find_by_project(self, repo, lib_video, lib_image, lib_other_project):
repo.create(lib_video)
repo.create(lib_image)
repo.create(lib_other_project)
p1_libs = repo.find_by_project("p1")
assert len(p1_libs) == 2
p2_libs = repo.find_by_project("p2")
assert len(p2_libs) == 1
assert p2_libs[0].id == lib_other_project.id
def test_find_by_project_with_kind_filter(self, repo, lib_video, lib_image):
repo.create(lib_video)
repo.create(lib_image)
video_libs = repo.find_by_project("p1", kind=AssetLibraryKind.VIDEO)
assert len(video_libs) == 1
assert video_libs[0].kind == AssetLibraryKind.VIDEO
image_libs = repo.find_by_project("p1", kind=AssetLibraryKind.IMAGE)
assert len(image_libs) == 1
def test_find_by_project_empty(self, repo):
assert repo.find_by_project("nonexistent") == []
def test_update(self, repo, lib_video):
repo.create(lib_video)
lib_video.name = "新名称"
result = repo.update(lib_video)
assert result.name == "新名称"
assert repo.get(lib_video.id).name == "新名称"
def test_delete(self, repo, lib_video):
repo.create(lib_video)
assert repo.delete(lib_video.id) is True
assert repo.get(lib_video.id) is None
def test_delete_nonexistent(self, repo):
assert repo.delete("nonexistent") is False
def test_increment_asset_count(self, repo, lib_video):
repo.create(lib_video)
repo.increment_asset_count(lib_video.id, 1024)
lib = repo.get(lib_video.id)
assert lib.asset_count == 1
assert lib.total_size == 1024
repo.increment_asset_count(lib_video.id, 512)
lib = repo.get(lib_video.id)
assert lib.asset_count == 2
assert lib.total_size == 1536
def test_increment_asset_count_nonexistent(self, repo):
# 不报错,静默忽略
repo.increment_asset_count("nonexistent", 100)
def test_decrement_asset_count(self, repo, lib_video):
repo.create(lib_video)
repo.increment_asset_count(lib_video.id, 1024)
repo.increment_asset_count(lib_video.id, 512)
repo.decrement_asset_count(lib_video.id, 512)
lib = repo.get(lib_video.id)
assert lib.asset_count == 1
assert lib.total_size == 1024
def test_decrement_asset_count_not_below_zero(self, repo, lib_video):
repo.create(lib_video)
repo.decrement_asset_count(lib_video.id, 9999)
lib = repo.get(lib_video.id)
assert lib.asset_count == 0
assert lib.total_size == 0
def test_decrement_asset_count_nonexistent(self, repo):
repo.decrement_asset_count("nonexistent", 100)
# ==================== Tag ====================
class TestInMemoryTagRepository:
@pytest.fixture
def repo(self):
return InMemoryTagRepository()
@pytest.fixture
def tag1(self):
return Tag.create(user_id="u1", name="风景")
@pytest.fixture
def tag2(self):
return Tag.create(user_id="u1", name="人物")
@pytest.fixture
def tag_other_user(self):
return Tag.create(user_id="u2", name="风景")
def test_create_and_get(self, repo, tag1):
result = repo.create(tag1)
assert result.id == tag1.id
assert result.name == "风景"
fetched = repo.get(tag1.id)
assert fetched is not None
assert fetched.id == tag1.id
def test_get_nonexistent(self, repo):
assert repo.get("nonexistent") is None
def test_find_by_name(self, repo, tag1, tag_other_user):
repo.create(tag1)
repo.create(tag_other_user)
# 同用户同名
found = repo.find_by_name("u1", "风景")
assert found is not None
assert found.id == tag1.id
# 不同用户同名不冲突
found2 = repo.find_by_name("u2", "风景")
assert found2 is not None
assert found2.id == tag_other_user.id
def test_find_by_name_not_found(self, repo, tag1):
repo.create(tag1)
assert repo.find_by_name("u1", "不存在") is None
assert repo.find_by_name("u2", "风景") is None
def test_list_by_user(self, repo, tag1, tag2, tag_other_user):
repo.create(tag1)
repo.create(tag2)
repo.create(tag_other_user)
u1_tags = repo.list_by_user("u1")
assert len(u1_tags) == 2
u2_tags = repo.list_by_user("u2")
assert len(u2_tags) == 1
assert u2_tags[0].id == tag_other_user.id
def test_list_by_user_pagination(self, repo):
for i in range(5):
repo.create(Tag.create(user_id="u1", name=f"tag-{i}"))
page1 = repo.list_by_user("u1", limit=2)
assert len(page1) == 2
page2 = repo.list_by_user("u1", skip=2, limit=2)
assert len(page2) == 2
def test_list_by_user_sorted_by_created_at_desc(self, repo):
t1 = Tag.create(user_id="u1", name="old")
t2 = Tag.create(user_id="u1", name="new")
repo.create(t1)
repo.create(t2)
tags = repo.list_by_user("u1")
# 新创建的排前面
assert tags[0].id == t2.id
assert tags[1].id == t1.id
def test_count_by_user(self, repo, tag1, tag2, tag_other_user):
repo.create(tag1)
repo.create(tag2)
repo.create(tag_other_user)
assert repo.count_by_user("u1") == 2
assert repo.count_by_user("u2") == 1
assert repo.count_by_user("u3") == 0
def test_delete(self, repo, tag1):
repo.create(tag1)
assert repo.delete(tag1.id) is True
assert repo.get(tag1.id) is None
def test_delete_nonexistent(self, repo):
assert repo.delete("nonexistent") is False
# ==================== Project ====================
class TestInMemoryProjectRepository:
@pytest.fixture
def repo(self):
return InMemoryProjectRepository()
@pytest.fixture
def project1(self):
return Project(id="proj-1", owner_user_id="u1", name="项目一", shared_users=[])
@pytest.fixture
def project2(self):
return Project(id="proj-2", owner_user_id="u1", name="项目二", shared_users=["u2"])
@pytest.fixture
def project_other(self):
return Project(id="proj-3", owner_user_id="u3", name="他人项目", shared_users=["u2"])
def test_save_and_find_by_id(self, repo, project1):
result = repo.save(project1)
assert result.id == "proj-1"
found = repo.find_by_id("proj-1")
assert found is not None
assert found.name == "项目一"
def test_find_by_id_not_found(self, repo):
assert repo.find_by_id("nonexistent") is None
def test_find_by_owner_user_id(self, repo, project1, project2, project_other):
repo.save(project1)
repo.save(project2)
repo.save(project_other)
u1_projects = repo.find_by_owner_user_id("u1")
assert len(u1_projects) == 2
u3_projects = repo.find_by_owner_user_id("u3")
assert len(u3_projects) == 1
def test_find_accessible_projects_owner(self, repo, project1, project_other):
repo.save(project1)
repo.save(project_other)
# u1 可以访问自己的项目
accessible = repo.find_accessible_projects("u1")
assert len(accessible) == 1
assert accessible[0].id == "proj-1"
def test_find_accessible_projects_shared(self, repo, project2, project_other):
repo.save(project2)
repo.save(project_other)
# u2 被两个项目共享
accessible = repo.find_accessible_projects("u2")
assert len(accessible) == 2
ids = {p.id for p in accessible}
assert ids == {"proj-2", "proj-3"}
def test_find_accessible_projects_none(self, repo, project1):
repo.save(project1)
assert repo.find_accessible_projects("nobody") == []
def test_count_by_owner(self, repo, project1, project2, project_other):
repo.save(project1)
repo.save(project2)
repo.save(project_other)
assert repo.count_by_owner("u1") == 2
assert repo.count_by_owner("u3") == 1
assert repo.count_by_owner("nobody") == 0
def test_delete(self, repo, project1):
repo.save(project1)
assert repo.delete("proj-1") is True
assert repo.find_by_id("proj-1") is None
def test_delete_nonexistent(self, repo):
assert repo.delete("nonexistent") is False
# ==================== IngestJob ====================
class TestInMemoryIngestJobRepository:
@pytest.fixture
def repo(self):
return InMemoryIngestJobRepository()
@pytest.fixture
def job(self):
return IngestJob.create(project_id="p1", library_id="l1", storage_key="key1", file_hash="hash1")
def test_create_and_get(self, repo, job):
result = repo.create(job)
assert result.id == job.id
assert result.status == IngestJobStatus.PENDING
fetched = repo.get(job.id)
assert fetched is not None
assert fetched.storage_key == "key1"
def test_get_nonexistent(self, repo):
assert repo.get("nonexistent") is None
def test_update(self, repo, job):
repo.create(job)
job.status = IngestJobStatus.PROCESSING
job.error_message = ""
result = repo.update(job)
assert result.status == IngestJobStatus.PROCESSING
fetched = repo.get(job.id)
assert fetched.status == IngestJobStatus.PROCESSING
def test_update_with_result(self, repo, job):
repo.create(job)
job.status = IngestJobStatus.COMPLETED
job.result_asset_id = "asset-123"
repo.update(job)
fetched = repo.get(job.id)
assert fetched.status == IngestJobStatus.COMPLETED
assert fetched.result_asset_id == "asset-123"
# ==================== ClassificationJob ====================
class TestInMemoryClassificationJobRepository:
@pytest.fixture
def repo(self):
return InMemoryClassificationJobRepository()
@pytest.fixture
def job(self):
return ClassificationJob.create(project_id="p1", asset_id="a1")
def test_create_and_get(self, repo, job):
result = repo.create(job)
assert result.id == job.id
assert result.status == ClassificationJobStatus.PENDING
assert result.confidence == 0.0
fetched = repo.get(job.id)
assert fetched is not None
assert fetched.asset_id == "a1"
def test_get_nonexistent(self, repo):
assert repo.get("nonexistent") is None
def test_update_status_and_result(self, repo, job):
repo.create(job)
job.status = ClassificationJobStatus.COMPLETED
job.classification = "video"
job.confidence = 0.95
result = repo.update(job)
assert result.status == ClassificationJobStatus.COMPLETED
assert result.classification == "video"
assert result.confidence == 0.95
def test_update_failed(self, repo, job):
repo.create(job)
job.status = ClassificationJobStatus.FAILED
job.error_message = "something went wrong"
repo.update(job)
fetched = repo.get(job.id)
assert fetched.status == ClassificationJobStatus.FAILED
assert fetched.error_message == "something went wrong"