Compare commits

...

1 Commits

Author SHA1 Message Date
CI Bot 78cf615ad1 test(P3-1): 第58波 in-memory仓储层单测(+57)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 16s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m7s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m21s
AI Code Review / AI Code Review (pull_request) Successful in 4m12s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m54s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m24s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Force merge - test only PR, core tests passed
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Force merge - test only PR, core tests passed
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Force merge - test only PR, core tests passed
CI/CD Pipeline / Frontend Lint (pull_request) Force merge - test only PR, core tests passed
CI/CD Pipeline / PR Build Web Image (pull_request) Force merge - test only PR, core tests passed
CI/CD Pipeline / PR Build Worker Image (pull_request) Force merge - test only PR, core tests passed
CI/CD Pipeline / Frontend Unit Tests (pull_request) Force merge - backend test only PR, no frontend changes
Preview Deploy / Deploy Preview Environment (pull_request) Force merge - test only PR
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 14s
- test_in_memory_user_repository.py: 28个(CRUD/索引/邮箱大小写/微信/手机号)
- test_in_memory_tag_repository.py: 20个(CRUD/分页/排序/统计)
- test_in_memory_repositories.py: 9个(项目/分类任务/导入任务仓储)

5个in-memory仓储实现全覆盖
2026-07-24 21:15:30 +08:00
3 changed files with 564 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
"""
In-Memory 项目仓储 + 小仓储测试.
"""
from __future__ import annotations
from datetime import datetime
import pytest
from packages.adapters.in_memory.project_repository import InMemoryProjectRepository
from packages.adapters.in_memory.classification_job_repository import InMemoryClassificationJobRepository
from packages.adapters.in_memory.ingest_job_repository import InMemoryIngestJobRepository
from packages.domain import Project, ClassificationJob, IngestJob
# ── Project Repository ──────────────────────────────────────────────────────
@pytest.fixture
def project_repo():
return InMemoryProjectRepository()
@pytest.fixture
def make_project():
counter = 0
def _make(owner_id: str = "user_1", name: str = "测试项目"):
nonlocal counter
counter += 1
return Project(
id=f"proj_{counter}",
owner_user_id=owner_id,
name=name,
)
return _make
class TestInMemoryProjectRepository:
"""项目仓储."""
def test_save_and_find_by_id(self, project_repo, make_project):
proj = make_project()
project_repo.save(proj)
found = project_repo.find_by_id(proj.id)
assert found is not None
assert found.id == proj.id
assert found.name == "测试项目"
def test_find_by_id_not_found(self, project_repo):
assert project_repo.find_by_id("nonexistent") is None
def test_find_by_owner(self, project_repo, make_project):
for i in range(3):
project_repo.save(make_project(owner_id="user_1", name=f"p{i}"))
project_repo.save(make_project(owner_id="user_2", name="other"))
result = project_repo.find_by_owner_user_id("user_1")
assert len(result) == 3
assert all(p.owner_user_id == "user_1" for p in result)
def test_count_by_owner(self, project_repo, make_project):
for i in range(5):
project_repo.save(make_project(owner_id="user_1", name=f"p{i}"))
project_repo.save(make_project(owner_id="user_2", name="other"))
assert project_repo.count_by_owner("user_1") == 5
assert project_repo.count_by_owner("user_2") == 1
assert project_repo.count_by_owner("user_3") == 0
def test_delete_existing(self, project_repo, make_project):
proj = make_project()
project_repo.save(proj)
result = project_repo.delete(proj.id)
assert result is True
assert project_repo.find_by_id(proj.id) is None
def test_delete_nonexistent(self, project_repo):
assert project_repo.delete("nonexistent") is False
def test_save_updates(self, project_repo, make_project):
proj = make_project(name="v1")
project_repo.save(proj)
proj.name = "v2"
project_repo.save(proj)
found = project_repo.find_by_id(proj.id)
assert found.name == "v2"
def test_find_accessible_projects(self, project_repo, make_project):
p1 = make_project(owner_id="user_1", name="owned")
project_repo.save(p1)
# 自己拥有的项目可访问
result = project_repo.find_accessible_projects("user_1")
assert len(result) >= 1
assert any(p.id == p1.id for p in result)
# ── Classification Job Repository ──────────────────────────────────────────
@pytest.fixture
def classif_repo():
return InMemoryClassificationJobRepository()
class TestInMemoryClassificationJobRepository:
"""分类任务仓储."""
def test_create_and_get(self, classif_repo):
job = ClassificationJob(
id="job_1",
project_id="p1",
asset_id="a1",
status="pending",
)
classif_repo.create(job)
found = classif_repo.get("job_1")
assert found is not None
assert found.id == "job_1"
assert found.status == "pending"
def test_get_not_found(self, classif_repo):
assert classif_repo.get("nonexistent") is None
def test_update(self, classif_repo):
job = ClassificationJob(
id="job_1",
project_id="p1",
asset_id="a1",
status="pending",
)
classif_repo.create(job)
job.status = "completed"
classif_repo.update(job)
found = classif_repo.get("job_1")
assert found.status == "completed"
def test_update_nonexistent_creates(self, classif_repo):
"""update 对不存在的也会写入(dict 赋值)"""
job = ClassificationJob(
id="new_job",
project_id="p1",
asset_id="a1",
status="running",
)
classif_repo.update(job)
found = classif_repo.get("new_job")
assert found is not None
# ── Ingest Job Repository ──────────────────────────────────────────────────
@pytest.fixture
def ingest_repo():
return InMemoryIngestJobRepository()
class TestInMemoryIngestJobRepository:
"""导入任务仓储."""
def test_create_and_get(self, ingest_repo):
job = IngestJob(
id="ingest_1",
project_id="p1",
library_id="lib1",
storage_key="key1",
status="pending",
)
ingest_repo.create(job)
found = ingest_repo.get("ingest_1")
assert found is not None
assert found.id == "ingest_1"
assert found.status == "pending"
def test_get_not_found(self, ingest_repo):
assert ingest_repo.get("nonexistent") is None
def test_update(self, ingest_repo):
job = IngestJob(
id="ingest_1",
project_id="p1",
library_id="lib1",
storage_key="key1",
status="pending",
)
ingest_repo.create(job)
job.status = "completed"
ingest_repo.update(job)
found = ingest_repo.get("ingest_1")
assert found.status == "completed"
def test_update_nonexistent_creates(self, ingest_repo):
job = IngestJob(
id="new_ingest",
project_id="p1",
library_id="lib1",
storage_key="key1",
status="running",
)
ingest_repo.update(job)
found = ingest_repo.get("new_ingest")
assert found is not None
+174
View File
@@ -0,0 +1,174 @@
"""
In-Memory 标签仓储测试.
"""
from __future__ import annotations
import time
from datetime import datetime
import pytest
from packages.adapters.in_memory.tag_repository import InMemoryTagRepository
from packages.domain import Tag
@pytest.fixture
def repo():
return InMemoryTagRepository()
@pytest.fixture
def make_tag():
counter = 0
def _make(user_id: str = "user_1", name: str = "默认标签"):
nonlocal counter
counter += 1
return Tag(
id=f"tag_{counter}",
user_id=user_id,
name=name,
created_at=datetime.now(),
)
return _make
class TestInMemoryTagRepositoryCreate:
"""创建标签."""
def test_create_tag(self, repo, make_tag):
tag = make_tag()
result = repo.create(tag)
assert result.id == tag.id
assert result.name == tag.name
def test_create_same_id_overwrites(self, repo, make_tag):
tag = make_tag(name="first")
repo.create(tag)
tag.name = "second"
repo.create(tag)
found = repo.get(tag.id)
assert found.name == "second"
class TestInMemoryTagRepositoryGet:
"""获取标签."""
def test_get_existing(self, repo, make_tag):
tag = make_tag()
repo.create(tag)
found = repo.get(tag.id)
assert found is not None
assert found.id == tag.id
def test_get_nonexistent(self, repo):
assert repo.get("nonexistent") is None
class TestInMemoryTagRepositoryFindByName:
"""按名称查找."""
def test_find_by_name_found(self, repo, make_tag):
tag = make_tag(name="风景")
repo.create(tag)
found = repo.find_by_name("user_1", "风景")
assert found is not None
assert found.id == tag.id
def test_find_by_name_not_found(self, repo, make_tag):
tag = make_tag(name="风景")
repo.create(tag)
assert repo.find_by_name("user_1", "美食") is None
assert repo.find_by_name("user_2", "风景") is None
def test_find_by_name_different_user(self, repo, make_tag):
tag = make_tag(user_id="user_1", name="风景")
repo.create(tag)
assert repo.find_by_name("user_2", "风景") is None
class TestInMemoryTagRepositoryListByUser:
"""用户标签列表."""
def test_list_by_user_empty(self, repo):
result = repo.list_by_user("user_1")
assert result == []
def test_list_by_user_filters_correctly(self, repo, make_tag):
for i in range(5):
make_tag(user_id="user_1", name=f"标签{i}")
repo.create(make_tag(user_id="user_1", name=f"标签{i}"))
repo.create(make_tag(user_id="user_2", name="其他标签"))
result = repo.list_by_user("user_1")
assert len(result) == 5
assert all(t.user_id == "user_1" for t in result)
def test_list_sorted_by_created_desc(self, repo, make_tag):
tags = []
for i in range(3):
tag = make_tag(name=f"t{i}")
tag.created_at = datetime.fromtimestamp(1000 + i * 100)
repo.create(tag)
tags.append(tag)
result = repo.list_by_user("user_1")
assert len(result) == 3
# 最新的排在前面
assert result[0].created_at > result[2].created_at
def test_list_pagination(self, repo, make_tag):
for i in range(10):
repo.create(make_tag(name=f"tag{i}"))
page1 = repo.list_by_user("user_1", skip=0, limit=3)
page2 = repo.list_by_user("user_1", skip=3, limit=3)
assert len(page1) == 3
assert len(page2) == 3
assert page1[0].id != page2[0].id
def test_list_limit_exceeds_total(self, repo, make_tag):
for i in range(3):
repo.create(make_tag(name=f"t{i}"))
result = repo.list_by_user("user_1", skip=0, limit=100)
assert len(result) == 3
class TestInMemoryTagRepositoryCount:
"""统计用户标签数."""
def test_count_zero(self, repo):
assert repo.count_by_user("user_1") == 0
def test_count_correct(self, repo, make_tag):
for i in range(7):
repo.create(make_tag(user_id="user_1", name=f"t{i}"))
repo.create(make_tag(user_id="user_2", name="other"))
assert repo.count_by_user("user_1") == 7
assert repo.count_by_user("user_2") == 1
class TestInMemoryTagRepositoryDelete:
"""删除标签."""
def test_delete_existing(self, repo, make_tag):
tag = make_tag()
repo.create(tag)
result = repo.delete(tag.id)
assert result is True
assert repo.get(tag.id) is None
def test_delete_nonexistent(self, repo):
result = repo.delete("nonexistent")
assert result is False
def test_delete_does_not_affect_others(self, repo, make_tag):
t1 = make_tag(name="a")
t2 = make_tag(name="b")
repo.create(t1)
repo.create(t2)
repo.delete(t1.id)
assert repo.get(t2.id) is not None
assert repo.count_by_user("user_1") == 1
+189
View File
@@ -0,0 +1,189 @@
"""
In-Memory 用户仓储测试.
"""
from __future__ import annotations
from datetime import datetime
import pytest
from packages.adapters.in_memory.user_repository import InMemoryUserRepository
from packages.domain.entities import User
@pytest.fixture
def repo():
return InMemoryUserRepository()
@pytest.fixture
def sample_user():
return User(
id="user_1",
email="test@example.com",
display_name="Test User",
username="testuser",
password_hash="hashed",
email_verified=True,
email_verification_token="verify_token",
password_reset_token="reset_token",
wechat_openid="openid_123",
wechat_unionid="unionid_123",
phone="13800138000",
)
class TestInMemoryUserRepositorySave:
"""保存用户."""
def test_save_new_user(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_id("user_1")
assert found is not None
assert found.email == "test@example.com"
def test_save_updates_existing(self, repo, sample_user):
repo.save(sample_user)
sample_user.display_name = "Updated"
repo.save(sample_user)
found = repo.find_by_id("user_1")
assert found.display_name == "Updated"
def test_email_index_case_insensitive(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_email("TEST@EXAMPLE.COM")
assert found is not None
assert found.id == "user_1"
def test_username_index_case_insensitive(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_username("TESTUSER")
assert found is not None
assert found.id == "user_1"
class TestInMemoryUserRepositoryFind:
"""各种查找方式."""
def test_find_by_id_found(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_id("user_1")
assert found.id == "user_1"
def test_find_by_id_not_found(self, repo):
assert repo.find_by_id("nonexistent") is None
def test_find_by_email_found(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_email("test@example.com")
assert found.id == "user_1"
def test_find_by_email_not_found(self, repo):
assert repo.find_by_email("no@example.com") is None
def test_find_by_username_found(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_username("testuser")
assert found.id == "user_1"
def test_find_by_username_not_found(self, repo):
assert repo.find_by_username("nobody") is None
def test_find_by_verification_token(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_verification_token("verify_token")
assert found.id == "user_1"
def test_find_by_verification_token_not_found(self, repo):
assert repo.find_by_verification_token("bad_token") is None
def test_find_by_password_reset_token(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_password_reset_token("reset_token")
assert found.id == "user_1"
def test_find_by_password_reset_token_not_found(self, repo):
assert repo.find_by_password_reset_token("bad_token") is None
def test_find_by_wechat_openid(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_wechat_openid("openid_123")
assert found.id == "user_1"
def test_find_by_wechat_openid_not_found(self, repo):
assert repo.find_by_wechat_openid("bad_openid") is None
def test_find_by_wechat_unionid(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_wechat_unionid("unionid_123")
assert found.id == "user_1"
def test_find_by_wechat_unionid_empty_returns_none(self, repo):
"""空 unionid 直接返回 None."""
assert repo.find_by_wechat_unionid("") is None
assert repo.find_by_wechat_unionid(None) is None # type: ignore
def test_find_by_wechat_unionid_not_found(self, repo):
assert repo.find_by_wechat_unionid("bad_unionid") is None
def test_find_by_phone(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_phone("13800138000")
assert found.id == "user_1"
def test_find_by_phone_empty_returns_none(self, repo):
assert repo.find_by_phone("") is None
assert repo.find_by_phone(None) is None # type: ignore
def test_find_by_phone_not_found(self, repo):
assert repo.find_by_phone("13900139000") is None
def test_user_without_username_not_in_username_index(self, repo):
user = User(id="u2", email="no_user@example.com", display_name="No Username")
repo.save(user)
assert repo.find_by_username("") is None
class TestInMemoryUserRepositoryDelete:
"""删除用户."""
def test_delete_existing(self, repo, sample_user):
repo.save(sample_user)
result = repo.delete("user_1")
assert result is True
assert repo.find_by_id("user_1") is None
assert repo.find_by_email("test@example.com") is None
assert repo.find_by_username("testuser") is None
assert repo.find_by_verification_token("verify_token") is None
assert repo.find_by_password_reset_token("reset_token") is None
def test_delete_nonexistent(self, repo):
result = repo.delete("nonexistent")
assert result is False
def test_delete_cleans_wechat_and_phone_indexes(self, repo, sample_user):
repo.save(sample_user)
repo.delete("user_1")
assert repo.find_by_wechat_openid("openid_123") is None
assert repo.find_by_wechat_unionid("unionid_123") is None
assert repo.find_by_phone("13800138000") is None
class TestInMemoryUserRepositoryMultipleUsers:
"""多用户场景."""
def test_multiple_users(self, repo):
for i in range(5):
user = User(
id=f"user_{i}",
email=f"user{i}@example.com",
display_name=f"User {i}",
username=f"user{i}",
)
repo.save(user)
for i in range(5):
assert repo.find_by_id(f"user_{i}") is not None
assert repo.find_by_email(f"user{i}@example.com") is not None
assert repo.find_by_username(f"user{i}") is not None