# Task Recon: Create contract test infrastructure with live GraphClient fixture

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

---

## Task Summary

Set up `tests/contract/` directory with `conftest.py` containing a live GraphClient fixture using real certificate-based authentication, test tenant configuration via environment variables, and pytest marker registration.

### 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 (new fields, renamed fields, removed fields).
**This Subtask's Role:** Provides the foundational infrastructure for contract tests - the live GraphClient fixture that subsequent subtasks (36.2, 36.3) will use to make actual API calls for validating Pydantic models.

### Dependencies
- Depends on: 9 (Pydantic models), 28 (api-check command), 32 (test fixtures), 42 (validation checkpoint)

### Acceptance Criteria
- `tests/contract/` directory exists with `__init__.py` and `conftest.py`
- `live_graph_client` async fixture that initializes real GraphClient with certificate-based auth
- Environment variable loading for `TEST_TENANT_ID`, `TEST_CLIENT_ID`, `TEST_CERT_PATH`
- Session-scoped fixture for expensive auth operations
- Skip marker when test credentials are not configured
- Tests skip gracefully when credentials missing
- `pytest tests/contract/ --collect-only` verifies test discovery

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 9 | Implement Pydantic Models | Models use `ConfigDict(extra="forbid", populate_by_name=True)` for strict validation. OData fields like `@odata.context` must be stripped before validation. |
| 28 | Implement API-Check Command | AuthService takes 2 parameters (config_manager, cache_manager). Uses `_strip_odata_metadata()` helper to remove @ prefixed fields before Pydantic validation. |
| 32 | Create Unit Test Fixtures | Adopted minimal consolidation approach - `mock_graph_client()` is in conftest.py. Local fixtures preferred for test-specific behavior. |
| 42 | Validation Checkpoint 4 | AsyncOperation model was fixed during live testing - `id` field made optional, `@odata.context` added. Validates that strict Pydantic catches real contract issues. |

### Reusable Patterns from Dependencies
- **OData stripping**: Implemented in `src/teams_phone/cli/api_check.py:61-68` - helper function to strip @ prefixed metadata fields
- **AuthService initialization**: Pattern from `api_check.py:188` - `AuthService(config_manager, cache_manager)`
- **GraphClient async context**: Pattern from `test_graph_client.py:132-139` - `async with GraphClient(auth_service) as client:`

### Warnings and Gotchas
- **AuthService signature**: AuthService takes only 2 parameters (config_manager, cache_manager), NOT 3. Tenant resolution is handled internally via config_manager.
- **Pydantic enum validation**: Test data must use exact enum values from the model (e.g., `"user"` not `"User"`, `"callingPlan"` not `"CallingPlan"`)
- **datetime string input in tests**: When testing datetime parsing, use string values with `# type: ignore[arg-type]` since mypy expects datetime objects
- **GraphClient context manager required**: Must use `async with GraphClient(...) as client:` - raises RuntimeError if used outside context

### Open Items Inherited
- None directly inherited, but Task 42 notes that AsyncOperation model needed runtime fix for contract mismatch - contract tests will help catch these earlier.

---

## Architecture Context

### Relevant Design Decisions
- **Layered Monolith Architecture**: CLI -> Services -> Infrastructure pattern
- **Certificate-based authentication**: Production uses cert auth via MSAL, client-secret only for dev/test
- **Strict Pydantic validation**: `extra="forbid"` catches API contract drift immediately
- **Pytest markers**: `@pytest.mark.contract` for contract tests (already registered in root conftest.py)

### Module Structure
Contract tests will sit alongside unit and integration tests:
```
tests/
├── conftest.py          # Root conftest with markers (already exists)
├── contract/            # NEW - Contract test directory
│   ├── __init__.py
│   └── conftest.py      # Live GraphClient fixture
├── integration/
│   └── __init__.py
└── unit/
    └── ... (28 test files)
```

### Conventions to Follow
- Pytest fixture naming: `live_graph_client` (not `real_graph_client` or `test_client`)
- Skip decorator pattern: `pytest.mark.skipif(condition, reason="...")`
- Async fixtures use `@pytest_asyncio.fixture` or `@pytest.fixture` with async def
- Session scope for expensive fixtures that can be reused across test module

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/graph_client.py` | 167-196 | GraphClient class initialization - takes AuthService, timeout, debug_logger |
| `src/teams_phone/infrastructure/graph_client.py` | 608-636 | Async context manager protocol (`__aenter__`, `__aexit__`) - MUST be used |
| `src/teams_phone/services/auth_service.py` | 53-64 | AuthService constructor - takes config_manager and cache_manager |
| `src/teams_phone/services/auth_service.py` | 388-417 | `get_token()` method - returns valid access token, refreshing if necessary |
| `src/teams_phone/cli/api_check.py` | 185-189 | Pattern for creating AuthService from config_manager and cache_manager |
| `tests/conftest.py` | 1-29 | Root conftest with marker registration - contract marker already defined |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `tests/unit/test_graph_client.py` | 29-34 | Mock AuthService fixture pattern |
| `tests/unit/test_graph_client.py` | 130-163 | Async context manager test patterns |
| `src/teams_phone/infrastructure/config_manager.py` | 19-38 | ConfigManager initialization |
| `src/teams_phone/infrastructure/cache_manager.py` | 14-52 | CacheManager initialization |
| `src/teams_phone/constants.py` | 9-19 | GRAPH_API_BASE and GRAPH_API_V1_BASE URLs |

### Code Patterns to Follow

```python
# GraphClient async context manager pattern (src/teams_phone/infrastructure/graph_client.py:608-617)
async def __aenter__(self) -> GraphClient:
    """Enter async context manager."""
    self._client = httpx.AsyncClient(timeout=self._timeout)
    return self

