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
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:
Executable
+408
@@ -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"
|
||||
Reference in New Issue
Block a user