# Task Recon: Implement number, async operation, and location contract tests

**Task ID:** 36.3
**Status:** pending
**Generated:** 2026-01-30T21:30:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create contract tests validating NumberAssignment, AsyncOperation, and EmergencyLocation models against live Graph API responses from /telecom/telephoneNumbers, async polling endpoints, and location endpoints.

### Parent Task Context

**Parent Task:** Create Contract Test Suite (ID: 36)
**Parent Goal:** Implement contract tests that validate Pydantic models against live Graph API responses using real test tenant authentication.
**This Subtask's Role:** This subtask extends the contract test suite (subtasks 36.1 infrastructure, 36.2 user contracts) by adding phone number, async operation, and emergency location contract validation tests.

### Dependencies
- Depends on: 36.1 (Create contract test infrastructure with live GraphClient fixture) - **COMPLETED**
- Subtask 36.2 (User contracts) also completed - provides patterns to follow

### Acceptance Criteria
From task details:
1. Create `tests/contract/test_number_contracts.py` with:
   - `test_number_assignment_schema()` - fetch `/admin/teams/telephoneNumberManagement/numberAssignments` with $top=5, validate each with `NumberAssignment.model_validate()`
   - `test_number_type_enum_values()` - verify NumberType enum covers all API values
   - `test_activation_state_enum()` - verify ActivationState enum values
   - `test_assignment_status_enum()` - verify AssignmentStatus enum values
2. Create `tests/contract/test_async_operation_contracts.py` with:
   - `test_async_operation_schema()` - validate AsyncOperation model with OperationStatus enum
3. Create `tests/contract/test_location_contracts.py` with:
   - `test_emergency_location_schema()` - fetch emergency locations if available, validate EmergencyLocation model
4. All tests use `@pytest.mark.contract`, `@pytest.mark.asyncio`
5. Handle empty tenant scenarios gracefully with `pytest.skip()`

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 36.1 | Create contract test infrastructure | Session-scoped fixtures reduce auth overhead; TestConfigManager pattern for bypassing config files |
| 36.2 | Implement user configuration and directory contract tests | `_strip_odata_metadata()` helper needed for extra='forbid' validation; GraphClient.get() returns dict directly |

### Reusable Patterns from Dependencies

- **OData stripping pattern**: Implemented in `tests/contract/test_user_contracts.py:26-33` - reuse for number contracts
- **Skip pattern for empty data**: See `tests/contract/test_user_contracts.py:48-49` - graceful skip when no test data
- **live_graph_client fixture**: Session-scoped async fixture in `tests/contract/conftest.py:160-186`

### Warnings and Gotchas

- **GraphClient.get() returns dict, not Response**: Don't use `.status_code` or `.json()` on the result
- **OData metadata must be stripped**: API responses include `@odata.context` and `@odata.type` fields that fail extra='forbid' validation
- **Tests skip when credentials not configured**: CREDENTIALS_CONFIGURED check in conftest.py handles this

### Open Items Inherited

- [ ] EmergencyLocation model is for CSV cache data, NOT Graph API - Graph API does not expose location endpoints (see Graph API contracts doc)
- [ ] AsyncOperation testing may require triggering an actual operation or using a known operation ID

---

## Architecture Context

### Relevant Design Decisions

- **Pydantic strict mode with extra='forbid'**: All models use `extra="forbid"` to catch API contract drift
- **Contract tests run against live API**: Use `@pytest.mark.contract` marker, run separately with `pytest -m contract`
- **Session-scoped fixtures**: Expensive auth operations cached across all contract tests

### Module Structure

```
tests/contract/
├── __init__.py                      # Package marker (exists)
├── conftest.py                      # live_graph_client fixture (exists)
├── test_user_contracts.py           # User/directory contracts (exists, from 36.2)
├── test_number_contracts.py         # NEW: Phone number contracts
├── test_async_operation_contracts.py # NEW: Async operation contracts
└── test_location_contracts.py       # NEW: Location contracts (CSV-based)
```

### Conventions to Follow

