Files
xiaoxia-saas/tests/unit/test_inmemory_user_repository.py
xiaoxia 4f09989df5
CI/CD Pipeline / Canary Release to Production (push) Failing after 1155h10m30s
CI/CD Pipeline / CI Gate (push) Failing after 1155h22m46s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1155h26m9s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1155h26m12s
CI/CD Pipeline / Build Production API Image (push) Failing after 1155h26m14s
CI/CD Pipeline / Deploy Production (push) Failing after 1155h26m10s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1155h36m47s
CI/CD Pipeline / PR Build API Image (push) Failing after 1155h36m49s
CI/CD Pipeline / Frontend Lint (push) Failing after 1155h36m49s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1155h36m51s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 8m1s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m37s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m31s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m29s
CI/CD Pipeline / Unit Tests (push) Failing after 10m32s
CI/CD Pipeline / Integration Tests (push) Successful in 3m43s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m7s
CI/CD Pipeline / Build Staging API Image (push) Successful in 15m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m29s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 46s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 10m21s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m41s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1155h59m20s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1156h9m55s
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.com,user-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)