mitlist/be/tests/crud/test_group.py
Mohamad.Elsena 29ccab2f7e feat: Implement chore management feature with personal and group chores
This commit introduces a comprehensive chore management system, allowing users to create, manage, and track both personal and group chores. Key changes include:
- Addition of new API endpoints for personal and group chores in `be/app/api/v1/endpoints/chores.py`.
- Implementation of chore models and schemas to support the new functionality in `be/app/models.py` and `be/app/schemas/chore.py`.
- Integration of chore services in the frontend to handle API interactions for chore management.
- Creation of new Vue components for displaying and managing chores, including `ChoresPage.vue` and `PersonalChoresPage.vue`.
- Updates to the router to include chore-related routes and navigation.

This feature enhances user collaboration and organization within shared living environments, aligning with the project's goal of streamlining household management.
2025-05-21 18:18:22 +02:00

354 lines
17 KiB
Python

import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError
from sqlalchemy.future import select
from sqlalchemy import delete, func
from app.crud.group import (
create_group,
get_user_groups,
get_group_by_id,
is_user_member,
get_user_role_in_group,
add_user_to_group,
remove_user_from_group,
get_group_member_count,
check_group_membership,
check_user_role_in_group,
update_group_member_role # Assuming this will be added
)
from app.schemas.group import GroupCreate, GroupUpdate # Added GroupUpdate
from app.models import User as UserModel, Group as GroupModel, UserGroup as UserGroupModel, UserRoleEnum
from app.core.exceptions import (
GroupOperationError,
GroupNotFoundError,
DatabaseConnectionError,
DatabaseIntegrityError,
DatabaseQueryError,
DatabaseTransactionError,
GroupMembershipError,
GroupPermissionError,
UserNotFoundError, # For adding user to group
ConflictError # For updates
)
# Fixtures
@pytest.fixture
def mock_db_session():
session = AsyncMock()
mock_transaction_context = AsyncMock()
session.begin = MagicMock(return_value=mock_transaction_context)
session.commit = AsyncMock()
session.rollback = AsyncMock()
session.refresh = AsyncMock()
session.add = MagicMock()
session.delete = MagicMock()
session.execute = AsyncMock()
session.get = AsyncMock()
session.flush = AsyncMock()
return session
@pytest.fixture
def group_create_data():
return GroupCreate(name="Test Group")
@pytest.fixture
def group_update_data():
return GroupUpdate(name="Updated Test Group", version=1)
@pytest.fixture
def creator_user_model():
return UserModel(id=1, name="Creator User", email="creator@example.com", version=1)
@pytest.fixture
def member_user_model():
return UserModel(id=2, name="Member User", email="member@example.com", version=1)
@pytest.fixture
def non_member_user_model():
return UserModel(id=3, name="Non Member User", email="nonmember@example.com", version=1)
@pytest.fixture
def db_group_model(creator_user_model):
return GroupModel(id=1, name="Test Group", created_by_id=creator_user_model.id, creator=creator_user_model, version=1)
@pytest.fixture
def db_user_group_owner_assoc(db_group_model, creator_user_model):
return UserGroupModel(id=1, user_id=creator_user_model.id, group_id=db_group_model.id, role=UserRoleEnum.owner, user=creator_user_model, group=db_group_model, version=1)
@pytest.fixture
def db_user_group_member_assoc(db_group_model, member_user_model):
return UserGroupModel(id=2, user_id=member_user_model.id, group_id=db_group_model.id, role=UserRoleEnum.member, user=member_user_model, group=db_group_model, version=1)
# --- create_group Tests ---
@pytest.mark.asyncio
async def test_create_group_success(mock_db_session, group_create_data, creator_user_model):
async def mock_refresh(instance, attribute_names=None, with_for_update=None):
if isinstance(instance, GroupModel):
instance.id = 1 # Simulate ID assignment by DB
instance.version = 1
# Simulate the UserGroup association being added and refreshed if done via relationship back_populates
instance.members = [UserGroupModel(user_id=creator_user_model.id, group_id=instance.id, role=UserRoleEnum.owner, version=1)]
elif isinstance(instance, UserGroupModel):
instance.id = 1 # Simulate ID for UserGroupModel
instance.version = 1
return None
mock_db_session.refresh.side_effect = mock_refresh
# Mock the user get for the creator
mock_db_session.get.return_value = creator_user_model
created_group = await create_group(mock_db_session, group_create_data, creator_user_model.id)
assert mock_db_session.add.call_count == 2 # Group and UserGroup
mock_db_session.flush.assert_called()
assert mock_db_session.refresh.call_count >= 1 # Called for group, maybe for UserGroup too
assert created_group is not None
assert created_group.name == group_create_data.name
assert created_group.created_by_id == creator_user_model.id
assert len(created_group.members) == 1
assert created_group.members[0].role == UserRoleEnum.owner
@pytest.mark.asyncio
async def test_create_group_integrity_error(mock_db_session, group_create_data, creator_user_model):
mock_db_session.get.return_value = creator_user_model # Creator user found
mock_db_session.flush.side_effect = IntegrityError("mock integrity error", "params", "orig")
with pytest.raises(DatabaseIntegrityError):
await create_group(mock_db_session, group_create_data, creator_user_model.id)
mock_db_session.rollback.assert_called_once()
# --- get_user_groups Tests ---
@pytest.mark.asyncio
async def test_get_user_groups_success(mock_db_session, db_group_model, creator_user_model):
# Mock the execute call that fetches groups for a user
mock_result_groups = AsyncMock()
mock_result_groups.scalars.return_value.all.return_value = [db_group_model]
mock_db_session.execute.return_value = mock_result_groups
groups = await get_user_groups(mock_db_session, creator_user_model.id)
assert len(groups) == 1
assert groups[0].name == db_group_model.name
mock_db_session.execute.assert_called_once()
# --- get_group_by_id Tests ---
@pytest.mark.asyncio
async def test_get_group_by_id_found(mock_db_session, db_group_model):
mock_db_session.get.return_value = db_group_model
group = await get_group_by_id(mock_db_session, db_group_model.id)
assert group is not None
assert group.id == db_group_model.id
mock_db_session.get.assert_called_once_with(GroupModel, db_group_model.id, options=ANY) # options for eager loading
@pytest.mark.asyncio
async def test_get_group_by_id_not_found(mock_db_session):
mock_db_session.get.return_value = None
group = await get_group_by_id(mock_db_session, 999)
assert group is None
# --- is_user_member Tests ---
from unittest.mock import ANY # For checking options in get
@pytest.mark.asyncio
async def test_is_user_member_true(mock_db_session, db_group_model, creator_user_model, db_user_group_owner_assoc):
mock_result = AsyncMock()
mock_result.scalar_one_or_none.return_value = db_user_group_owner_assoc.id
mock_db_session.execute.return_value = mock_result
is_member = await is_user_member(mock_db_session, db_group_model.id, creator_user_model.id)
assert is_member is True
@pytest.mark.asyncio
async def test_is_user_member_false(mock_db_session, db_group_model, non_member_user_model):
mock_result = AsyncMock()
mock_result.scalar_one_or_none.return_value = None
mock_db_session.execute.return_value = mock_result
is_member = await is_user_member(mock_db_session, db_group_model.id, non_member_user_model.id)
assert is_member is False
# --- get_user_role_in_group Tests ---
@pytest.mark.asyncio
async def test_get_user_role_in_group_owner(mock_db_session, db_group_model, creator_user_model):
mock_result = AsyncMock()
mock_result.scalar_one_or_none.return_value = UserRoleEnum.owner
mock_db_session.execute.return_value = mock_result
role = await get_user_role_in_group(mock_db_session, db_group_model.id, creator_user_model.id)
assert role == UserRoleEnum.owner
# --- add_user_to_group Tests ---
@pytest.mark.asyncio
async def test_add_user_to_group_new_member(mock_db_session, db_group_model, member_user_model, non_member_user_model):
# Mock is_user_member to return False initially
with patch('app.crud.group.is_user_member', new_callable=AsyncMock) as mock_is_member:
mock_is_member.return_value = False
# Mock get for the user to be added
mock_db_session.get.return_value = non_member_user_model
async def mock_refresh_user_group(instance, attribute_names=None, with_for_update=None):
instance.id = 100
instance.version = 1
return None
mock_db_session.refresh.side_effect = mock_refresh_user_group
user_group_assoc = await add_user_to_group(mock_db_session, db_group_model, non_member_user_model.id, UserRoleEnum.member)
mock_db_session.add.assert_called_once()
mock_db_session.flush.assert_called_once()
mock_db_session.refresh.assert_called_once()
assert user_group_assoc is not None
assert user_group_assoc.user_id == non_member_user_model.id
assert user_group_assoc.group_id == db_group_model.id
assert user_group_assoc.role == UserRoleEnum.member
@pytest.mark.asyncio
async def test_add_user_to_group_already_member(mock_db_session, db_group_model, creator_user_model):
with patch('app.crud.group.is_user_member', new_callable=AsyncMock) as mock_is_member:
mock_is_member.return_value = True # User is already a member
# No need to mock session.get for the user if is_user_member is true first
user_group_assoc = await add_user_to_group(mock_db_session, db_group_model, creator_user_model.id)
assert user_group_assoc is None # Should return None if user already member
mock_db_session.add.assert_not_called()
@pytest.mark.asyncio
async def test_add_user_to_group_user_not_found(mock_db_session, db_group_model):
with patch('app.crud.group.is_user_member', new_callable=AsyncMock) as mock_is_member:
mock_is_member.return_value = False # User not member initially
mock_db_session.get.return_value = None # User to be added not found
with pytest.raises(UserNotFoundError):
await add_user_to_group(mock_db_session, db_group_model, 999, UserRoleEnum.member)
mock_db_session.add.assert_not_called()
# --- remove_user_from_group Tests ---
@pytest.mark.asyncio
async def test_remove_user_from_group_success(mock_db_session, db_group_model, member_user_model, db_user_group_member_assoc):
# Mock get_user_role_in_group to confirm user is not owner
with patch('app.crud.group.get_user_role_in_group', new_callable=AsyncMock) as mock_get_role:
mock_get_role.return_value = UserRoleEnum.member
# Mock the execute call for the delete statement
mock_delete_result = AsyncMock()
mock_delete_result.rowcount = 1 # Simulate one row was affected/deleted
mock_db_session.execute.return_value = mock_delete_result
removed = await remove_user_from_group(mock_db_session, db_group_model, member_user_model.id)
assert removed is True
mock_db_session.execute.assert_called_once()
# Check that the delete statement was indeed called, e.g., by checking the structure of the query passed to execute
# This is a bit more involved if you want to match the exact SQLAlchemy delete object.
# For now, assert_called_once() confirms it was called.
@pytest.mark.asyncio
async def test_remove_user_from_group_owner_last_member(mock_db_session, db_group_model, creator_user_model):
with patch('app.crud.group.get_user_role_in_group', new_callable=AsyncMock) as mock_get_role, \
patch('app.crud.group.get_group_member_count', new_callable=AsyncMock) as mock_member_count:
mock_get_role.return_value = UserRoleEnum.owner
mock_member_count.return_value = 1 # This user is the last member
with pytest.raises(GroupOperationError, match="Cannot remove the sole owner of a group. Delete the group instead."):
await remove_user_from_group(mock_db_session, db_group_model, creator_user_model.id)
mock_db_session.execute.assert_not_called() # Delete should not be called
@pytest.mark.asyncio
async def test_remove_user_from_group_not_member(mock_db_session, db_group_model, non_member_user_model):
# Mock get_user_role_in_group to return None, indicating not a member or role not found (effectively not a member for removal purposes)
with patch('app.crud.group.get_user_role_in_group', new_callable=AsyncMock) as mock_get_role:
mock_get_role.return_value = None
# For this specific test, we might not even need to mock `execute` if `get_user_role_in_group` returning None
# already causes the function to exit or raise an error handled by `GroupMembershipError`.
# However, if the function proceeds to attempt a delete that affects 0 rows, then `rowcount = 0` is the correct mock.
mock_delete_result = AsyncMock()
mock_delete_result.rowcount = 0
mock_db_session.execute.return_value = mock_delete_result
with pytest.raises(GroupMembershipError, match="User is not a member of the group or cannot be removed."):
await remove_user_from_group(mock_db_session, db_group_model, non_member_user_model.id)
# Depending on the implementation: execute might be called or not.
# If there's a check before executing delete, it might not be called.
# If it tries to delete and finds nothing, it would be called.
# For now, let's assume it could be called. If your function logic prevents it, adjust this.
# mock_db_session.execute.assert_called_once() <--- This might fail if not called
# --- get_group_member_count Tests ---
@pytest.mark.asyncio
async def test_get_group_member_count_success(mock_db_session, db_group_model):
mock_result_count = AsyncMock()
mock_result_count.scalar_one.return_value = 5 # Example count
mock_db_session.execute.return_value = mock_result_count
count = await get_group_member_count(mock_db_session, db_group_model.id)
assert count == 5
# --- check_group_membership Tests ---
@pytest.mark.asyncio
async def test_check_group_membership_is_member(mock_db_session, db_group_model, creator_user_model):
# Mock get_group_by_id
with patch('app.crud.group.get_group_by_id', new_callable=AsyncMock) as mock_get_group, \
patch('app.crud.group.is_user_member', new_callable=AsyncMock) as mock_is_member:
mock_get_group.return_value = db_group_model
mock_is_member.return_value = True
group = await check_group_membership(mock_db_session, db_group_model.id, creator_user_model.id)
assert group is db_group_model
@pytest.mark.asyncio
async def test_check_group_membership_group_not_found(mock_db_session, creator_user_model):
with patch('app.crud.group.get_group_by_id', new_callable=AsyncMock) as mock_get_group:
mock_get_group.return_value = None
with pytest.raises(GroupNotFoundError):
await check_group_membership(mock_db_session, 999, creator_user_model.id)
@pytest.mark.asyncio
async def test_check_group_membership_not_member(mock_db_session, db_group_model, non_member_user_model):
with patch('app.crud.group.get_group_by_id', new_callable=AsyncMock) as mock_get_group, \
patch('app.crud.group.is_user_member', new_callable=AsyncMock) as mock_is_member:
mock_get_group.return_value = db_group_model
mock_is_member.return_value = False
with pytest.raises(GroupMembershipError, match="User is not a member of the specified group"):
await check_group_membership(mock_db_session, db_group_model.id, non_member_user_model.id)
# --- check_user_role_in_group (standalone check, not just membership) ---
@pytest.mark.asyncio
async def test_check_user_role_in_group_sufficient_role(mock_db_session, db_group_model, creator_user_model):
# This test assumes check_group_membership is called internally first, or similar logic applies
with patch('app.crud.group.check_group_membership', new_callable=AsyncMock) as mock_check_membership, \
patch('app.crud.group.get_user_role_in_group', new_callable=AsyncMock) as mock_get_role:
mock_check_membership.return_value = db_group_model # Group exists and user is member
mock_get_role.return_value = UserRoleEnum.owner
# Check if owner has owner role (should pass)
await check_user_role_in_group(mock_db_session, db_group_model.id, creator_user_model.id, UserRoleEnum.owner)
# Check if owner has member role (should pass, as owner is implicitly a member with higher privileges)
await check_user_role_in_group(mock_db_session, db_group_model.id, creator_user_model.id, UserRoleEnum.member)
@pytest.mark.asyncio
async def test_check_user_role_in_group_insufficient_role(mock_db_session, db_group_model, member_user_model):
with patch('app.crud.group.check_group_membership', new_callable=AsyncMock) as mock_check_membership, \
patch('app.crud.group.get_user_role_in_group', new_callable=AsyncMock) as mock_get_role:
mock_check_membership.return_value = db_group_model
mock_get_role.return_value = UserRoleEnum.member
with pytest.raises(GroupPermissionError, match="User does not have the required role in the group."):
await check_user_role_in_group(mock_db_session, db_group_model.id, member_user_model.id, UserRoleEnum.owner)
# Future test ideas, to be moved to a proper test planning tool or issue tracker.
# Consider these during major refactors or when expanding test coverage.
# Example of a DB operational error test (can be adapted for other functions)
# @pytest.mark.asyncio
# async def test_create_group_operational_error(mock_db_session, group_create_data, creator_user_model):
# mock_db_session.get.return_value = creator_user_model
# mock_db_session.flush.side_effect = OperationalError("mock operational error", "params", "orig")
# with pytest.raises(DatabaseConnectionError): # Assuming OperationalError maps to this
# await create_group(mock_db_session, group_create_data, creator_user_model.id)
# mock_db_session.rollback.assert_called_once()