
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.
354 lines
17 KiB
Python
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() |