# Task Recon: Create CLI integration test infrastructure with conftest.py and shared fixtures

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

---

## Task Summary

Set up the `tests/integration/cli/` directory structure with `conftest.py` containing reusable fixtures for CliRunner, mocked service dependencies, and shared test helpers for CLI integration testing.

### Parent Task Context

**Parent Task:** Write CLI Integration Tests (ID: 35)
**Parent Goal:** Create integration tests for CLI commands using CliRunner with mocked services, testing all commands with default (table) output, JSON output, error cases, and global flags (--tenant, --verbose).
**This Subtask's Role:** Establish the foundational test infrastructure that all subsequent CLI integration tests will depend on. This includes the CliRunner fixture, service mock fixtures (AuthService, UserService, NumberService, PolicyService, LocationService, TenantService), model factory helpers, and pytest markers.

### Dependencies
- Depends on: None (this subtask has no dependencies within Task 35)
- Parent task dependencies: 11, 12, 14, 16, 18, 22, 23, 25, 32, 42

### Acceptance Criteria
- Directory structure `tests/integration/cli/` created
- `conftest.py` with CliRunner fixture
- Service mock fixtures following existing unit test patterns
- Model factory helpers for test data generation
- GraphClient async context manager fixture
- Composite fixture patching all services simultaneously
- pytest markers registered for `@pytest.mark.integration`

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 32 | Create Unit Test Fixtures and Conftest | Adopted minimal consolidation approach - local fixtures work well. Shared `mock_graph_client` in `tests/conftest.py`. |
| 11 | Implement Root CLI App with Global Options | Exit code 2 for `no_args_is_help` is expected Typer/Click behavior. Context uses `CLIContext` dataclass not raw dict. |
| 42 | Validation Checkpoint 4 | Strict Pydantic validation catches real API contract issues - important for test data to match actual schemas. |

### Reusable Patterns from Dependencies

- **CliRunner pattern**: Implemented in `tests/unit/test_cli_users.py:10-25` using `from typer.testing import CliRunner`
- **AsyncMock context manager pattern**: See `tests/unit/test_cli_users.py:96-102` for GraphClient mock setup
- **Model factory helpers**: See `_make_user()` in `tests/unit/test_cli_users.py:29-67` and `_make_location()` in `tests/unit/test_cli_locations.py:19-44`
- **Service patching pattern**: Patch at module level like `patch("teams_phone.cli.users.UserService")`

### Warnings and Gotchas

- **Exit code 2 for no_args_is_help**: When testing with no arguments, Typer returns exit code 2 not 0
- **Context field naming**: `CLIContext.json_output` vs old `ctx.obj["json"]` - tests should use `CLIContext`
- **AsyncMock for async services**: UserService, NumberService use async methods; LocationService, PolicyService are sync
- **GraphClient async context manager**: Must properly mock `__aenter__` and `__aexit__`

### Open Items Inherited

- None directly applicable to this subtask

---

## Architecture Context

### Relevant Design Decisions

- **Test Types** (from `.taskmaster/docs/architecture/13-test-strategy.md`):
  - Unit tests in `tests/unit/` - Fast tests with mocked dependencies
  - Integration tests in `tests/integration/` - Real Graph API calls OR full CLI command execution with mocks
- **Coverage Goals**: CLI target is >75%
- **Fixture approach** (from Task 32): Local fixtures preferred for readability; shared fixtures only for truly common patterns

### Module Structure

```
tests/
├── conftest.py              # Root conftest with markers + mock_graph_client
├── integration/
│   ├── __init__.py          # Empty (exists)
│   └── cli/                  # NEW - Create this directory
│       ├── __init__.py
│       └── conftest.py      # CLI integration test fixtures
└── unit/
    └── test_cli_*.py        # Existing unit tests to reference
```

### Conventions to Follow

