""" Integration tests for Authentication and Linking Flow Tests the complete authentication flow including: - Linking Telegram accounts to Oracle accounts - Token management and refresh - User verification - Account unlinking """ import pytest import pytest_asyncio import sys import aiosqlite import secrets import string from pathlib import Path from unittest.mock import AsyncMock, Mock, patch from datetime import datetime, timedelta # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) from app.auth.linking import ( link_telegram_account, get_user_auth_data, check_user_linked, get_user_companies, unlink_user ) from app.db.database import init_database, DB_PATH from app.db.operations import ( create_auth_code, create_or_update_user, get_user ) # ============================================================================ # HELPER FUNCTIONS # ============================================================================ def generate_auth_code() -> str: """Generate an 8-character auth code""" chars = string.ascii_uppercase + string.digits chars = chars.replace('O', '').replace('0', '').replace('I', '').replace('1', '') return ''.join(secrets.choice(chars) for _ in range(8)) # ============================================================================ # FIXTURES # ============================================================================ @pytest_asyncio.fixture async def clean_test_database(): """Create a clean test database before each test""" await init_database() async with aiosqlite.connect(DB_PATH) as db: await db.execute("DELETE FROM telegram_sessions") await db.execute("DELETE FROM telegram_auth_codes") await db.execute("DELETE FROM telegram_users") await db.commit() yield @pytest.fixture def mock_telegram_user(): """Mock Telegram User object""" user = Mock() user.id = 123456789 user.username = "testuser" user.first_name = "Test" user.last_name = "User" return user @pytest.fixture def mock_oracle_username(): """Mock Oracle username""" return "test_oracle_user" @pytest.fixture def mock_jwt_token(): """Mock JWT token""" return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.token" @pytest.fixture def mock_companies_data(): """Mock companies list""" return [ {"id": 1, "nume_firma": "Test Company SRL", "cui": "12345678"}, {"id": 2, "nume_firma": "Another Corp SRL", "cui": "87654321"} ] @pytest.fixture def mock_backend_verify_response(mock_jwt_token, mock_companies_data): """Mock backend verify user response""" return { "jwt_token": mock_jwt_token, "jwt_refresh_token": f"{mock_jwt_token}_refresh", "companies": mock_companies_data, "permissions": ["read", "write"] } async def create_test_auth_code(telegram_user_id: int, oracle_username: str) -> str: """Helper to create auth code for tests""" code = generate_auth_code() await create_auth_code(code, telegram_user_id, oracle_username) return code # ============================================================================ # TEST: link_telegram_account # ============================================================================ @pytest.mark.asyncio async def test_link_telegram_account_success( clean_test_database, mock_telegram_user, mock_oracle_username, mock_backend_verify_response ): """Test successful linking of Telegram account to Oracle account""" code = await create_test_auth_code(mock_telegram_user.id, mock_oracle_username) with patch('app.auth.linking.get_backend_client') as mock_get_client: mock_client = AsyncMock() mock_client.verify_user.return_value = mock_backend_verify_response mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None mock_get_client.return_value = mock_client result = await link_telegram_account(mock_telegram_user, code) assert result is not None assert result["success"] is True assert result["telegram_user_id"] == mock_telegram_user.id assert result["username"] == mock_oracle_username mock_client.verify_user.assert_called_once_with(mock_oracle_username) @pytest.mark.asyncio async def test_link_telegram_account_invalid_code( clean_test_database, mock_telegram_user ): """Test linking with invalid auth code""" result = await link_telegram_account(mock_telegram_user, "INVALID1") assert result is None @pytest.mark.asyncio async def test_link_telegram_account_expired_code( clean_test_database, mock_telegram_user, mock_oracle_username ): """Test linking with expired auth code""" code = await create_test_auth_code(mock_telegram_user.id, mock_oracle_username) # Manually expire the code async with aiosqlite.connect(DB_PATH) as db: expired_time = datetime.now() - timedelta(minutes=20) await db.execute(""" UPDATE telegram_auth_codes SET expires_at = ? WHERE code = ? """, (expired_time.isoformat(), code)) await db.commit() result = await link_telegram_account(mock_telegram_user, code) assert result is None # ============================================================================ # TEST: get_user_auth_data # ============================================================================ @pytest.mark.asyncio async def test_get_user_auth_data_success( clean_test_database, mock_telegram_user, mock_oracle_username, mock_backend_verify_response, mock_companies_data ): """Test getting auth data for linked user""" code = await create_test_auth_code(mock_telegram_user.id, mock_oracle_username) with patch('app.auth.linking.get_backend_client') as mock_get_client: mock_client = AsyncMock() mock_client.verify_user.return_value = mock_backend_verify_response mock_client.get_user_companies.return_value = mock_companies_data mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None mock_get_client.return_value = mock_client await link_telegram_account(mock_telegram_user, code) auth_data = await get_user_auth_data(mock_telegram_user.id) assert auth_data is not None assert auth_data["username"] == mock_oracle_username assert len(auth_data["companies"]) == 2 @pytest.mark.asyncio async def test_get_user_auth_data_not_linked( clean_test_database, mock_telegram_user ): """Test getting auth data for user who is not linked""" await create_or_update_user( telegram_user_id=mock_telegram_user.id, username=mock_telegram_user.username, first_name=mock_telegram_user.first_name, last_name=mock_telegram_user.last_name ) auth_data = await get_user_auth_data(mock_telegram_user.id) assert auth_data is None # ============================================================================ # TEST: check_user_linked # ============================================================================ @pytest.mark.asyncio async def test_check_user_linked_true( clean_test_database, mock_telegram_user, mock_oracle_username, mock_backend_verify_response ): """Test checking if user is linked (linked user)""" code = await create_test_auth_code(mock_telegram_user.id, mock_oracle_username) with patch('app.auth.linking.get_backend_client') as mock_get_client: mock_client = AsyncMock() mock_client.verify_user.return_value = mock_backend_verify_response mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None mock_get_client.return_value = mock_client await link_telegram_account(mock_telegram_user, code) is_linked = await check_user_linked(mock_telegram_user.id) assert is_linked is True @pytest.mark.asyncio async def test_check_user_linked_false( clean_test_database, mock_telegram_user ): """Test checking if user is linked (not linked user)""" await create_or_update_user( telegram_user_id=mock_telegram_user.id, username=mock_telegram_user.username, first_name=mock_telegram_user.first_name, last_name=mock_telegram_user.last_name ) is_linked = await check_user_linked(mock_telegram_user.id) assert is_linked is False # ============================================================================ # TEST: get_user_companies # ============================================================================ @pytest.mark.asyncio async def test_get_user_companies_success( clean_test_database, mock_telegram_user, mock_oracle_username, mock_backend_verify_response, mock_companies_data ): """Test getting companies for linked user""" code = await create_test_auth_code(mock_telegram_user.id, mock_oracle_username) with patch('app.auth.linking.get_backend_client') as mock_get_client: mock_client = AsyncMock() mock_client.verify_user.return_value = mock_backend_verify_response mock_client.get_user_companies.return_value = mock_companies_data mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None mock_get_client.return_value = mock_client await link_telegram_account(mock_telegram_user, code) companies = await get_user_companies(mock_telegram_user.id) assert companies is not None assert len(companies) == 2 # ============================================================================ # TEST: unlink_user # ============================================================================ @pytest.mark.asyncio async def test_unlink_user_success( clean_test_database, mock_telegram_user, mock_oracle_username, mock_backend_verify_response ): """Test unlinking a linked user""" code = await create_test_auth_code(mock_telegram_user.id, mock_oracle_username) with patch('app.auth.linking.get_backend_client') as mock_get_client: mock_client = AsyncMock() mock_client.verify_user.return_value = mock_backend_verify_response mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None mock_get_client.return_value = mock_client await link_telegram_account(mock_telegram_user, code) # Verify linked assert await check_user_linked(mock_telegram_user.id) is True # Unlink result = await unlink_user(mock_telegram_user.id) assert result is True # Verify unlinked assert await check_user_linked(mock_telegram_user.id) is False # ============================================================================ # TEST: Complete Integration Workflow # ============================================================================ @pytest.mark.asyncio async def test_complete_auth_workflow( clean_test_database, mock_telegram_user, mock_oracle_username, mock_backend_verify_response, mock_companies_data ): """Test complete authentication workflow from start to finish""" with patch('app.auth.linking.get_backend_client') as mock_get_client: mock_client = AsyncMock() mock_client.verify_user.return_value = mock_backend_verify_response mock_client.get_user_companies.return_value = mock_companies_data mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None mock_get_client.return_value = mock_client # Step 1: User is not linked initially assert await check_user_linked(mock_telegram_user.id) is False # Step 2: Create auth code and link code = await create_test_auth_code(mock_telegram_user.id, mock_oracle_username) link_result = await link_telegram_account(mock_telegram_user, code) assert link_result["success"] is True # Step 3: User is now linked assert await check_user_linked(mock_telegram_user.id) is True # Step 4: Get auth data auth_data = await get_user_auth_data(mock_telegram_user.id) assert auth_data["username"] == mock_oracle_username # Step 5: Get companies companies = await get_user_companies(mock_telegram_user.id) assert len(companies) == 2 # Step 6: Unlink account assert await unlink_user(mock_telegram_user.id) is True # Step 7: User is no longer linked assert await check_user_linked(mock_telegram_user.id) is False if __name__ == "__main__": pytest.main([__file__, "-v"])