import pytest from nanoid import generate as generate_nanoid from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src import crud, models, schemas from src.exceptions import ConflictException, ResourceNotFoundException class TestWorkspaceCRUD: """Test suite for workspace CRUD operations""" @pytest.mark.asyncio async def test_delete_workspace_not_found(self, db_session: AsyncSession): """Test delete_workspace with non-existent workspace raises ResourceNotFoundException""" with pytest.raises(ResourceNotFoundException): await crud.delete_workspace(db_session, "nonexistent_workspace") @pytest.mark.asyncio async def test_delete_workspace_cascade_peers( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that deleting a workspace cascades to delete peers""" test_workspace, _test_peer = sample_data # Create additional peer peer2 = models.Peer( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add(peer2) await db_session.flush() # Verify peers exist stmt = select(models.Peer).where( models.Peer.workspace_name == test_workspace.name ) result = await db_session.execute(stmt) peers = result.scalars().all() assert len(peers) == 2 # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify peers are deleted result = await db_session.execute(stmt) peers = result.scalars().all() assert len(peers) == 0 @pytest.mark.asyncio async def test_check_no_active_sessions_raises( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that check_no_active_sessions raises ConflictException when active sessions exist""" test_workspace, _test_peer = sample_data # Create sessions session1 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) session2 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add_all([session1, session2]) await db_session.flush() # check_no_active_sessions should raise ConflictException with pytest.raises(ConflictException, match="active session"): await crud.check_no_active_sessions(db_session, test_workspace.name) @pytest.mark.asyncio async def test_delete_workspace_succeeds_with_active_sessions( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that delete_workspace cascade-deletes active sessions (no guard in crud)""" test_workspace, _test_peer = sample_data # Create active sessions session1 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) session2 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add_all([session1, session2]) await db_session.flush() # crud.delete_workspace should succeed — the guard lives in the router result = await crud.delete_workspace(db_session, test_workspace.name) assert result.workspace.name == test_workspace.name assert result.sessions_deleted == 2 @pytest.mark.asyncio async def test_delete_workspace_cascade_messages( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that deleting a workspace cascades to delete messages""" test_workspace, test_peer = sample_data # Create session session = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add(session) await db_session.flush() # Create messages message1 = models.Message( content="Test message 1", workspace_name=test_workspace.name, session_name=session.name, peer_name=test_peer.name, seq_in_session=1, ) message2 = models.Message( content="Test message 2", workspace_name=test_workspace.name, session_name=session.name, peer_name=test_peer.name, seq_in_session=2, ) db_session.add_all([message1, message2]) await db_session.flush() # Verify messages exist stmt = select(models.Message).where( models.Message.workspace_name == test_workspace.name ) result = await db_session.execute(stmt) messages = result.scalars().all() assert len(messages) == 2 # Mark session inactive so workspace deletion is allowed session.is_active = False await db_session.flush() # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify messages are deleted result = await db_session.execute(stmt) messages = result.scalars().all() assert len(messages) == 0 @pytest.mark.asyncio async def test_delete_workspace_cascade_collections( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that deleting a workspace cascades to delete collections""" test_workspace, test_peer = sample_data # Create collection collection = models.Collection( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer.name, ) db_session.add(collection) await db_session.flush() # Verify collection exists stmt = select(models.Collection).where( models.Collection.workspace_name == test_workspace.name ) result = await db_session.execute(stmt) collections = result.scalars().all() assert len(collections) == 1 # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify collection is deleted result = await db_session.execute(stmt) collections = result.scalars().all() assert len(collections) == 0 @pytest.mark.asyncio async def test_delete_workspace_cascade_documents( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that deleting a workspace cascades to delete documents""" test_workspace, test_peer = sample_data # Create collection collection = models.Collection( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer.name, ) db_session.add(collection) await db_session.flush() # Create session for document session = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add(session) await db_session.flush() # Create document document = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, observed=test_peer.name, session_name=session.name, content="Test document content", ) db_session.add(document) await db_session.flush() # Verify document exists stmt = select(models.Document).where( models.Document.workspace_name == test_workspace.name ) result = await db_session.execute(stmt) documents = result.scalars().all() assert len(documents) == 1 # Mark session inactive so workspace deletion is allowed session.is_active = False await db_session.flush() # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify document is deleted result = await db_session.execute(stmt) documents = result.scalars().all() assert len(documents) == 0 @pytest.mark.asyncio async def test_delete_workspace_cascade_session_peers( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that deleting a workspace cascades to delete session_peers associations""" test_workspace, test_peer = sample_data # Create session session = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add(session) await db_session.flush() # Add peer to session from src.models import session_peers_table stmt = session_peers_table.insert().values( workspace_name=test_workspace.name, session_name=session.name, peer_name=test_peer.name, ) await db_session.execute(stmt) await db_session.flush() # Verify session_peer association exists stmt = select(session_peers_table).where( session_peers_table.c.workspace_name == test_workspace.name ) result = await db_session.execute(stmt) session_peers = result.all() assert len(session_peers) == 1 # Mark session inactive so workspace deletion is allowed session.is_active = False await db_session.flush() # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify session_peer association is deleted result = await db_session.execute(stmt) session_peers = result.all() assert len(session_peers) == 0 @pytest.mark.asyncio async def test_delete_workspace_cascade_webhooks( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that deleting a workspace cascades to delete webhook endpoints""" test_workspace, _test_peer = sample_data # Create webhook endpoint webhook = models.WebhookEndpoint( workspace_name=test_workspace.name, url="https://example.com/webhook", ) db_session.add(webhook) await db_session.flush() # Verify webhook exists stmt = select(models.WebhookEndpoint).where( models.WebhookEndpoint.workspace_name == test_workspace.name ) result = await db_session.execute(stmt) webhooks = result.scalars().all() assert len(webhooks) == 1 # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify webhook is deleted result = await db_session.execute(stmt) webhooks = result.scalars().all() assert len(webhooks) == 0 @pytest.mark.asyncio async def test_delete_workspace_cascade_queue_items( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that deleting a workspace cascades to delete queue items""" test_workspace, test_peer = sample_data # Create session session = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add(session) await db_session.flush() # Create queue item with work_unit_key containing workspace name # Format: {task_type}:{workspace_name}:{...} queue_item = models.QueueItem( work_unit_key=f"representation:{test_workspace.name}:{session.name}:{test_peer.name}:{test_peer.name}", task_type="representation", payload={"test": "data"}, workspace_name=test_workspace.name, ) db_session.add(queue_item) await db_session.flush() # Verify queue item exists stmt = select(models.QueueItem) result = await db_session.execute(stmt) queue_items = result.scalars().all() assert len(queue_items) == 1 # Mark session inactive so workspace deletion is allowed session.is_active = False await db_session.flush() # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify queue item is deleted result = await db_session.execute(stmt) queue_items = result.scalars().all() assert len(queue_items) == 0 @pytest.mark.asyncio async def test_delete_workspace_cascade_active_queue_sessions( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that deleting a workspace cascades to delete active queue sessions""" test_workspace, test_peer = sample_data # Create session session = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add(session) await db_session.flush() # Create active queue session with work_unit_key containing workspace name # Format: {task_type}:{workspace_name}:{...} active_queue = models.ActiveQueueSession( work_unit_key=f"representation:{test_workspace.name}:{session.name}:{test_peer.name}:{test_peer.name}", ) db_session.add(active_queue) await db_session.flush() # Verify active queue session exists stmt = select(models.ActiveQueueSession) result = await db_session.execute(stmt) active_queues = result.scalars().all() assert len(active_queues) == 1 # Mark session inactive so workspace deletion is allowed session.is_active = False await db_session.flush() # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify active queue session is deleted result = await db_session.execute(stmt) active_queues = result.scalars().all() assert len(active_queues) == 0 @pytest.mark.asyncio async def test_delete_workspace_returns_deleted_workspace( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that delete_workspace returns WorkspaceDeletionResult with cascade counts""" test_workspace, _test_peer = sample_data # Store workspace details before deletion workspace_name = test_workspace.name # Delete workspace result = await crud.delete_workspace(db_session, test_workspace.name) # Verify returned result is WorkspaceDeletionResult with correct workspace assert isinstance(result, crud.WorkspaceDeletionResult) assert result.workspace.name == workspace_name assert isinstance(result.workspace, schemas.Workspace) # Verify cascade counts are present (should have 1 peer from sample_data) assert result.peers_deleted == 1 assert result.sessions_deleted >= 0 assert result.messages_deleted >= 0 assert result.conclusions_deleted >= 0 @pytest.mark.asyncio async def test_delete_workspace_complex_cascade( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test deleting a workspace with multiple related resources of different types""" test_workspace, test_peer = sample_data # Create additional peer peer2 = models.Peer( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add(peer2) # Create sessions session1 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) session2 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add_all([session1, session2]) await db_session.flush() # Create messages message1 = models.Message( content="Test message 1", workspace_name=test_workspace.name, session_name=session1.name, peer_name=test_peer.name, seq_in_session=1, ) message2 = models.Message( content="Test message 2", workspace_name=test_workspace.name, session_name=session2.name, peer_name=peer2.name, seq_in_session=1, ) db_session.add_all([message1, message2]) # Create collection and document collection = models.Collection( workspace_name=test_workspace.name, observer=test_peer.name, observed=peer2.name, ) db_session.add(collection) await db_session.flush() document = models.Document( workspace_name=test_workspace.name, observer=test_peer.name, observed=peer2.name, session_name=session1.name, content="Test document", ) db_session.add(document) # Create webhook webhook = models.WebhookEndpoint( workspace_name=test_workspace.name, url="https://example.com/webhook", ) db_session.add(webhook) await db_session.flush() # Count all resources before deletion peer_stmt = select(models.Peer).where( models.Peer.workspace_name == test_workspace.name ) session_stmt = select(models.Session).where( models.Session.workspace_name == test_workspace.name ) message_stmt = select(models.Message).where( models.Message.workspace_name == test_workspace.name ) collection_stmt = select(models.Collection).where( models.Collection.workspace_name == test_workspace.name ) document_stmt = select(models.Document).where( models.Document.workspace_name == test_workspace.name ) webhook_stmt = select(models.WebhookEndpoint).where( models.WebhookEndpoint.workspace_name == test_workspace.name ) # Verify all resources exist assert len((await db_session.execute(peer_stmt)).scalars().all()) == 2 assert len((await db_session.execute(session_stmt)).scalars().all()) == 2 assert len((await db_session.execute(message_stmt)).scalars().all()) == 2 assert len((await db_session.execute(collection_stmt)).scalars().all()) == 1 assert len((await db_session.execute(document_stmt)).scalars().all()) == 1 assert len((await db_session.execute(webhook_stmt)).scalars().all()) == 1 # Mark sessions inactive so workspace deletion is allowed session1.is_active = False session2.is_active = False await db_session.flush() # Delete workspace await crud.delete_workspace(db_session, test_workspace.name) # Verify all related resources are deleted assert len((await db_session.execute(peer_stmt)).scalars().all()) == 0 assert len((await db_session.execute(session_stmt)).scalars().all()) == 0 assert len((await db_session.execute(message_stmt)).scalars().all()) == 0 assert len((await db_session.execute(collection_stmt)).scalars().all()) == 0 assert len((await db_session.execute(document_stmt)).scalars().all()) == 0 assert len((await db_session.execute(webhook_stmt)).scalars().all()) == 0 @pytest.mark.asyncio async def test_delete_workspace_allows_inactive_sessions( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that workspace deletion succeeds when all sessions are inactive""" test_workspace, _test_peer = sample_data # Create sessions and mark them inactive session1 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name, is_active=False, ) session2 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name, is_active=False, ) db_session.add_all([session1, session2]) await db_session.flush() # Delete workspace should succeed result = await crud.delete_workspace(db_session, test_workspace.name) assert result.workspace.name == test_workspace.name # Verify sessions are cleaned up stmt = select(models.Session).where( models.Session.workspace_name == test_workspace.name ) remaining = await db_session.execute(stmt) assert len(remaining.scalars().all()) == 0 @pytest.mark.asyncio async def test_check_no_active_sessions_passes_after_deletion( self, db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer], ): """Test that check_no_active_sessions passes after sessions are deleted""" test_workspace, _test_peer = sample_data # Create active sessions session1 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) session2 = models.Session( name=str(generate_nanoid()), workspace_name=test_workspace.name ) db_session.add_all([session1, session2]) await db_session.flush() # check_no_active_sessions should fail with active sessions with pytest.raises(ConflictException): await crud.check_no_active_sessions(db_session, test_workspace.name) # Delete the sessions await db_session.delete(session1) await db_session.delete(session2) await db_session.flush() # Now check_no_active_sessions should pass (no exception) await crud.check_no_active_sessions(db_session, test_workspace.name) # And workspace deletion should succeed result = await crud.delete_workspace(db_session, test_workspace.name) assert result.workspace.name == test_workspace.name