""" 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 @patch('app.make_api_request') @patch('models.ChatSession.save_message') @patch('models.ChatSession.get_session_context') 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]) @patch('models.ChatSession.get_user_history') 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]) @patch('app.make_api_request') 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)