# Task ID: 36

**Title:** Create Contract Test Suite

**Status:** pending

**Dependencies:** 9, 28, 32

**Priority:** medium

**Description:** Implement contract tests that validate Pydantic models against live Graph API responses.

**Details:**

Create `tests/contract/test_api_contracts.py`:
```python
import pytest

@pytest.mark.contract
@pytest.mark.asyncio
async def test_user_configuration_schema(live_graph_client):
    """Validate UserConfiguration model matches live API."""
    response = await live_graph_client.get(
        "/admin/teams/userConfigurations",
        params={"$top": 5}
    )
    for item in response.get("value", []):
        # This will raise ValidationError if schema doesn't match
        UserConfiguration.model_validate(item)

@pytest.mark.contract
@pytest.mark.asyncio
async def test_number_assignment_schema(live_graph_client):
    ...
```

These tests require a test tenant with real data.
Run separately: `pytest -m contract`

**Test Strategy:**

Tests pass when Pydantic models match live API. Failures indicate API contract drift requiring model updates.

---

## Implementation Notes

### Session: 2026-01-30 13:45 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 36.1 - Create contract test infrastructure with live GraphClient fixture

**Status:** Implemented

#### What Was Done
- Created `tests/contract/__init__.py` - Package marker for test discovery
- Created `tests/contract/conftest.py` - Live GraphClient fixture with test tenant config

#### Key Components Created

**Environment Variables (required for contract tests):**
- `TEST_TENANT_ID` - Azure AD tenant GUID
- `TEST_CLIENT_ID` - App registration client ID
- `TEST_CERT_PATH` - Path to PEM certificate file (supports ~ expansion)

**TestConfigManager Class:**
- Subclasses ConfigManager to bypass config file loading
- Returns a TenantProfile created from environment variables
- Implements `get_tenant()` and `get_default_tenant()` methods

**Fixtures (all session-scoped for efficiency):**
1. `test_tenant_profile` - Creates TenantProfile from env vars, skips if credentials missing
2. `test_config_manager` - TestConfigManager wrapping the test profile
3. `test_auth_service` - AuthService using test credentials
4. `live_graph_client` - Async fixture yielding live GraphClient (primary fixture for contract tests)

#### Key Decisions Made
- **Session scope**: All fixtures are session-scoped to minimize authentication overhead across tests
- **TestConfigManager pattern**: Created subclass rather than mocking to maintain type safety
- **Skip behavior**: Tests skip gracefully when credentials not configured via `pytest.skip()`
- **Path expansion**: Certificate path supports `~` expansion for cross-platform compatibility

#### Patterns Discovered
- AuthService uses ConfigManager.get_tenant() and get_default_tenant() - needed both methods
- GraphClient MUST be used as async context manager (`async with ... as client:`)
- TenantProfile requires UUID objects for tenant_id and client_id

#### Usage Example
```python
@pytest.mark.contract
@pytest.mark.asyncio
async def test_users_endpoint(live_graph_client: GraphClient) -> None:
    response = await live_graph_client.get("/users?$top=1")
    assert response.status_code == 200
```

#### Gotchas and Warnings
- mypy must run on src/ and tests/ together to resolve types properly
- Pre-existing mypy errors in unit tests are unrelated to this work
- pytest.skip() in fixtures skips all dependent tests automatically

---

### Session: 2026-01-30 21:10 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 36.2 - Implement user configuration and directory contract tests

**Status:** Implemented

#### What Was Done
- Created `tests/contract/test_user_contracts.py` with 5 contract tests:
  1. `test_user_configuration_schema()` - Validates UserConfiguration model against /admin/teams/userConfigurations
  2. `test_telephone_number_nested_model()` - Validates TelephoneNumber parsing in users with phone numbers
  3. `test_effective_policy_assignment_nested_model()` - Validates EffectivePolicyAssignment and PolicyAssignmentDetail
  4. `test_user_directory_lookup()` - Validates v1.0 /users endpoint structure
  5. `test_odata_metadata_stripping()` - Verifies @odata.* fields are handled correctly

