annator-command-center / tests /e2e_api /test_mobile_endpoints.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 7)
cc036ff verified
Raw
History Blame Contribute Delete
20.3 kB
"""
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
# Add backend to path for imports
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
# =============================================================================
# Helper Functions
# =============================================================================
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:
# Log but don't fail test if cleanup fails
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:
# Log but don't fail test if cleanup fails
print(f"Warning: Failed to cleanup user {user_id}: {e}")
# =============================================================================
# Mobile Agent Spawn Tests
# =============================================================================
@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
"""
# Setup: Create test user
unique_id = str(uuid.uuid4())[:8]
user = create_test_user(
db_session,
f"mobile_spawn_{unique_id}@test.com",
"MobileTest123!"
)
# Test data
agent_data = {
"name": f"MobileTestAgent_{unique_id}",
"maturity": "INTERN",
"user_id": str(user.id)
}
try:
# 1. Create agent via mobile endpoint
response = await async_client.post("/api/v1/mobile/agents", json=agent_data)
# Mobile endpoint might not exist, try standard endpoint
if response.status_code == 404:
response = await async_client.post("/api/v1/agents/spawn", json=agent_data)
# Assert 201 Created (or 200 OK depending on implementation)
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"
# 2. Verify agent exists
get_response = await async_client.get(f"/api/v1/agents/{agent_id}")
# GET endpoint might not exist or return 404, that's okay
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
# 3. Cleanup: Delete agent
delete_response = await async_client.delete(f"/api/v1/agents/{agent_id}")
# DELETE might not exist or return 404, that's okay
assert delete_response.status_code in [200, 204, 404], \
f"Delete failed: {delete_response.status_code}"
except Exception as e:
# Mobile endpoints might not be implemented yet
pytest.skip(f"Mobile agent spawn endpoint not implemented: {e}")
finally:
# Cleanup user
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:
# Test missing required fields
response = await async_client.post("/api/v1/mobile/agents", json={})
# Should return validation error
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}"
# Test invalid maturity
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}")
# =============================================================================
# Mobile Navigation Tests
# =============================================================================
@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
"""
# Setup: Create test user
unique_id = str(uuid.uuid4())[:8]
user = create_test_user(
db_session,
f"mobile_nav_{unique_id}@test.com",
"MobileTest123!"
)
# Test data
navigation_data = {
"screen": "canvas",
"params": {
"canvas_id": f"test-{unique_id}"
}
}
try:
# 1. Navigate via mobile endpoint
response = await async_client.post("/api/v1/mobile/navigation", json=navigation_data)
# Mobile endpoint might not exist
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"
# 2. Test navigation history
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"
# Verify our navigation is in history
if isinstance(history_data, list):
assert len(history_data) > 0, "History should not be empty"
# Check if our navigation exists
found = any(
nav.get("screen") == "canvas" or
nav.get("params", {}).get("canvas_id") == f"test-{unique_id}"
for nav in history_data
)
# Might not be found if history is empty or cleaned up
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 user
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:
# Test navigation back
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()
# Should contain route or empty if no previous navigation
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}")
# =============================================================================
# Mobile Device Capabilities Tests
# =============================================================================
@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:
# 1. Get device capabilities
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()
# 2. Verify capability structure
# Response might be object or array depending on implementation
if isinstance(response_data, dict):
# Check for individual capability flags
capabilities = response_data.get("capabilities", response_data)
assert isinstance(capabilities, dict), "Capabilities should be object"
# Verify expected capability keys exist (might be False if not available)
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):
# Array of capability objects
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:
# Check feature permission
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()
# Verify permission status in response
# Response format varies: {feature: "granted"} or {status: "granted"} etc.
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
# =============================================================================
# Mobile Device Location Tests
# =============================================================================
@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:
# Get device location
response = await async_client.get("/api/v1/mobile/device/location")
if response.status_code == 404:
pytest.skip("Mobile device location endpoint not implemented")
# Location might return 200 with data or 403 if permission denied
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()
# Verify location structure
# Response might contain latitude/longitude or error message
if "latitude" in response_data or "location" in response_data:
location = response_data.get("location", response_data)
# Check for common location fields
possible_fields = ["latitude", "longitude", "accuracy", "city", "region"]
has_location_data = any(field in location for field in possible_fields)
# Should have at least some location data
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}")
# =============================================================================
# Mobile Integration Tests
# =============================================================================
@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
"""
# Setup: Create test user
unique_id = str(uuid.uuid4())[:8]
user = create_test_user(
db_session,
f"mobile_integration_{unique_id}@test.com",
"MobileTest123!"
)
agent_id = None
try:
# 1. Spawn agent
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:
# Mobile endpoint not implemented, try standard
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")
# 2. Navigate to canvas
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}"
# 3. Check device capabilities
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:
# Cleanup
if agent_id:
cleanup_test_agent(db_session, agent_id)
cleanup_test_user(db_session, str(user.id))
# =============================================================================
# Pytest Fixtures
# =============================================================================
@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
# Cleanup any test agents with mobile API prefix
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}")
# Cleanup test users
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}")