feat: refactor groups
This commit is contained in:
parent
230c2460c9
commit
071eeb0bfa
57
Back/alembic/versions/36ed70229177_add_owner_id_to_group.py
Normal file
57
Back/alembic/versions/36ed70229177_add_owner_id_to_group.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""add owner_id to group
|
||||
|
||||
Revision ID: 36ed70229177
|
||||
Revises: b5ace31192c3
|
||||
Create Date: 2026-03-28 13:26:31.817104
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '36ed70229177'
|
||||
down_revision: Union[str, Sequence[str], None] = 'b5ace31192c3'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# 1. Add column as nullable
|
||||
op.add_column('groups', sa.Column('owner_id', sa.UUID(), nullable=True))
|
||||
op.create_index(op.f('ix_groups_owner_id'), 'groups', ['owner_id'], unique=False)
|
||||
op.create_foreign_key(None, 'groups', 'users', ['owner_id'], ['id'], ondelete='CASCADE')
|
||||
|
||||
# 2. Update existing groups with an owner (using the first manager or any member)
|
||||
op.execute("""
|
||||
UPDATE groups g
|
||||
SET owner_id = (
|
||||
SELECT user_id
|
||||
FROM group_members
|
||||
WHERE group_id = g.id
|
||||
ORDER BY (CASE WHEN role::text = 'MANAGER' THEN 0 ELSE 1 END), user_id
|
||||
LIMIT 1
|
||||
)
|
||||
""")
|
||||
|
||||
# 3. For any group that still has NULL (unlikely), set to the first user in the system
|
||||
op.execute("""
|
||||
UPDATE groups
|
||||
SET owner_id = (SELECT id FROM users LIMIT 1)
|
||||
WHERE owner_id IS NULL
|
||||
""")
|
||||
|
||||
# 4. Set NOT NULL constraint
|
||||
op.alter_column('groups', 'owner_id', nullable=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'groups', type_='foreignkey')
|
||||
op.drop_index(op.f('ix_groups_owner_id'), table_name='groups')
|
||||
op.drop_column('groups', 'owner_id')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -11,9 +11,6 @@ from domains.admin.schemas import (
|
|||
AdminUserResponse
|
||||
)
|
||||
|
||||
from domains.groups.schemas import GroupResponse
|
||||
from domains.groups.repo import get_all_groups
|
||||
|
||||
from domains.admin.service import (
|
||||
admin_create_user,
|
||||
admin_reset_user_secret
|
||||
|
|
@ -102,12 +99,8 @@ async def list_users(
|
|||
return await get_all_users(db, include_admin=True)
|
||||
|
||||
|
||||
@router.get("/groups", response_model=list[GroupResponse])
|
||||
async def list_groups(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(get_current_admin)
|
||||
):
|
||||
return await get_all_groups(db)
|
||||
return await get_all_users(db, include_admin=True)
|
||||
|
||||
|
||||
@router.get("/notifications", response_model=list[NotificationResponse])
|
||||
async def list_notifications(
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from db.session import get_db
|
||||
from core.deps import get_current_admin, get_current_user
|
||||
|
||||
from domains.groups.schemas import (
|
||||
GroupCreate,
|
||||
GroupResponse,
|
||||
AddMemberRequest,
|
||||
GroupMemberResponse
|
||||
)
|
||||
|
||||
from domains.groups.service import (
|
||||
create_new_group,
|
||||
list_user_groups,
|
||||
list_all_groups_admin,
|
||||
list_group_members_api,
|
||||
invite_member_to_group,
|
||||
remove_member_from_group
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/groups",
|
||||
tags=["groups"]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=GroupResponse
|
||||
)
|
||||
async def create_group(
|
||||
payload: GroupCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Admin creates Public groups, Regular users create Private groups.
|
||||
"""
|
||||
group = await create_new_group(
|
||||
db,
|
||||
payload.name,
|
||||
user.id,
|
||||
user.is_admin
|
||||
)
|
||||
|
||||
return group
|
||||
|
||||
|
||||
@router.get("/my", response_model=list[GroupResponse])
|
||||
async def my_groups(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
List groups the user is a member of.
|
||||
"""
|
||||
return await list_user_groups(db, user.id)
|
||||
|
||||
|
||||
@router.get("/admin/all", response_model=list[GroupResponse])
|
||||
async def list_all_groups(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(get_current_admin)
|
||||
):
|
||||
"""
|
||||
Admin can see all groups.
|
||||
"""
|
||||
return await list_all_groups_admin(db)
|
||||
|
||||
|
||||
@router.get("/{group_id}/members", response_model=list[GroupMemberResponse])
|
||||
async def group_members(
|
||||
group_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
List group members with username and online status.
|
||||
"""
|
||||
return await list_group_members_api(db, group_id)
|
||||
|
||||
|
||||
@router.post("/{group_id}/invite")
|
||||
async def invite_member(
|
||||
group_id: str,
|
||||
payload: AddMemberRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Invite a user by username. Sends a notification.
|
||||
"""
|
||||
try:
|
||||
notification = await invite_member_to_group(
|
||||
db,
|
||||
group_id,
|
||||
user.id,
|
||||
payload.username
|
||||
)
|
||||
return {"message": "دعوت ارسال شد", "notification_id": notification.id}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{group_id}/members/{user_id}")
|
||||
async def remove_member(
|
||||
group_id: str,
|
||||
user_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Admin or Group Manager can remove a member.
|
||||
"""
|
||||
try:
|
||||
await remove_member_from_group(
|
||||
db,
|
||||
group_id,
|
||||
user_id,
|
||||
user
|
||||
)
|
||||
return {"message": "عضو با موفقیت حذف شد"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=str(e)
|
||||
)
|
||||
6
Back/domains/groups/api/__init__.py
Normal file
6
Back/domains/groups/api/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from fastapi import APIRouter
|
||||
from domains.groups.api.admin import router as admin_router
|
||||
from domains.groups.api.client import router as client_router
|
||||
|
||||
# The API is split into two specialized routers
|
||||
# This will be included in main.py
|
||||
111
Back/domains/groups/api/admin.py
Normal file
111
Back/domains/groups/api/admin.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import uuid
|
||||
|
||||
from db.session import get_db
|
||||
from core.deps import get_current_admin
|
||||
from domains.groups.schemas import (
|
||||
AdminGroupCreate,
|
||||
GroupResponse,
|
||||
AdminAddMemberRequest,
|
||||
GroupMemberResponse
|
||||
)
|
||||
from domains.groups.service import (
|
||||
create_admin_group,
|
||||
list_all_groups_admin,
|
||||
add_member_to_group,
|
||||
list_group_members_api,
|
||||
remove_member_from_group,
|
||||
delete_group_service
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/admin/groups",
|
||||
tags=["admin-groups"]
|
||||
)
|
||||
|
||||
@router.post("/", response_model=GroupResponse)
|
||||
async def admin_create_group(
|
||||
payload: AdminGroupCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(get_current_admin)
|
||||
):
|
||||
"""
|
||||
Admin always creates public groups and is not auto-added as a member.
|
||||
"""
|
||||
return await create_admin_group(
|
||||
db,
|
||||
name=payload.name,
|
||||
owner_id=admin.id
|
||||
)
|
||||
|
||||
@router.get("/", response_model=list[GroupResponse])
|
||||
async def list_all_groups(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(get_current_admin)
|
||||
):
|
||||
"""
|
||||
List all groups in the system.
|
||||
"""
|
||||
return await list_all_groups_admin(db)
|
||||
|
||||
@router.get("/{group_id}/members", response_model=list[GroupMemberResponse])
|
||||
async def admin_list_members(
|
||||
group_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(get_current_admin)
|
||||
):
|
||||
return await list_group_members_api(db, group_id)
|
||||
|
||||
@router.post("/{group_id}/members", response_model=None)
|
||||
async def force_add_member(
|
||||
group_id: uuid.UUID,
|
||||
payload: AdminAddMemberRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(get_current_admin)
|
||||
):
|
||||
"""
|
||||
Force add a user to a group with a specific role. Bypasses invitation.
|
||||
"""
|
||||
from domains.users.repo import get_user_by_username
|
||||
|
||||
target_user = await get_user_by_username(db, payload.username)
|
||||
if not target_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
try:
|
||||
await add_member_to_group(db, group_id, target_user.id, payload.role)
|
||||
return {"message": f"User {payload.username} added successfully"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.delete("/{group_id}/members/{user_id}")
|
||||
async def admin_remove_member(
|
||||
group_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(get_current_admin)
|
||||
):
|
||||
"""
|
||||
Admin can remove any user from any group.
|
||||
"""
|
||||
try:
|
||||
await remove_member_from_group(db, group_id, user_id, admin)
|
||||
return {"message": "Member removed successfully"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.delete("/{group_id}")
|
||||
async def admin_delete_group(
|
||||
group_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(get_current_admin)
|
||||
):
|
||||
"""
|
||||
Admin can delete any group.
|
||||
"""
|
||||
try:
|
||||
await delete_group_service(db, group_id, admin)
|
||||
return {"message": "Group deleted successfully"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=403, detail=str(e))
|
||||
90
Back/domains/groups/api/client.py
Normal file
90
Back/domains/groups/api/client.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import uuid
|
||||
|
||||
from db.session import get_db
|
||||
from core.deps import get_current_user
|
||||
from domains.groups.schemas import (
|
||||
GroupCreate,
|
||||
GroupResponse,
|
||||
AddMemberRequest,
|
||||
GroupMemberResponse
|
||||
)
|
||||
from domains.groups.service import (
|
||||
create_user_group,
|
||||
list_user_groups,
|
||||
list_group_members_api,
|
||||
invite_member_to_group,
|
||||
delete_group_service
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/groups",
|
||||
tags=["groups"]
|
||||
)
|
||||
|
||||
@router.post("/", response_model=GroupResponse)
|
||||
async def user_create_group(
|
||||
payload: GroupCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Regular users always create private groups and become the first manager.
|
||||
"""
|
||||
return await create_user_group(
|
||||
db,
|
||||
name=payload.name,
|
||||
owner_id=user.id
|
||||
)
|
||||
|
||||
@router.get("/my", response_model=list[GroupResponse])
|
||||
async def my_groups(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
List groups the current user is a member of.
|
||||
"""
|
||||
return await list_user_groups(db, user.id)
|
||||
|
||||
@router.get("/{group_id}/members", response_model=list[GroupMemberResponse])
|
||||
async def list_members(
|
||||
group_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
return await list_group_members_api(db, str(group_id))
|
||||
|
||||
@router.post("/{group_id}/invite")
|
||||
async def invite_member(
|
||||
group_id: uuid.UUID,
|
||||
payload: AddMemberRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
try:
|
||||
notification = await invite_member_to_group(
|
||||
db,
|
||||
group_id,
|
||||
user.id,
|
||||
payload.username
|
||||
)
|
||||
return {"message": "Invitation sent", "notification_id": str(notification.id)}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.delete("/{group_id}")
|
||||
async def delete_my_group(
|
||||
group_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Only the Group Owner (creator) can delete the group.
|
||||
"""
|
||||
try:
|
||||
await delete_group_service(db, group_id, user)
|
||||
return {"message": "Group deleted successfully"}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=403, detail=str(e))
|
||||
|
|
@ -39,6 +39,12 @@ class Group(Base):
|
|||
index=True
|
||||
)
|
||||
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
# Relationship to members
|
||||
members: Mapped[list["GroupMember"]] = relationship(back_populates="group", cascade="all, delete-orphan")
|
||||
|
||||
|
|
|
|||
|
|
@ -72,3 +72,11 @@ async def delete_group_member(db: AsyncSession, group_id: uuid.UUID, user_id: uu
|
|||
.where(GroupMember.user_id == user_id)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async def delete_group(db: AsyncSession, group_id: uuid.UUID):
|
||||
from sqlalchemy import delete
|
||||
await db.execute(
|
||||
delete(Group)
|
||||
.where(Group.id == group_id)
|
||||
)
|
||||
await db.commit()
|
||||
|
|
|
|||
|
|
@ -6,10 +6,15 @@ from domains.groups.models import GroupType, GroupMemberRole
|
|||
class GroupCreate(BaseModel):
|
||||
name: str = Field(..., max_length=50, description='name of the group')
|
||||
|
||||
|
||||
class AdminGroupCreate(BaseModel):
|
||||
name: str = Field(..., max_length=50, description='name of the group')
|
||||
|
||||
class GroupResponse(BaseModel):
|
||||
id: uuid.UUID = Field(...)
|
||||
name: str = Field(..., max_length=50, description='name of the group')
|
||||
type: GroupType = Field(..., description='type of the group')
|
||||
owner_id: uuid.UUID = Field(...)
|
||||
is_active: bool = Field(..., description='is active')
|
||||
|
||||
class Config:
|
||||
|
|
@ -18,6 +23,11 @@ class GroupResponse(BaseModel):
|
|||
class AddMemberRequest(BaseModel):
|
||||
username: str = Field(..., max_length=20, description='username of the user')
|
||||
|
||||
|
||||
class AdminAddMemberRequest(BaseModel):
|
||||
username: str = Field(..., max_length=20, description='username of the user')
|
||||
role: GroupMemberRole = Field(GroupMemberRole.MEMBER, description='role of the user in the group')
|
||||
|
||||
class GroupMemberResponse(BaseModel):
|
||||
user_id: uuid.UUID = Field(...)
|
||||
username: str = Field(..., max_length=20, description='username of the user')
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from domains.groups.repo import (
|
|||
get_user_groups,
|
||||
get_group_members_with_details,
|
||||
delete_group_member,
|
||||
delete_group,
|
||||
get_group_member,
|
||||
get_all_groups as repo_get_all_groups
|
||||
)
|
||||
|
|
@ -18,25 +19,24 @@ from domains.realtime.presence_service import list_online_users
|
|||
from domains.users.repo import get_user_by_username
|
||||
from domains.notifications.service import send_join_request
|
||||
|
||||
async def create_new_group(
|
||||
|
||||
async def create_user_group(
|
||||
db: AsyncSession,
|
||||
name: str,
|
||||
creator_id: uuid.UUID,
|
||||
is_admin: bool
|
||||
owner_id: uuid.UUID,
|
||||
):
|
||||
group_type = GroupType.PUBLIC if is_admin else GroupType.PRIVATE
|
||||
|
||||
group = Group(
|
||||
name=name,
|
||||
type=group_type
|
||||
type=GroupType.PRIVATE,
|
||||
owner_id=owner_id
|
||||
)
|
||||
|
||||
await create_group(db, group)
|
||||
|
||||
# Creator becomes Manager
|
||||
# Owner becomes Manager
|
||||
membership = GroupMember(
|
||||
group_id=group.id,
|
||||
user_id=creator_id,
|
||||
user_id=owner_id,
|
||||
role=GroupMemberRole.MANAGER
|
||||
)
|
||||
await add_group_member(db, membership)
|
||||
|
|
@ -44,6 +44,21 @@ async def create_new_group(
|
|||
return group
|
||||
|
||||
|
||||
async def create_admin_group(
|
||||
db: AsyncSession,
|
||||
name: str,
|
||||
owner_id: uuid.UUID,
|
||||
):
|
||||
group = Group(
|
||||
name=name,
|
||||
type=GroupType.PUBLIC,
|
||||
owner_id=owner_id
|
||||
)
|
||||
|
||||
await create_group(db, group)
|
||||
return group
|
||||
|
||||
|
||||
async def invite_member_to_group(
|
||||
db: AsyncSession,
|
||||
group_id: str | uuid.UUID,
|
||||
|
|
@ -116,10 +131,10 @@ async def list_all_groups_admin(db: AsyncSession):
|
|||
return await repo_get_all_groups(db)
|
||||
|
||||
|
||||
async def list_group_members_api(db: AsyncSession, group_id: str):
|
||||
group_id_uuid = uuid.UUID(group_id)
|
||||
async def list_group_members_api(db: AsyncSession, group_id: str | uuid.UUID):
|
||||
group_id_uuid = group_id if isinstance(group_id, uuid.UUID) else uuid.UUID(group_id)
|
||||
members_data = await get_group_members_with_details(db, group_id_uuid)
|
||||
online_users = await list_online_users(str(group_id))
|
||||
online_users = await list_online_users(str(group_id_uuid))
|
||||
|
||||
result = []
|
||||
for member_info in members_data:
|
||||
|
|
@ -148,4 +163,28 @@ async def remove_member_from_group(
|
|||
if not membership or membership.role != GroupMemberRole.MANAGER:
|
||||
raise ValueError("دسترسی لازم را ندارید")
|
||||
|
||||
group = await get_group_by_id(db, group_id_uuid)
|
||||
if not group:
|
||||
raise ValueError("گروهی یافت نشد")
|
||||
if group.owner_id == target_user_id_uuid:
|
||||
raise ValueError("حذف سازنده گروه مجاز نیست")
|
||||
|
||||
await delete_group_member(db, group_id_uuid, target_user_id_uuid)
|
||||
|
||||
|
||||
async def delete_group_service(
|
||||
db: AsyncSession,
|
||||
group_id: str | uuid.UUID,
|
||||
requesting_user
|
||||
):
|
||||
group_id_uuid = group_id if isinstance(group_id, uuid.UUID) else uuid.UUID(group_id)
|
||||
|
||||
group = await get_group_by_id(db, group_id_uuid)
|
||||
if not group:
|
||||
raise ValueError("گروهی یافت نشد")
|
||||
|
||||
# Permission check: System Admin or Group Owner
|
||||
if not requesting_user.is_admin and group.owner_id != requesting_user.id:
|
||||
raise ValueError("شما دسترسی لازم برای حذف گروه را ندارید (فقط سازنده گروه یا ادمین سیستم)")
|
||||
|
||||
await delete_group(db, group_id_uuid)
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ async def group_ws(websocket: WebSocket, group_id: str):
|
|||
|
||||
# anti spam
|
||||
current_time = time.time()
|
||||
if current_time - last_action_time < 0.5:
|
||||
if current_time - last_action_time < 0.1:
|
||||
continue
|
||||
last_action_time = current_time
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from fastapi_swagger import patch_fastapi
|
|||
from domains.auth.api import router as auth_router
|
||||
from domains.users.api import router as users_router
|
||||
from domains.admin.api import router as admin_router
|
||||
from domains.groups.api import router as groups_router
|
||||
from domains.groups.api.admin import router as groups_admin_router
|
||||
from domains.groups.api.client import router as groups_client_router
|
||||
from domains.realtime.ws import router as realtime_router
|
||||
from domains.notifications.api import router as notifications_router
|
||||
from integrations.livekit.client import close_livekit_api
|
||||
|
|
@ -34,7 +35,7 @@ async def lifespan(app: FastAPI):
|
|||
await close_livekit_api()
|
||||
await redis_client.close()
|
||||
|
||||
global_limiter = RateLimiter(requests=30, window_seconds=60, scope="global")
|
||||
global_limiter = RateLimiter(requests=120, window_seconds=60, scope="global")
|
||||
|
||||
app = FastAPI(
|
||||
title="NEDA API",
|
||||
|
|
@ -76,6 +77,7 @@ app.add_middleware(
|
|||
app.include_router(auth_router)
|
||||
app.include_router(users_router)
|
||||
app.include_router(admin_router)
|
||||
app.include_router(groups_router)
|
||||
app.include_router(groups_client_router)
|
||||
app.include_router(groups_admin_router)
|
||||
app.include_router(realtime_router)
|
||||
app.include_router(notifications_router)
|
||||
108
Back/tests/test_groups_service.py
Normal file
108
Back/tests/test_groups_service.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from domains.groups.models import GroupMemberRole, GroupType
|
||||
from domains.groups import service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_group_creates_private_group_and_owner_membership(monkeypatch):
|
||||
created_group = None
|
||||
added_membership = None
|
||||
|
||||
async def fake_create_group(db, group):
|
||||
nonlocal created_group
|
||||
created_group = group
|
||||
group.id = uuid.uuid4()
|
||||
return group
|
||||
|
||||
async def fake_add_group_member(db, membership):
|
||||
nonlocal added_membership
|
||||
added_membership = membership
|
||||
return membership
|
||||
|
||||
monkeypatch.setattr(service, "create_group", fake_create_group)
|
||||
monkeypatch.setattr(service, "add_group_member", fake_add_group_member)
|
||||
|
||||
owner_id = uuid.uuid4()
|
||||
group = await service.create_user_group(object(), "team-alpha", owner_id)
|
||||
|
||||
assert group.type == GroupType.PRIVATE
|
||||
assert group.owner_id == owner_id
|
||||
assert created_group is group
|
||||
assert added_membership is not None
|
||||
assert added_membership.user_id == owner_id
|
||||
assert added_membership.role == GroupMemberRole.MANAGER
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_admin_group_creates_public_group_without_membership(monkeypatch):
|
||||
create_group_mock = AsyncMock(side_effect=lambda db, group: setattr(group, "id", uuid.uuid4()) or group)
|
||||
add_group_member_mock = AsyncMock()
|
||||
|
||||
monkeypatch.setattr(service, "create_group", create_group_mock)
|
||||
monkeypatch.setattr(service, "add_group_member", add_group_member_mock)
|
||||
|
||||
owner_id = uuid.uuid4()
|
||||
group = await service.create_admin_group(object(), "ops-room", owner_id)
|
||||
|
||||
assert group.type == GroupType.PUBLIC
|
||||
assert group.owner_id == owner_id
|
||||
add_group_member_mock.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_group_members_api_returns_only_real_members(monkeypatch):
|
||||
group_id = uuid.uuid4()
|
||||
member_id = uuid.uuid4()
|
||||
members_data = [
|
||||
(
|
||||
SimpleNamespace(
|
||||
user_id=member_id,
|
||||
role=GroupMemberRole.MEMBER,
|
||||
),
|
||||
"neda-user",
|
||||
)
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"get_group_members_with_details",
|
||||
AsyncMock(return_value=members_data),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"list_online_users",
|
||||
AsyncMock(return_value=[str(member_id)]),
|
||||
)
|
||||
|
||||
response = await service.list_group_members_api(object(), group_id)
|
||||
|
||||
assert response == [
|
||||
{
|
||||
"user_id": member_id,
|
||||
"username": "neda-user",
|
||||
"role": GroupMemberRole.MEMBER,
|
||||
"is_online": True,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_group_routes_are_namespaced():
|
||||
from main import app
|
||||
|
||||
route_map = {
|
||||
(route.path, method)
|
||||
for route in app.routes
|
||||
if getattr(route, "methods", None)
|
||||
for method in route.methods
|
||||
}
|
||||
|
||||
assert ("/groups/", "POST") in route_map
|
||||
assert ("/admin/groups/", "POST") in route_map
|
||||
assert ("/admin/groups/", "GET") in route_map
|
||||
assert ("/admin/groups/{group_id}/members", "GET") in route_map
|
||||
assert ("/admin/groups/{group_id}/members", "POST") in route_map
|
||||
Loading…
Reference in New Issue
Block a user