- Test functions prefixed with `test_`
- Use `@pytest.mark.contract` and `@pytest.mark.asyncio` decorators
- Type hints for all function signatures
- Docstrings explaining what each test validates
- Use `pytest.skip()` for graceful handling when test data unavailable

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `tests/contract/test_user_contracts.py` | 1-216 | Pattern for contract tests - OData stripping, skip handling, validation |
| `tests/contract/conftest.py` | 160-186 | live_graph_client fixture implementation |
| `src/teams_phone/models/number.py` | 1-187 | NumberAssignment, AsyncOperation, enums (NumberType, ActivationState, etc.) |
| `src/teams_phone/models/location.py` | 1-37 | EmergencyLocation model (CSV-based, NOT Graph API) |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/number_service.py` | 48-114 | list_numbers endpoint usage pattern |
| `src/teams_phone/services/number_service.py` | 198-249 | poll_operation for async operations |
| `src/teams_phone/services/location_service.py` | 38-55 | Location loaded from CSV cache, not API |
| `tests/unit/test_number_service.py` | 22-91 | Test data factory functions for numbers |

### Code Patterns to Follow

```python
# Example pattern from tests/contract/test_user_contracts.py:26-65
def _strip_odata_metadata(data: dict[str, Any]) -> dict[str, Any]:
    """Strip OData metadata fields from a response dict."""
    return {k: v for k, v in data.items() if not k.startswith("@")}

@pytest.mark.contract
@pytest.mark.asyncio
async def test_user_configuration_schema(live_graph_client: GraphClient) -> None:
    """Validate model against live endpoint."""
    data = await live_graph_client.get("/admin/teams/userConfigurations?$top=5")
    users = data.get("value", [])

    if not users:
        pytest.skip("No data available in test tenant")

    for user_data in users:
        clean_data = _strip_odata_metadata(user_data)
        user = UserConfiguration.model_validate(clean_data)
        assert user.id  # Sanity check
```

---

## Dependencies

### Internal Modules
- `teams_phone.infrastructure.graph_client.GraphClient` - API client for live requests
- `teams_phone.models.NumberAssignment` - Phone number model
- `teams_phone.models.AsyncOperation` - Async operation polling model
- `teams_phone.models.OperationStatus` - Operation status enum
- `teams_phone.models.NumberType` - Phone number type enum
- `teams_phone.models.ActivationState` - Activation state enum
- `teams_phone.models.AssignmentStatus` - Assignment status enum
- `teams_phone.models.EmergencyLocation` - Location model (CSV-based)

### External Packages
- `pytest` - Test framework
- `pytest-asyncio` - Async test support

### New Dependencies Needed
None required - all dependencies already in place

---

## Testing Guidance

### Test File Locations
- `tests/contract/test_number_contracts.py` - NEW
- `tests/contract/test_async_operation_contracts.py` - NEW
- `tests/contract/test_location_contracts.py` - NEW

### Test Patterns in Use

```python
# From tests/contract/test_user_contracts.py - enum value coverage pattern
@pytest.mark.contract
@pytest.mark.asyncio
async def test_telephone_number_nested_model(live_graph_client: GraphClient) -> None:
    """Validate nested model parsing."""
    data = await live_graph_client.get("/admin/teams/userConfigurations?$top=20")
    users = data.get("value", [])

    if not users:
        pytest.skip("No data available in test tenant")

    # Find items with the nested model
    items_with_nested = []
    for item_data in users:
        clean_data = _strip_odata_metadata(item_data)
        item = Model.model_validate(clean_data)
        if item.nested_field:
            items_with_nested.append(item)

    if not items_with_nested:
        pytest.skip("No items with nested data found")

    # Validate nested model structure
    for item in items_with_nested:
        assert isinstance(item.nested_field, NestedType)
