# Task Recon: Implement user configuration and directory contract tests

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

---

## Task Summary

Create contract tests validating UserConfiguration, TelephoneNumber, and EffectivePolicyAssignment models against live /admin/teams/userConfigurations and /users Graph API 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 to detect API contract drift before it impacts production code.
**This Subtask's Role:** Validate the user-related models (UserConfiguration, TelephoneNumber, EffectivePolicyAssignment, PolicyAssignmentDetail) against actual API responses from the Teams administration and directory endpoints.

### Dependencies
- Depends on: 36.1 (Create contract test infrastructure with live GraphClient fixture) - **DONE**

### Acceptance Criteria
1. Create `tests/contract/test_user_contracts.py` with:
   - `test_user_configuration_schema()` - fetch /admin/teams/userConfigurations with $top=5, validate each with UserConfiguration.model_validate()
   - `test_telephone_number_nested_model()` - verify TelephoneNumber parsing in users with assigned numbers
   - `test_effective_policy_assignment_nested_model()` - verify EffectivePolicyAssignment and PolicyAssignmentDetail parsing
   - `test_user_directory_lookup()` - fetch /users endpoint and validate response structure
   - `test_odata_metadata_stripping()` - verify @odata.context and @odata.type are handled correctly
2. All tests use `@pytest.mark.contract` and `@pytest.mark.asyncio` decorators
3. Use `extra='forbid'` validation to catch unexpected API fields
4. Tests pass when run against test tenant: `pytest tests/contract/test_user_contracts.py -v`

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 36.1 | Create contract test infrastructure | TestConfigManager pattern used to bypass config file; session-scoped fixtures for efficiency; pytest.skip() for missing credentials |

### Reusable Patterns from Dependencies
- **TestConfigManager subclass**: See `tests/contract/conftest.py:55-91` - subclasses ConfigManager without calling super().__init__()
- **Session-scoped async fixture**: See `tests/contract/conftest.py:160-187` - `live_graph_client` fixture using `@pytest_asyncio.fixture(scope="session")`
- **Credentials skip behavior**: See `tests/contract/conftest.py:110-111` - `pytest.skip(SKIP_REASON)` when credentials not configured

### Warnings and Gotchas
- **GraphClient MUST be used as async context manager**: The fixture handles this, but direct usage requires `async with GraphClient(...) as client:`
- **TenantProfile requires UUID objects**: Pass UUID instances, not strings, for tenant_id and client_id
- **OData metadata fields must be stripped**: Use `_strip_odata_metadata()` helper from user_service.py before validation with extra='forbid' models

### Open Items Inherited
- None from dependency tasks

---

## Architecture Context

### Relevant Design Decisions
- **Pydantic strict mode with `extra="forbid"`**: All models use this to catch API contract drift - any unexpected field causes ValidationError
- **Field aliases (camelCase)**: Models use `populate_by_name=True` to accept both camelCase (API) and snake_case (Python) field names
- **OData metadata handling**: The `@odata.context`, `@odata.type` fields are stripped before validation since they're metadata, not contract fields

### Module Structure
```
tests/contract/
├── __init__.py          # Package marker
├── conftest.py          # live_graph_client fixture (from Task 36.1)
└── test_user_contracts.py  # THIS TASK: User model contract tests
```

### Conventions to Follow
- Use `@pytest.mark.contract` decorator for all tests
- Use `@pytest.mark.asyncio` for async tests
- Test function names: `test_<model>_<aspect>` (e.g., `test_user_configuration_schema`)
- Handle empty tenant scenarios gracefully with `pytest.skip()` when no data available
- Use `model_validate()` for Pydantic v2 validation

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `C:\Users\frank\Projects\teams-phone\tests\contract\conftest.py` | 160-187 | live_graph_client fixture - how to use GraphClient in tests |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\models\user.py` | 82-121 | UserConfiguration model - all fields to validate |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\services\user_service.py` | 20-28 | _strip_odata_metadata helper - how to strip @odata.* fields |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\models\user.py` | 34-46 | TelephoneNumber model |
| `C:\Users\frank\Projects\teams-phone\src\teams_phone\models\user.py` | 48-80 | PolicyAssignmentDetail and EffectivePolicyAssignment models |
| `C:\Users\frank\Projects\teams-phone\tests\unit\test_models.py` | 714-751 | Unit test pattern for UserConfiguration from API response |
| `C:\Users\frank\Projects\teams-phone\.taskmaster\docs\graph-api-contracts\1-user-operations-verified.md` | 1-56 | API contract documentation |
| `C:\Users\frank\Projects\teams-phone\.taskmaster\docs\graph-api-contracts\5-directory-operations-verified.md` | 1-32 | Directory API contract documentation |

### Code Patterns to Follow

```python
# Pattern from tests/contract/conftest.py:179-186 - Using live_graph_client fixture
@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
```

```python
# Pattern from tests/unit/test_models.py:714-750 - Validating UserConfiguration from API
api_response = {
    "id": "12345678-1234-5678-1234-567812345678",
    "userPrincipalName": "user@contoso.com",
    "tenantId": "87654321-4321-8765-4321-876543218765",
    "displayName": "John Doe",
    "accountType": "user",
    "isEnterpriseVoiceEnabled": True,
    "featureTypes": ["Teams", "CallingPlan"],
    "telephoneNumbers": [...],
    "effectivePolicyAssignments": [...],
    "createdDateTime": "2024-01-15T10:00:00Z",
    "modifiedDateTime": "2024-01-20T14:30:00Z",
}
user = UserConfiguration.model_validate(api_response)
```

```python
# Pattern from services/user_service.py:20-28 - Stripping OData metadata
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("@")}
```

---

## Dependencies

### Internal Modules
- `teams_phone.models` - UserConfiguration, TelephoneNumber, EffectivePolicyAssignment, PolicyAssignmentDetail
- `teams_phone.infrastructure.graph_client` - GraphClient for making API requests

### External Packages
- `pytest` - Test framework
- `pytest-asyncio` - Async test support
- `pydantic` - Model validation (ValidationError for catching extra fields)

### New Dependencies Needed
- None required - all dependencies already installed

---

## Testing Guidance

### Test File Location
`tests/contract/test_user_contracts.py`

### Test Patterns in Use
```python
# Pattern from tests/unit/test_models.py showing extra='forbid' validation
@pytest.mark.unit
def test_extra_field_rejected(self) -> None:
    """UserConfiguration rejects unknown fields (extra='forbid')."""
    with pytest.raises(ValidationError) as exc_info:
        UserConfiguration(
            id="12345678-1234-5678-1234-567812345678",
            userPrincipalName="user@contoso.com",
            # ... required fields ...
            unknown_field="should fail",  # type: ignore[call-arg]
        )
    assert "extra_forbidden" in str(exc_info.value)
