Files
xiaoxia-saas/tests/unit/test_inmemory_user_repository.py
xiaoxia 4f09989df5
CI/CD Pipeline / Check if frontend-only change (push) Blocked by required conditions
CI/CD Pipeline / Validate - Code Quality (push) Blocked by required conditions
CI/CD Pipeline / Validate - Type Check (mypy) (push) Blocked by required conditions
CI/CD Pipeline / Validate - Migration (alembic) (push) Blocked by required conditions
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Blocked by required conditions
test(wave207): InMemory小型仓储单测补全 +41测
2026-07-30 11:50:48 +08:00

314 lines
11 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""InMemoryUserRepository 单元测试."""
import copy
from datetime import datetime, timezone
import pytest
from packages.adapters.in_memory.user_repository import InMemoryUserRepository
from packages.domain.entities import User
@pytest.fixture
def repo() -> InMemoryUserRepository:
return InMemoryUserRepository()
@pytest.fixture
def sample_user() -> User:
return User(
id="user-1",
email="Test@Example.com",
display_name="Test User",
username="testuser",
password_hash="hashed-pw",
email_verification_token="verify-token-123",
password_reset_token="reset-token-456",
wechat_openid="wx-openid-abc",
wechat_unionid="wx-unionid-def",
phone="13800138000",
created_at=datetime.now(timezone.utc),
)
class TestSaveAndFindById:
def test_save_and_find_by_id(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_id("user-1")
assert found is not None
assert found.id == "user-1"
assert found.email == "Test@Example.com"
def test_find_by_id_not_found(self, repo):
assert repo.find_by_id("nonexistent") is None
def test_save_overwrite_existing(self, repo, sample_user):
repo.save(sample_user)
sample_user.display_name = "Updated Name"
repo.save(sample_user)
found = repo.find_by_id("user-1")
assert found.display_name == "Updated Name"
class TestFindByEmail:
def test_find_by_email_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_find_by_email_exact_case(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_email("Test@Example.com")
assert found is not None
def test_find_by_email_not_found(self, repo):
assert repo.find_by_email("notfound@example.com") is None
class TestFindByUsername:
def test_find_by_username_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"
def test_find_by_username_not_found(self, repo):
assert repo.find_by_username("nobody") is None
def test_find_by_username_empty(self, repo, sample_user):
sample_user.username = ""
repo.save(sample_user)
# 空 username 不应该建立索引,但查找空字符串应该返回None
found = repo.find_by_username("")
assert found is None
class TestFindByVerificationToken:
def test_find_by_verification_token(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_verification_token("verify-token-123")
assert found is not None
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
class TestFindByPasswordResetToken:
def test_find_by_password_reset_token(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_password_reset_token("reset-token-456")
assert found is not None
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
class TestFindByWechat:
def test_find_by_wechat_openid(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_wechat_openid("wx-openid-abc")
assert found is not None
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("wx-unionid-def")
assert found is not None
assert found.id == "user-1"
def test_find_by_wechat_unionid_not_found(self, repo):
assert repo.find_by_wechat_unionid("bad-unionid") is None
def test_find_by_wechat_unionid_empty(self, repo, sample_user):
sample_user.wechat_unionid = None
repo.save(sample_user)
assert repo.find_by_wechat_unionid("") is None
class TestFindByPhone:
def test_find_by_phone(self, repo, sample_user):
repo.save(sample_user)
found = repo.find_by_phone("13800138000")
assert found is not None
assert found.id == "user-1"
def test_find_by_phone_not_found(self, repo):
assert repo.find_by_phone("13900139000") is None
def test_find_by_phone_empty(self, repo, sample_user):
sample_user.phone = None
repo.save(sample_user)
assert repo.find_by_phone("") is None
class TestDelete:
def test_delete_existing_user(self, repo, sample_user):
repo.save(sample_user)
assert repo.delete("user-1") is True
assert repo.find_by_id("user-1") is None
def test_delete_cleans_all_indexes(self, repo, sample_user):
repo.save(sample_user)
repo.delete("user-1")
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-123") is None
assert repo.find_by_password_reset_token("reset-token-456") is None
def test_delete_nonexistent_user(self, repo):
assert repo.delete("nonexistent") is False
def test_delete_twice_returns_false(self, repo, sample_user):
repo.save(sample_user)
assert repo.delete("user-1") is True
assert repo.delete("user-1") is False
class TestIndexUpdates:
def test_save_new_user_with_same_email_raises_uniqueness_error(self, repo, sample_user):
"""不同用户同邮箱应触发唯一约束异常."""
repo.save(sample_user)
user2 = User(
id="user-2",
email="test@example.com", # 同邮箱
display_name="User 2",
username="user2",
)
with pytest.raises(ValueError, match="Email already in use"):
repo.save(user2)
def test_same_user_update_email_allowed(self, repo, sample_user):
"""同一用户更新邮箱不触发唯一约束."""
repo.save(sample_user)
sample_user.email = "new@example.com"
repo.save(sample_user)
found = repo.find_by_email("new@example.com")
assert found is not None
assert found.id == "user-1"
assert repo.find_by_email("test@example.com") is None
def test_update_email_conflict_does_not_corrupt_indexes(self, repo, sample_user):
"""更新邮箱与其他用户冲突时,索引必须保持一致,旧邮箱索引不丢失."""
repo.save(sample_user)
# 第二个用户
user2 = User(
id="user-2",
email="other@example.com",
display_name="User 2",
username="user2",
)
repo.save(user2)
# 尝试把 user2 的邮箱改成 sample_user 的邮箱(冲突)
user2_new = copy.deepcopy(user2)
user2_new.email = "test@example.com"
with pytest.raises(ValueError, match="Email already in use"):
repo.save(user2_new)
# 索引必须保持一致(user-1仍占test@example.comuser-2仍占other@example.com
assert repo.find_by_email("test@example.com").id == "user-1"
assert repo.find_by_email("other@example.com").id == "user-2"
assert repo.find_by_username("testuser").id == "user-1"
assert repo.find_by_username("user2").id == "user-2"
def test_update_username_conflict_does_not_corrupt_indexes(self, repo, sample_user):
"""更新用户名与他人冲突时,各索引保持一致不丢失."""
repo.save(sample_user)
user2 = User(
id="user-2",
email="other@example.com",
display_name="User 2",
username="user2",
phone="13900000002",
)
repo.save(user2)
# 尝试把 user2 用户名改成 testuser(冲突)
user2_new = copy.deepcopy(user2)
user2_new.username = "testuser"
with pytest.raises(ValueError, match="Username already in use"):
repo.save(user2_new)
# 各索引必须保持一致
assert repo.find_by_username("testuser").id == "user-1"
assert repo.find_by_username("user2").id == "user-2"
assert repo.find_by_email("other@example.com").id == "user-2"
assert repo.find_by_phone("13900000002").id == "user-2"
def test_save_stores_independent_copy(self, repo, sample_user):
"""save存储独立副本,外部修改对象不影响仓储内部状态."""
repo.save(sample_user)
# 外部修改对象属性
original_email = sample_user.email
sample_user.email = "hacked@example.com"
sample_user.display_name = "Hacked"
# 仓储中数据不应受影响
stored = repo.find_by_id(sample_user.id)
assert stored.email == original_email
assert repo.find_by_email(original_email) is not None
assert repo.find_by_email("hacked@example.com") is None
def test_save_empty_email_raises(self, repo):
"""空邮箱应抛出异常."""
user = User(
id="user-empty",
email="",
display_name="Empty Email",
username="emptyuser",
)
with pytest.raises(ValueError, match="email cannot be empty"):
repo.save(user)
def test_find_by_empty_email_returns_none(self, repo, sample_user):
"""空邮箱查询返回None."""
repo.save(sample_user)
assert repo.find_by_email("") is None
assert repo.find_by_email(None) is None
def test_find_by_empty_username_returns_none(self, repo, sample_user):
"""空用户名查询返回None."""
repo.save(sample_user)
assert repo.find_by_username("") is None
def test_duplicate_verification_token_raises(self, repo, sample_user):
"""相同邮箱验证令牌应触发唯一约束."""
sample_user.email_verification_token = "verify-token-abc"
repo.save(sample_user)
user2 = User(
id="user-2",
email="user2@example.com",
display_name="User 2",
username="user2",
email_verification_token="verify-token-abc", # 重复
)
with pytest.raises(ValueError, match="verification token already in use"):
repo.save(user2)
def test_duplicate_reset_token_raises(self, repo, sample_user):
"""相同密码重置令牌应触发唯一约束."""
sample_user.password_reset_token = "reset-token-xyz"
repo.save(sample_user)
user2 = User(
id="user-2",
email="user2@example.com",
display_name="User 2",
username="user2",
password_reset_token="reset-token-xyz", # 重复
)
with pytest.raises(ValueError, match="reset token already in use"):
repo.save(user2)