Phase 1 backend implementation: Add invite management endpoints
Implements core invite management features for band admins:
- GET /bands/{band_id}/invites - List all invites for a band (admin only)
- DELETE /invites/{invite_id} - Revoke pending invite (admin only)
- GET /invites/{token}/info - Get invite details (public)
Backend changes:
- Add invites router with 3 endpoints
- Update BandRepository with get_invites_for_band and get_invite_by_id methods
- Add new schemas for invite listing and info
- Register invites router in main.py
Tests:
- Integration tests for all 3 endpoints
- Permission tests (admin vs non-admin)
- Edge cases (not found, expired, etc.)
This addresses the core requirements:
- Admins can see pending invites
- Admins can revoke pending invites
- Users can view invite details before accepting
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -15,6 +15,7 @@ from rehearsalhub.routers import (
|
|||||||
annotations_router,
|
annotations_router,
|
||||||
auth_router,
|
auth_router,
|
||||||
bands_router,
|
bands_router,
|
||||||
|
invites_router,
|
||||||
internal_router,
|
internal_router,
|
||||||
members_router,
|
members_router,
|
||||||
sessions_router,
|
sessions_router,
|
||||||
@@ -71,6 +72,7 @@ def create_app() -> FastAPI:
|
|||||||
prefix = "/api/v1"
|
prefix = "/api/v1"
|
||||||
app.include_router(auth_router, prefix=prefix)
|
app.include_router(auth_router, prefix=prefix)
|
||||||
app.include_router(bands_router, prefix=prefix)
|
app.include_router(bands_router, prefix=prefix)
|
||||||
|
app.include_router(invites_router, prefix=prefix)
|
||||||
app.include_router(sessions_router, prefix=prefix)
|
app.include_router(sessions_router, prefix=prefix)
|
||||||
app.include_router(songs_router, prefix=prefix)
|
app.include_router(songs_router, prefix=prefix)
|
||||||
app.include_router(versions_router, prefix=prefix)
|
app.include_router(versions_router, prefix=prefix)
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ class BandRepository(BaseRepository[Band]):
|
|||||||
result = await self.session.execute(stmt)
|
result = await self.session.execute(stmt)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_invite_by_id(self, invite_id: uuid.UUID) -> BandInvite | None:
|
||||||
|
stmt = select(BandInvite).where(BandInvite.id == invite_id)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_invites_for_band(self, band_id: uuid.UUID) -> list[BandInvite]:
|
||||||
|
"""Get all invites for a specific band."""
|
||||||
|
stmt = select(BandInvite).where(BandInvite.band_id == band_id)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
async def get_by_nc_folder_prefix(self, path: str) -> Band | None:
|
async def get_by_nc_folder_prefix(self, path: str) -> Band | None:
|
||||||
"""Return the band whose nc_folder_path is a prefix of path."""
|
"""Return the band whose nc_folder_path is a prefix of path."""
|
||||||
stmt = select(Band).where(Band.nc_folder_path.is_not(None))
|
stmt = select(Band).where(Band.nc_folder_path.is_not(None))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from rehearsalhub.routers.annotations import router as annotations_router
|
from rehearsalhub.routers.annotations import router as annotations_router
|
||||||
from rehearsalhub.routers.auth import router as auth_router
|
from rehearsalhub.routers.auth import router as auth_router
|
||||||
from rehearsalhub.routers.bands import router as bands_router
|
from rehearsalhub.routers.bands import router as bands_router
|
||||||
|
from rehearsalhub.routers.invites import router as invites_router
|
||||||
from rehearsalhub.routers.internal import router as internal_router
|
from rehearsalhub.routers.internal import router as internal_router
|
||||||
from rehearsalhub.routers.members import router as members_router
|
from rehearsalhub.routers.members import router as members_router
|
||||||
from rehearsalhub.routers.sessions import router as sessions_router
|
from rehearsalhub.routers.sessions import router as sessions_router
|
||||||
@@ -11,6 +12,7 @@ from rehearsalhub.routers.ws import router as ws_router
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"auth_router",
|
"auth_router",
|
||||||
"bands_router",
|
"bands_router",
|
||||||
|
"invites_router",
|
||||||
"internal_router",
|
"internal_router",
|
||||||
"members_router",
|
"members_router",
|
||||||
"sessions_router",
|
"sessions_router",
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from rehearsalhub.db.engine import get_session
|
from rehearsalhub.db.engine import get_session
|
||||||
from rehearsalhub.db.models import Member
|
from rehearsalhub.db.models import BandInvite, Member
|
||||||
from rehearsalhub.dependencies import get_current_member
|
from rehearsalhub.dependencies import get_current_member
|
||||||
from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandUpdate
|
from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandUpdate
|
||||||
|
from rehearsalhub.schemas.invite import BandInviteList, BandInviteListItem, InviteInfoRead
|
||||||
from rehearsalhub.repositories.band import BandRepository
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
from rehearsalhub.services.band import BandService
|
from rehearsalhub.services.band import BandService
|
||||||
from rehearsalhub.storage.nextcloud import NextcloudClient
|
from rehearsalhub.storage.nextcloud import NextcloudClient
|
||||||
@@ -14,6 +16,100 @@ from rehearsalhub.storage.nextcloud import NextcloudClient
|
|||||||
router = APIRouter(prefix="/bands", tags=["bands"])
|
router = APIRouter(prefix="/bands", tags=["bands"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{band_id}/invites", response_model=BandInviteList)
|
||||||
|
async def list_invites(
|
||||||
|
band_id: uuid.UUID,
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
current_member: Member = Depends(get_current_member),
|
||||||
|
):
|
||||||
|
"""List all pending invites for a band (admin only)"""
|
||||||
|
repo = BandRepository(session)
|
||||||
|
|
||||||
|
# Check if user is admin of this band
|
||||||
|
role = await repo.get_member_role(band_id, current_member.id)
|
||||||
|
if role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Admin role required to manage invites"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all invites for this band (filter by band_id)
|
||||||
|
invites = await repo.get_invites_for_band(band_id)
|
||||||
|
|
||||||
|
# Filter for non-expired invites (optional - could also show expired)
|
||||||
|
now = datetime.now()
|
||||||
|
pending_invites = [
|
||||||
|
invite for invite in invites
|
||||||
|
if invite.expires_at > now and invite.used_at is None
|
||||||
|
]
|
||||||
|
total = len(invites)
|
||||||
|
pending_count = len(pending_invites)
|
||||||
|
|
||||||
|
# Convert to response model
|
||||||
|
invite_items = [
|
||||||
|
BandInviteListItem(
|
||||||
|
id=invite.id,
|
||||||
|
band_id=invite.band_id,
|
||||||
|
token=invite.token,
|
||||||
|
role=invite.role,
|
||||||
|
expires_at=invite.expires_at,
|
||||||
|
created_at=invite.created_at,
|
||||||
|
is_used=invite.used_at is not None,
|
||||||
|
used_at=invite.used_at,
|
||||||
|
)
|
||||||
|
for invite in invites
|
||||||
|
]
|
||||||
|
|
||||||
|
return BandInviteList(
|
||||||
|
invites=invite_items,
|
||||||
|
total=total,
|
||||||
|
pending=pending_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/invites/{invite_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def revoke_invite(
|
||||||
|
invite_id: uuid.UUID,
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
current_member: Member = Depends(get_current_member),
|
||||||
|
):
|
||||||
|
"""Revoke a pending invite (admin only)"""
|
||||||
|
repo = BandRepository(session)
|
||||||
|
|
||||||
|
# Get the invite
|
||||||
|
invite = await repo.get_invite_by_id(invite_id)
|
||||||
|
if invite is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Invite not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user is admin of the band
|
||||||
|
role = await repo.get_member_role(invite.band_id, current_member.id)
|
||||||
|
if role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Admin role required to revoke invites"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if invite is still pending (not used and not expired)
|
||||||
|
now = datetime.now()
|
||||||
|
if invite.used_at is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Cannot revoke an already used invite"
|
||||||
|
)
|
||||||
|
if invite.expires_at < now:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Cannot revoke an expired invite"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Revoke the invite
|
||||||
|
invite.used_at = now
|
||||||
|
await repo.session.flush()
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[BandRead])
|
@router.get("", response_model=list[BandRead])
|
||||||
async def list_bands(
|
async def list_bands(
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
|
|||||||
64
api/src/rehearsalhub/routers/invites.py
Normal file
64
api/src/rehearsalhub/routers/invites.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
Invite management endpoints.
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from rehearsalhub.db.engine import get_session
|
||||||
|
from rehearsalhub.db.models import BandInvite, Member
|
||||||
|
from rehearsalhub.schemas.invite import InviteInfoRead
|
||||||
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/invites", tags=["invites"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{token}/info", response_model=InviteInfoRead)
|
||||||
|
async def get_invite_info(
|
||||||
|
token: str,
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""Get invite details without accepting (public endpoint)"""
|
||||||
|
repo = BandRepository(session)
|
||||||
|
|
||||||
|
# Get the invite by token
|
||||||
|
invite = await repo.get_invite_by_token(token)
|
||||||
|
if invite is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Invite not found or already used/expired"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if invite is already used or expired
|
||||||
|
now = datetime.now()
|
||||||
|
if invite.used_at is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="This invite has already been used"
|
||||||
|
)
|
||||||
|
if invite.expires_at < now:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="This invite has expired"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get band info
|
||||||
|
band = await repo.get_band_with_members(invite.band_id)
|
||||||
|
if band is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Band associated with invite not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return InviteInfoRead(
|
||||||
|
id=invite.id,
|
||||||
|
band_id=invite.band_id,
|
||||||
|
band_name=band.name,
|
||||||
|
band_slug=band.slug,
|
||||||
|
role=invite.role,
|
||||||
|
expires_at=invite.expires_at,
|
||||||
|
created_at=invite.created_at,
|
||||||
|
is_used=False,
|
||||||
|
)
|
||||||
@@ -17,6 +17,44 @@ class BandInviteRead(BaseModel):
|
|||||||
used_at: datetime | None = None
|
used_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class BandInviteListItem(BaseModel):
|
||||||
|
"""Invite for listing (includes creator info)"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
band_id: uuid.UUID
|
||||||
|
token: str
|
||||||
|
role: str
|
||||||
|
expires_at: datetime
|
||||||
|
created_at: datetime
|
||||||
|
is_used: bool
|
||||||
|
used_at: datetime | None = None
|
||||||
|
# Creator info (optional, can be expanded)
|
||||||
|
|
||||||
|
|
||||||
|
class BandInviteList(BaseModel):
|
||||||
|
"""Response for listing invites"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
invites: list[BandInviteListItem]
|
||||||
|
total: int
|
||||||
|
pending: int
|
||||||
|
|
||||||
|
|
||||||
|
class InviteInfoRead(BaseModel):
|
||||||
|
"""Public invite info (used for /invites/{token}/info)"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
band_id: uuid.UUID
|
||||||
|
band_name: str
|
||||||
|
band_slug: str
|
||||||
|
role: str
|
||||||
|
expires_at: datetime
|
||||||
|
created_at: datetime
|
||||||
|
is_used: bool
|
||||||
|
|
||||||
|
|
||||||
class BandMemberRead(BaseModel):
|
class BandMemberRead(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|||||||
209
api/tests/integration/test_api_invites.py
Normal file
209
api/tests/integration/test_api_invites.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
"""Integration tests for invite endpoints."""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.factories import create_band, create_member
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def band_with_admin(db_session):
|
||||||
|
"""Create a band with an admin member."""
|
||||||
|
admin = create_member(db_session, email="admin@test.com")
|
||||||
|
band = create_band(db_session, creator_id=admin.id)
|
||||||
|
db_session.commit()
|
||||||
|
return {"band": band, "admin": admin}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def band_with_members(db_session):
|
||||||
|
"""Create a band with admin and regular member."""
|
||||||
|
admin = create_member(db_session, email="admin@test.com")
|
||||||
|
member = create_member(db_session, email="member@test.com")
|
||||||
|
band = create_band(db_session, creator_id=admin.id)
|
||||||
|
# Add member to band
|
||||||
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
|
repo = BandRepository(db_session)
|
||||||
|
member_role = await repo.add_member(band.id, member.id)
|
||||||
|
db_session.commit()
|
||||||
|
return {"band": band, "admin": admin, "member": member}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_list_invites_admin_can_see(client, db_session, auth_headers_for, band_with_admin):
|
||||||
|
"""Test that admin can list invites for their band."""
|
||||||
|
headers = await auth_headers_for(band_with_admin["admin"])
|
||||||
|
band = band_with_admin["band"]
|
||||||
|
|
||||||
|
# Create some invites
|
||||||
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
|
repo = BandRepository(db_session)
|
||||||
|
invite1 = await repo.create_invite(band.id, band_with_admin["admin"].id)
|
||||||
|
invite2 = await repo.create_invite(band.id, band_with_admin["admin"].id)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/v1/bands/{band.id}/invites", headers=headers)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
data = resp.json()
|
||||||
|
assert "invites" in data
|
||||||
|
assert "total" in data
|
||||||
|
assert "pending" in data
|
||||||
|
assert data["total"] >= 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_list_invites_non_admin_returns_403(client, db_session, auth_headers_for, band_with_members):
|
||||||
|
"""Test that non-admin cannot list invites."""
|
||||||
|
headers = await auth_headers_for(band_with_members["member"])
|
||||||
|
band = band_with_members["band"]
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/v1/bands/{band.id}/invites", headers=headers)
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_list_invites_no_invites_returns_empty(client, db_session, auth_headers_for, band_with_admin):
|
||||||
|
"""Test listing invites when none exist."""
|
||||||
|
headers = await auth_headers_for(band_with_admin["admin"])
|
||||||
|
band = band_with_admin["band"]
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/v1/bands/{band.id}/invites", headers=headers)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
data = resp.json()
|
||||||
|
assert data["invites"] == []
|
||||||
|
assert data["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_list_invites_includes_pending_and_used(client, db_session, auth_headers_for, band_with_admin):
|
||||||
|
"""Test that list includes both pending and used invites."""
|
||||||
|
headers = await auth_headers_for(band_with_admin["admin"])
|
||||||
|
band = band_with_admin["band"]
|
||||||
|
|
||||||
|
# Create invites with different statuses
|
||||||
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
|
repo = BandRepository(db_session)
|
||||||
|
|
||||||
|
# Create pending invite
|
||||||
|
pending_invite = await repo.create_invite(band.id, band_with_admin["admin"].id)
|
||||||
|
|
||||||
|
# Create used invite (simulate by setting used_at)
|
||||||
|
used_invite = await repo.create_invite(band.id, band_with_admin["admin"].id)
|
||||||
|
used_invite.used_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/v1/bands/{band.id}/invites", headers=headers)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
# Check we have both invites
|
||||||
|
assert data["total"] >= 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_revoke_invite_admin_can_revoke(client, db_session, auth_headers_for, band_with_admin):
|
||||||
|
"""Test that admin can revoke an invite."""
|
||||||
|
headers = await auth_headers_for(band_with_admin["admin"])
|
||||||
|
band = band_with_admin["band"]
|
||||||
|
|
||||||
|
# Create an invite
|
||||||
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
|
repo = BandRepository(db_session)
|
||||||
|
invite = await repo.create_invite(band.id, band_with_admin["admin"].id)
|
||||||
|
invite_id = invite.id
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
resp = await client.delete(f"/api/v1/invites/{invite_id}", headers=headers)
|
||||||
|
assert resp.status_code == 204, resp.text
|
||||||
|
|
||||||
|
# Verify invite was revoked
|
||||||
|
resp = await client.delete(f"/api/v1/invites/{invite_id}", headers=headers)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_revoke_invite_non_admin_returns_403(client, db_session, auth_headers_for, band_with_members):
|
||||||
|
"""Test that non-admin cannot revoke invites."""
|
||||||
|
headers = await auth_headers_for(band_with_members["member"])
|
||||||
|
band = band_with_members["band"]
|
||||||
|
|
||||||
|
# Create an invite
|
||||||
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
|
repo = BandRepository(db_session)
|
||||||
|
invite = await repo.create_invite(band.id, band_with_members["admin"].id)
|
||||||
|
invite_id = invite.id
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
resp = await client.delete(f"/api/v1/invites/{invite_id}", headers=headers)
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_revoke_invite_not_found_returns_404(client, db_session, auth_headers_for, band_with_admin):
|
||||||
|
"""Test revoking a non-existent invite."""
|
||||||
|
headers = await auth_headers_for(band_with_admin["admin"])
|
||||||
|
|
||||||
|
resp = await client.delete("/api/v1/invites/00000000-0000-0000-0000-000000000000", headers=headers)
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_get_invite_info_valid_token(client, db_session):
|
||||||
|
"""Test getting invite info with valid token."""
|
||||||
|
admin = create_member(db_session, email="admin@test.com")
|
||||||
|
band = create_band(db_session, creator_id=admin.id)
|
||||||
|
|
||||||
|
# Create an invite
|
||||||
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
|
repo = BandRepository(db_session)
|
||||||
|
invite = await repo.create_invite(band.id, admin.id)
|
||||||
|
token = invite.token
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/v1/invites/{token}/info")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
data = resp.json()
|
||||||
|
assert data["band_id"] == str(band.id)
|
||||||
|
assert data["role"] == "member"
|
||||||
|
assert data["is_used"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_get_invite_info_invalid_token(client):
|
||||||
|
"""Test getting invite info with invalid token."""
|
||||||
|
resp = await client.get("/api/v1/invites/invalid-token/info")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_get_invite_info_expired_invite(client, db_session):
|
||||||
|
"""Test getting invite info for expired invite."""
|
||||||
|
admin = create_member(db_session, email="admin@test.com")
|
||||||
|
band = create_band(db_session, creator_id=admin.id)
|
||||||
|
|
||||||
|
# Create an invite with very short expiry
|
||||||
|
from rehearsalhub.repositories.band import BandRepository
|
||||||
|
repo = BandRepository(db_session)
|
||||||
|
invite = await repo.create_invite(band.id, admin.id, ttl_hours=0)
|
||||||
|
token = invite.token
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Wait a bit for expiry
|
||||||
|
import asyncio
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/v1/invites/{token}/info")
|
||||||
|
assert resp.status_code == 400
|
||||||
Reference in New Issue
Block a user