| """ |
| API-Level Tests for Mobile Endpoints. |
| |
| Tests verify mobile workflow endpoints via API contract testing: |
| - Mobile agent spawn endpoint |
| - Mobile navigation endpoint |
| - Mobile device capabilities endpoint |
| |
| Run with: pytest backend/tests/e2e_api/test_mobile_endpoints.py -v |
| |
| Note: Detox E2E is BLOCKED by expo-dev-client requirement (see RESEARCH.md Pitfall 1). |
| These API-level tests satisfy ROADMAP success criteria #2 for mobile workflows |
| via contract testing rather than full UI automation. |
| """ |
|
|
| import pytest |
| import uuid |
| from httpx import AsyncClient, ASGITransport |
| from sqlalchemy.orm import Session |
|
|
| |
| import os |
| import sys |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| from main import app |
| from core.models import User, AgentRegistry |
| from core.auth import get_password_hash |
| from datetime import datetime |
|
|
|
|
| |
| |
| |
|
|
| def create_test_user(db_session: Session, email: str, password: str) -> User: |
| """Create a test user in the database. |
| |
| Args: |
| db_session: Database session |
| email: User email |
| password: Plain text password (will be hashed) |
| |
| Returns: |
| User: Created user instance |
| """ |
| user = User( |
| email=email, |
| username=f"mobileapi_{str(uuid.uuid4())[:8]}", |
| password_hash=get_password_hash(password), |
| is_active=True, |
| status="active", |
| email_verified=True, |
| created_at=datetime.utcnow() |
| ) |
|
|
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| return user |
|
|
|
|
| def cleanup_test_agent(db_session: Session, agent_id: str): |
| """Cleanup test agent after test. |
| |
| Args: |
| db_session: Database session |
| agent_id: Agent ID to delete |
| """ |
| try: |
| agent = db_session.query(AgentRegistry).filter( |
| AgentRegistry.id == agent_id |
| ).first() |
|
|
| if agent: |
| db_session.delete(agent) |
| db_session.commit() |
| except Exception as e: |
| |
| print(f"Warning: Failed to cleanup agent {agent_id}: {e}") |
|
|
|
|
| def cleanup_test_user(db_session: Session, user_id: str): |
| """Cleanup test user after test. |
| |
| Args: |
| db_session: Database session |
| user_id: User ID to delete |
| """ |
| try: |
| user = db_session.query(User).filter( |
| User.id == user_id |
| ).first() |
|
|
| if user: |
| db_session.delete(user) |
| db_session.commit() |
| except Exception as e: |
| |
| print(f"Warning: Failed to cleanup user {user_id}: {e}") |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.e2e |
| async def test_mobile_agent_spawn_api(async_client: AsyncClient, db_session: Session): |
| """Test mobile agent spawn endpoint. |
| |
| This test verifies: |
| 1. POST /api/v1/mobile/agents with valid data |
| 2. Assert 201 response with agent_id in response |
| 3. Verify agent created via GET /api/v1/agents/{agent_id} |
| 4. Cleanup: DELETE /api/v1/agents/{agent_id} |
| |
| Args: |
| async_client: HTTPX async client fixture |
| db_session: Database session fixture |
| """ |
| |
| unique_id = str(uuid.uuid4())[:8] |
| user = create_test_user( |
| db_session, |
| f"mobile_spawn_{unique_id}@test.com", |
| "MobileTest123!" |
| ) |
|
|
| |
| agent_data = { |
| "name": f"MobileTestAgent_{unique_id}", |
| "maturity": "INTERN", |
| "user_id": str(user.id) |
| } |
|
|
| try: |
| |
| response = await async_client.post("/api/v1/mobile/agents", json=agent_data) |
|
|
| |
| if response.status_code == 404: |
| response = await async_client.post("/api/v1/agents/spawn", json=agent_data) |
|
|
| |
| assert response.status_code in [200, 201], f"Expected 200/201, got {response.status_code}: {response.text}" |
|
|
| response_data = response.json() |
| assert "agent_id" in response_data or "id" in response_data, "Response should contain agent_id" |
|
|
| agent_id = response_data.get("agent_id") or response_data.get("id") |
| assert agent_id is not None, "Agent ID should not be None" |
|
|
| |
| get_response = await async_client.get(f"/api/v1/agents/{agent_id}") |
|
|
| |
| if get_response.status_code == 200: |
| agent_data = get_response.json() |
| assert agent_data.get("id") == agent_id or agent_data.get("agent_id") == agent_id |
|
|
| |
| delete_response = await async_client.delete(f"/api/v1/agents/{agent_id}") |
|
|
| |
| assert delete_response.status_code in [200, 204, 404], \ |
| f"Delete failed: {delete_response.status_code}" |
|
|
| except Exception as e: |
| |
| pytest.skip(f"Mobile agent spawn endpoint not implemented: {e}") |
|
|
| finally: |
| |
| cleanup_test_user(db_session, str(user.id)) |
|
|
|
|
| @pytest.mark.e2e |
| async def test_mobile_agent_spawn_validation(async_client: AsyncClient, db_session: Session): |
| """Test mobile agent spawn validation. |
| |
| This test verifies: |
| 1. POST with missing required fields returns 422 |
| 2. POST with invalid maturity returns 400 |
| 3. Validation error messages are descriptive |
| |
| Args: |
| async_client: HTTPX async client fixture |
| db_session: Database session fixture |
| """ |
| try: |
| |
| response = await async_client.post("/api/v1/mobile/agents", json={}) |
|
|
| |
| if response.status_code == 404: |
| pytest.skip("Mobile endpoint not implemented") |
|
|
| assert response.status_code in [400, 422], f"Expected 400/422, got {response.status_code}" |
|
|
| |
| invalid_data = { |
| "name": "InvalidAgent", |
| "maturity": "INVALID_MATURITY" |
| } |
|
|
| response = await async_client.post("/api/v1/mobile/agents", json=invalid_data) |
|
|
| if response.status_code != 404: |
| assert response.status_code in [400, 422], f"Expected 400/422, got {response.status_code}" |
|
|
| except Exception as e: |
| pytest.skip(f"Mobile endpoint not implemented: {e}") |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.e2e |
| async def test_mobile_navigation_api(async_client: AsyncClient, db_session: Session): |
| """Test mobile navigation endpoint. |
| |
| This test verifies: |
| 1. POST /api/v1/mobile/navigation with screen and params |
| 2. Assert 200 response with navigation_route in response |
| 3. Test navigation history: GET /api/v1/mobile/navigation/history |
| 4. Assert route contains screen and params |
| |
| Args: |
| async_client: HTTPX async client fixture |
| db_session: Database session fixture |
| """ |
| |
| unique_id = str(uuid.uuid4())[:8] |
| user = create_test_user( |
| db_session, |
| f"mobile_nav_{unique_id}@test.com", |
| "MobileTest123!" |
| ) |
|
|
| |
| navigation_data = { |
| "screen": "canvas", |
| "params": { |
| "canvas_id": f"test-{unique_id}" |
| } |
| } |
|
|
| try: |
| |
| response = await async_client.post("/api/v1/mobile/navigation", json=navigation_data) |
|
|
| |
| if response.status_code == 404: |
| pytest.skip("Mobile navigation endpoint not implemented") |
|
|
| assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" |
|
|
| response_data = response.json() |
| assert "navigation_route" in response_data or "route" in response_data, \ |
| "Response should contain navigation_route" |
|
|
| |
| history_response = await async_client.get("/api/v1/mobile/navigation/history") |
|
|
| if history_response.status_code == 200: |
| history_data = history_response.json() |
| assert isinstance(history_data, list) or "history" in history_data, \ |
| "History should be list or contain history key" |
|
|
| |
| if isinstance(history_data, list): |
| assert len(history_data) > 0, "History should not be empty" |
| |
| found = any( |
| nav.get("screen") == "canvas" or |
| nav.get("params", {}).get("canvas_id") == f"test-{unique_id}" |
| for nav in history_data |
| ) |
| |
| else: |
| history = history_data.get("history", []) |
| assert isinstance(history, list) |
|
|
| except Exception as e: |
| pytest.skip(f"Mobile navigation endpoint not implemented: {e}") |
|
|
| finally: |
| |
| cleanup_test_user(db_session, str(user.id)) |
|
|
|
|
| @pytest.mark.e2e |
| async def test_mobile_navigation_back(async_client: AsyncClient): |
| """Test mobile navigation back button. |
| |
| This test verifies: |
| 1. POST /api/v1/mobile/navigation/back |
| 2. Assert 200 response with previous route |
| 3. Verify back stack management |
| |
| Args: |
| async_client: HTTPX async client fixture |
| """ |
| try: |
| |
| response = await async_client.post("/api/v1/mobile/navigation/back") |
|
|
| if response.status_code == 404: |
| pytest.skip("Mobile navigation endpoint not implemented") |
|
|
| assert response.status_code == 200, f"Expected 200, got {response.status_code}" |
|
|
| response_data = response.json() |
| |
| assert "route" in response_data or "message" in response_data, \ |
| "Response should contain route or message" |
|
|
| except Exception as e: |
| pytest.skip(f"Mobile navigation endpoint not implemented: {e}") |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.e2e |
| async def test_mobile_device_capabilities_api(async_client: AsyncClient): |
| """Test mobile device capabilities endpoint. |
| |
| This test verifies: |
| 1. GET /api/v1/mobile/device/capabilities |
| 2. Assert 200 response with capability flags |
| 3. Verify response contains camera, location, notifications flags |
| |
| Args: |
| async_client: HTTPX async client fixture |
| """ |
| try: |
| |
| response = await async_client.get("/api/v1/mobile/device/capabilities") |
|
|
| if response.status_code == 404: |
| pytest.skip("Mobile device capabilities endpoint not implemented") |
|
|
| assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" |
|
|
| response_data = response.json() |
|
|
| |
| |
| if isinstance(response_data, dict): |
| |
| capabilities = response_data.get("capabilities", response_data) |
| assert isinstance(capabilities, dict), "Capabilities should be object" |
|
|
| |
| expected_keys = ["camera", "location", "notifications"] |
| for key in expected_keys: |
| if key in capabilities: |
| assert isinstance(capabilities[key], bool), \ |
| f"Capability '{key}' should be boolean" |
|
|
| elif isinstance(response_data, list): |
| |
| assert len(response_data) > 0, "Capabilities array should not be empty" |
|
|
| except Exception as e: |
| pytest.skip(f"Mobile device capabilities endpoint not implemented: {e}") |
|
|
|
|
| @pytest.mark.e2e |
| async def test_mobile_device_feature_check(async_client: AsyncClient): |
| """Test mobile device feature check endpoint. |
| |
| This test verifies: |
| 1. POST /api/v1/mobile/device/check with feature name |
| 2. Assert 200 response with permission status |
| 3. Test multiple features: camera, location, notifications |
| |
| Args: |
| async_client: HTTPX async client fixture |
| """ |
| features_to_test = ["camera", "location", "notifications", "microphone"] |
|
|
| for feature in features_to_test: |
| try: |
| |
| response = await async_client.post( |
| "/api/v1/mobile/device/check", |
| json={"feature": feature} |
| ) |
|
|
| if response.status_code == 404: |
| pytest.skip(f"Mobile device check endpoint not implemented") |
| break |
|
|
| assert response.status_code == 200, \ |
| f"Expected 200 for {feature}, got {response.status_code}: {response.text}" |
|
|
| response_data = response.json() |
|
|
| |
| |
| assert "status" in response_data or "permission" in response_data or feature in response_data, \ |
| f"Response should contain permission status for {feature}" |
|
|
| except Exception as e: |
| pytest.skip(f"Mobile device check endpoint not implemented: {e}") |
| break |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.e2e |
| async def test_mobile_device_location(async_client: AsyncClient): |
| """Test mobile device location endpoint. |
| |
| This test verifies: |
| 1. GET /api/v1/mobile/device/location |
| 2. Assert 200 response with location data |
| 3. Verify latitude, longitude, accuracy fields |
| |
| Args: |
| async_client: HTTPX async client fixture |
| """ |
| try: |
| |
| response = await async_client.get("/api/v1/mobile/device/location") |
|
|
| if response.status_code == 404: |
| pytest.skip("Mobile device location endpoint not implemented") |
|
|
| |
| assert response.status_code in [200, 403], \ |
| f"Expected 200/403, got {response.status_code}: {response.text}" |
|
|
| if response.status_code == 200: |
| response_data = response.json() |
|
|
| |
| |
| if "latitude" in response_data or "location" in response_data: |
| location = response_data.get("location", response_data) |
|
|
| |
| possible_fields = ["latitude", "longitude", "accuracy", "city", "region"] |
| has_location_data = any(field in location for field in possible_fields) |
|
|
| |
| assert has_location_data or "error" in response_data, \ |
| "Response should contain location data or error" |
|
|
| except Exception as e: |
| pytest.skip(f"Mobile device location endpoint not implemented: {e}") |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.e2e |
| async def test_mobile_workflow_integration(async_client: AsyncClient, db_session: Session): |
| """Test complete mobile workflow integration. |
| |
| This test verifies: |
| 1. Spawn agent via mobile endpoint |
| 2. Navigate to canvas screen |
| 3. Check device capabilities |
| 4. Cleanup resources |
| |
| Args: |
| async_client: HTTPX async client fixture |
| db_session: Database session fixture |
| """ |
| |
| unique_id = str(uuid.uuid4())[:8] |
| user = create_test_user( |
| db_session, |
| f"mobile_integration_{unique_id}@test.com", |
| "MobileTest123!" |
| ) |
|
|
| agent_id = None |
|
|
| try: |
| |
| agent_data = { |
| "name": f"IntegrationAgent_{unique_id}", |
| "maturity": "INTERN", |
| "user_id": str(user.id) |
| } |
|
|
| response = await async_client.post("/api/v1/mobile/agents", json=agent_data) |
|
|
| if response.status_code == 404: |
| |
| response = await async_client.post("/api/v1/agents/spawn", json=agent_data) |
|
|
| if response.status_code in [200, 201]: |
| response_data = response.json() |
| agent_id = response_data.get("agent_id") or response_data.get("id") |
|
|
| |
| nav_response = await async_client.post( |
| "/api/v1/mobile/navigation", |
| json={ |
| "screen": "agent_chat", |
| "params": {"agent_id": agent_id} |
| } |
| ) |
|
|
| if nav_response.status_code == 404: |
| pytest.skip("Mobile navigation endpoint not implemented") |
|
|
| assert nav_response.status_code == 200, \ |
| f"Navigation failed: {nav_response.status_code}" |
|
|
| |
| caps_response = await async_client.get("/api/v1/mobile/device/capabilities") |
|
|
| if caps_response.status_code == 200: |
| caps_data = caps_response.json() |
| assert isinstance(caps_data, (dict, list)), "Capabilities should be object or array" |
|
|
| except Exception as e: |
| pytest.skip(f"Mobile workflow integration test failed: {e}") |
|
|
| finally: |
| |
| if agent_id: |
| cleanup_test_agent(db_session, agent_id) |
|
|
| cleanup_test_user(db_session, str(user.id)) |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture |
| async def async_client(): |
| """Create async HTTP client for testing. |
| |
| Yields: |
| AsyncClient: HTTPX async client with ASGI transport |
| """ |
| transport = ASGITransport(app=app) |
| async with AsyncClient(transport=transport, base_url="http://test") as client: |
| yield client |
|
|
|
|
| @pytest.fixture(autouse=True) |
| def cleanup_test_data(db_session: Session): |
| """Cleanup test data after each test. |
| |
| Args: |
| db_session: Database session fixture |
| |
| Yields: |
| None: Allows test to execute |
| """ |
| yield |
|
|
| |
| try: |
| test_agents = db_session.query(AgentRegistry).filter( |
| AgentRegistry.name.like("%MobileTestAgent%") | |
| AgentRegistry.name.like("%IntegrationAgent%") |
| ).all() |
|
|
| for agent in test_agents: |
| db_session.delete(agent) |
|
|
| db_session.commit() |
| except Exception as e: |
| print(f"Warning: Failed to cleanup test agents: {e}") |
|
|
| |
| try: |
| test_users = db_session.query(User).filter( |
| User.email.like("%mobile_%@test.com") | |
| User.email.like("%mobile_spawn_%@test.com") | |
| User.email.like("%mobile_nav_%@test.com") | |
| User.email.like("%mobile_integration_%@test.com") |
| ).all() |
|
|
| for user in test_users: |
| db_session.delete(user) |
|
|
| db_session.commit() |
| except Exception as e: |
| print(f"Warning: Failed to cleanup test users: {e}") |
|
|