```

### Fixtures Available
- `live_graph_client` (session-scoped) - Authenticated GraphClient for live API calls
- `test_tenant_profile` (session-scoped) - TenantProfile from env vars
- `test_config_manager` (session-scoped) - TestConfigManager wrapper
- `test_auth_service` (session-scoped) - AuthService using test credentials

### Test Utilities
- `tests/conftest.py` - Root conftest with marker registration
- `tests/contract/conftest.py` - Contract-specific fixtures and skip handling

---

## Risks and Blockers

### Technical Risks

- [ ] **EmergencyLocation is CSV-based, not API**: The EmergencyLocation model is designed for CSV cache data exported via PowerShell - Graph API does NOT expose location endpoints. The test should validate CSV parsing, not API responses.
- [ ] **AsyncOperation testing challenge**: Getting a real async operation response may require triggering an actual assignment/unassignment which is destructive. Consider testing poll endpoint with a recently-completed operation if one exists, or skip gracefully.
- [ ] **Test tenant data requirements**: Tests need phone numbers in the tenant inventory to validate NumberAssignment. If tenant has no numbers, tests should skip gracefully.

### Dependency Risks

- [x] Subtask 36.1 complete - infrastructure available
- [x] Subtask 36.2 complete - patterns established

### Open Questions

- [ ] Should the location test validate CSV parsing (from CacheManager) instead of API responses?
- [ ] How to obtain a valid AsyncOperation for testing without performing destructive operations?
- [ ] Are there known completed operations in the test tenant that can be queried?

---

## Implementation Recommendations

### Suggested Approach

1. **Start with test_number_contracts.py** - Most straightforward, follows existing patterns
   - Copy OData stripping helper from test_user_contracts.py
   - Implement test_number_assignment_schema() first
   - Add enum coverage tests (may need to check what enum values appear in live data)

2. **Implement test_location_contracts.py** - Adjust for CSV-based validation
   - Note: EmergencyLocation is for CSV cache, NOT Graph API
   - Test should use CacheManager or mock CSV data, OR skip gracefully noting that location data comes from CSV export not API

3. **Implement test_async_operation_contracts.py** - Handle carefully
   - Check if NumberService.poll_operation endpoint can be queried for recent operations
   - If no operations available, skip gracefully
   - Consider documenting the limitation

### Files to Create/Modify

| Action | File | Reason |
|--------|------|--------|
| Create | `tests/contract/test_number_contracts.py` | Phone number model contract tests |
| Create | `tests/contract/test_async_operation_contracts.py` | Async operation model contract tests |
| Create | `tests/contract/test_location_contracts.py` | Location model contract tests (CSV-based) |

### Quick Wins

- `test_number_assignment_schema()` - Direct port of user contract pattern to number endpoint
- Enum tests can validate that live API values are covered by checking what values appear in actual data

---

## Cheat Sheet

### Key Imports

```python
from __future__ import annotations
from typing import Any

import pytest

from teams_phone.infrastructure.graph_client import GraphClient
from teams_phone.models import (
    ActivationState,
    AssignmentStatus,
    AsyncOperation,
    NumberAssignment,
    NumberType,
    OperationStatus,
    EmergencyLocation,
)
```

### Key Endpoints

- `/admin/teams/telephoneNumberManagement/numberAssignments` - List phone numbers
- `/admin/teams/telephoneNumberManagement/operations/{id}` - Poll async operation

### Key Functions to Use

- `live_graph_client.get(endpoint, params=...)` - Returns dict directly
- `Model.model_validate(data)` - Pydantic strict validation
- `pytest.skip(reason)` - Skip test with explanation

### Common Patterns

- **OData stripping**: See `tests/contract/test_user_contracts.py:26-33`
- **Empty data skip**: See `tests/contract/test_user_contracts.py:48-49`
- **Nested model validation**: See `tests/contract/test_user_contracts.py:67-100`

### Number Endpoint Query Parameters

```python
# Fetch first 5 numbers
params = {"$top": "5"}
data = await live_graph_client.get(
    "/admin/teams/telephoneNumberManagement/numberAssignments",
    params=params,
)
numbers = data.get("value", [])
```

### Important Notes

1. **EmergencyLocation caveat**: Graph API does NOT expose location endpoints per `.taskmaster/docs/graph-api-contracts/index.md` section 4. The EmergencyLocation model is for CSV cache data. The contract test should either:
   - Test CSV parsing from CacheManager
   - Skip with clear explanation that locations come from PowerShell export

2. **AsyncOperation caveat**: Need a valid operation ID to test. Options:
   - Query for recent operations (if endpoint supports list)
   - Trigger test operation (risky in production-like tenant)
   - Skip gracefully with explanation

---

*Generated by task-recon (claude-opus-4-5-20251101) | Ready for implementation*
