134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
|
|
"""Tests for UserRepository."""
|
||
|
|
|
||
|
|
from uuid import uuid4
|
||
|
|
|
||
|
|
import asyncpg
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from app.repositories.user import UserRepository
|
||
|
|
|
||
|
|
|
||
|
|
class TestUserRepository:
|
||
|
|
"""Tests for UserRepository conforming to SPECS.md."""
|
||
|
|
|
||
|
|
async def test_create_user_returns_user_data(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Creating a user returns the user data with id, email, created_at."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
user_id = uuid4()
|
||
|
|
email = "test@example.com"
|
||
|
|
password_hash = "hashed_password_123"
|
||
|
|
|
||
|
|
result = await repo.create(user_id, email, password_hash)
|
||
|
|
|
||
|
|
assert result["id"] == user_id
|
||
|
|
assert result["email"] == email
|
||
|
|
assert result["created_at"] is not None
|
||
|
|
|
||
|
|
async def test_create_user_stores_password_hash(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Password hash is stored correctly in the database."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
user_id = uuid4()
|
||
|
|
email = "hash_test@example.com"
|
||
|
|
password_hash = "bcrypt_hashed_value"
|
||
|
|
|
||
|
|
await repo.create(user_id, email, password_hash)
|
||
|
|
user = await repo.get_by_id(user_id)
|
||
|
|
|
||
|
|
assert user["password_hash"] == password_hash
|
||
|
|
|
||
|
|
async def test_create_user_email_must_be_unique(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Email uniqueness constraint per SPECS.md users table."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
email = "duplicate@example.com"
|
||
|
|
|
||
|
|
await repo.create(uuid4(), email, "hash1")
|
||
|
|
|
||
|
|
with pytest.raises(asyncpg.UniqueViolationError):
|
||
|
|
await repo.create(uuid4(), email, "hash2")
|
||
|
|
|
||
|
|
async def test_get_by_id_returns_user(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_id returns the correct user."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
user_id = uuid4()
|
||
|
|
email = "getbyid@example.com"
|
||
|
|
|
||
|
|
await repo.create(user_id, email, "hash")
|
||
|
|
result = await repo.get_by_id(user_id)
|
||
|
|
|
||
|
|
assert result is not None
|
||
|
|
assert result["id"] == user_id
|
||
|
|
assert result["email"] == email
|
||
|
|
|
||
|
|
async def test_get_by_id_returns_none_for_nonexistent(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_id returns None for non-existent user."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_by_id(uuid4())
|
||
|
|
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
async def test_get_by_email_returns_user(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_email returns the correct user."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
user_id = uuid4()
|
||
|
|
email = "getbyemail@example.com"
|
||
|
|
|
||
|
|
await repo.create(user_id, email, "hash")
|
||
|
|
result = await repo.get_by_email(email)
|
||
|
|
|
||
|
|
assert result is not None
|
||
|
|
assert result["id"] == user_id
|
||
|
|
assert result["email"] == email
|
||
|
|
|
||
|
|
async def test_get_by_email_returns_none_for_nonexistent(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_email returns None for non-existent email."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_by_email("nonexistent@example.com")
|
||
|
|
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
async def test_get_by_email_is_case_sensitive(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Email lookup is case-sensitive (stored as provided)."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
email = "CaseSensitive@Example.com"
|
||
|
|
|
||
|
|
await repo.create(uuid4(), email, "hash")
|
||
|
|
|
||
|
|
# Exact match works
|
||
|
|
result = await repo.get_by_email(email)
|
||
|
|
assert result is not None
|
||
|
|
|
||
|
|
# Different case returns None
|
||
|
|
result = await repo.get_by_email(email.lower())
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
async def test_exists_by_email_returns_true_when_exists(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""exists_by_email returns True when email exists."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
email = "exists@example.com"
|
||
|
|
|
||
|
|
await repo.create(uuid4(), email, "hash")
|
||
|
|
result = await repo.exists_by_email(email)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
|
||
|
|
async def test_exists_by_email_returns_false_when_not_exists(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""exists_by_email returns False when email doesn't exist."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.exists_by_email("notexists@example.com")
|
||
|
|
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
async def test_user_id_is_uuid_primary_key(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""User ID must be a valid UUID (primary key)."""
|
||
|
|
repo = UserRepository(db_conn)
|
||
|
|
user_id = uuid4()
|
||
|
|
|
||
|
|
await repo.create(user_id, "pk_test@example.com", "hash")
|
||
|
|
|
||
|
|
# Duplicate ID should fail
|
||
|
|
with pytest.raises(asyncpg.UniqueViolationError):
|
||
|
|
await repo.create(user_id, "other@example.com", "hash")
|