async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
    """Exit async context manager."""
    if self._client is not None:
        await self._client.aclose()
        self._client = None
```

```python
# AuthService initialization pattern (src/teams_phone/cli/api_check.py:186-188)
config_manager = cli_ctx.get_config_manager()
cache_manager = CacheManager()
auth_service = AuthService(config_manager, cache_manager)
```

```python
# Skip marker pattern for missing config (tests/unit pattern)
@pytest.mark.skipif(
    not os.environ.get("TEST_TENANT_ID"),
    reason="Test tenant credentials not configured"
)
```

---

## Dependencies

### Internal Modules
- `teams_phone.infrastructure.GraphClient` - HTTP client for Graph API (async context manager)
- `teams_phone.infrastructure.ConfigManager` - TOML config file management
- `teams_phone.infrastructure.CacheManager` - Token and CSV caching
- `teams_phone.services.AuthService` - Certificate-based authentication via MSAL

### External Packages
- `pytest>=7.4.0` - Testing framework
- `pytest-asyncio>=0.21.0` - Async fixture and test support
- `httpx>=0.25.0` - Async HTTP client (used by GraphClient)

### New Dependencies Needed
- None required - all dependencies already in pyproject.toml

---

## Testing Guidance

### Test File Location
`tests/contract/conftest.py` (fixture definition)
Test discovery validation: `pytest tests/contract/ --collect-only`

### Test Patterns in Use
```python
# Async fixture pattern (from test_graph_client.py:130-139)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_enter_creates_client(self, mock_auth_service: MagicMock) -> None:
    """__aenter__ creates httpx.AsyncClient."""
    client = GraphClient(mock_auth_service)

    async with client as entered:
        assert entered is client
        assert client._client is not None
        assert isinstance(client._client, httpx.AsyncClient)
```

### Fixtures Available
- `mock_graph_client()` - MagicMock in root conftest (for unit tests)
- Contract tests need NEW `live_graph_client` fixture using real authentication

### Test Utilities
- `tests/conftest.py` - Root conftest with pytest markers registration
- Markers already registered: `unit`, `integration`, `contract`

---

## Risks and Blockers

### Technical Risks
- [ ] **Environment variable handling**: Need robust error messages when credentials missing
- [ ] **Certificate path expansion**: Must handle `~` expansion on Windows and Unix
- [ ] **Session scope cleanup**: Async fixtures with session scope need careful cleanup

### Dependency Risks
- [ ] **pytest-asyncio compatibility**: Session-scoped async fixtures may need special handling with pytest-asyncio
- [ ] **Real network dependency**: Contract tests will fail if network unavailable or test tenant inaccessible

### Open Questions
- [ ] Should `live_graph_client` be function-scoped (fresh for each test) or session-scoped (reused across all tests)?
  - Recommendation: Start with session-scoped for efficiency, can change if issues arise
- [ ] How to handle TenantProfile creation when using environment variables instead of config file?
  - Recommendation: Create TenantProfile directly with AuthMethod.CERTIFICATE

---

## Implementation Recommendations

### Suggested Approach
1. Create `tests/contract/__init__.py` (empty file for package)
2. Create `tests/contract/conftest.py` with:
   - Environment variable loading at module level
   - Skip condition check function
   - `live_graph_client` async fixture (session-scoped)
3. Verify with `pytest tests/contract/ --collect-only`

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `tests/contract/__init__.py` | Package marker for test discovery |
| Create | `tests/contract/conftest.py` | Live GraphClient fixture with test tenant config |

### Quick Wins
- Environment variable check at module import time with clear error message
- Use `pytest.mark.skipif` decorator on the fixture itself for clean skip behavior

---

## Cheat Sheet

### Key Imports
```python
import os
from pathlib import Path

import pytest
import pytest_asyncio

from teams_phone.infrastructure import CacheManager, ConfigManager, GraphClient
from teams_phone.models import AuthMethod, TenantProfile
from teams_phone.services import AuthService
```

### Key Functions to Use
- `TenantProfile(...)` - Create tenant config with test credentials
- `AuthService(config_manager, cache_manager)` - Initialize auth (but need custom approach for test tenant)
- `GraphClient(auth_service)` - Create Graph client (MUST use async context manager)
- `os.environ.get("ENV_VAR")` - Get environment variables

### Common Patterns
- **TenantProfile creation**: See `src/teams_phone/models/tenant.py` for required fields
- **Async fixture**: Use `@pytest_asyncio.fixture(scope="session")` for session-scoped async fixtures
- **Skip when no credentials**: `pytest.mark.skipif(not os.environ.get("TEST_TENANT_ID"), reason="...")`

### Environment Variables Required
```bash
TEST_TENANT_ID=<Azure AD tenant GUID>
TEST_CLIENT_ID=<App registration client ID>
TEST_CERT_PATH=<Path to PEM certificate file>
```

### Key Insight: Direct TenantProfile Creation
Since test credentials come from environment variables (not config file), create TenantProfile directly:
```python
profile = TenantProfile(
    name="test-tenant",
    tenant_id=os.environ["TEST_TENANT_ID"],
    client_id=os.environ["TEST_CLIENT_ID"],
    cert_path=os.environ["TEST_CERT_PATH"],
    auth_method=AuthMethod.CERTIFICATE,
)
```

Then create a minimal ConfigManager (or mock) that returns this profile when asked for a tenant.

---

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