- Use `@pytest.mark.integration` marker for integration tests
- Test files named `test_*_integration.py`
- Test classes named `Test*Integration`
- Factory helpers prefixed with `_make_*()` or `make_*()`
- Fixtures use descriptive names like `mock_user_service`, `runner`

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `tests/unit/test_cli_users.py` | 1-78 | Complete example of CliRunner usage, model factories, service mocking patterns |
| `tests/unit/test_cli_auth.py` | 1-50 | AuthService mocking pattern with sync methods |
| `tests/unit/test_cli_locations.py` | 1-44 | LocationService mocking pattern (sync), EmergencyLocation factory |
| `tests/conftest.py` | 1-29 | Root conftest with markers and basic mock_graph_client |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `tests/unit/test_cli_tenants.py` | 1-100 | TenantService mocking, TenantProfile factory |
| `tests/unit/test_cli_numbers.py` | 1-60 | NumberService async mocking, NumberAssignment factory |
| `tests/unit/test_cli_policies.py` | 1-80 | PolicyService mocking, Policy and EffectivePolicyAssignment factories |
| `tests/contract/conftest.py` | 1-187 | Session-scoped fixtures pattern, AsyncGenerator for async fixtures |
| `src/teams_phone/models/__init__.py` | 1-75 | All model exports - reference for factory helpers |

### Code Patterns to Follow

```python
# CliRunner pattern (tests/unit/test_cli_users.py:10-25)
from typer.testing import CliRunner
from teams_phone.cli.main import app

runner = CliRunner()

def test_command():
    result = runner.invoke(app, ["users", "list"])
    assert result.exit_code == 0
```

```python
# GraphClient async context manager mock (tests/unit/test_cli_users.py:93-110)
with (
    patch("teams_phone.cli.users.CacheManager"),
    patch("teams_phone.cli.users.AuthService"),
    patch("teams_phone.cli.users.GraphClient") as mock_gc_class,
    patch("teams_phone.cli.users.UserService") as mock_us_class,
):
    mock_gc = AsyncMock()
    mock_gc.__aenter__.return_value = mock_gc
    mock_gc.__aexit__.return_value = None
    mock_gc_class.return_value = mock_gc

    mock_service = MagicMock()
    mock_service.list_users = AsyncMock(return_value=[])
    mock_us_class.return_value = mock_service
```

```python
# Model factory pattern (tests/unit/test_cli_users.py:29-67)
def _make_user(
    user_id: str = "11111111-1111-1111-1111-111111111111",
    upn: str = "john.doe@contoso.com",
    display_name: str = "John Doe",
    account_type: AccountType = AccountType.USER,
    is_enterprise_voice_enabled: bool = True,
    phone_number: str | None = "+14255551234",
) -> UserConfiguration:
    """Create a UserConfiguration for testing."""
    # Implementation with default values and optional overrides
```

---

## Dependencies

### Internal Modules

- `teams_phone.cli.main` - Main Typer app with command groups
- `teams_phone.models` - All Pydantic models for factories
- `teams_phone.services` - Service classes to mock
- `teams_phone.infrastructure` - CacheManager, GraphClient, AuthService

### External Packages

- `pytest` - Test framework (already installed)
- `pytest-asyncio` - For async test support (already in dev dependencies)
- `typer` - CliRunner for testing (already installed)
- `unittest.mock` - MagicMock, AsyncMock, patch (stdlib)

### New Dependencies Needed

- None required - all dependencies already exist in `pyproject.toml`

---

## Testing Guidance

### Test File Location

`tests/integration/cli/conftest.py` - Fixtures file
`tests/integration/cli/__init__.py` - Package marker

### Test Patterns in Use

```python
# pytest_configure for markers (tests/conftest.py:8-12)
def pytest_configure(config: pytest.Config) -> None:
    """Register custom markers for test categorization."""
    config.addinivalue_line("markers", "unit: Fast unit tests with mocks")
    config.addinivalue_line("markers", "integration: Integration tests with real API")
    config.addinivalue_line("markers", "contract: API contract validation tests")
```

