2:I[7012,["4765","static/chunks/4765-f5afdf8061f456f3.js","9856","static/chunks/9856-3b185291364d9bef.js","6687","static/chunks/app/docs/%5B...slug%5D/page-e07536548216bee4.js"],"MarkdownRenderer"] 4:I[9856,["4765","static/chunks/4765-f5afdf8061f456f3.js","9856","static/chunks/9856-3b185291364d9bef.js","6687","static/chunks/app/docs/%5B...slug%5D/page-e07536548216bee4.js"],""] 5:I[4126,[],""] 7:I[9630,[],""] 8:I[4278,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"HeadingProvider"] 9:I[1476,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"Header"] a:I[3167,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"Sidebar"] b:I[7409,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"PageFrame"] 3:T2fff, # Contract Testing Guide **Last Updated**: 2025-11-21 (Phase 7 - P3.4) **Purpose**: Guide for implementing and running contract tests with Pact --- ## Overview VoiceAssist V2 uses **Pact** for consumer-driven contract testing. Contract tests ensure that the API Gateway meets the expectations of all consumers (frontend, mobile apps) without requiring integration tests with real services. **Benefits**: - Catch breaking API changes before deployment - Test independently of consumer/provider availability - Document API contracts in executable form - Enable parallel development of frontend and backend --- ## Architecture ``` ┌─────────────────────┐ │ Consumer (Frontend)│ │ Defines Contract │ └──────────┬──────────┘ │ Pact File ▼ ┌─────────────────────┐ │ Pact Broker │ │ Stores Contracts │ └──────────┬──────────┘ │ Verification ▼ ┌─────────────────────┐ │ Provider (API GW) │ │ Verifies Contract │ └─────────────────────┘ ``` **Components**: 1. **Consumer Tests**: Frontend defines what it expects from API 2. **Pact Files**: JSON contracts generated by consumer tests 3. **Pact Broker**: Central repository for contracts 4. **Provider Tests**: API Gateway verifies it meets contracts --- ## Setup ### 1. Install Dependencies Already added to `requirements.txt`: ``` pact-python==2.2.0 ``` Rebuild containers: ```bash docker compose build voiceassist-server ``` ### 2. Start Pact Broker ```bash # Start Pact Broker (already in docker-compose.yml) docker compose up -d pact-broker # Verify broker is running curl http://localhost:9292/diagnostic/status/heartbeat # Access UI open http://localhost:9292 # Login: pact / pact ``` ### 3. Database Setup Pact Broker uses PostgreSQL. Create database: ```bash docker compose exec postgres psql -U voiceassist -c "CREATE DATABASE pact_broker;" ``` --- ## Writing Consumer Tests Consumer tests define what the frontend expects from the API. ### Example: Auth Login Contract ```python # tests/contract/test_auth_contract.py from pact import Consumer, Provider, Like @pytest.fixture(scope="session") def pact(request): pact = Consumer("VoiceAssistFrontend").has_pact_with( Provider("VoiceAssistAPI"), host_name="localhost", port=1234, # Mock server port pact_dir="./pacts" ) pact.start_service() yield pact pact.stop_service() def test_login_success_contract(self, pact): """Contract: POST /api/auth/login returns tokens.""" expected_response = { "access_token": Like("eyJ..."), # Match any string "refresh_token": Like("eyJ..."), "token_type": "bearer", "expires_in": Like(900) # Match any number } (pact .given("user exists with valid credentials") # Provider state .upon_receiving("a request to login") # Interaction name .with_request( method="POST", path="/api/auth/login", body={"email": "test@example.com", "password": "testpass"} ) .will_respond_with(200, body=expected_response)) with pact: # Make request to mock server response = requests.post(f"{pact.uri}/api/auth/login", ...) assert response.status_code == 200 ``` ### Run Consumer Tests ```bash # Run consumer tests - generates pact files pytest tests/contract/test_auth_contract.py -k Consumer # Pact files created in: ./pacts/ ls pacts/ # voiceassistfrontend-voiceassistapi.json ``` ### Publish to Broker ```bash # Publish pact to broker pact-broker publish \ pacts/voiceassistfrontend-voiceassistapi.json \ --consumer-app-version=1.0.0 \ --broker-base-url=http://localhost:9292 \ --broker-username=pact \ --broker-password=pact ``` --- ## Writing Provider Tests Provider tests verify the actual API meets contracts. ### Example: Verify Auth Contract ```python def test_verify_pact_with_provider(): """Verify API Gateway meets contract.""" from pact import Verifier verifier = Verifier( provider="VoiceAssistAPI", provider_base_url="http://localhost:8000" ) success, logs = verifier.verify_pacts( "./pacts/voiceassistfrontend-voiceassistapi.json", provider_states_setup_url="http://localhost:8000/pact/provider-states", enable_pending=False, publish_version="1.0.0", broker_url="http://localhost:9292", broker_username="pact", broker_password="pact" ) assert success == 0, f"Verification failed: {logs}" ``` ### Provider State Setup Provider needs to be in correct state for each test: ```python # app/api/pact_states.py from fastapi import APIRouter router = APIRouter(prefix="/pact") @router.post("/provider-states") async def setup_provider_state(request: dict): """Setup provider state for contract verification.""" state = request.get("state") if state == "user exists with valid credentials": # Create test user in DB create_test_user("test@example.com", "testpass") elif state == "user does not exist": # Delete test user if exists delete_test_user("newuser@example.com") return {"result": "state setup complete"} ``` ### Run Provider Tests ```bash # Ensure API is running docker compose up -d voiceassist-server # Run provider verification pytest tests/contract/test_auth_contract.py::test_verify_pact_with_provider ``` --- ## Contract Testing Best Practices ### 1. Use Matchers, Not Exact Values **Bad**: ```python body={"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."} # Fragile ``` **Good**: ```python body={"access_token": Like("eyJ...")} # Match type, not value ``` **Available Matchers**: - `Like(value)`: Match type and structure - `EachLike(value, minimum=1)`: Match array elements - `Term(regex, example)`: Match regex pattern - `Includes(value)`: Contains substring ### 2. Keep Contracts Minimal **Bad**: ```python body={ "id": "123", "email": "test@example.com", "full_name": "Test User", "created_at": "2025-11-21T...", "updated_at": "2025-11-21T...", "last_login": None, "preferences": {...}, # Too much detail "metadata": {...} } ``` **Good**: ```python body={ "id": Like("123"), "email": "test@example.com", "is_active": True } # Only what frontend needs ``` ### 3. Use Provider States Always define provider states for setup: ```python .given("user exists with email test@example.com") .given("knowledge base has 10 documents") .given("user is admin") ``` ### 4. One Contract Per Interaction **Bad**: Single test for login + get user profile **Good**: Separate tests for each API call ### 5. Test Both Success and Failure Cases ```python def test_login_success(): ... def test_login_invalid_credentials(): ... def test_login_inactive_user(): ... def test_login_rate_limit_exceeded(): ... ``` --- ## CI/CD Integration ### GitHub Actions Workflow ```yaml # .github/workflows/contract-tests.yml name: Contract Tests on: [push, pull_request] jobs: consumer-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run consumer tests run: | pytest tests/contract/ -k Consumer pact-broker publish pacts/ \ --consumer-app-version=$GITHUB_SHA \ --broker-base-url=$PACT_BROKER_URL provider-tests: runs-on: ubuntu-latest needs: consumer-tests steps: - uses: actions/checkout@v3 - name: Start services run: docker compose up -d - name: Run provider verification run: | pytest tests/contract/ -k Provider ``` ### Can-I-Deploy Check Before deploying, verify contracts are met: ```bash # Check if frontend v1.2.0 can work with API v2.0.0 pact-broker can-i-deploy \ --pacticipant=VoiceAssistFrontend \ --version=1.2.0 \ --to-environment=production \ --broker-base-url=http://localhost:9292 \ --broker-username=pact \ --broker-password=pact ``` --- ## Extending Contract Tests ### Adding New Endpoint Contracts 1. **Create Consumer Test**: ```python # tests/contract/test_users_contract.py def test_get_user_profile_contract(pact): (pact .given("user is authenticated") .upon_receiving("a request for user profile") .with_request( method="GET", path="/api/users/me", headers={"Authorization": Like("Bearer eyJ...")} ) .will_respond_with(200, body={ "id": Like("uuid"), "email": "user@example.com", "full_name": Like("John Doe") })) with pact: response = requests.get(f"{pact.uri}/api/users/me", ...) assert response.status_code == 200 ``` 2. **Add Provider State**: ```python # app/api/pact_states.py @router.post("/provider-states") async def setup_provider_state(request: dict): state = request.get("state") if state == "user is authenticated": # Create session, return token token = create_test_session() return {"token": token} ``` 3. **Run Tests**: ```bash pytest tests/contract/test_users_contract.py ``` ### Testing External Service Contracts For Nextcloud, OpenAI, Qdrant: ```python # tests/contract/test_nextcloud_contract.py def test_nextcloud_caldav_contract(pact): """Contract: Nextcloud CalDAV returns events.""" (pact .given("calendar has 3 events") .upon_receiving("PROPFIND request for events") .with_request( method="PROPFIND", path="/remote.php/dav/calendars/user/personal/", headers={"Depth": "1"} ) .will_respond_with(207, body=EachLike({ "href": Like("/remote.php/dav/calendars/user/personal/event1.ics"), "etag": Like("\"12345\""), "calendar-data": Like("BEGIN:VCALENDAR...") }, minimum=1))) with pact: # Test VoiceAssist's Nextcloud client events = caldav_service.get_events() assert len(events) >= 1 ``` --- ## Troubleshooting ### Issue: Pact Broker Not Starting **Error**: `Connection refused to localhost:9292` **Solutions**: ```bash # Check broker logs docker compose logs pact-broker # Verify PostgreSQL is running docker compose ps postgres # Create pact_broker database docker compose exec postgres psql -U voiceassist -c "CREATE DATABASE pact_broker;" # Restart broker docker compose restart pact-broker ``` ### Issue: Provider Verification Fails **Error**: `Provider state setup failed` **Solutions**: 1. Implement provider state endpoint 2. Ensure API is running: `docker compose up -d voiceassist-server` 3. Check state setup logs: `docker compose logs voiceassist-server` ### Issue: Contract Mismatch **Error**: `Expected status 200 but got 401` **Root Causes**: - Consumer expects different response than provider returns - Breaking API change not reflected in contract - Provider state not set up correctly **Fix**: 1. Update consumer test to match new API 2. Or update API to match contract 3. Publish new contract version --- ## Contract Testing vs Integration Testing | Aspect | Contract Tests | Integration Tests | | ----------- | ---------------------- | -------------------- | | Scope | Single API interaction | Full system flow | | Speed | Fast (mock server) | Slow (real services) | | Reliability | High (isolated) | Lower (dependencies) | | When | Every commit | Pre-deployment | | Coverage | API contracts | Business logic | **Use Both**: Contract tests for API stability, integration tests for workflows. --- ## Related Documentation - [E2E Testing Guide](testing/E2E_TESTING_GUIDE.md) - Full integration tests - [API Reference](API_REFERENCE.md) - OpenAPI specs - [Production Deployment Runbook](PRODUCTION_DEPLOYMENT_RUNBOOK.md) - Production deployment --- **Document Version**: 1.0 **Last Updated**: 2025-11-21 **Maintained By**: VoiceAssist QA Team **Review Cycle**: When API changes occur 6:["slug","TESTING_CONTRACTS","c"] 0:["X7oMT3VrOffzp0qvbeOas",[[["",{"children":["docs",{"children":[["slug","TESTING_CONTRACTS","c"],{"children":["__PAGE__?{\"slug\":[\"TESTING_CONTRACTS\"]}",{}]}]}]},"$undefined","$undefined",true],["",{"children":["docs",{"children":[["slug","TESTING_CONTRACTS","c"],{"children":["__PAGE__",{},[["$L1",["$","div",null,{"children":[["$","div",null,{"className":"mb-6 flex items-center justify-between gap-4","children":[["$","div",null,{"children":[["$","p",null,{"className":"text-sm text-gray-500 dark:text-gray-400","children":"Docs / Raw"}],["$","h1",null,{"className":"text-3xl font-bold text-gray-900 dark:text-white","children":"Testing Contracts"}],["$","p",null,{"className":"text-sm text-gray-600 dark:text-gray-400","children":["Sourced from"," ",["$","code",null,{"className":"font-mono text-xs","children":["docs/","TESTING_CONTRACTS.md"]}]]}]]}],["$","a",null,{"href":"https://github.com/mohammednazmy/VoiceAssist/edit/main/docs/TESTING_CONTRACTS.md","target":"_blank","rel":"noreferrer","className":"inline-flex items-center gap-2 rounded-md border border-gray-200 dark:border-gray-700 px-3 py-1.5 text-sm text-gray-700 dark:text-gray-200 hover:border-primary-500 dark:hover:border-primary-400 hover:text-primary-700 dark:hover:text-primary-300","children":"Edit on GitHub"}]]}],["$","div",null,{"className":"rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-6","children":["$","$L2",null,{"content":"$3"}]}],["$","div",null,{"className":"mt-6 flex flex-wrap gap-2 text-sm","children":[["$","$L4",null,{"href":"/reference/all-docs","className":"inline-flex items-center gap-1 rounded-md bg-gray-100 px-3 py-1 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700","children":"← All documentation"}],["$","$L4",null,{"href":"/","className":"inline-flex items-center gap-1 rounded-md bg-gray-100 px-3 py-1 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700","children":"Home"}]]}]]}],null],null],null]},[null,["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children","docs","children","$6","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L7",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[null,["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children","docs","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L7",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7f586cdbbaa33ff7.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","className":"h-full","children":["$","body",null,{"className":"__className_f367f3 h-full bg-white dark:bg-gray-900","children":[["$","a",null,{"href":"#main-content","className":"skip-to-content","children":"Skip to main content"}],["$","$L8",null,{"children":[["$","$L9",null,{}],["$","$La",null,{}],["$","main",null,{"id":"main-content","className":"lg:pl-64","role":"main","aria-label":"Documentation content","children":["$","$Lb",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L7",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]]}]]}]}]],null],null],["$Lc",null]]]] c:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Testing Contracts | Docs | VoiceAssist Docs"}],["$","meta","3",{"name":"description","content":"**Last Updated**: 2025-11-21 (Phase 7 - P3.4)"}],["$","meta","4",{"name":"keywords","content":"VoiceAssist,documentation,medical AI,voice assistant,healthcare,HIPAA,API"}],["$","meta","5",{"name":"robots","content":"index, follow"}],["$","meta","6",{"name":"googlebot","content":"index, follow"}],["$","link","7",{"rel":"canonical","href":"https://assistdocs.asimo.io"}],["$","meta","8",{"property":"og:title","content":"VoiceAssist Documentation"}],["$","meta","9",{"property":"og:description","content":"Comprehensive documentation for VoiceAssist - Enterprise Medical AI Assistant"}],["$","meta","10",{"property":"og:url","content":"https://assistdocs.asimo.io"}],["$","meta","11",{"property":"og:site_name","content":"VoiceAssist Docs"}],["$","meta","12",{"property":"og:type","content":"website"}],["$","meta","13",{"name":"twitter:card","content":"summary"}],["$","meta","14",{"name":"twitter:title","content":"VoiceAssist Documentation"}],["$","meta","15",{"name":"twitter:description","content":"Comprehensive documentation for VoiceAssist - Enterprise Medical AI Assistant"}],["$","meta","16",{"name":"next-size-adjust"}]] 1:null