initial commit
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
102
tests/conftest.py
Normal file
102
tests/conftest.py
Normal file
@ -0,0 +1,102 @@
|
||||
from collections.abc import Callable, Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from src.app.core.config import settings
|
||||
from src.app.main import app
|
||||
|
||||
DATABASE_URI = settings.POSTGRES_URI
|
||||
DATABASE_PREFIX = settings.POSTGRES_SYNC_PREFIX
|
||||
|
||||
sync_engine = create_engine(DATABASE_PREFIX + DATABASE_URI)
|
||||
local_session = sessionmaker(autocommit=False, autoflush=False, bind=sync_engine)
|
||||
|
||||
|
||||
fake = Faker()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def client() -> Generator[TestClient, Any, None]:
|
||||
with TestClient(app) as _client:
|
||||
yield _client
|
||||
app.dependency_overrides = {}
|
||||
sync_engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db() -> Generator[Session, Any, None]:
|
||||
session = local_session()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
|
||||
def override_dependency(dependency: Callable[..., Any], mocked_response: Any) -> None:
|
||||
app.dependency_overrides[dependency] = lambda: mocked_response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
"""Mock database session for unit tests."""
|
||||
return Mock(spec=AsyncSession)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_redis():
|
||||
"""Mock Redis connection for unit tests."""
|
||||
mock_redis = Mock()
|
||||
mock_redis.get = AsyncMock(return_value=None)
|
||||
mock_redis.set = AsyncMock(return_value=True)
|
||||
mock_redis.delete = AsyncMock(return_value=True)
|
||||
return mock_redis
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_user_data():
|
||||
"""Generate sample user data for tests."""
|
||||
return {
|
||||
"name": fake.name(),
|
||||
"username": fake.user_name(),
|
||||
"email": fake.email(),
|
||||
"password": fake.password(),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_user_read():
|
||||
"""Generate a sample UserRead object."""
|
||||
from uuid6 import uuid7
|
||||
|
||||
from src.app.schemas.user import UserRead
|
||||
|
||||
return UserRead(
|
||||
id=1,
|
||||
uuid=uuid7(),
|
||||
name=fake.name(),
|
||||
username=fake.user_name(),
|
||||
email=fake.email(),
|
||||
profile_image_url=fake.image_url(),
|
||||
is_superuser=False,
|
||||
created_at=fake.date_time(),
|
||||
updated_at=fake.date_time(),
|
||||
tier_id=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def current_user_dict():
|
||||
"""Mock current user from auth dependency."""
|
||||
return {
|
||||
"id": 1,
|
||||
"username": fake.user_name(),
|
||||
"email": fake.email(),
|
||||
"name": fake.name(),
|
||||
"is_superuser": False,
|
||||
}
|
||||
25
tests/helpers/generators.py
Normal file
25
tests/helpers/generators.py
Normal file
@ -0,0 +1,25 @@
|
||||
from uuid6 import uuid7 #126
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.app import models
|
||||
from src.app.core.security import get_password_hash
|
||||
from tests.conftest import fake
|
||||
|
||||
|
||||
def create_user(db: Session, is_super_user: bool = False) -> models.User:
|
||||
_user = models.User(
|
||||
name=fake.name(),
|
||||
username=fake.user_name(),
|
||||
email=fake.email(),
|
||||
hashed_password=get_password_hash(fake.password()),
|
||||
profile_image_url=fake.image_url(),
|
||||
uuid=uuid7,
|
||||
is_superuser=is_super_user,
|
||||
)
|
||||
|
||||
db.add(_user)
|
||||
db.commit()
|
||||
db.refresh(_user)
|
||||
|
||||
return _user
|
||||
17
tests/helpers/mocks.py
Normal file
17
tests/helpers/mocks.py
Normal file
@ -0,0 +1,17 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from src.app import models
|
||||
from tests.conftest import fake
|
||||
|
||||
|
||||
def get_current_user(user: models.User) -> dict[str, Any]:
|
||||
return jsonable_encoder(user)
|
||||
|
||||
|
||||
def oauth2_scheme() -> str:
|
||||
token = fake.sha256()
|
||||
if isinstance(token, bytes):
|
||||
token = token.decode("utf-8")
|
||||
return token # type: ignore
|
||||
195
tests/test_user.py
Normal file
195
tests/test_user.py
Normal file
@ -0,0 +1,195 @@
|
||||
"""Unit tests for user API endpoints."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.app.api.v1.users import erase_user, patch_user, read_user, read_users, write_user
|
||||
from src.app.core.exceptions.http_exceptions import DuplicateValueException, ForbiddenException, NotFoundException
|
||||
from src.app.schemas.user import UserCreate, UserRead, UserUpdate
|
||||
|
||||
|
||||
class TestWriteUser:
|
||||
"""Test user creation endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_success(self, mock_db, sample_user_data, sample_user_read):
|
||||
"""Test successful user creation."""
|
||||
user_create = UserCreate(**sample_user_data)
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
# Mock that email and username don't exist
|
||||
mock_crud.exists = AsyncMock(side_effect=[False, False]) # email, then username
|
||||
mock_crud.create = AsyncMock(return_value=Mock(id=1))
|
||||
mock_crud.get = AsyncMock(return_value=sample_user_read)
|
||||
|
||||
with patch("src.app.api.v1.users.get_password_hash") as mock_hash:
|
||||
mock_hash.return_value = "hashed_password"
|
||||
|
||||
result = await write_user(Mock(), user_create, mock_db)
|
||||
|
||||
assert result == sample_user_read
|
||||
mock_crud.exists.assert_any_call(db=mock_db, email=user_create.email)
|
||||
mock_crud.exists.assert_any_call(db=mock_db, username=user_create.username)
|
||||
mock_crud.create.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_duplicate_email(self, mock_db, sample_user_data):
|
||||
"""Test user creation with duplicate email."""
|
||||
user_create = UserCreate(**sample_user_data)
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
# Mock that email already exists
|
||||
mock_crud.exists = AsyncMock(return_value=True)
|
||||
|
||||
with pytest.raises(DuplicateValueException, match="Email is already registered"):
|
||||
await write_user(Mock(), user_create, mock_db)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_duplicate_username(self, mock_db, sample_user_data):
|
||||
"""Test user creation with duplicate username."""
|
||||
user_create = UserCreate(**sample_user_data)
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
# Mock email doesn't exist, but username does
|
||||
mock_crud.exists = AsyncMock(side_effect=[False, True])
|
||||
|
||||
with pytest.raises(DuplicateValueException, match="Username not available"):
|
||||
await write_user(Mock(), user_create, mock_db)
|
||||
|
||||
|
||||
class TestReadUser:
|
||||
"""Test user retrieval endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_user_success(self, mock_db, sample_user_read):
|
||||
"""Test successful user retrieval."""
|
||||
username = "test_user"
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
mock_crud.get = AsyncMock(return_value=sample_user_read)
|
||||
|
||||
result = await read_user(Mock(), username, mock_db)
|
||||
|
||||
assert result == sample_user_read
|
||||
mock_crud.get.assert_called_once_with(
|
||||
db=mock_db, username=username, is_deleted=False, schema_to_select=UserRead
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_user_not_found(self, mock_db):
|
||||
"""Test user retrieval when user doesn't exist."""
|
||||
username = "nonexistent_user"
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
mock_crud.get = AsyncMock(return_value=None)
|
||||
|
||||
with pytest.raises(NotFoundException, match="User not found"):
|
||||
await read_user(Mock(), username, mock_db)
|
||||
|
||||
|
||||
class TestReadUsers:
|
||||
"""Test users list endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_users_success(self, mock_db):
|
||||
"""Test successful users list retrieval."""
|
||||
mock_users_data = {"data": [{"id": 1}, {"id": 2}], "count": 2}
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
mock_crud.get_multi = AsyncMock(return_value=mock_users_data)
|
||||
|
||||
with patch("src.app.api.v1.users.paginated_response") as mock_paginated:
|
||||
expected_response = {"data": [{"id": 1}, {"id": 2}], "pagination": {}}
|
||||
mock_paginated.return_value = expected_response
|
||||
|
||||
result = await read_users(Mock(), mock_db, page=1, items_per_page=10)
|
||||
|
||||
assert result == expected_response
|
||||
mock_crud.get_multi.assert_called_once()
|
||||
mock_paginated.assert_called_once()
|
||||
|
||||
|
||||
class TestPatchUser:
|
||||
"""Test user update endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_user_success(self, mock_db, current_user_dict, sample_user_read):
|
||||
"""Test successful user update."""
|
||||
username = current_user_dict["username"]
|
||||
user_update = UserUpdate(name="New Name")
|
||||
|
||||
|
||||
user_dict = sample_user_read.model_dump()
|
||||
user_dict["username"] = username
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
mock_crud.get = AsyncMock(return_value=user_dict)
|
||||
mock_crud.exists = AsyncMock(return_value=False)
|
||||
mock_crud.update = AsyncMock(return_value=None)
|
||||
|
||||
result = await patch_user(Mock(), user_update, username, current_user_dict, mock_db)
|
||||
|
||||
assert result == {"message": "User updated"}
|
||||
mock_crud.update.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_user_forbidden(self, mock_db, current_user_dict, sample_user_read):
|
||||
"""Test user update when user tries to update another user."""
|
||||
username = "different_user"
|
||||
user_update = UserUpdate(name="New Name")
|
||||
user_dict = sample_user_read.model_dump()
|
||||
user_dict["username"] = username
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
mock_crud.get = AsyncMock(return_value=user_dict)
|
||||
|
||||
with pytest.raises(ForbiddenException):
|
||||
await patch_user(Mock(), user_update, username, current_user_dict, mock_db)
|
||||
|
||||
|
||||
class TestEraseUser:
|
||||
"""Test user deletion endpoint."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_erase_user_success(self, mock_db, current_user_dict, sample_user_read):
|
||||
"""Test successful user deletion."""
|
||||
username = current_user_dict["username"]
|
||||
sample_user_read.username = username
|
||||
token = "mock_token"
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
mock_crud.get = AsyncMock(return_value=sample_user_read)
|
||||
mock_crud.delete = AsyncMock(return_value=None)
|
||||
|
||||
with patch("src.app.api.v1.users.blacklist_token", new_callable=AsyncMock) as mock_blacklist:
|
||||
result = await erase_user(Mock(), username, current_user_dict, mock_db, token)
|
||||
|
||||
assert result == {"message": "User deleted"}
|
||||
mock_crud.delete.assert_called_once_with(db=mock_db, username=username)
|
||||
mock_blacklist.assert_called_once_with(token=token, db=mock_db)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_erase_user_not_found(self, mock_db, current_user_dict):
|
||||
"""Test user deletion when user doesn't exist."""
|
||||
username = "nonexistent_user"
|
||||
token = "mock_token"
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
mock_crud.get = AsyncMock(return_value=None)
|
||||
|
||||
with pytest.raises(NotFoundException, match="User not found"):
|
||||
await erase_user(Mock(), username, current_user_dict, mock_db, token)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_erase_user_forbidden(self, mock_db, current_user_dict, sample_user_read):
|
||||
"""Test user deletion when user tries to delete another user."""
|
||||
username = "different_user"
|
||||
sample_user_read.username = username
|
||||
token = "mock_token"
|
||||
|
||||
with patch("src.app.api.v1.users.crud_users") as mock_crud:
|
||||
mock_crud.get = AsyncMock(return_value=sample_user_read)
|
||||
|
||||
with pytest.raises(ForbiddenException):
|
||||
await erase_user(Mock(), username, current_user_dict, mock_db, token)
|
||||
Reference in New Issue
Block a user