```python
# Session-scoped fixture pattern (tests/contract/conftest.py:98-128)
@pytest.fixture(scope="session")
def test_tenant_profile() -> TenantProfile:
    """Session-scoped fixture for reuse across tests."""
    return TenantProfile(...)
```

### Fixtures Available

- `mock_graph_client` - Basic MagicMock from `tests/conftest.py`
- pytest markers: `unit`, `integration`, `contract`

### Test Utilities

- `tests/conftest.py` - Root conftest with markers
- No existing test utilities in `tests/fixtures/` (directory reserved but empty)

---

## Risks and Blockers

### Technical Risks

- [ ] **Async mock complexity**: AsyncMock context managers need careful setup with `__aenter__` and `__aexit__` - follow existing patterns exactly

### Dependency Risks

- [ ] None - all dependencies satisfied

### Open Questions

- [ ] Should fixtures be function-scoped or module-scoped? Recommendation: function-scoped for isolation, matching unit test pattern
- [ ] Should the composite `mock_cli_services` fixture use `autouse=True`? Recommendation: No, explicitly request it in tests that need it

---

## Implementation Recommendations

### Suggested Approach

1. Create directory structure `tests/integration/cli/` with `__init__.py`
2. Create `conftest.py` with pytest_configure if `integration` marker not already registered (it is in root conftest)
3. Add `runner` fixture wrapping CliRunner()
4. Add individual service mock fixtures following existing unit test patterns
5. Add model factory helpers (can import/adapt from unit tests)
6. Add GraphClient async context manager fixture
7. Add composite `mock_cli_services` fixture for convenience
8. Verify fixtures load with `pytest tests/integration/cli/conftest.py --collect-only`

### Files to Create/Modify

| Action | File | Reason |
|--------|------|--------|
| Create | `tests/integration/cli/__init__.py` | Package marker |
| Create | `tests/integration/cli/conftest.py` | All CLI integration fixtures |

### Quick Wins

- Start with `runner` fixture - simplest and most critical
- Copy `_make_user()` pattern directly from unit tests, adapt names to `make_user_config()`
- Individual service mocks can be minimal (just mock the methods actually used by CLI commands)

---

## Cheat Sheet

### Key Imports

```python
from __future__ import annotations

from datetime import datetime, timezone
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import UUID

import pytest
from typer.testing import CliRunner

from teams_phone.cli.main import app
from teams_phone.models import (
    AccountType,
    AuthMethod,
    AuthStatus,
    AuthStatusType,
    CachedToken,
    ConnectionTestResult,
    EffectivePolicyAssignment,
    EmergencyLocation,
    NumberAssignment,
    Policy,
    TenantProfile,
    UserConfiguration,
)
```

### Key Functions to Use

- `CliRunner()` - Create test runner for Typer apps
- `runner.invoke(app, ["command", "subcommand", "--option"])` - Invoke CLI commands
- `MagicMock()` - Sync service mock
- `AsyncMock()` - Async service mock
- `patch("module.path.Class")` - Patch services at import location

### Common Patterns

- **CliRunner fixture**: See `tests/unit/test_cli_users.py:25` - simple `runner = CliRunner()`
- **GraphClient mock setup**: See `tests/unit/test_cli_users.py:96-110`
- **Model factories**: See `tests/unit/test_cli_users.py:29-67`, `tests/unit/test_cli_locations.py:19-44`
- **Service mock with AsyncMock**: `mock_service.list_users = AsyncMock(return_value=[])`

---

## Exit Code Reference

| Exit Code | Exception | Use Case |
|-----------|-----------|----------|
| 0 | SUCCESS | Operation completed |
| 2 | (Typer) | Missing arguments, --help triggered |
| 2 | AUTH_ERROR | Authentication failure |
| 3 | AUTHZ_ERROR | Authorization failure |
| 4 | NOT_FOUND | Resource not found |
| 5 | VALIDATION_ERROR | Input validation failure |
| 6 | RATE_LIMIT | Rate limit exceeded |
| 7 | CONFIG_ERROR | Configuration error |
| 8 | CONTRACT_ERROR | API contract violation |

---

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