```

### 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 instance
- `test_auth_service` (session-scoped) - AuthService with test credentials

### Test Utilities
- `tests/contract/conftest.py` - All fixtures for contract tests
- `SKIP_REASON` constant at `conftest.py:44-47` - Standard skip message when credentials missing

---

## Risks and Blockers

### Technical Risks
- [ ] **Empty tenant data**: Test tenant may not have users with phone numbers or policy assignments - mitigate with graceful `pytest.skip()` when data not available
- [ ] **API throttling**: Multiple tests hitting live API may trigger rate limits - session-scoped client helps, but consider using `$top=5` to minimize requests

### Dependency Risks
- [ ] None - Task 36.1 is complete and provides the infrastructure

### Open Questions
- [ ] Should we validate the exact set of fields returned by the API, or just that our model can parse them? (Answer: Use `model_validate()` with `extra='forbid'` - any unexpected field fails the test, indicating API contract drift)
- [ ] How to handle @odata.context and @odata.type in the list response? (Answer: Strip them before validation using `_strip_odata_metadata()` pattern)

---

## Implementation Recommendations

### Suggested Approach
1. Create `tests/contract/test_user_contracts.py` with imports from models and fixtures
2. Implement `test_user_configuration_schema()` first - fetch list with $top=5, strip OData, validate each item
3. Implement `test_telephone_number_nested_model()` - filter for users with telephoneNumbers, validate nested model
4. Implement `test_effective_policy_assignment_nested_model()` - filter for users with policy assignments
5. Implement `test_user_directory_lookup()` - call v1.0 /users endpoint, validate basic structure
6. Implement `test_odata_metadata_stripping()` - verify @odata.* fields don't cause validation errors
7. Add graceful skip handling when test data not available

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `tests/contract/test_user_contracts.py` | Main test file for user model contract tests |

### Quick Wins
- Start with `test_user_configuration_schema()` - it validates the core model and confirms the infrastructure works
- Copy the `_strip_odata_metadata()` helper function into the test file or import it

---

## Cheat Sheet

### Key Imports
```python
from typing import Any

import pytest

from teams_phone.infrastructure.graph_client import GraphClient
from teams_phone.models import (
    EffectivePolicyAssignment,
    PolicyAssignmentDetail,
    TelephoneNumber,
    UserConfiguration,
)
```

### Key Functions to Use
- `live_graph_client.get(endpoint)` - Make GET request, returns dict
- `live_graph_client.get_paginated(endpoint, params)` - Paginated GET, async iterator
- `UserConfiguration.model_validate(data)` - Pydantic v2 validation
- `pytest.skip(reason)` - Skip test gracefully when data not available

### Common Patterns
- **Strip OData metadata**: `{k: v for k, v in data.items() if not k.startswith("@")}`
- **Fetch users list**: `await live_graph_client.get("/admin/teams/userConfigurations?$top=5")`
- **Fetch directory users**: Use GRAPH_API_V1_BASE for v1.0 endpoint
- **Check for nested data**: `if user.telephone_numbers:` before testing nested model

### API Endpoints
- User configurations (beta): `GET /admin/teams/userConfigurations`
- Single user config (beta): `GET /admin/teams/userConfigurations/{id}`
- Directory users (v1.0): `GET /users`

### Constants
- `GRAPH_API_V1_BASE = "https://graph.microsoft.com/v1.0"` (from constants.py)

---

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