diff --git a/backends/advanced/src/advanced_omi_backend/controllers/user_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/user_controller.py index dd00f8a9..dfb4b752 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/user_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/user_controller.py @@ -17,7 +17,7 @@ from advanced_omi_backend.client_manager import get_user_clients_all from advanced_omi_backend.database import chunks_col, db, users_col from advanced_omi_backend.memory import get_memory_service -from advanced_omi_backend.users import User, UserCreate +from advanced_omi_backend.users import User, UserCreate, UserUpdate logger = logging.getLogger(__name__) @@ -78,8 +78,9 @@ async def create_user(user_data: UserCreate): ) -async def update_user(user_id: str, user_data: UserCreate): +async def update_user(user_id: str, user_data: UserUpdate): """Update an existing user.""" + print("DEBUG: New update_user function is being called!") try: # Validate ObjectId format try: @@ -106,23 +107,9 @@ async def update_user(user_id: str, user_data: UserCreate): # Convert to User object for the manager user_obj = User(**existing_user) - - # Prepare update data - only include non-None fields - update_data = {} - if user_data.email: - update_data["email"] = user_data.email - if user_data.display_name is not None: - update_data["display_name"] = user_data.display_name - if hasattr(user_data, 'is_superuser'): - update_data["is_superuser"] = user_data.is_superuser - if hasattr(user_data, 'is_active'): - update_data["is_active"] = user_data.is_active - if user_data.password: - # Hash the password if provided - update_data["hashed_password"] = user_manager.password_helper.hash(user_data.password) - - # Update the user - updated_user = await user_manager.update(user_obj, update_data) + + # Update the user using the fastapi-users manager (now with fix for missing method) + updated_user = await user_manager.update(user_obj, user_data) return JSONResponse( status_code=200, diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/user_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/user_routes.py index 8cf70117..808b8185 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/user_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/user_routes.py @@ -10,7 +10,7 @@ from advanced_omi_backend.auth import current_superuser from advanced_omi_backend.controllers import user_controller -from advanced_omi_backend.users import User, UserCreate +from advanced_omi_backend.users import User, UserCreate, UserUpdate logger = logging.getLogger(__name__) @@ -30,7 +30,7 @@ async def create_user(user_data: UserCreate, current_user: User = Depends(curren @router.put("/{user_id}") -async def update_user(user_id: str, user_data: UserCreate, current_user: User = Depends(current_superuser)): +async def update_user(user_id: str, user_data: UserUpdate, current_user: User = Depends(current_superuser)): """Update a user. Admin only.""" return await user_controller.update_user(user_id, user_data) diff --git a/backends/advanced/src/advanced_omi_backend/users.py b/backends/advanced/src/advanced_omi_backend/users.py index b016c68c..b249db6b 100644 --- a/backends/advanced/src/advanced_omi_backend/users.py +++ b/backends/advanced/src/advanced_omi_backend/users.py @@ -16,6 +16,7 @@ class UserCreate(BaseUserCreate): """Schema for creating new users.""" display_name: Optional[str] = None + is_superuser: Optional[bool] = False class UserRead(BaseUser[PydanticObjectId]): @@ -28,8 +29,26 @@ class UserRead(BaseUser[PydanticObjectId]): class UserUpdate(BaseUserUpdate): """Schema for updating user data.""" - + display_name: Optional[str] = None + is_superuser: Optional[bool] = None + + def create_update_dict_superuser(self): + """Create update dictionary for superuser operations.""" + update_dict = {} + if self.email is not None: + update_dict["email"] = self.email + if self.password is not None: + update_dict["password"] = self.password + if self.is_active is not None: + update_dict["is_active"] = self.is_active + if self.is_verified is not None: + update_dict["is_verified"] = self.is_verified + if self.is_superuser is not None: + update_dict["is_superuser"] = self.is_superuser + if self.display_name is not None: + update_dict["display_name"] = self.display_name + return update_dict class User(BeanieBaseUser, Document): diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..c8104a52 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,230 @@ +# Friend-Lite API Tests + +Comprehensive Robot Framework test suite for the Friend-Lite advanced backend API endpoints. + +## Test Structure + +### Test Files +- **`auth_tests.robot`** - Authentication and user management tests +- **`memory_tests.robot`** - Memory management and search tests +- **`conversation_tests.robot`** - Conversation management and versioning tests +- **`health_tests.robot`** - Health check and status endpoint tests +- **`chat_tests.robot`** - Chat service and session management tests +- **`client_queue_tests.robot`** - Client management and queue monitoring tests +- **`system_admin_tests.robot`** - System administration and configuration tests +- **`all_api_tests.robot`** - Master test suite runner + +### Resource Files +- **`resources/auth_keywords.robot`** - Authentication helper keywords +- **`resources/memory_keywords.robot`** - Memory management keywords +- **`resources/conversation_keywords.robot`** - Conversation management keywords +- **`resources/chat_keywords.robot`** - Chat service keywords +- **`resources/setup_resources.robot`** - Basic setup and health check keywords +- **`resources/login_resources.robot`** - Login-specific utilities + +### Configuration +- **`test_env.py`** - Environment configuration and test data +- **`.env`** - Environment variables (create from template) + +## Running Tests + +### Prerequisites +1. Friend-Lite backend running at `http://localhost:8001` (or set `API_URL` in `.env`) +2. Admin user credentials configured in `.env` +3. Robot Framework and RequestsLibrary installed + +### Environment Setup +```bash +# Copy environment template +cp .env.template .env + +# Edit .env with your configuration +API_URL=http://localhost:8001 +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your-secure-admin-password +``` + +### Running Individual Test Suites +```bash +# Authentication and user tests +robot auth_tests.robot + +# Memory management tests +robot memory_tests.robot + +# Conversation management tests +robot conversation_tests.robot + +# Health and status tests +robot health_tests.robot + +# Chat service tests +robot chat_tests.robot + +# Client and queue tests +robot client_queue_tests.robot + +# System administration tests +robot system_admin_tests.robot +``` + +### Running All Tests +```bash +# Run complete test suite +robot *.robot + +# Run with specific tags +robot --include auth *.robot +robot --include positive *.robot +robot --include admin *.robot +``` + +### Test Output +```bash +# Custom output directory +robot --outputdir results *.robot + +# Verbose logging +robot --loglevel DEBUG *.robot + +# Parallel execution +pabot --processes 4 *.robot +``` + +## Test Coverage + +### Authentication & Users (`/api/users`, `/auth`) +- ✅ Login with valid/invalid credentials +- ✅ Get current user information +- ✅ Create/update/delete users (admin only) +- ✅ User authorization and access control +- ✅ Admin privilege enforcement + +### Memory Management (`/api/memories`) +- ✅ Get user memories with pagination +- ✅ Search memories with similarity thresholds +- ✅ Get memories with transcripts +- ✅ Delete specific memories +- ✅ Admin memory access across users +- ✅ Unfiltered memory access for debugging + +### Conversation Management (`/api/conversations`) +- ✅ List and retrieve conversations +- ✅ Conversation version history +- ✅ Transcript reprocessing +- ✅ Memory reprocessing with version selection +- ✅ Version activation (transcript/memory) +- ✅ Conversation deletion and cleanup +- ✅ User data isolation + +### Health & Status (`/health`, `/readiness`) +- ✅ Main health check with service details +- ✅ Readiness check for orchestration +- ✅ Authentication service health +- ✅ Queue system health status +- ✅ Chat service health check +- ✅ System metrics (admin only) + +### Chat Service (`/api/chat`) +- ✅ Session creation and management +- ✅ Session title updates +- ✅ Message retrieval +- ✅ Chat statistics +- ✅ Memory extraction from sessions +- ✅ Session deletion and cleanup + +### Client & Queue Management +- ✅ Active client monitoring +- ✅ Queue job listing with pagination +- ✅ Queue statistics and health +- ✅ User job isolation +- ✅ Processing task monitoring (admin only) + +### System Administration +- ✅ Authentication configuration +- ✅ Diarization settings management +- ✅ Speaker configuration +- ✅ Memory configuration (YAML) +- ✅ Configuration validation and reload +- ✅ Bulk memory deletion + +## Test Categories + +### By Access Level +- **Public**: Health checks, auth config +- **User**: Memories, conversations, chat sessions +- **Admin**: User management, system config, metrics + +### By Test Type +- **Positive**: Valid operations and expected responses +- **Negative**: Invalid inputs, unauthorized access +- **Security**: Authentication, authorization, data isolation +- **Integration**: Cross-service functionality + +### By Component +- **Auth**: Authentication and authorization +- **Memory**: Memory storage and retrieval +- **Conversation**: Audio processing and transcription +- **Chat**: Interactive chat functionality +- **System**: Configuration and administration + +## Key Features Tested + +### Security +- JWT token authentication +- Role-based access control (admin vs user) +- Data isolation between users +- Unauthorized access prevention + +### Data Management +- CRUD operations for all entities +- Pagination and filtering +- Search functionality with thresholds +- Versioning and history tracking + +### System Integration +- Service health monitoring +- Configuration management +- Queue system monitoring +- Cross-service communication + +### Error Handling +- Invalid input validation +- Non-existent resource handling +- Permission denied scenarios +- Service unavailability graceful degradation + +## Maintenance + +### Adding New Tests +1. Create test file or add to existing suite +2. Use appropriate resource keywords +3. Follow naming conventions (`Test Name Test`) +4. Include proper tags and documentation +5. Add cleanup in teardown if needed + +### Updating Keywords +1. Modify resource files for reusable functionality +2. Keep keywords focused and single-purpose +3. Use proper argument handling +4. Include documentation strings + +### Environment Variables +Update `test_env.py` when adding new configuration options or test data. + +## Troubleshooting + +### Common Issues +- **401 Unauthorized**: Check admin credentials in `.env` +- **Connection Refused**: Ensure backend is running +- **Test Failures**: Check service health endpoints first +- **Timeout Errors**: Increase timeouts in test configuration + +### Debug Mode +```bash +# Run with detailed logging +robot --loglevel TRACE auth_tests.robot + +# Stop on first failure +robot --exitonfailure *.robot +``` \ No newline at end of file diff --git a/tests/all_api_tests.robot b/tests/all_api_tests.robot new file mode 100644 index 00000000..2e775fdc --- /dev/null +++ b/tests/all_api_tests.robot @@ -0,0 +1,58 @@ +*** Settings *** +Documentation Master Test Suite for All Friend-Lite API Endpoints +Suite Setup Master Suite Setup +Suite Teardown Master Suite Teardown + +*** Test Cases *** + +Run Auth Tests + [Documentation] Execute authentication and user management tests + [Tags] auth users suite + Run Tests auth_tests.robot + +Run Memory Tests + [Documentation] Execute memory management tests + [Tags] memory suite + Run Tests memory_tests.robot + +Run Conversation Tests + [Documentation] Execute conversation management tests + [Tags] conversation suite + Run Tests conversation_tests.robot + +Run Health Tests + [Documentation] Execute health and status tests + [Tags] health status suite + Run Tests health_tests.robot + +Run Chat Tests + [Documentation] Execute chat service tests + [Tags] chat suite + Run Tests chat_tests.robot + +Run Client Queue Tests + [Documentation] Execute client and queue management tests + [Tags] client queue suite + Run Tests client_queue_tests.robot + +Run System Admin Tests + [Documentation] Execute system and admin tests + [Tags] system admin suite + Run Tests system_admin_tests.robot + +*** Keywords *** + +Master Suite Setup + [Documentation] Setup for the entire test suite + Log Starting Friend-Lite API Test Suite + Log Testing against: ${API_URL} + +Master Suite Teardown + [Documentation] Cleanup for the entire test suite + Log Friend-Lite API Test Suite completed + +Run Tests + [Documentation] Run a specific test file + [Arguments] ${test_file} + Log Executing: ${test_file} + # Note: This is a placeholder - actual test execution handled by robot command \ No newline at end of file diff --git a/tests/auth_tests.robot b/tests/auth_tests.robot new file mode 100644 index 00000000..9d76c60d --- /dev/null +++ b/tests/auth_tests.robot @@ -0,0 +1,175 @@ +*** Settings *** +Documentation Authentication and User Management API Tests +Library RequestsLibrary +Library Collections +Library String +Library BuiltIn +Resource resources/setup_resources.robot +Resource resources/auth_keywords.robot +Suite Setup Suite Setup +Suite Teardown Delete All Sessions + +*** Variables *** +# Test users are now imported from test_env.py via resource files + +*** Test Cases *** + +Login With Valid Credentials Test + [Documentation] Test successful login with admin credentials + [Tags] auth login positive + Setup Auth Session + + ${token}= Get Admin Token + Should Not Be Empty ${token} + Log Successfully obtained admin token + + # Verify token works + ${user}= Verify Admin User ${token} + Should Be Equal ${user}[email] ${ADMIN_EMAIL} + +Login With Invalid Credentials Test + [Documentation] Test login failure with invalid credentials + [Tags] auth login negative + Setup Auth Session + + &{auth_data}= Create Dictionary username=${ADMIN_EMAIL} password=wrong-password + &{headers}= Create Dictionary Content-Type=application/x-www-form-urlencoded + + ${response}= POST On Session api /auth/jwt/login data=${auth_data} headers=${headers} expected_status=400 + Should Be Equal As Integers ${response.status_code} 400 + +Get Current User Test + [Documentation] Test getting current authenticated user + [Tags] auth user positive + Setup Auth Session + + ${token}= Get Admin Token + ${user}= Verify Admin User ${token} + + Dictionary Should Contain Key ${user} email + Dictionary Should Contain Key ${user} id + Should Be Equal ${user}[email] ${ADMIN_EMAIL} + +Unauthorized Access Test + [Documentation] Test that endpoints require authentication + [Tags] auth security negative + Setup Auth Session + + # Try to access protected endpoint without token + ${response}= GET On Session api /users/me expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + +Create User Test + [Documentation] Test creating a new user (admin only) + [Tags] users admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + ${test_email}= Set Variable test-user-${RANDOM_ID}@example.com + ${user}= Create Test User ${admin_token} ${test_email} ${TEST_USER_PASSWORD} + + Should Be Equal ${user}[user_email] ${test_email} + Should Contain ${user}[message] created successfully + + # Cleanup + Delete Test User ${admin_token} ${user}[user_id] + +Create Admin User Test + [Documentation] Test creating an admin user + [Tags] users admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + ${test_admin_email}= Set Variable test-admin-${RANDOM_ID}@example.com + ${user}= Create Test User ${admin_token} ${test_admin_email} ${TEST_USER_PASSWORD} ${True} + + Should Be Equal ${user}[user_email] ${test_admin_email} + Should Contain ${user}[message] created successfully + + # Cleanup + Delete Test User ${admin_token} ${user}[user_id] + +Get All Users Test + [Documentation] Test getting all users (admin only) + [Tags] users admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + ${response}= Make Authenticated Request GET /api/users ${admin_token} + + Should Be Equal As Integers ${response.status_code} 200 + Should Be True isinstance($response.json(), list) + + # Should contain at least the admin user + ${users}= Set Variable ${response.json()} + ${admin_found}= Set Variable ${False} + FOR ${user} IN @{users} + IF '${user}[email]' == '${ADMIN_EMAIL}' + ${admin_found}= Set Variable ${True} + END + END + Should Be True ${admin_found} + +Non-Admin User Cannot Create Users Test + [Documentation] Test that non-admin users cannot create users + [Tags] users security negative + Setup Auth Session + + ${admin_token}= Get Admin Token + + # Create a non-admin user + ${test_email}= Set Variable test-user-${RANDOM_ID}@example.com + ${test_user}= Create Test User ${admin_token} ${test_email} ${TEST_USER}[password] + ${user_token}= Get User Token ${test_email} ${TEST_USER_PASSWORD} + + # Try to create another user with non-admin token + &{headers}= Create Dictionary Authorization=Bearer ${user_token} Content-Type=application/json + &{user_data}= Create Dictionary email=another-user@example.com password=password123 + + ${response}= POST On Session api /api/users json=${user_data} headers=${headers} expected_status=403 + Should Be Equal As Integers ${response.status_code} 403 + + # Cleanup + Delete Test User ${admin_token} ${test_user}[user_id] + +Update User Test + [Documentation] Test updating a user (admin only) + [Tags] users admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + ${test_email}= Set Variable test-user-${RANDOM_ID}@example.com + ${user}= Create Test User ${admin_token} ${test_email} ${TEST_USER_PASSWORD} + + # Update user to admin + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} Content-Type=application/json + &{update_data}= Create Dictionary email=${test_email} password=${TEST_USER_PASSWORD} is_superuser=${True} + + ${response}= PUT On Session api /api/users/${user}[user_id] json=${update_data} headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${updated_user}= Set Variable ${response.json()} + Should Be True ${updated_user}[is_superuser] + + # Cleanup + Delete Test User ${admin_token} ${user}[user_id] + +Delete User Test + [Documentation] Test deleting a user (admin only) + [Tags] users admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + ${test_email}= Set Variable test-user-${RANDOM_ID}@example.com + ${user}= Create Test User ${admin_token} ${test_email} ${TEST_USER_PASSWORD} + + # Delete the user + Delete Test User ${admin_token} ${user}[user_id] + + # Verify user is deleted by trying to login + &{auth_data}= Create Dictionary username=${test_email} password=${TEST_USER_PASSWORD} + &{headers}= Create Dictionary Content-Type=application/x-www-form-urlencoded + + ${response}= POST On Session api /auth/jwt/login data=${auth_data} headers=${headers} expected_status=400 + Should Be Equal As Integers ${response.status_code} 400 + diff --git a/tests/chat_tests.robot b/tests/chat_tests.robot new file mode 100644 index 00000000..96e72855 --- /dev/null +++ b/tests/chat_tests.robot @@ -0,0 +1,288 @@ +*** Settings *** +Documentation Chat Service API Tests +Library RequestsLibrary +Library Collections +Library String +Resource resources/setup_resources.robot +Resource resources/auth_keywords.robot +Resource resources/chat_keywords.robot +Suite Setup Suite Setup +Suite Teardown Delete All Sessions + +*** Test Cases *** + +Create Chat Session Test + [Documentation] Test creating a new chat session + [Tags] chat session create positive + Setup Auth Session + + ${token}= Get Admin Token + ${session}= Create Test Chat Session ${token} + + # Verify chat session structure + Dictionary Should Contain Key ${session} session_id + Dictionary Should Contain Key ${session} title + Dictionary Should Contain Key ${session} created_at + Dictionary Should Contain Key ${session} updated_at + Should Contain ${session}[title] Test Session + + # Cleanup + Cleanup Test Chat Session ${token} ${session}[session_id] + +Create Chat Session With Custom Title Test + [Documentation] Test creating chat session with custom title + [Tags] chat session create positive + Setup Auth Session + + ${token}= Get Admin Token + ${custom_title}= Set Variable Custom Chat Title ${RANDOM_ID} + ${response}= Create Chat Session ${token} ${custom_title} + + Should Be Equal As Integers ${response.status_code} 200 + ${session}= Set Variable ${response.json()} + Should Be Equal ${session}[title] ${custom_title} + + # Cleanup + Cleanup Test Chat Session ${token} ${session}[session_id] + +Get Chat Sessions Test + [Documentation] Test getting all chat sessions for user + [Tags] chat session list positive + Setup Auth Session + + ${token}= Get Admin Token + + # Create a test session first + ${test_session}= Create Test Chat Session ${token} + + # Get all sessions + ${response}= Get Chat Sessions ${token} + Should Be Equal As Integers ${response.status_code} 200 + + ${sessions}= Set Variable ${response.json()} + Should Be True isinstance($sessions, list) + + # Should contain our test session + ${found}= Set Variable ${False} + FOR ${session} IN @{sessions} + # Verify chat session structure + Dictionary Should Contain Key ${session} session_id + Dictionary Should Contain Key ${session} title + Dictionary Should Contain Key ${session} created_at + Dictionary Should Contain Key ${session} updated_at + IF '${session}[session_id]' == '${test_session}[session_id]' + ${found}= Set Variable ${True} + END + END + Should Be True ${found} + + # Cleanup + Cleanup Test Chat Session ${token} ${test_session}[session_id] + +Get Specific Chat Session Test + [Documentation] Test getting a specific chat session + [Tags] chat session individual positive + Setup Auth Session + + ${token}= Get Admin Token + ${test_session}= Create Test Chat Session ${token} + + ${response}= Get Chat Session ${token} ${test_session}[session_id] + Should Be Equal As Integers ${response.status_code} 200 + + ${session}= Set Variable ${response.json()} + # Verify chat session structure + Dictionary Should Contain Key ${session} session_id + Dictionary Should Contain Key ${session} title + Dictionary Should Contain Key ${session} created_at + Dictionary Should Contain Key ${session} updated_at + Should Be Equal ${session}[session_id] ${test_session}[session_id] + + # Cleanup + Cleanup Test Chat Session ${token} ${test_session}[session_id] + +Update Chat Session Test + [Documentation] Test updating a chat session title + [Tags] chat session update positive + Setup Auth Session + + ${token}= Get Admin Token + ${test_session}= Create Test Chat Session ${token} + + ${new_title}= Set Variable Updated Title ${RANDOM_ID} + ${response}= Update Chat Session ${token} ${test_session}[session_id] ${new_title} + + Should Be Equal As Integers ${response.status_code} 200 + ${updated_session}= Set Variable ${response.json()} + Should Be Equal ${updated_session}[title] ${new_title} + + # Cleanup + Cleanup Test Chat Session ${token} ${test_session}[session_id] + +Delete Chat Session Test + [Documentation] Test deleting a chat session + [Tags] chat session delete positive + Setup Auth Session + + ${token}= Get Admin Token + ${test_session}= Create Test Chat Session ${token} + + ${response}= Delete Chat Session ${token} ${test_session}[session_id] + Should Be Equal As Integers ${response.status_code} 200 + +Get Session Messages Test + [Documentation] Test getting messages from a chat session + [Tags] chat messages positive + Setup Auth Session + + ${token}= Get Admin Token + ${test_session}= Create Test Chat Session ${token} + + ${response}= Get Session Messages ${token} ${test_session}[session_id] + Should Be Equal As Integers ${response.status_code} 200 + + ${messages}= Set Variable ${response.json()} + Should Be True isinstance($messages, list) + + # New session should have no messages + ${count}= Get Length ${messages} + Should Be Equal As Integers ${count} 0 + + # Cleanup + Cleanup Test Chat Session ${token} ${test_session}[session_id] + +Get Chat Statistics Test + [Documentation] Test getting chat statistics for user + [Tags] chat statistics positive + Setup Auth Session + + ${token}= Get Admin Token + ${response}= Get Chat Statistics ${token} + + Should Be Equal As Integers ${response.status_code} 200 + ${stats}= Set Variable ${response.json()} + # Verify chat statistics structure + Dictionary Should Contain Key ${stats} total_sessions + Dictionary Should Contain Key ${stats} total_messages + + # Statistics should be non-negative + Should Be True ${stats}[total_sessions] >= 0 + Should Be True ${stats}[total_messages] >= 0 + +Chat Session Pagination Test + [Documentation] Test chat session pagination + [Tags] chat session pagination positive + Setup Auth Session + + ${token}= Get Admin Token + + # Test with different limits + ${response1}= Get Chat Sessions ${token} 5 + Should Be Equal As Integers ${response1.status_code} 200 + + ${response2}= Get Chat Sessions ${token} 50 + Should Be Equal As Integers ${response2.status_code} 200 + + ${sessions1}= Set Variable ${response1.json()} + ${sessions2}= Set Variable ${response2.json()} + ${count1}= Get Length ${sessions1} + ${count2}= Get Length ${sessions2} + + # Second request should have >= first request count + Should Be True ${count2} >= ${count1} + +Unauthorized Chat Access Test + [Documentation] Test that chat endpoints require authentication + [Tags] chat security negative + Setup Auth Session + + # Try to access sessions without token + ${response}= GET On Session api /api/chat/sessions expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + + # Try to get statistics without token + ${response}= GET On Session api /api/chat/statistics expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + +Non-Existent Session Operations Test + [Documentation] Test operations on non-existent chat sessions + [Tags] chat session negative notfound + Setup Auth Session + + ${token}= Get Admin Token + ${fake_id}= Set Variable non-existent-session-id + + # Try to get non-existent session + &{headers}= Create Dictionary Authorization=Bearer ${token} + ${response}= GET On Session api /api/chat/sessions/${fake_id} headers=${headers} expected_status=404 + Should Be Equal As Integers ${response.status_code} 404 + + # Try to update non-existent session + &{headers}= Create Dictionary Authorization=Bearer ${token} Content-Type=application/json + &{data}= Create Dictionary title=New Title + ${response}= PUT On Session api /api/chat/sessions/${fake_id} json=${data} headers=${headers} expected_status=404 + Should Be Equal As Integers ${response.status_code} 404 + + # Try to delete non-existent session + &{headers}= Create Dictionary Authorization=Bearer ${token} + ${response}= DELETE On Session api /api/chat/sessions/${fake_id} headers=${headers} expected_status=404 + Should Be Equal As Integers ${response.status_code} 404 + + # Try to get messages from non-existent session + &{headers}= Create Dictionary Authorization=Bearer ${token} + ${response}= GET On Session api /api/chat/sessions/${fake_id}/messages headers=${headers} expected_status=404 + Should Be Equal As Integers ${response.status_code} 404 + +Invalid Chat Session Data Test + [Documentation] Test creating chat session with invalid data + [Tags] chat session negative validation + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} Content-Type=application/json + + # Test with title too long (over 200 characters) + ${long_title}= Generate Random String 201 [LETTERS] + &{data}= Create Dictionary title=${long_title} + ${response}= POST On Session api /api/chat/sessions json=${data} headers=${headers} expected_status=422 + Should Be Equal As Integers ${response.status_code} 422 + + # Test updating with empty title + ${test_session}= Create Test Chat Session ${token} + &{data}= Create Dictionary title=${EMPTY} + ${response}= PUT On Session api /api/chat/sessions/${test_session}[session_id] json=${data} headers=${headers} expected_status=422 + Should Be Equal As Integers ${response.status_code} 422 + + # Cleanup + Cleanup Test Chat Session ${token} ${test_session}[session_id] + +User Isolation Test + [Documentation] Test that users can only access their own chat sessions + [Tags] chat security isolation + Setup Auth Session + + ${admin_token}= Get Admin Token + + # Create a test user + ${test_user}= Create Test User ${admin_token} test-user-${RANDOM_ID}@example.com test-password-123 + ${user_token}= Get User Token test-user-${RANDOM_ID}@example.com test-password-123 + + # Create session as admin + ${admin_session}= Create Test Chat Session ${admin_token} + + # User should not be able to access admin's session + &{headers}= Create Dictionary Authorization=Bearer ${user_token} + ${response}= GET On Session api /api/chat/sessions/${admin_session}[session_id] headers=${headers} expected_status=404 + Should Be Equal As Integers ${response.status_code} 404 + + # User should see empty session list + ${user_sessions}= Get Chat Sessions ${user_token} + Should Be Equal As Integers ${user_sessions.status_code} 200 + ${sessions}= Set Variable ${user_sessions.json()} + ${count}= Get Length ${sessions} + Should Be Equal As Integers ${count} 0 + + # Cleanup + Cleanup Test Chat Session ${admin_token} ${admin_session}[session_id] + Delete Test User ${admin_token} ${test_user}[user_id] + diff --git a/tests/client_queue_tests.robot b/tests/client_queue_tests.robot new file mode 100644 index 00000000..2f3b0401 --- /dev/null +++ b/tests/client_queue_tests.robot @@ -0,0 +1,224 @@ +*** Settings *** +Documentation Client and Queue Management API Tests +Library RequestsLibrary +Library Collections +Resource resources/setup_resources.robot +Resource resources/auth_keywords.robot +Suite Setup Suite Setup +Suite Teardown Delete All Sessions + +*** Test Cases *** + +Get Active Clients Test + [Documentation] Test getting active client information + [Tags] client active positive + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/clients/active headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${clients}= Set Variable ${response.json()} + Should Be True isinstance($clients, (dict, list)) + + # Structure depends on implementation - may be dict with client info or list + IF isinstance($clients, list) + FOR ${client} IN @{clients} + Should Be True isinstance($client, dict) + END + END + +Get Queue Jobs Test + [Documentation] Test getting queue jobs with pagination + [Tags] queue jobs positive + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary limit=20 offset=0 + + ${response}= GET On Session api /api/queue/jobs headers=${headers} params=${params} + Should Be Equal As Integers ${response.status_code} 200 + + ${result}= Set Variable ${response.json()} + Dictionary Should Contain Key ${result} jobs + Dictionary Should Contain Key ${result} pagination + + ${jobs}= Set Variable ${result}[jobs] + Should Be True isinstance($jobs, list) + + ${pagination}= Set Variable ${result}[pagination] + Dictionary Should Contain Key ${pagination} total + Dictionary Should Contain Key ${pagination} limit + Dictionary Should Contain Key ${pagination} offset + Dictionary Should Contain Key ${pagination} has_more + +Get Queue Jobs With Different Limits Test + [Documentation] Test queue jobs pagination with different limits + [Tags] queue jobs pagination positive + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} + + # Test with small limit + &{params1}= Create Dictionary limit=5 offset=0 + ${response1}= GET On Session api /api/queue/jobs headers=${headers} params=${params1} + Should Be Equal As Integers ${response1.status_code} 200 + + # Test with larger limit + &{params2}= Create Dictionary limit=50 offset=0 + ${response2}= GET On Session api /api/queue/jobs headers=${headers} params=${params2} + Should Be Equal As Integers ${response2.status_code} 200 + + ${result1}= Set Variable ${response1.json()} + ${result2}= Set Variable ${response2.json()} + ${count1}= Get Length ${result1}[jobs] + ${count2}= Get Length ${result2}[jobs] + + # Second request should have >= first request count + Should Be True ${count2} >= ${count1} + +Get Queue Statistics Test + [Documentation] Test getting queue statistics + [Tags] queue statistics positive + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/queue/stats headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${stats}= Set Variable ${response.json()} + Dictionary Should Contain Key ${stats} queued + Dictionary Should Contain Key ${stats} processing + Dictionary Should Contain Key ${stats} completed + Dictionary Should Contain Key ${stats} failed + + # All counts should be non-negative + Should Be True ${stats}[queued] >= 0 + Should Be True ${stats}[processing] >= 0 + Should Be True ${stats}[completed] >= 0 + Should Be True ${stats}[failed] >= 0 + +Get Queue Health Test + [Documentation] Test getting queue health status + [Tags] queue health positive + Setup Auth Session + + ${response}= GET On Session api /api/queue/health + Should Be Equal As Integers ${response.status_code} 200 + + ${health}= Set Variable ${response.json()} + Dictionary Should Contain Key ${health} status + Dictionary Should Contain Key ${health} worker_running + Dictionary Should Contain Key ${health} message + + # Status should be one of expected values + Should Be True '${health}[status]' in ['healthy', 'stopped', 'unhealthy'] + +Queue Jobs User Isolation Test + [Documentation] Test that regular users only see their own queue jobs + [Tags] queue security isolation + Setup Auth Session + + ${admin_token}= Get Admin Token + + # Create a test user + ${test_user}= Create Test User ${admin_token} test-user-${RANDOM_ID}@example.com test-password-123 + ${user_token}= Get User Token test-user-${RANDOM_ID}@example.com test-password-123 + + # Get user's jobs (should be filtered to their user_id) + &{headers}= Create Dictionary Authorization=Bearer ${user_token} + ${response}= GET On Session api /api/queue/jobs headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${result}= Set Variable ${response.json()} + ${jobs}= Set Variable ${result}[jobs] + + # All jobs should belong to the test user + FOR ${job} IN @{jobs} + IF 'user_id' in $job + Should Be Equal ${job}[user_id] ${test_user}[id] + END + END + + # Cleanup + Delete Test User ${admin_token} ${test_user}[id] + +Invalid Queue Parameters Test + [Documentation] Test queue endpoints with invalid parameters + [Tags] queue negative validation + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} + + # Test with invalid limit (too high) + &{params}= Create Dictionary limit=1000 offset=0 + ${response}= GET On Session api /api/queue/jobs headers=${headers} params=${params} expected_status=422 + Should Be Equal As Integers ${response.status_code} 422 + + # Test with negative offset + &{params}= Create Dictionary limit=20 offset=-1 + ${response}= GET On Session api /api/queue/jobs headers=${headers} params=${params} expected_status=422 + Should Be Equal As Integers ${response.status_code} 422 + + # Test with invalid limit (too low) + &{params}= Create Dictionary limit=0 offset=0 + ${response}= GET On Session api /api/queue/jobs headers=${headers} params=${params} expected_status=422 + Should Be Equal As Integers ${response.status_code} 422 + +Unauthorized Client Access Test + [Documentation] Test that client endpoints require authentication + [Tags] client security negative + Setup Auth Session + + # Try to access active clients without token + ${response}= GET On Session api /api/clients/active expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + +Unauthorized Queue Access Test + [Documentation] Test that queue endpoints require authentication + [Tags] queue security negative + Setup Auth Session + + # Try to access queue jobs without token + ${response}= GET On Session api /api/queue/jobs expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + + # Try to access queue stats without token + ${response}= GET On Session api /api/queue/stats expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + +Queue Health Public Access Test + [Documentation] Test that queue health endpoint is publicly accessible + [Tags] queue health public + Setup Auth Session + + # Queue health should be accessible without authentication + ${response}= GET On Session api /api/queue/health + Should Be Equal As Integers ${response.status_code} 200 + + ${health}= Set Variable ${response.json()} + Dictionary Should Contain Key ${health} status + +Client Manager Integration Test + [Documentation] Test client manager functionality + [Tags] client manager integration + Setup Auth Session + + ${admin_token}= Get Admin Token + + # Get active clients (may be empty) + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + ${response}= GET On Session api /api/clients/active headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${clients}= Set Variable ${response.json()} + # Verify structure - should be a valid JSON response + Should Be True isinstance($clients, (dict, list)) + diff --git a/tests/conversation_tests.robot b/tests/conversation_tests.robot new file mode 100644 index 00000000..53cc3b0f --- /dev/null +++ b/tests/conversation_tests.robot @@ -0,0 +1,245 @@ +*** Settings *** +Documentation Conversation Management API Tests +Library RequestsLibrary +Library Collections +Library String +Resource resources/setup_resources.robot +Resource resources/auth_keywords.robot +Resource resources/conversation_keywords.robot +Suite Setup Suite Setup +Suite Teardown Delete All Sessions + +*** Test Cases *** + +Get User Conversations Test + [Documentation] Test getting conversations for authenticated user + [Tags] conversation user positive + Setup Auth Session + + ${token}= Get Admin Token + ${response}= Get User Conversations ${token} + + Should Be Equal As Integers ${response.status_code} 200 + Should Be True isinstance($response.json(), dict) + Dictionary Should Contain Key ${response.json()} conversations + + # Verify conversation structure if any exist + ${conversations_data}= Set Variable ${response.json()}[conversations] + IF isinstance($conversations_data, dict) and len($conversations_data) > 0 + ${client_ids}= Get Dictionary Keys ${conversations_data} + FOR ${client_id} IN @{client_ids} + ${client_conversations}= Set Variable ${conversations_data}[${client_id}] + FOR ${conversation} IN @{client_conversations} + # Verify conversation structure + Dictionary Should Contain Key ${conversation} conversation_id + Dictionary Should Contain Key ${conversation} audio_uuid + Dictionary Should Contain Key ${conversation} created_at + END + END + END + +Get Conversation By ID Test + [Documentation] Test getting a specific conversation by ID + [Tags] conversation individual positive + Setup Auth Session + + ${token}= Get Admin Token + ${test_conversation}= Find Test Conversation ${token} + + IF $test_conversation != $None + ${conversation_id}= Set Variable ${test_conversation}[conversation_id] + ${response}= Get Conversation By ID ${token} ${conversation_id} + + Should Be Equal As Integers ${response.status_code} 200 + # Verify conversation structure + ${conversation}= Set Variable ${response.json()}[conversation] + Dictionary Should Contain Key ${conversation} conversation_id + Dictionary Should Contain Key ${conversation} audio_uuid + Dictionary Should Contain Key ${conversation} created_at + Should Be Equal ${conversation}[conversation_id] ${conversation_id} + ELSE + Log No conversations available for testing + Pass Execution No conversations available for individual conversation test + END + +Get Conversation Versions Test + [Documentation] Test getting version history for a conversation + [Tags] conversation versions positive + Setup Auth Session + + ${token}= Get Admin Token + ${test_conversation}= Find Test Conversation ${token} + + IF $test_conversation != $None + ${conversation_id}= Set Variable ${test_conversation}[conversation_id] + ${response}= Get Conversation Versions ${token} ${conversation_id} + + Should Be Equal As Integers ${response.status_code} 200 + # Verify version history structure + ${versions}= Set Variable ${response.json()} + Dictionary Should Contain Key ${versions} transcript_versions + Dictionary Should Contain Key ${versions} memory_versions + Dictionary Should Contain Key ${versions} active_transcript_version + Dictionary Should Contain Key ${versions} active_memory_version + ELSE + Log No conversations available for testing + Pass Execution No conversations available for version history test + END + +Unauthorized Conversation Access Test + [Documentation] Test that conversation endpoints require authentication + [Tags] conversation security negative + Setup Auth Session + + # Try to access conversations without token + ${response}= GET On Session api /api/conversations expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + +Non-Existent Conversation Test + [Documentation] Test accessing a non-existent conversation + [Tags] conversation negative notfound + Setup Auth Session + + ${token}= Get Admin Token + ${fake_id}= Set Variable non-existent-conversation-id + + &{headers}= Create Dictionary Authorization=Bearer ${token} + ${response}= GET On Session api /api/conversations/${fake_id} headers=${headers} expected_status=404 + Should Be Equal As Integers ${response.status_code} 404 + +Reprocess Transcript Test + [Documentation] Test triggering transcript reprocessing + [Tags] conversation reprocess positive + Setup Auth Session + + ${token}= Get Admin Token + ${test_conversation}= Find Test Conversation ${token} + + IF $test_conversation != $None + ${conversation_id}= Set Variable ${test_conversation}[conversation_id] + ${response}= Reprocess Transcript ${token} ${conversation_id} + + # Reprocessing might return 200 (success) or 202 (accepted) depending on implementation + Should Be True ${response.status_code} in [200, 202] + ELSE + Log No conversations available for reprocessing test + Pass Execution No conversations available for transcript reprocessing test + END + +Reprocess Memory Test + [Documentation] Test triggering memory reprocessing + [Tags] conversation reprocess memory positive + Setup Auth Session + + ${token}= Get Admin Token + ${test_conversation}= Find Test Conversation ${token} + + IF $test_conversation != $None + ${conversation_id}= Set Variable ${test_conversation}[conversation_id] + ${response}= Reprocess Memory ${token} ${conversation_id} + + # Memory reprocessing might return 200 (success) or 202 (accepted) + Should Be True ${response.status_code} in [200, 202] + ELSE + Log No conversations available for memory reprocessing test + Pass Execution No conversations available for memory reprocessing test + END + +Close Conversation Test + [Documentation] Test closing current conversation for a client + [Tags] conversation close positive + Setup Auth Session + + ${token}= Get Admin Token + ${client_id}= Set Variable test-client-${RANDOM_ID} + + # This might return 404 if client doesn't exist, which is expected + &{headers}= Create Dictionary Authorization=Bearer ${token} + ${response}= POST On Session api /api/conversations/${client_id}/close headers=${headers} expected_status=any + Should Be True ${response.status_code} in [200, 404] + +Invalid Conversation Operations Test + [Documentation] Test invalid operations on conversations + [Tags] conversation negative invalid + Setup Auth Session + + ${token}= Get Admin Token + ${fake_id}= Set Variable invalid-conversation-id + + # Test reprocessing non-existent conversation + &{headers}= Create Dictionary Authorization=Bearer ${token} + ${response}= POST On Session api /api/conversations/${fake_id}/reprocess-transcript headers=${headers} expected_status=404 + Should Be Equal As Integers ${response.status_code} 404 + + # Test getting versions of non-existent conversation + ${response}= GET On Session api /api/conversations/${fake_id}/versions headers=${headers} expected_status=404 + Should Be Equal As Integers ${response.status_code} 404 + +Version Management Test + [Documentation] Test version activation (if versions exist) + [Tags] conversation versions activation + Setup Auth Session + + ${token}= Get Admin Token + ${test_conversation}= Find Test Conversation ${token} + + IF $test_conversation != $None + ${conversation_id}= Set Variable ${test_conversation}[conversation_id] + ${versions_response}= Get Conversation Versions ${token} ${conversation_id} + + Should Be Equal As Integers ${versions_response.status_code} 200 + ${versions}= Set Variable ${versions_response.json()} + + # Test activating existing active version (should succeed) + ${active_transcript}= Set Variable ${versions}[active_transcript_version] + IF '${active_transcript}' != '${None}' and '${active_transcript}' != 'null' + ${response}= Activate Transcript Version ${token} ${conversation_id} ${active_transcript} + Should Be Equal As Integers ${response.status_code} 200 + END + + ${active_memory}= Set Variable ${versions}[active_memory_version] + IF '${active_memory}' != '${None}' and '${active_memory}' != 'null' + ${response}= Activate Memory Version ${token} ${conversation_id} ${active_memory} + Should Be Equal As Integers ${response.status_code} 200 + END + ELSE + Log No conversations available for version management test + Pass Execution No conversations available for version management test + END + +User Isolation Test + [Documentation] Test that users can only access their own conversations + [Tags] conversation security isolation + Setup Auth Session + + ${admin_token}= Get Admin Token + + # Create a test user + ${test_user}= Create Test User ${admin_token} test-user-${RANDOM_ID}@example.com test-password-123 + ${user_token}= Get User Token test-user-${RANDOM_ID}@example.com test-password-123 + + # Get admin conversations + ${admin_conversations}= Get User Conversations ${admin_token} + Should Be Equal As Integers ${admin_conversations.status_code} 200 + + # Get user conversations (should be empty for new user) + ${user_conversations}= Get User Conversations ${user_token} + Should Be Equal As Integers ${user_conversations.status_code} 200 + + # User should see empty or only their own conversations + ${user_conv_data}= Set Variable ${user_conversations.json()}[conversations] + IF isinstance($user_conv_data, dict) and len($user_conv_data) > 0 + ${client_ids}= Get Dictionary Keys ${user_conv_data} + FOR ${client_id} IN @{client_ids} + ${client_conversations}= Set Variable ${user_conv_data}[${client_id}] + FOR ${conversation} IN @{client_conversations} + # Note: The actual conversation structure doesn't have user_id field exposed + # This test should verify that only this user's conversations are returned + Dictionary Should Contain Key ${conversation} conversation_id + END + END + END + + # Cleanup + Delete Test User ${admin_token} ${test_user}[user_id] + diff --git a/tests/health_tests.robot b/tests/health_tests.robot new file mode 100644 index 00000000..0a0e6883 --- /dev/null +++ b/tests/health_tests.robot @@ -0,0 +1,231 @@ +*** Settings *** +Documentation Health and Status Endpoint API Tests +Library RequestsLibrary +Library Collections +Library BuiltIn +Resource resources/setup_resources.robot +Resource resources/auth_keywords.robot +Suite Setup Suite Setup +Suite Teardown Delete All Sessions + +*** Test Cases *** + +Health Check Test + [Documentation] Test main health check endpoint + [Tags] health status positive + Setup Auth Session + + ${response}= GET On Session api /health + Should Be Equal As Integers ${response.status_code} 200 + + ${health}= Set Variable ${response.json()} + Dictionary Should Contain Key ${health} status + Dictionary Should Contain Key ${health} timestamp + Dictionary Should Contain Key ${health} services + Dictionary Should Contain Key ${health} overall_healthy + Dictionary Should Contain Key ${health} critical_services_healthy + + # Verify status is one of expected values + Should Be True '${health}[status]' in ['healthy', 'degraded', 'critical'] + +Readiness Check Test + [Documentation] Test readiness check endpoint for container orchestration + [Tags] readiness status positive + Setup Auth Session + + ${response}= GET On Session api /readiness + Should Be Equal As Integers ${response.status_code} 200 + + ${readiness}= Set Variable ${response.json()} + Dictionary Should Contain Key ${readiness} status + Dictionary Should Contain Key ${readiness} timestamp + Should Be Equal ${readiness}[status] ready + +Auth Health Check Test + [Documentation] Test authentication service health check + [Tags] auth health positive + Setup Auth Session + + ${response}= GET On Session api /api/auth/health + Should Be Equal As Integers ${response.status_code} 200 + + ${auth_health}= Set Variable ${response.json()} + Dictionary Should Contain Key ${auth_health} status + Dictionary Should Contain Key ${auth_health} database + Dictionary Should Contain Key ${auth_health} memory_service + Dictionary Should Contain Key ${auth_health} timestamp + +Queue Health Check Test + [Documentation] Test queue system health check + [Tags] queue health positive + Setup Auth Session + + ${response}= GET On Session api /api/queue/health + Should Be Equal As Integers ${response.status_code} 200 + + ${queue_health}= Set Variable ${response.json()} + Dictionary Should Contain Key ${queue_health} status + Dictionary Should Contain Key ${queue_health} worker_running + Dictionary Should Contain Key ${queue_health} message + +Chat Health Check Test + [Documentation] Test chat service health check + [Tags] chat health positive + Setup Auth Session + + ${response}= GET On Session api /api/chat/health + Should Be Equal As Integers ${response.status_code} 200 + + ${chat_health}= Set Variable ${response.json()} + Dictionary Should Contain Key ${chat_health} status + Dictionary Should Contain Key ${chat_health} service + Dictionary Should Contain Key ${chat_health} timestamp + Should Be Equal ${chat_health}[service] chat + +System Metrics Test + [Documentation] Test system metrics endpoint (admin only) + [Tags] metrics admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + + ${response}= GET On Session api /api/metrics headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${metrics}= Set Variable ${response.json()} + # Metrics structure may vary, just verify it's a valid response + Should Be True isinstance($metrics, dict) + +Processor Status Test + [Documentation] Test processor status endpoint (admin only) + [Tags] processor admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + + ${response}= GET On Session api /api/processor/status headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${status}= Set Variable ${response.json()} + # Processor status structure may vary + Should Be True isinstance($status, dict) + +Processing Tasks Test + [Documentation] Test processing tasks endpoint (admin only) + [Tags] processor tasks admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + + ${response}= GET On Session api /api/processor/tasks headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${tasks}= Set Variable ${response.json()} + Should Be True isinstance($tasks, (dict, list)) + +Health Check Service Details Test + [Documentation] Test detailed service health information + [Tags] health services detailed + Setup Auth Session + + ${response}= GET On Session api /health + Should Be Equal As Integers ${response.status_code} 200 + + ${health}= Set Variable ${response.json()} + ${services}= Set Variable ${health}[services] + + # Check for expected services + ${expected_services}= Create List mongodb audioai memory_service speech_to_text + + FOR ${service} IN @{expected_services} + IF '${service}' in $services + ${service_info}= Set Variable ${services}[${service}] + Dictionary Should Contain Key ${service_info} status + Dictionary Should Contain Key ${service_info} healthy + Dictionary Should Contain Key ${service_info} critical + END + END + +Non-Admin Cannot Access Admin Endpoints Test + [Documentation] Test that non-admin users cannot access admin health endpoints + [Tags] health security negative + Setup Auth Session + + ${admin_token}= Get Admin Token + + # Create a non-admin user + ${test_user}= Create Test User ${admin_token} test-user-${RANDOM_ID}@example.com test-password-123 + ${user_token}= Get User Token test-user-${RANDOM_ID}@example.com test-password-123 + + # Try to access admin endpoints + &{headers}= Create Dictionary Authorization=Bearer ${user_token} + + # Metrics endpoint should be forbidden + ${response}= GET On Session api /api/metrics headers=${headers} expected_status=403 + Should Be Equal As Integers ${response.status_code} 403 + + # Processor status should be forbidden + ${response}= GET On Session api /api/processor/status headers=${headers} expected_status=403 + Should Be Equal As Integers ${response.status_code} 403 + + # Processing tasks should be forbidden + ${response}= GET On Session api /api/processor/tasks headers=${headers} expected_status=403 + Should Be Equal As Integers ${response.status_code} 403 + + # Cleanup + Delete Test User ${admin_token} ${test_user}[user_id] + +Unauthorized Health Access Test + [Documentation] Test health endpoints that require authentication + [Tags] health security negative + Setup Auth Session + + # Admin-only endpoints should require authentication + ${response}= GET On Session api /api/metrics expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + + ${response}= GET On Session api /api/processor/status expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + +Health Configuration Test + [Documentation] Test that health check returns configuration information + [Tags] health config positive + Setup Auth Session + + ${response}= GET On Session api /health + Should Be Equal As Integers ${response.status_code} 200 + + ${health}= Set Variable ${response.json()} + Dictionary Should Contain Key ${health} config + + ${config}= Set Variable ${health}[config] + Dictionary Should Contain Key ${config} mongodb_uri + Dictionary Should Contain Key ${config} qdrant_url + Dictionary Should Contain Key ${config} transcription_service + Dictionary Should Contain Key ${config} asr_uri + Dictionary Should Contain Key ${config} provider_type + Dictionary Should Contain Key ${config} chunk_dir + Dictionary Should Contain Key ${config} active_clients + Dictionary Should Contain Key ${config} new_conversation_timeout_minutes + Dictionary Should Contain Key ${config} audio_cropping_enabled + Dictionary Should Contain Key ${config} llm_provider + Dictionary Should Contain Key ${config} llm_model + Dictionary Should Contain Key ${config} llm_base_url + + # Verify config values are not empty + Should Not Be Empty ${config}[mongodb_uri] + Should Not Be Empty ${config}[qdrant_url] + Should Not Be Empty ${config}[transcription_service] + Should Not Be Empty ${config}[asr_uri] + Should Not Be Empty ${config}[provider_type] + Should Not Be Empty ${config}[chunk_dir] + Should Be True isinstance(${config}[active_clients], int) + Should Be True ${config}[new_conversation_timeout_minutes] > 0 + Should Be True isinstance(${config}[audio_cropping_enabled], bool) + Should Not Be Empty ${config}[llm_provider] + Should Not Be Empty ${config}[llm_model] + Should Not Be Empty ${config}[llm_base_url] + diff --git a/tests/memory_tests.robot b/tests/memory_tests.robot new file mode 100644 index 00000000..aab40411 --- /dev/null +++ b/tests/memory_tests.robot @@ -0,0 +1,217 @@ +*** Settings *** +Documentation Memory Management API Tests +Library RequestsLibrary +Library Collections +Library String +Resource resources/setup_resources.robot +Resource resources/auth_keywords.robot +Resource resources/memory_keywords.robot +Suite Setup Suite Setup +Suite Teardown Delete All Sessions + +*** Test Cases *** + +Get User Memories Test + [Documentation] Test getting memories for authenticated user + [Tags] memory user positive + Setup Auth Session + + ${token}= Get Admin Token + ${response}= Get User Memories ${token} + + Should Be Equal As Integers ${response.status_code} 200 + Should Be True isinstance($response.json(), list) + + # Verify memory structure if any exist + ${memories}= Set Variable ${response.json()} + FOR ${memory} IN @{memories} + # Verify memory structure + Dictionary Should Contain Key ${memory} id + Dictionary Should Contain Key ${memory} user_id + Dictionary Should Contain Key ${memory} text + Dictionary Should Contain Key ${memory} created_at + END + +Get Memories With Transcripts Test + [Documentation] Test getting memories with their source transcripts + [Tags] memory transcripts positive + Setup Auth Session + + ${token}= Get Admin Token + ${response}= Get Memories With Transcripts ${token} + + Should Be Equal As Integers ${response.status_code} 200 + Should Be True isinstance($response.json(), list) + + # Verify enhanced structure if any exist + ${memories}= Set Variable ${response.json()} + FOR ${memory} IN @{memories} + # Verify memory structure + Dictionary Should Contain Key ${memory} id + Dictionary Should Contain Key ${memory} user_id + Dictionary Should Contain Key ${memory} text + Dictionary Should Contain Key ${memory} created_at + # May have additional transcript fields + END + +Search Memories Test + [Documentation] Test searching memories by query + [Tags] memory search positive + Setup Auth Session + + ${token}= Get Admin Token + ${response}= Search Memories ${token} test 20 0.0 + + Should Be Equal As Integers ${response.status_code} 200 + Should Be True isinstance($response.json(), list) + + # Verify search results structure + ${results}= Set Variable ${response.json()} + # Verify search results structure + FOR ${memory} IN @{results} + # Verify memory structure + Dictionary Should Contain Key ${memory} id + Dictionary Should Contain Key ${memory} user_id + Dictionary Should Contain Key ${memory} text + Dictionary Should Contain Key ${memory} created_at + END + +Search Memories With High Threshold Test + [Documentation] Test searching memories with high similarity threshold + [Tags] memory search threshold + Setup Auth Session + + ${token}= Get Admin Token + ${response}= Search Memories ${token} nonexistent-query 10 0.9 + + Should Be Equal As Integers ${response.status_code} 200 + Should Be True isinstance($response.json(), list) + + # High threshold might return fewer results + ${results}= Set Variable ${response.json()} + FOR ${memory} IN @{results} + # Verify memory structure + Dictionary Should Contain Key ${memory} id + Dictionary Should Contain Key ${memory} user_id + Dictionary Should Contain Key ${memory} text + Dictionary Should Contain Key ${memory} created_at + END + +Get Unfiltered Memories Test + [Documentation] Test getting unfiltered memories for debugging + [Tags] memory debug positive + Setup Auth Session + + ${token}= Get Admin Token + ${response}= Get Unfiltered Memories ${token} + + Should Be Equal As Integers ${response.status_code} 200 + Should Be True isinstance($response.json(), list) + + # Unfiltered may include more memories than filtered + ${memories}= Set Variable ${response.json()} + FOR ${memory} IN @{memories} + # Verify memory structure + Dictionary Should Contain Key ${memory} id + Dictionary Should Contain Key ${memory} user_id + Dictionary Should Contain Key ${memory} text + Dictionary Should Contain Key ${memory} created_at + END + +Get All Memories Admin Test + [Documentation] Test getting all memories across users (admin only) + [Tags] memory admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + ${response}= Get All Memories Admin ${admin_token} + + Should Be Equal As Integers ${response.status_code} 200 + Should Be True isinstance($response.json(), list) + + # Admin endpoint should return memories from all users + ${memories}= Set Variable ${response.json()} + FOR ${memory} IN @{memories} + # Verify memory structure + Dictionary Should Contain Key ${memory} id + Dictionary Should Contain Key ${memory} user_id + Dictionary Should Contain Key ${memory} text + Dictionary Should Contain Key ${memory} created_at + # Should have user_id from potentially different users + Dictionary Should Contain Key ${memory} user_id + END + +Memory Pagination Test + [Documentation] Test memory pagination with different limits + [Tags] memory pagination positive + Setup Auth Session + + ${token}= Get Admin Token + + # Test with small limit + ${response1}= Get User Memories ${token} 5 + Should Be Equal As Integers ${response1.status_code} 200 + ${memories1}= Set Variable ${response1.json()} + ${count1}= Get Length ${memories1} + Should Be True ${count1} <= 5 + + # Test with larger limit + ${response2}= Get User Memories ${token} 100 + Should Be Equal As Integers ${response2.status_code} 200 + ${memories2}= Set Variable ${response2.json()} + ${count2}= Get Length ${memories2} + + # Second request should have >= first request count + Should Be True ${count2} >= ${count1} + +Non-Admin Cannot Access Admin Memories Test + [Documentation] Test that non-admin users cannot access admin memory endpoint + [Tags] memory security negative + Setup Auth Session + + ${admin_token}= Get Admin Token + + # Create a non-admin user + ${test_user}= Create Test User ${admin_token} test-user-${RANDOM_ID}@example.com test-password-123 + ${user_token}= Get User Token test-user-${RANDOM_ID}@example.com test-password-123 + + # Try to access admin memories endpoint + &{headers}= Create Dictionary Authorization=Bearer ${user_token} + ${response}= GET On Session api /api/memories/admin headers=${headers} expected_status=403 + Should Be Equal As Integers ${response.status_code} 403 + + # Cleanup + Delete Test User ${admin_token} ${test_user}[user_id] + +Unauthorized Memory Access Test + [Documentation] Test that memory endpoints require authentication + [Tags] memory security negative + Setup Auth Session + + # Try to access memories without token + ${response}= GET On Session api /api/memories expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + + # Try to search memories without token + &{params}= Create Dictionary query=test + ${response}= GET On Session api /api/memories/search params=${params} expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + +Invalid Search Parameters Test + [Documentation] Test search with invalid parameters + [Tags] memory search negative + Setup Auth Session + + ${token}= Get Admin Token + + # Test with empty query (should fail) + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary query=${EMPTY} + ${response}= GET On Session api /api/memories/search headers=${headers} params=${params} expected_status=422 + Should Be Equal As Integers ${response.status_code} 422 + + # Test with invalid score threshold + &{params}= Create Dictionary query=test score_threshold=2.0 + ${response}= GET On Session api /api/memories/search headers=${headers} params=${params} expected_status=422 + Should Be Equal As Integers ${response.status_code} 422 + diff --git a/tests/resources/auth_keywords.robot b/tests/resources/auth_keywords.robot new file mode 100644 index 00000000..a4d2fdfb --- /dev/null +++ b/tests/resources/auth_keywords.robot @@ -0,0 +1,87 @@ +*** Settings *** +Documentation Authentication and User Management Keywords +Library RequestsLibrary +Library Collections +Variables ../test_env.py + +*** Keywords *** + +Setup Auth Session + [Documentation] Create API session for authentication tests + [Arguments] ${base_url}=${API_URL} + Create Session api ${base_url} verify=True + RETURN api + +Get Admin Token + [Documentation] Get authentication token for admin user + &{auth_data}= Create Dictionary username=${ADMIN_EMAIL} password=${ADMIN_PASSWORD} + &{headers}= Create Dictionary Content-Type=application/x-www-form-urlencoded + + ${response}= POST On Session api /auth/jwt/login data=${auth_data} headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${json_response}= Set Variable ${response.json()} + ${token}= Get From Dictionary ${json_response} access_token + RETURN ${token} + +Verify Admin User + [Documentation] Verify current user is admin + [Arguments] ${token} + &{headers}= Create Dictionary Authorization=Bearer ${token} + ${response}= GET On Session api /users/me headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + Should Be Equal ${response.json()}[email] ${ADMIN_EMAIL} + RETURN ${response.json()} + +Create Test User + [Documentation] Create a test user for testing + [Arguments] ${token} ${email} ${password} ${is_superuser}=False + &{headers}= Create Dictionary Authorization=Bearer ${token} Content-Type=application/json + &{user_data}= Create Dictionary email=${email} password=${password} is_superuser=${is_superuser} + + ${response}= POST On Session api /api/users json=${user_data} headers=${headers} + Should Be Equal As Integers ${response.status_code} 201 + RETURN ${response.json()} + +Delete Test User + [Documentation] Delete a test user + [Arguments] ${token} ${user_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + ${response}= DELETE On Session api /api/users/${user_id} headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + +Get User Token + [Documentation] Get authentication token for any user + [Arguments] ${email} ${password} + &{auth_data}= Create Dictionary username=${email} password=${password} + &{headers}= Create Dictionary Content-Type=application/x-www-form-urlencoded + + ${response}= POST On Session api /auth/jwt/login data=${auth_data} headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${json_response}= Set Variable ${response.json()} + ${token}= Get From Dictionary ${json_response} access_token + RETURN ${token} + +Make Authenticated Request + [Documentation] Make an authenticated API request + [Arguments] ${method} ${endpoint} ${token} &{kwargs} + + &{headers}= Create Dictionary Authorization=Bearer ${token} + Set To Dictionary ${kwargs} headers=${headers} + + ${response}= Run Keyword ${method} On Session api ${endpoint} &{kwargs} + RETURN ${response} + +Verify Unauthorized Access + [Documentation] Verify endpoint requires authentication + [Arguments] ${method} ${endpoint} &{kwargs} + ${response}= Run Keyword And Expect Error * Run Keyword ${method} On Session api ${endpoint} expected_status=401 &{kwargs} + +Verify Admin Required + [Documentation] Verify endpoint requires admin privileges + [Arguments] ${method} ${endpoint} ${non_admin_token} &{kwargs} + &{headers}= Create Dictionary Authorization=Bearer ${non_admin_token} + Set To Dictionary ${kwargs} headers=${headers} + ${response}= Run Keyword ${method} On Session api ${endpoint} expected_status=403 &{kwargs} + Should Be Equal As Integers ${response.status_code} 403 \ No newline at end of file diff --git a/tests/resources/chat_keywords.robot b/tests/resources/chat_keywords.robot new file mode 100644 index 00000000..189d89ea --- /dev/null +++ b/tests/resources/chat_keywords.robot @@ -0,0 +1,111 @@ +*** Settings *** +Documentation Chat Service Keywords +Library RequestsLibrary +Library Collections +Variables ../test_env.py + +*** Keywords *** + +Create Chat Session + [Documentation] Create a new chat session + [Arguments] ${token} ${title}=${None} + &{headers}= Create Dictionary Authorization=Bearer ${token} Content-Type=application/json + &{data}= Create Dictionary + + IF '${title}' != '${None}' + Set To Dictionary ${data} title=${title} + END + + ${response}= POST On Session api /api/chat/sessions json=${data} headers=${headers} + RETURN ${response} + +Get Chat Sessions + [Documentation] Get all chat sessions for user + [Arguments] ${token} ${limit}=50 + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary limit=${limit} + + ${response}= GET On Session api /api/chat/sessions headers=${headers} params=${params} + RETURN ${response} + +Get Chat Session + [Documentation] Get a specific chat session + [Arguments] ${token} ${session_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/chat/sessions/${session_id} headers=${headers} + RETURN ${response} + +Update Chat Session + [Documentation] Update a chat session title + [Arguments] ${token} ${session_id} ${new_title} + &{headers}= Create Dictionary Authorization=Bearer ${token} Content-Type=application/json + &{data}= Create Dictionary title=${new_title} + + ${response}= PUT On Session api /api/chat/sessions/${session_id} json=${data} headers=${headers} + RETURN ${response} + +Delete Chat Session + [Documentation] Delete a chat session + [Arguments] ${token} ${session_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= DELETE On Session api /api/chat/sessions/${session_id} headers=${headers} + RETURN ${response} + +Get Session Messages + [Documentation] Get messages from a chat session + [Arguments] ${token} ${session_id} ${limit}=100 + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary limit=${limit} + + ${response}= GET On Session api /api/chat/sessions/${session_id}/messages headers=${headers} params=${params} + RETURN ${response} + +Send Chat Message + [Documentation] Send a message to chat (non-streaming) + [Arguments] ${token} ${message} ${session_id}=${None} + &{headers}= Create Dictionary Authorization=Bearer ${token} Content-Type=application/json + &{data}= Create Dictionary message=${message} + + IF '${session_id}' != '${None}' + Set To Dictionary ${data} session_id=${session_id} + END + + # Note: This endpoint streams, so we expect a streaming response + ${response}= POST On Session api /api/chat/send json=${data} headers=${headers} + RETURN ${response} + +Get Chat Statistics + [Documentation] Get chat statistics for user + [Arguments] ${token} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/chat/statistics headers=${headers} + RETURN ${response} + +Extract Memories From Session + [Documentation] Extract memories from a chat session + [Arguments] ${token} ${session_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= POST On Session api /api/chat/sessions/${session_id}/extract-memories headers=${headers} + RETURN ${response} + + +Create Test Chat Session + [Documentation] Create a test chat session with random title + [Arguments] ${token} ${title_prefix}=Test Session + ${random_suffix}= Generate Random String 6 [LETTERS][NUMBERS] + ${title}= Set Variable ${title_prefix} ${random_suffix} + + ${response}= Create Chat Session ${token} ${title} + Should Be Equal As Integers ${response.status_code} 200 + + RETURN ${response.json()} + +Cleanup Test Chat Session + [Documentation] Clean up a test chat session + [Arguments] ${token} ${session_id} + ${response}= Delete Chat Session ${token} ${session_id} + Should Be Equal As Integers ${response.status_code} 200 \ No newline at end of file diff --git a/tests/resources/conversation_keywords.robot b/tests/resources/conversation_keywords.robot new file mode 100644 index 00000000..433e3b88 --- /dev/null +++ b/tests/resources/conversation_keywords.robot @@ -0,0 +1,184 @@ +*** Settings *** +Documentation Conversation Management Keywords +Library RequestsLibrary +Library Collections +Library Process +Variables ../test_env.py + +*** Keywords *** + +Get User Conversations + [Documentation] Get conversations for authenticated user + [Arguments] ${token} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/conversations headers=${headers} + RETURN ${response} + +Get Conversation By ID + [Documentation] Get a specific conversation by ID + [Arguments] ${token} ${conversation_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/conversations/${conversation_id} headers=${headers} + RETURN ${response} + +Get Conversation Versions + [Documentation] Get version history for a conversation + [Arguments] ${token} ${conversation_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/conversations/${conversation_id}/versions headers=${headers} + RETURN ${response} + +Reprocess Transcript + [Documentation] Trigger transcript reprocessing for a conversation + [Arguments] ${token} ${conversation_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= POST On Session api /api/conversations/${conversation_id}/reprocess-transcript headers=${headers} + RETURN ${response} + +Reprocess Memory + [Documentation] Trigger memory reprocessing for a conversation + [Arguments] ${token} ${conversation_id} ${transcript_version_id}=active + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary transcript_version_id=${transcript_version_id} + + ${response}= POST On Session api /api/conversations/${conversation_id}/reprocess-memory headers=${headers} params=${params} + RETURN ${response} + +Activate Transcript Version + [Documentation] Activate a specific transcript version + [Arguments] ${token} ${conversation_id} ${version_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= POST On Session api /api/conversations/${conversation_id}/activate-transcript/${version_id} headers=${headers} + RETURN ${response} + +Activate Memory Version + [Documentation] Activate a specific memory version + [Arguments] ${token} ${conversation_id} ${version_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= POST On Session api /api/conversations/${conversation_id}/activate-memory/${version_id} headers=${headers} + RETURN ${response} + +Delete Conversation + [Documentation] Delete a conversation + [Arguments] ${token} ${audio_uuid} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= DELETE On Session api /api/conversations/${audio_uuid} headers=${headers} + RETURN ${response} + +Delete Conversation Version + [Documentation] Delete a specific version from a conversation + [Arguments] ${token} ${conversation_id} ${version_type} ${version_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= DELETE On Session api /api/conversations/${conversation_id}/versions/${version_type}/${version_id} headers=${headers} + RETURN ${response} + +Close Current Conversation + [Documentation] Close the current conversation for a client + [Arguments] ${token} ${client_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= POST On Session api /api/conversations/${client_id}/close headers=${headers} + RETURN ${response} + +Get Cropped Audio Info + [Documentation] Get cropped audio information for a conversation + [Arguments] ${token} ${audio_uuid} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/conversations/${audio_uuid}/cropped headers=${headers} + RETURN ${response} + +Add Speaker To Conversation + [Documentation] Add a speaker to the speakers_identified list + [Arguments] ${token} ${audio_uuid} ${speaker_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary speaker_id=${speaker_id} + + ${response}= POST On Session api /api/conversations/${audio_uuid}/speakers headers=${headers} params=${params} + RETURN ${response} + +Update Transcript Segment + [Documentation] Update a specific transcript segment + [Arguments] ${token} ${audio_uuid} ${segment_index} ${speaker_id}=${None} ${start_time}=${None} ${end_time}=${None} + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary + + IF '${speaker_id}' != '${None}' + Set To Dictionary ${params} speaker_id=${speaker_id} + END + IF '${start_time}' != '${None}' + Set To Dictionary ${params} start_time=${start_time} + END + IF '${end_time}' != '${None}' + Set To Dictionary ${params} end_time=${end_time} + END + + ${response}= PUT On Session api /api/conversations/${audio_uuid}/transcript/${segment_index} headers=${headers} params=${params} + RETURN ${response} + + +Create Test Conversation + [Documentation] Create a test conversation by processing a test audio file + [Arguments] ${token} ${device_name}=test-device + + # Upload test audio file to create a conversation + ${test_audio_file}= Set Variable /Users/stu/repos/friend-lite/pytest/extras/test-audios/DIY Experts Glass Blowing_16khz_mono_4min.wav + + # Use curl to upload the file (Robot Framework doesn't handle file uploads well) + ${curl_cmd}= Set Variable curl -s -X POST -H "Authorization: Bearer ${token}" -F "files=@${test_audio_file}" -F "device_name=${device_name}" http://localhost:8001/api/process-audio-files + ${result}= Run Process ${curl_cmd} shell=True + + Should Be Equal As Integers ${result.rc} 0 + ${response_json}= Evaluate json.loads('''${result.stdout}''') json + + Should Be True ${response_json}[successful] > 0 + + # Wait a moment for processing to complete + Sleep 2s + + # Now get the actual conversation data + ${conversations_response}= Get User Conversations ${token} + Should Be Equal As Integers ${conversations_response.status_code} 200 + + ${conversations_data}= Set Variable ${conversations_response.json()}[conversations] + ${client_ids}= Get Dictionary Keys ${conversations_data} + ${count}= Get Length ${client_ids} + + Should Be True ${count} > 0 + ${first_client_id}= Set Variable ${client_ids}[0] + ${client_conversations}= Set Variable ${conversations_data}[${first_client_id}] + ${first_conv}= Set Variable ${client_conversations}[0] + + RETURN ${first_conv} + +Find Test Conversation + [Documentation] Find a conversation that exists for testing (returns first available) + [Arguments] ${token} + ${response}= Get User Conversations ${token} + Should Be Equal As Integers ${response.status_code} 200 + + ${conversations_data}= Set Variable ${response.json()}[conversations] + ${client_ids}= Get Dictionary Keys ${conversations_data} + ${count}= Get Length ${client_ids} + + IF ${count} > 0 + ${first_client_id}= Set Variable ${client_ids}[0] + ${client_conversations}= Set Variable ${conversations_data}[${first_client_id}] + ${conv_count}= Get Length ${client_conversations} + IF ${conv_count} > 0 + ${first_conv}= Set Variable ${client_conversations}[0] + RETURN ${first_conv} + END + END + + # If no conversations exist, create one + ${new_conversation}= Create Test Conversation ${token} + RETURN ${new_conversation} \ No newline at end of file diff --git a/tests/resources/login_resources.robot b/tests/resources/login_resources.robot new file mode 100644 index 00000000..475b7d4e --- /dev/null +++ b/tests/resources/login_resources.robot @@ -0,0 +1,25 @@ +*** Settings *** +Library RequestsLibrary +Library Collections +Library OperatingSystem +Variables ../test_env.py + +*** Keywords *** + +Create Basic Auth Header + [Documentation] + # Concatenate the username and password in "user:pass" format + [Arguments] ${username} ${password} + ${credentials}= Set Variable ${username}:${password} + Log To Console Credentials: ${credentials} + + # Encode the credentials using base64 encoding + ${encoded}= Evaluate str(base64.b64encode('${credentials}'.encode('utf-8')), 'utf-8') modules=base64 + Log To Console Encoded Credentials: ${encoded} + + # Create a headers dictionary and add the Authorization header + ${headers}= Create Dictionary Content-Type=application/json + Set To Dictionary ${headers} Authorization=Basic ${encoded} + RETURN ${headers} + + diff --git a/tests/resources/memory_keywords.robot b/tests/resources/memory_keywords.robot new file mode 100644 index 00000000..ae9ba8b6 --- /dev/null +++ b/tests/resources/memory_keywords.robot @@ -0,0 +1,74 @@ +*** Settings *** +Documentation Memory Management Keywords +Library RequestsLibrary +Library Collections +Variables ../test_env.py + +*** Keywords *** + +Get User Memories + [Documentation] Get memories for authenticated user + [Arguments] ${token} ${limit}=50 ${user_id}=${None} + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary limit=${limit} + + IF '${user_id}' != '${None}' + Set To Dictionary ${params} user_id=${user_id} + END + + ${response}= GET On Session api /api/memories headers=${headers} params=${params} + RETURN ${response} + +Get Memories With Transcripts + [Documentation] Get memories with their source transcripts + [Arguments] ${token} ${limit}=50 + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary limit=${limit} + + ${response}= GET On Session api /api/memories/with-transcripts headers=${headers} params=${params} + RETURN ${response} + +Search Memories + [Documentation] Search memories by query + [Arguments] ${token} ${query} ${limit}=20 ${score_threshold}=0.0 + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary query=${query} limit=${limit} score_threshold=${score_threshold} + + ${response}= GET On Session api /api/memories/search headers=${headers} params=${params} + RETURN ${response} + +Delete Memory + [Documentation] Delete a specific memory + [Arguments] ${token} ${memory_id} + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= DELETE On Session api /api/memories/${memory_id} headers=${headers} + RETURN ${response} + +Get Unfiltered Memories + [Documentation] Get all memories including fallback transcript memories + [Arguments] ${token} ${limit}=50 + &{headers}= Create Dictionary Authorization=Bearer ${token} + &{params}= Create Dictionary limit=${limit} + + ${response}= GET On Session api /api/memories/unfiltered headers=${headers} params=${params} + RETURN ${response} + +Get All Memories Admin + [Documentation] Get all memories across all users (admin only) + [Arguments] ${admin_token} ${limit}=200 + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + &{params}= Create Dictionary limit=${limit} + + ${response}= GET On Session api /api/memories/admin headers=${headers} params=${params} + RETURN ${response} + + +Count User Memories + [Documentation] Count memories for a user + [Arguments] ${token} + ${response}= Get User Memories ${token} 1000 + Should Be Equal As Integers ${response.status_code} 200 + ${memories}= Set Variable ${response.json()} + ${count}= Get Length ${memories} + RETURN ${count} \ No newline at end of file diff --git a/tests/resources/setup_resources.robot b/tests/resources/setup_resources.robot new file mode 100644 index 00000000..3045b0c6 --- /dev/null +++ b/tests/resources/setup_resources.robot @@ -0,0 +1,80 @@ +*** Settings *** +Documentation Reusable keywords for API testing +Library RequestsLibrary +Library Collections +Library OperatingSystem +Library String +Variables ../test_env.py +Resource login_resources.robot + + +*** Keywords *** + +Suite Setup + [Documentation] Setup for auth test suite + ${random_id}= Generate Random String 8 [LETTERS][NUMBERS] + Set Suite Variable ${RANDOM_ID} ${random_id} + + # Ensure server is running + Health Check ${API_URL} + +Start advanced-server + [Documentation] Start the server using docker-compose + Run docker-compose -f docker-compose.test.yml up -d --build + Sleep 5s + Health Check ${API_URL} + +Stop advanced-server + [Documentation] Stop the server using docker-compose + Run docker-compose -f docker-compose.test.yml down + +Health Check + [Documentation] Verify that the health endpoint is accessible + [Tags] health api + [Arguments] ${base_url}= ${API_URL} + Wait Until Keyword Succeeds 30s 5s GET ${base_url}/health + ${response}= GET ${base_url}/health + Should Be Equal As Integers ${response.status_code} 200 + +Setup API Session + [Documentation] Create session for API testing + [Arguments] ${base_url}=http://localhost:8001 &{headers} + IF ${headers} == '{}' + &{headers}= Create Basic Auth Header ${ADMIN_EMAIL} ${ADMIN_PASSWORD} + END + Create Session api ${base_url} verify=True headers=${headers} + RETURN api + +# Get Auth Token +# [Documentation] Get authentication token for API calls +# [Arguments] ${email} ${password} + +# &{auth_data}= Create Dictionary username=${email} password=${password} +# &{headers}= Create Dictionary Content-Type=application/x-www-form-urlencoded + +# ${response}= POST On Session api /auth/jwt/login data=${auth_data} headers=${headers} +# Should Be Equal As Integers ${response.status_code} 200 + +# ${token}= Get Value From Json ${response.json()} $.access_token +# RETURN ${token}[0] + +# Make Authenticated Request +# [Documentation] Make an authenticated API request +# [Arguments] ${method} ${endpoint} ${token} &{kwargs} + +# &{headers}= Create Dictionary Authorization=Bearer ${token} +# Set To Dictionary ${kwargs} headers=${headers} + +# ${response}= Run Keyword ${method} On Session api ${endpoint} &{kwargs} +# RETURN ${response} + +# Verify JSON Response Contains +# [Documentation] Verify JSON response contains expected data +# [Arguments] ${response} ${key} ${expected_value}=None + +# ${json_data}= Set Variable ${response.json()} +# Should Contain ${json_data} ${key} + +# IF '${expected_value}' != 'None' +# Should Be Equal ${json_data}[${key}] ${expected_value} +# END \ No newline at end of file diff --git a/tests/system_admin_tests.robot b/tests/system_admin_tests.robot new file mode 100644 index 00000000..ded873ac --- /dev/null +++ b/tests/system_admin_tests.robot @@ -0,0 +1,302 @@ +*** Settings *** +Documentation System and Admin API Tests +Library RequestsLibrary +Library Collections +Library String +Library OperatingSystem +Resource resources/setup_resources.robot +Resource resources/auth_keywords.robot +Suite Setup Suite Setup +Suite Teardown Delete All Sessions + +*** Test Cases *** + +Get Auth Config Test + [Documentation] Test getting authentication configuration (public endpoint) + [Tags] auth config public positive + Setup Auth Session + + ${response}= GET On Session api /api/auth/config + Should Be Equal As Integers ${response.status_code} 200 + + ${config}= Set Variable ${response.json()} + # Auth config structure may vary, just verify it's valid JSON + Should Be True isinstance($config, dict) + +Get Diarization Settings Test + [Documentation] Test getting diarization settings (admin only) + [Tags] system diarization admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + + ${response}= GET On Session api /api/diarization-settings headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${settings}= Set Variable ${response.json()} + Should Be True isinstance($settings, dict) + +Save Diarization Settings Test + [Documentation] Test saving diarization settings (admin only) + [Tags] system diarization admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} Content-Type=application/json + + # First get current settings + ${get_response}= GET On Session api /api/diarization-settings headers=${headers} + Should Be Equal As Integers ${get_response.status_code} 200 + ${current_settings}= Set Variable ${get_response.json()} + + # Save the same settings (should succeed) + ${response}= POST On Session api /api/diarization-settings json=${current_settings} headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + +Get Speaker Configuration Test + [Documentation] Test getting user's speaker configuration + [Tags] system speakers user positive + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= GET On Session api /api/speaker-configuration headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${config}= Set Variable ${response.json()} + Should Be True isinstance($config, (dict, list)) + +Update Speaker Configuration Test + [Documentation] Test updating user's speaker configuration + [Tags] system speakers user positive + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} Content-Type=application/json + + # Update with empty speaker list + ${speakers}= Create List + ${response}= POST On Session api /api/speaker-configuration json=${speakers} headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + +Get Enrolled Speakers Test + [Documentation] Test getting enrolled speakers from speaker recognition service + [Tags] system speakers service positive + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} + + # This might fail if speaker service is not available + ${response}= GET On Session api /api/enrolled-speakers headers=${headers} expected_status=any + Should Be True ${response.status_code} in [200, 503] + + IF ${response.status_code} == 200 + ${speakers}= Set Variable ${response.json()} + Should Be True isinstance($speakers, (dict, list)) + END + +Get Speaker Service Status Test + [Documentation] Test checking speaker recognition service status (admin only) + [Tags] system speakers service admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + + ${response}= GET On Session api /api/speaker-service-status headers=${headers} expected_status=any + Should Be True ${response.status_code} in [200, 503] + + ${status}= Set Variable ${response.json()} + Should Be True isinstance($status, dict) + +Get Memory Config Raw Test + [Documentation] Test getting raw memory configuration (admin only) + [Tags] system memory config admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + + ${response}= GET On Session api /api/admin/memory/config/raw headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + # Raw config should be text/yaml + ${config}= Set Variable ${response.text} + Should Not Be Empty ${config} + +Validate Memory Config Test + [Documentation] Test validating memory configuration YAML (admin only) + [Tags] system memory config validation admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} Content-Type=application/json + + # Test with valid YAML + ${valid_yaml}= Set Variable memory_provider: "friend_lite"\nextraction:\n enabled: true + &{data}= Create Dictionary config_yaml=${valid_yaml} + ${response}= POST On Session api /api/admin/memory/config/validate json=${data} headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + # Test with invalid YAML + ${invalid_yaml}= Set Variable invalid: yaml: structure: + &{data}= Create Dictionary config_yaml=${invalid_yaml} + ${response}= POST On Session api /api/admin/memory/config/validate json=${data} headers=${headers} expected_status=any + Should Be True ${response.status_code} in [400, 422] + +Reload Memory Config Test + [Documentation] Test reloading memory configuration (admin only) + [Tags] system memory config reload admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + + ${response}= POST On Session api /api/admin/memory/config/reload headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + +Delete All User Memories Test + [Documentation] Test deleting all memories for current user + [Tags] system memory delete user positive + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} + + ${response}= DELETE On Session api /api/admin/memory/delete-all headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${result}= Set Variable ${response.json()} + Dictionary Should Contain Key ${result} message + +List Processing Jobs Test + [Documentation] Test listing processing jobs (admin only) + [Tags] system processing jobs admin positive + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} + + ${response}= GET On Session api /api/process-audio-files/jobs headers=${headers} + Should Be Equal As Integers ${response.status_code} 200 + + ${jobs}= Set Variable ${response.json()} + Should Be True isinstance($jobs, (dict, list)) + +Non-Admin Cannot Access Admin Endpoints Test + [Documentation] Test that non-admin users cannot access admin endpoints + [Tags] system security negative + Setup Auth Session + + ${admin_token}= Get Admin Token + + # Create a non-admin user + ${test_user}= Create Test User ${admin_token} test-user-${RANDOM_ID}@example.com test-password-123 + ${user_token}= Get User Token test-user-${RANDOM_ID}@example.com test-password-123 + + &{headers}= Create Dictionary Authorization=Bearer ${user_token} + + # Test various admin endpoints + ${endpoints}= Create List + ... /api/diarization-settings + ... /api/speaker-service-status + ... /api/admin/memory/config/raw + ... /api/admin/memory/config/reload + ... /api/process-audio-files/jobs + + FOR ${endpoint} IN @{endpoints} + ${response}= GET On Session api ${endpoint} headers=${headers} expected_status=403 + Should Be Equal As Integers ${response.status_code} 403 + END + + # Cleanup + Delete Test User ${admin_token} ${test_user}[id] + +Unauthorized System Access Test + [Documentation] Test that system endpoints require authentication + [Tags] system security negative + Setup Auth Session + + # Test endpoints that require authentication + ${auth_endpoints}= Create List + ... /api/diarization-settings + ... /api/speaker-configuration + ... /api/enrolled-speakers + ... /api/admin/memory/delete-all + + FOR ${endpoint} IN @{auth_endpoints} + ${response}= GET On Session api ${endpoint} expected_status=401 + Should Be Equal As Integers ${response.status_code} 401 + END + +Invalid System Operations Test + [Documentation] Test invalid operations on system endpoints + [Tags] system negative validation + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} Content-Type=application/json + + # Test saving invalid diarization settings + ${invalid_settings}= Create Dictionary invalid_key=invalid_value + ${response}= POST On Session api /api/diarization-settings json=${invalid_settings} headers=${headers} expected_status=any + Should Be True ${response.status_code} in [400, 422] + + # Test updating memory config with invalid YAML + ${invalid_yaml}= Set Variable {invalid yaml content + &{data}= Create Dictionary config_yaml=${invalid_yaml} + ${response}= POST On Session api /api/admin/memory/config/raw json=${data} headers=${headers} expected_status=any + Should Be True ${response.status_code} in [400, 422] + +Memory Configuration Workflow Test + [Documentation] Test complete memory configuration workflow (admin only) + [Tags] system memory config workflow admin + Setup Auth Session + + ${admin_token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${admin_token} Content-Type=application/json + + # 1. Get current config + ${get_response}= GET On Session api /api/admin/memory/config/raw headers=${headers} + Should Be Equal As Integers ${get_response.status_code} 200 + ${original_config}= Set Variable ${get_response.text} + + # 2. Validate the current config + &{validate_data}= Create Dictionary config_yaml=${original_config} + ${validate_response}= POST On Session api /api/admin/memory/config/validate json=${validate_data} headers=${headers} + Should Be Equal As Integers ${validate_response.status_code} 200 + + # 3. Reload config (should succeed) + ${reload_response}= POST On Session api /api/admin/memory/config/reload headers=${headers} + Should Be Equal As Integers ${reload_response.status_code} 200 + +Speaker Configuration Workflow Test + [Documentation] Test complete speaker configuration workflow + [Tags] system speakers workflow user + Setup Auth Session + + ${token}= Get Admin Token + &{headers}= Create Dictionary Authorization=Bearer ${token} Content-Type=application/json + + # 1. Get current speaker configuration + ${get_response}= GET On Session api /api/speaker-configuration headers=${headers} + Should Be Equal As Integers ${get_response.status_code} 200 + ${current_config}= Set Variable ${get_response.json()} + + # 2. Update speaker configuration (with empty list) + ${empty_speakers}= Create List + ${update_response}= POST On Session api /api/speaker-configuration json=${empty_speakers} headers=${headers} + Should Be Equal As Integers ${update_response.status_code} 200 + + # 3. Verify the update + ${verify_response}= GET On Session api /api/speaker-configuration headers=${headers} + Should Be Equal As Integers ${verify_response.status_code} 200 + ${updated_config}= Set Variable ${verify_response.json()} + + # Should be empty list now + ${length}= Get Length ${updated_config} + Should Be Equal As Integers ${length} 0 + diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 00000000..8dc06185 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,61 @@ +""" +Test data for Robot Framework tests +""" + +# API Configuration +BASE_URL = "http://localhost:8001" +API_TIMEOUT = 30 + +# Test Users +ADMIN_USER = { + "email": "admin-test@example.com", + "password": "admin-test-password-123" +} + +TEST_USER = { + "email": "test@example.com", + "password": "test-password" +} + +# Test Data +SAMPLE_CONVERSATIONS = [ + { + "id": "conv_001", + "transcript": "This is a test conversation about AI development.", + "created_at": "2025-01-15T10:00:00Z" + }, + { + "id": "conv_002", + "transcript": "Another test conversation discussing machine learning.", + "created_at": "2025-01-15T11:00:00Z" + } +] + +SAMPLE_MEMORIES = [ + { + "text": "User prefers AI discussions in the morning", + "importance": 0.8 + }, + { + "text": "User is interested in machine learning applications", + "importance": 0.7 + } +] + +# API Endpoints +ENDPOINTS = { + "health": "/health", + "readiness": "/readiness", + "auth": "/auth/jwt/login", + "conversations": "/api/conversations", + "memories": "/api/memories", + "memory_search": "/api/memories/search", + "users": "/api/users" +} + +# Test Configuration +TEST_CONFIG = { + "retry_count": 3, + "retry_delay": 1, + "default_timeout": 30 +} \ No newline at end of file diff --git a/tests/test_env.py b/tests/test_env.py new file mode 100644 index 00000000..ddb0bafe --- /dev/null +++ b/tests/test_env.py @@ -0,0 +1,71 @@ +# Test Environment Configuration +import os +from dotenv import load_dotenv + +load_dotenv() + +# API Configuration +API_URL = os.getenv('API_URL', 'http://localhost:8001') +API_BASE = f"{API_URL}/api" + +# Admin user credentials (Robot Framework format) +ADMIN_USER = { + "email": os.getenv('ADMIN_EMAIL', 'test-admin@example.com'), + "password": os.getenv('ADMIN_PASSWORD', 'test-admin-password-123') +} + +# Individual variables for Robot Framework +ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'test-admin@example.com') +ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'test-admin-password-123') + +TEST_USER = { + "email": "test@example.com", + "password": "test-password" +} + +# Individual variables for Robot Framework +TEST_USER_EMAIL = "test@example.com" +TEST_USER_PASSWORD = "test-password" + +# Test Data +SAMPLE_CONVERSATIONS = [ + { + "id": "conv_001", + "transcript": "This is a test conversation about AI development.", + "created_at": "2025-01-15T10:00:00Z" + }, + { + "id": "conv_002", + "transcript": "Another test conversation discussing machine learning.", + "created_at": "2025-01-15T11:00:00Z" + } +] + +SAMPLE_MEMORIES = [ + { + "text": "User prefers AI discussions in the morning", + "importance": 0.8 + }, + { + "text": "User is interested in machine learning applications", + "importance": 0.7 + } +] + +# API Endpoints +ENDPOINTS = { + "health": "/health", + "readiness": "/readiness", + "auth": "/auth/jwt/login", + "conversations": "/api/conversations", + "memories": "/api/memories", + "memory_search": "/api/memories/search", + "users": "/api/users" +} + +# Test Configuration +TEST_CONFIG = { + "retry_count": 3, + "retry_delay": 1, + "default_timeout": 30 +} \ No newline at end of file