Spaces:
Sleeping
Sleeping
| """ | |
| Integration tests for authentication flows | |
| Tests complete registration → login → logout cycle, route protection, | |
| redirect behavior, chat integration with authenticated users, and error scenarios. | |
| """ | |
| import unittest | |
| from unittest.mock import Mock, patch, MagicMock | |
| from datetime import datetime | |
| from bson import ObjectId | |
| from flask import Flask, url_for | |
| from flask_login import current_user | |
| import json | |
| # Import the Flask app and models | |
| from app import app | |
| from models import User, ChatSession | |
| from database import get_users_collection, get_chat_sessions_collection | |
| class TestAuthenticationFlows(unittest.TestCase): | |
| """Integration tests for complete authentication flows""" | |
| def setUp(self): | |
| """Set up test client and mock database""" | |
| self.app = app | |
| self.app.config['TESTING'] = True | |
| self.app.config['WTF_CSRF_ENABLED'] = False # Disable CSRF for testing | |
| self.app.config['SECRET_KEY'] = 'test-secret-key' | |
| self.client = self.app.test_client() | |
| # Sample user data for testing | |
| self.test_user_data = { | |
| '_id': ObjectId(), | |
| 'email': 'test@example.com', | |
| 'password_hash': 'hashed_password', | |
| 'created_at': datetime.utcnow(), | |
| 'is_active': True | |
| } | |
| self.test_user = User(self.test_user_data) | |
| def test_complete_registration_login_logout_cycle(self): | |
| """Test complete user journey: registration → login → logout""" | |
| # Mock database operations for registration | |
| with patch('models.get_users_collection') as mock_users_collection, \ | |
| patch('models.User.find_by_email') as mock_find_by_email, \ | |
| patch('models.User.validate_email_format', return_value=True), \ | |
| patch('models.User.validate_password_strength', return_value=True): | |
| # Setup mocks for registration | |
| mock_collection = Mock() | |
| mock_users_collection.return_value = mock_collection | |
| mock_find_by_email.return_value = None # User doesn't exist initially | |
| mock_collection.insert_one.return_value = Mock(inserted_id=self.test_user_data['_id']) | |
| mock_collection.find_one.return_value = self.test_user_data | |
| # Step 1: Registration | |
| registration_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'password123', | |
| 'confirm_password': 'password123' | |
| } | |
| response = self.client.post('/register', data=registration_data, follow_redirects=False) | |
| # Should redirect to login page after successful registration | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/login', response.location) | |
| # Step 2: Login | |
| with patch('models.User.find_by_email') as mock_find_login, \ | |
| patch.object(User, 'check_password', return_value=True): | |
| mock_find_login.return_value = self.test_user | |
| login_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'password123' | |
| } | |
| response = self.client.post('/login', data=login_data, follow_redirects=False) | |
| # Should redirect to chat interface after successful login | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/chat', response.location) | |
| # Step 3: Access protected route (should work now) | |
| response = self.client.get('/chat') | |
| self.assertEqual(response.status_code, 200) | |
| # Step 4: Logout | |
| response = self.client.post('/logout', follow_redirects=False) | |
| # Should redirect to login page after logout | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/login', response.location) | |
| # Step 5: Try to access protected route after logout (should redirect) | |
| response = self.client.get('/chat', follow_redirects=False) | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/login', response.location) | |
| def test_registration_with_existing_email(self): | |
| """Test registration attempt with already existing email""" | |
| with patch('models.User.find_by_email') as mock_find_by_email, \ | |
| patch('models.User.validate_email_format', return_value=True), \ | |
| patch('models.User.validate_password_strength', return_value=True): | |
| # User already exists | |
| mock_find_by_email.return_value = self.test_user | |
| registration_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'password123', | |
| 'confirm_password': 'password123' | |
| } | |
| response = self.client.post('/register', data=registration_data) | |
| # Should stay on registration page and show error | |
| self.assertEqual(response.status_code, 200) | |
| self.assertIn(b'Email already registered', response.data) | |
| def test_registration_with_password_mismatch(self): | |
| """Test registration with mismatched passwords""" | |
| registration_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'password123', | |
| 'confirm_password': 'different_password' | |
| } | |
| response = self.client.post('/register', data=registration_data) | |
| # Should stay on registration page and show error | |
| self.assertEqual(response.status_code, 200) | |
| self.assertIn(b'Passwords do not match', response.data) | |
| def test_registration_with_weak_password(self): | |
| """Test registration with weak password""" | |
| registration_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'weak', # Too short | |
| 'confirm_password': 'weak' | |
| } | |
| response = self.client.post('/register', data=registration_data) | |
| # Should stay on registration page and show error | |
| self.assertEqual(response.status_code, 200) | |
| self.assertIn(b'Password must be at least 8 characters long', response.data) | |
| def test_login_with_invalid_credentials(self): | |
| """Test login with invalid email/password combination""" | |
| with patch('models.User.find_by_email') as mock_find_by_email: | |
| mock_find_by_email.return_value = None # User doesn't exist | |
| login_data = { | |
| 'email': 'nonexistent@example.com', | |
| 'password': 'password123' | |
| } | |
| response = self.client.post('/login', data=login_data) | |
| # Should stay on login page and show error | |
| self.assertEqual(response.status_code, 200) | |
| self.assertIn(b'Invalid email or password', response.data) | |
| def test_login_with_wrong_password(self): | |
| """Test login with correct email but wrong password""" | |
| with patch('models.User.find_by_email') as mock_find_by_email, \ | |
| patch.object(User, 'check_password', return_value=False): | |
| mock_find_by_email.return_value = self.test_user | |
| login_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'wrongpassword' | |
| } | |
| response = self.client.post('/login', data=login_data) | |
| # Should stay on login page and show error | |
| self.assertEqual(response.status_code, 200) | |
| self.assertIn(b'Invalid email or password', response.data) | |
| def test_login_with_inactive_user(self): | |
| """Test login attempt with inactive user account""" | |
| # Create inactive user | |
| inactive_user_data = self.test_user_data.copy() | |
| inactive_user_data['is_active'] = False | |
| inactive_user = User(inactive_user_data) | |
| with patch('models.User.find_by_email') as mock_find_by_email: | |
| mock_find_by_email.return_value = inactive_user | |
| login_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'password123' | |
| } | |
| response = self.client.post('/login', data=login_data) | |
| # Should stay on login page and show error | |
| self.assertEqual(response.status_code, 200) | |
| self.assertIn(b'disabled', response.data) | |
| class TestRouteProtection(unittest.TestCase): | |
| """Test route protection and redirect behavior""" | |
| def setUp(self): | |
| """Set up test client""" | |
| self.app = app | |
| self.app.config['TESTING'] = True | |
| self.app.config['WTF_CSRF_ENABLED'] = False | |
| self.app.config['SECRET_KEY'] = 'test-secret-key' | |
| self.client = self.app.test_client() | |
| self.test_user_data = { | |
| '_id': ObjectId(), | |
| 'email': 'test@example.com', | |
| 'password_hash': 'hashed_password', | |
| 'created_at': datetime.utcnow(), | |
| 'is_active': True | |
| } | |
| self.test_user = User(self.test_user_data) | |
| def test_unauthenticated_access_to_protected_routes(self): | |
| """Test that unauthenticated users are redirected from protected routes""" | |
| protected_routes = [ | |
| ('/chat', 'GET'), | |
| ('/api/chat', 'POST'), | |
| ('/api/user-history', 'GET') | |
| ] | |
| for route, method in protected_routes: | |
| with self.subTest(route=route, method=method): | |
| if method == 'POST': | |
| response = self.client.post(route, json={'message': 'test'}) | |
| else: | |
| response = self.client.get(route, follow_redirects=False) | |
| if route.startswith('/api/'): | |
| # API routes should return JSON error or redirect | |
| self.assertIn(response.status_code, [401, 302]) | |
| else: | |
| # Regular routes should redirect | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/login', response.location) | |
| def test_authenticated_access_to_protected_routes(self): | |
| """Test that authenticated users can access protected routes""" | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| # Mock the user loader to return our test user | |
| with patch('auth.load_user', return_value=self.test_user): | |
| response = self.client.get('/chat') | |
| self.assertEqual(response.status_code, 200) | |
| def test_root_route_redirect_behavior(self): | |
| """Test root route redirects based on authentication status""" | |
| # Test unauthenticated access to root | |
| response = self.client.get('/', follow_redirects=False) | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/login', response.location) | |
| # Test authenticated access to root | |
| with patch('auth.load_user', return_value=self.test_user): | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| response = self.client.get('/', follow_redirects=False) | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/chat', response.location) | |
| def test_login_page_redirect_when_authenticated(self): | |
| """Test that authenticated users are redirected away from login page""" | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Try to access login page while authenticated | |
| response = self.client.get('/login', follow_redirects=False) | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/chat', response.location) | |
| def test_register_page_redirect_when_authenticated(self): | |
| """Test that authenticated users are redirected away from register page""" | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Try to access register page while authenticated | |
| response = self.client.get('/register', follow_redirects=False) | |
| self.assertEqual(response.status_code, 302) | |
| self.assertIn('/chat', response.location) | |
| class TestChatIntegration(unittest.TestCase): | |
| """Test chat functionality integration with authentication""" | |
| def setUp(self): | |
| """Set up test client and authenticated session""" | |
| self.app = app | |
| self.app.config['TESTING'] = True | |
| self.app.config['WTF_CSRF_ENABLED'] = False | |
| self.app.config['SECRET_KEY'] = 'test-secret-key' | |
| self.client = self.app.test_client() | |
| self.test_user_data = { | |
| '_id': ObjectId(), | |
| 'email': 'test@example.com', | |
| 'password_hash': 'hashed_password', | |
| 'created_at': datetime.utcnow(), | |
| 'is_active': True | |
| } | |
| self.test_user = User(self.test_user_data) | |
| def login_user(self): | |
| """Helper method to login test user""" | |
| with self.client.session_transaction() as sess: | |
| # Manually set up the session for testing | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| # Also patch the user loader to return our test user | |
| with patch('auth.load_user', return_value=self.test_user): | |
| return True | |
| def test_authenticated_chat_message_flow(self, mock_get_context, mock_save_message, mock_api_request): | |
| """Test complete chat message flow with authenticated user""" | |
| # Setup mocks | |
| mock_get_context.return_value = [] # No previous history | |
| mock_api_request.return_value = { | |
| 'success': True, | |
| 'response': 'This is a test response from the API', | |
| 'error': None | |
| } | |
| mock_save_message.return_value = True | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Send chat message | |
| chat_data = { | |
| 'message': 'Hello, this is a test message' | |
| } | |
| response = self.client.post( | |
| '/api/chat', | |
| data=json.dumps(chat_data), | |
| content_type='application/json' | |
| ) | |
| # Verify response | |
| self.assertEqual(response.status_code, 200) | |
| data = response.get_json() | |
| self.assertTrue(data['success']) | |
| self.assertEqual(data['response'], 'This is a test response from the API') | |
| self.assertEqual(data['user_id'], str(self.test_user_data['_id'])) | |
| # Verify API was called with user context | |
| mock_get_context.assert_called_once_with(str(self.test_user_data['_id']), limit=10) | |
| mock_api_request.assert_called_once_with('Hello, this is a test message', []) | |
| # Verify message was saved with user_id | |
| mock_save_message.assert_called_once() | |
| save_call_args = mock_save_message.call_args | |
| self.assertEqual(save_call_args[1]['user_id'], str(self.test_user_data['_id'])) | |
| self.assertEqual(save_call_args[1]['message'], 'Hello, this is a test message') | |
| self.assertEqual(save_call_args[1]['response'], 'This is a test response from the API') | |
| def test_unauthenticated_chat_request(self): | |
| """Test chat request without authentication""" | |
| chat_data = { | |
| 'message': 'Hello, this should fail' | |
| } | |
| response = self.client.post( | |
| '/api/chat', | |
| data=json.dumps(chat_data), | |
| content_type='application/json' | |
| ) | |
| # Should return 401 or redirect | |
| self.assertIn(response.status_code, [401, 302]) | |
| def test_user_history_retrieval(self, mock_get_history): | |
| """Test authenticated user history retrieval""" | |
| # Setup mock history | |
| mock_history = [ | |
| { | |
| 'id': str(ObjectId()), | |
| 'message': 'Previous message', | |
| 'response': 'Previous response', | |
| 'timestamp': datetime.utcnow(), | |
| 'session_data': {} | |
| } | |
| ] | |
| mock_get_history.return_value = mock_history | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Get user history | |
| response = self.client.get('/api/user-history') | |
| self.assertEqual(response.status_code, 200) | |
| data = response.get_json() | |
| self.assertTrue(data['success']) | |
| self.assertEqual(len(data['history']), 1) | |
| self.assertEqual(data['history'][0]['message'], 'Previous message') | |
| self.assertEqual(data['user_id'], str(self.test_user_data['_id'])) | |
| # Verify correct user_id was used | |
| mock_get_history.assert_called_once_with(str(self.test_user_data['_id']), limit=50) | |
| def test_unauthenticated_history_request(self): | |
| """Test history request without authentication""" | |
| response = self.client.get('/api/user-history') | |
| # Should return 401 or redirect | |
| self.assertIn(response.status_code, [401, 302]) | |
| def test_chat_with_api_error(self, mock_api_request): | |
| """Test chat flow when external API returns error""" | |
| # Setup API error | |
| mock_api_request.return_value = { | |
| 'success': False, | |
| 'response': '', | |
| 'error': 'API request failed: Connection timeout' | |
| } | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Send chat message | |
| chat_data = { | |
| 'message': 'This will cause an API error' | |
| } | |
| response = self.client.post( | |
| '/api/chat', | |
| data=json.dumps(chat_data), | |
| content_type='application/json' | |
| ) | |
| self.assertEqual(response.status_code, 200) | |
| data = response.get_json() | |
| self.assertFalse(data['success']) | |
| self.assertIn('request took too long', data['error']) # Enhanced error message | |
| def test_chat_with_empty_message(self): | |
| """Test chat request with empty message""" | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Send empty message | |
| chat_data = { | |
| 'message': '' | |
| } | |
| response = self.client.post( | |
| '/api/chat', | |
| data=json.dumps(chat_data), | |
| content_type='application/json' | |
| ) | |
| self.assertEqual(response.status_code, 400) | |
| data = response.get_json() | |
| self.assertFalse(data['success']) | |
| self.assertIn('cannot be empty', data['error']) | |
| def test_chat_with_oversized_message(self): | |
| """Test chat request with message that's too long""" | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Send oversized message | |
| chat_data = { | |
| 'message': 'x' * 5001 # Over 5000 character limit | |
| } | |
| response = self.client.post( | |
| '/api/chat', | |
| data=json.dumps(chat_data), | |
| content_type='application/json' | |
| ) | |
| self.assertEqual(response.status_code, 400) | |
| data = response.get_json() | |
| self.assertFalse(data['success']) | |
| self.assertIn('too long', data['error']) | |
| class TestErrorScenarios(unittest.TestCase): | |
| """Test error scenarios and edge cases""" | |
| def setUp(self): | |
| """Set up test client""" | |
| self.app = app | |
| self.app.config['TESTING'] = True | |
| self.app.config['WTF_CSRF_ENABLED'] = False | |
| self.app.config['SECRET_KEY'] = 'test-secret-key' | |
| self.client = self.app.test_client() | |
| def test_database_connection_error_during_login(self): | |
| """Test login behavior when database is unavailable""" | |
| with patch('models.User.find_by_email') as mock_find_by_email: | |
| mock_find_by_email.side_effect = Exception("Database connection failed") | |
| login_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'password123' | |
| } | |
| response = self.client.post('/login', data=login_data) | |
| # Should handle error gracefully | |
| self.assertEqual(response.status_code, 200) | |
| self.assertIn(b'We're experiencing technical difficulties', response.data) | |
| def test_database_connection_error_during_registration(self): | |
| """Test registration behavior when database is unavailable""" | |
| with patch('models.User.create_user') as mock_create_user: | |
| mock_create_user.side_effect = Exception("Database connection failed") | |
| registration_data = { | |
| 'email': 'test@example.com', | |
| 'password': 'password123', | |
| 'confirm_password': 'password123' | |
| } | |
| response = self.client.post('/register', data=registration_data) | |
| # Should handle error gracefully | |
| self.assertEqual(response.status_code, 200) | |
| self.assertIn(b'We're experiencing technical difficulties', response.data) | |
| def test_session_expiry_during_chat(self): | |
| """Test chat behavior when session expires mid-conversation""" | |
| # This would require more complex session manipulation | |
| # For now, test that unauthenticated requests are handled properly | |
| chat_data = { | |
| 'message': 'This should fail due to no session' | |
| } | |
| response = self.client.post( | |
| '/api/chat', | |
| data=json.dumps(chat_data), | |
| content_type='application/json' | |
| ) | |
| # Should return authentication error | |
| self.assertIn(response.status_code, [401, 302]) | |
| def test_malformed_json_in_chat_request(self): | |
| """Test chat API with malformed JSON""" | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Send malformed JSON | |
| response = self.client.post( | |
| '/api/chat', | |
| data='{"malformed": json}', # Invalid JSON | |
| content_type='application/json' | |
| ) | |
| # Should handle JSON parsing error | |
| self.assertEqual(response.status_code, 400) | |
| def test_missing_message_field_in_chat_request(self): | |
| """Test chat API with missing message field""" | |
| # Set up authenticated session | |
| with self.client.session_transaction() as sess: | |
| sess['_user_id'] = str(self.test_user_data['_id']) | |
| sess['_fresh'] = True | |
| with patch('auth.load_user', return_value=self.test_user): | |
| # Send request without message field | |
| response = self.client.post( | |
| '/api/chat', | |
| data=json.dumps({'not_message': 'test'}), | |
| content_type='application/json' | |
| ) | |
| self.assertEqual(response.status_code, 400) | |
| data = response.get_json() | |
| self.assertFalse(data['success']) | |
| self.assertIn('provide a message', data['error']) | |
| if __name__ == '__main__': | |
| # Run the tests | |
| unittest.main(verbosity=2) |