#### Key Decisions Made
- **OData metadata stripping**: Used local `_strip_odata_metadata()` helper (same pattern as user_service.py) to remove @odata.* fields before Pydantic validation with extra='forbid'
- **Graceful skip handling**: Tests skip when no data available (empty tenant, no users with phones, etc.)
- **v1.0 API access**: Used `get_paginated_url()` with full URL for directory endpoint since `get()` hardcodes beta base URL
- **Fetch limits**: Used $top=5 or $top=20 to minimize API requests while getting enough data

#### Patterns Discovered
- `GraphClient.get()` returns `dict[str, Any]` directly, not httpx.Response
- `GraphClient.get_paginated_url()` accepts full URLs - useful for v1.0 endpoints
- Individual user items may or may not contain @odata.* fields (list-level has @odata.context)

#### Open Items for Next Agent
- [ ] Task 36.3: Implement phone number assignment contract tests
- [ ] Task 36.4: Implement policy contract tests
- [ ] Task 36.5: Implement location contract tests

#### Gotchas and Warnings
- GraphClient.get() returns dict, not Response object - don't use .status_code or .json()
- Tests correctly skip when credentials not configured (CREDENTIALS_CONFIGURED in conftest.py)
- mypy errors on test files are pre-existing (import-untyped) - run mypy on src/ only

---

### Session: 2026-01-30 21:40 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 36.3 - Implement number, async operation, and location contract tests

**Status:** Implemented

#### What Was Done
- Created `tests/contract/test_number_contracts.py` with 6 contract tests:
  1. `test_number_assignment_schema()` - Validates NumberAssignment model against /numberAssignments endpoint
  2. `test_number_type_enum_values()` - Verifies NumberType enum covers all API values
  3. `test_activation_state_enum_values()` - Verifies ActivationState enum coverage
  4. `test_assignment_status_enum_values()` - Verifies AssignmentStatus enum coverage
  5. `test_assignment_category_enum_values()` - Verifies AssignmentCategory enum coverage
  6. `test_number_with_location()` - Validates numbers with location data

- Created `tests/contract/test_async_operation_contracts.py` with 4 tests:
  1. `test_operation_status_enum_values()` - Static validation of OperationStatus enum
  2. `test_async_operation_model_structure()` - Validates AsyncOperation with synthetic data
  3. `test_async_operation_minimal_response()` - Tests minimal response structure
  4. `test_async_operation_all_status_values()` - Tests all status enum values

- Created `tests/contract/test_location_contracts.py` with 5 tests:
  1. `test_emergency_location_schema()` - Validates full CSV row parsing
  2. `test_emergency_location_minimal_fields()` - Tests minimal required fields
  3. `test_emergency_location_string_boolean_conversion()` - Tests boolean handling
  4. `test_emergency_location_field_aliases()` - Tests camelCase CSV header aliases
  5. `test_emergency_location_extra_fields_rejected()` - Tests extra='forbid' behavior

#### Key Decisions Made
- **AsyncOperation synthetic testing**: Since async operations can only be discovered by triggering destructive operations (assign/unassign), tests use synthetic data matching the documented API contract instead of live API calls
- **EmergencyLocation CSV validation**: Graph API does NOT expose location endpoints; EmergencyLocation is for CSV cache from PowerShell export. Tests validate CSV-to-model parsing contract
- **OData in AsyncOperation**: AsyncOperation model includes @odata.context and @odata.type as explicit fields (unlike other models that strip OData metadata)
- **Enum coverage tests**: Fetch 50 items to increase coverage of enum values appearing in live data

#### Patterns Discovered
- AsyncOperation.model_validate() accepts OData fields directly (defined as optional fields with aliases)
- NumberAssignment endpoint: `/admin/teams/telephoneNumberManagement/numberAssignments`
- Operations endpoint: `/admin/teams/telephoneNumberManagement/operations/{id}` (requires known operation ID)

#### Open Items for Next Agent
- [ ] Consider adding live AsyncOperation test if a safe way to obtain operation IDs is found
- [ ] Future: May want to test CacheManager integration for location CSV loading

#### Gotchas and Warnings
- Number contract tests skip when no credentials configured (expected behavior)
- AsyncOperation tests use synthetic data - cannot test against live API without destructive operations
- Location tests are purely structural - they validate CSV parsing contract, not live API
- mypy errors on tests are pre-existing infrastructure issues (import-untyped)

---
