# Task Recon: Implement users and locations CLI integration tests

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

---

## Task Summary

Create integration tests for users commands (list/show/search with filters) and locations commands (list/show/search) testing both output formats (table and JSON) and staleness warnings.

### Parent Task Context

**Parent Task:** Write CLI Integration Tests (ID: 35)
**Parent Goal:** Create comprehensive integration tests for CLI commands using CliRunner with mocked services, testing all command groups with default (table) output, JSON output, error cases (not found, validation), and global flags (--tenant, --verbose).
**This Subtask's Role:** Implements integration tests for two specific command groups: `users` (list/show/search with filters like --licensed, --assigned, --account-type, --limit) and `locations` (list/show/search with city filter and staleness warnings).

### Dependencies
- Depends on: 35.1 (CLI integration test infrastructure with conftest.py and shared fixtures) - **DONE**

### Acceptance Criteria
- Create `tests/integration/cli/test_users_integration.py` with `TestUsersListIntegration`, `TestUsersShowIntegration`, `TestUsersSearchIntegration` classes
- Create `tests/integration/cli/test_locations_integration.py` with `TestLocationsListIntegration`, `TestLocationsShowIntegration`, `TestLocationsSearchIntegration` classes
- Test all filter options (--licensed/--unlicensed, --assigned/--unassigned, --account-type, --limit, --all, --city, --field)
- Validate both table and JSON output formats
- Test error cases (NotFoundError with exit code 4, ValidationError with exit code 5 for mutually exclusive flags)
- Test staleness warnings for locations cache
- All tests marked with `@pytest.mark.integration`
- Tests pass with >80% coverage of CLI command modules

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 35.1 | Create CLI integration test infrastructure | Provides composite fixtures (`mock_cli_services_users`, `mock_cli_services_locations`) and factory functions (`make_user_config()`, `make_location()`) |
| 35.2 | Implement auth and tenants CLI integration tests | Established pattern for integration tests - use composite fixtures from conftest.py, JSON validation with `json.loads()`, ANSI escape code handling |

### Reusable Patterns from Dependencies
- **Composite fixtures**: Use `mock_cli_services_users` for users commands, `mock_cli_services_locations` for locations commands (implemented in `tests/integration/cli/conftest.py:556-648`)
- **Factory functions**: `make_user_config()` at line 67, `make_location()` at line 147 in conftest.py
- **Test class structure**: Use `TestXxxWorkflows` or `TestXxxIntegration` class naming (see `tests/integration/cli/test_auth_integration.py:25`)

### Warnings and Gotchas
- **ANSI escape codes in table output**: Table output with numeric values may contain ANSI escape codes. Use non-numeric test data or check for presence rather than exact match. From Task 35.2: "Used non-numeric tenant IDs in table output tests to avoid ANSI escape code issues with number highlighting"
- **AsyncMock for GraphClient**: Users CLI commands use async context manager pattern. The `mock_cli_services_users` fixture handles this automatically.
- **Locations CLI is synchronous**: Locations uses sync services (cache-based), simpler mocking pattern than users.
- **Exit codes matter**: NotFoundError = 4, ValidationError = 5. Must verify these in tests.

### Open Items Inherited
- None - 35.1 and 35.2 are complete with all infrastructure in place

---

## Architecture Context

### Relevant Design Decisions
- **CLI layered architecture**: CLI commands in `src/teams_phone/cli/` call services in `src/teams_phone/services/` which abstract Graph API or CSV cache access
- **Users CLI uses async GraphClient**: Uses `_run_async()` helper function that wraps async operations with `asyncio.run()`
- **Locations CLI is sync/cache-based**: Uses `_get_service()` helper that creates `LocationService(CacheManager())`
- **Global options**: `--json`, `--verbose`, `--tenant`, `--no-color` are captured in `CLIContext` via callback

### Module Structure
```
tests/integration/cli/
  __init__.py              # Package marker
  conftest.py              # Shared fixtures (runner, mocks, factories)
  test_auth_integration.py # Auth command tests (reference for patterns)
  test_tenants_integration.py # Tenants command tests (reference)
  test_users_integration.py    # TO BE CREATED
  test_locations_integration.py # TO BE CREATED
```

### Conventions to Follow
- Mark all tests with `@pytest.mark.integration`
- Use composite fixtures from conftest.py (`mock_cli_services_users`, `mock_cli_services_locations`)
- Use factory functions for test data (`make_user_config()`, `make_location()`)
- Test both table output (default) and JSON output (`--json` flag)
- Validate JSON structure with `json.loads()` and assertions on fields
- Check exit codes for error scenarios

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `tests/integration/cli/conftest.py` | 556-593 | `mock_cli_services_users` fixture - patches CacheManager, AuthService, GraphClient, UserService |
| `tests/integration/cli/conftest.py` | 626-648 | `mock_cli_services_locations` fixture - patches CacheManager, LocationService |
| `tests/integration/cli/conftest.py` | 67-144 | `make_user_config()` factory function |
| `tests/integration/cli/conftest.py` | 147-196 | `make_location()` factory function |
| `src/teams_phone/cli/users.py` | 151-233 | `list_users` command - filters, limit, all options |
| `src/teams_phone/cli/users.py` | 236-315 | `show_user` command - single user display |
| `src/teams_phone/cli/users.py` | 318-393 | `search_users` command - field filtering |
| `src/teams_phone/cli/locations.py` | 84-155 | `list_locations` command - city filter, staleness |
| `src/teams_phone/cli/locations.py` | 158-206 | `show_location` command |
| `src/teams_phone/cli/locations.py` | 209-295 | `search_locations` command - field options |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `tests/integration/cli/test_auth_integration.py` | 25-216 | Pattern for integration test classes and JSON validation |
| `tests/integration/cli/test_tenants_integration.py` | 22-348 | Pattern for comprehensive workflow tests |
| `tests/unit/test_cli_users.py` | 80-384 | Unit tests to understand CLI behavior expectations |
| `tests/unit/test_cli_locations.py` | 47-725 | Unit tests for locations commands |
| `src/teams_phone/exceptions.py` | 72-95 | NotFoundError (exit code 4) and ValidationError (exit code 5) |

### Code Patterns to Follow

```python
# Example integration test pattern from test_auth_integration.py:29-52
class TestAuthWorkflows:
    """Integration tests for auth command workflows."""

    @pytest.mark.integration
    def test_login_then_status(
        self,
        runner: CliRunner,
        mock_cli_services_auth: dict[str, MagicMock],
    ) -> None:
        """Login flow followed by status check verifying AUTHENTICATED state."""
        mocks = mock_cli_services_auth
        token = make_cached_token()
        auth_status = make_auth_status(status=AuthStatusType.AUTHENTICATED)

        mocks["auth_service"].login.return_value = token
        mocks["auth_service"].get_status.return_value = auth_status

        # Invoke command
        login_result = runner.invoke(app, ["auth", "login"])
        assert login_result.exit_code == 0
        assert "authenticated" in login_result.stdout.lower()
```

```python
# Example JSON validation from test_tenants_integration.py:270-302
@pytest.mark.integration
def test_tenants_json_output(
    self,
    runner: CliRunner,
    mock_cli_services_tenants: dict[str, MagicMock],
) -> None:
    """All commands produce valid JSON with --json flag."""
    mocks = mock_cli_services_tenants
    tenant = make_tenant()

    mocks["tenant_service"].list_tenants.return_value = [tenant]

    # Test list JSON
    list_result = runner.invoke(app, ["--json", "tenants", "list"])
    assert list_result.exit_code == 0
    list_data = json.loads(list_result.stdout)
    assert isinstance(list_data, list)
    assert len(list_data) == 1
```

---

## Dependencies

### Internal Modules
- `teams_phone.cli.main.app` - Main Typer app to invoke commands
- `teams_phone.cli.users` - Users command group
- `teams_phone.cli.locations` - Locations command group
- `teams_phone.models` - UserConfiguration, EmergencyLocation, AccountType, etc.
- `teams_phone.exceptions` - NotFoundError, ValidationError for error testing

### External Packages
- `pytest` - Test framework with markers
- `typer.testing.CliRunner` - CLI command invocation
- `json` - JSON output parsing

### New Dependencies Needed
- None required - all dependencies available in existing test infrastructure

---

## Testing Guidance

### Test File Location
- `tests/integration/cli/test_users_integration.py`
- `tests/integration/cli/test_locations_integration.py`

### Test Patterns in Use

```python
# Pattern from test_auth_integration.py for using composite fixtures
@pytest.mark.integration
def test_list_all_users(
    self,
    runner: CliRunner,
    mock_cli_services_users: dict[str, MagicMock],
) -> None:
    """List without filters returns all users in table format."""
    mocks = mock_cli_services_users
    users = [
        make_user_config(upn="john.doe@contoso.com", display_name="John Doe"),
        make_user_config(upn="jane.smith@contoso.com", display_name="Jane Smith"),
    ]
    mocks["user_service"].list_users.return_value = users

    result = runner.invoke(app, ["--no-color", "users", "list"])
    assert result.exit_code == 0
    assert "John Doe" in result.stdout
    assert "Jane Smith" in result.stdout
```

```python
# Pattern for testing exit codes on errors
@pytest.mark.integration
def test_show_user_not_found(
    self,
    runner: CliRunner,
    mock_cli_services_users: dict[str, MagicMock],
) -> None:
    """NotFoundError with exit code 4."""
    mocks = mock_cli_services_users
    mocks["user_service"].resolve_user.side_effect = NotFoundError(
        "User 'nonexistent@contoso.com' not found",
        remediation="Verify the user principal name is correct.",
    )

    result = runner.invoke(app, ["users", "show", "nonexistent@contoso.com"])
    assert result.exit_code == 4  # NotFoundError exit code
```

### Fixtures Available
- `runner` - CliRunner for invoking CLI commands
- `mock_cli_services_users` - Patches CacheManager, AuthService, GraphClient, UserService for users commands
- `mock_cli_services_locations` - Patches CacheManager, LocationService for locations commands
- `make_user_config()` - Factory function for UserConfiguration test data
- `make_location()` - Factory function for EmergencyLocation test data

### Test Utilities
- `tests/integration/cli/conftest.py` - All CLI integration fixtures and factory functions

---

## Risks and Blockers

### Technical Risks
- [ ] **AsyncMock behavior**: Users CLI uses async operations. The composite fixture handles this, but ensure `list_users`, `resolve_user`, `search_users` return values are set correctly (they are AsyncMock by default)

### Dependency Risks
- None - all infrastructure from 35.1 is complete and working

### Open Questions
- None identified - task details are comprehensive

---

## Implementation Recommendations

### Suggested Approach
1. Create `tests/integration/cli/test_users_integration.py` with three test classes:
   - `TestUsersListIntegration` (9 tests)
   - `TestUsersShowIntegration` (3 tests)
   - `TestUsersSearchIntegration` (4 tests)
2. Create `tests/integration/cli/test_locations_integration.py` with three test classes:
   - `TestLocationsListIntegration` (4 tests)
   - `TestLocationsShowIntegration` (2 tests)
   - `TestLocationsSearchIntegration` (3 tests)
3. Use composite fixtures from conftest.py
4. Follow the established pattern from test_auth_integration.py and test_tenants_integration.py

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `tests/integration/cli/test_users_integration.py` | Integration tests for users commands |
| Create | `tests/integration/cli/test_locations_integration.py` | Integration tests for locations commands |

### Quick Wins
- Start with `test_list_all_users` and `test_list_all_locations` to validate fixture setup works
- Then add filter tests, JSON output tests, and error cases

---

## Cheat Sheet

### Key Imports
```python
from __future__ import annotations

import json
from unittest.mock import MagicMock

import pytest
from typer.testing import CliRunner

from teams_phone.cli.main import app
from teams_phone.exceptions import NotFoundError, ValidationError
from teams_phone.models import AccountType

from tests.integration.cli.conftest import make_user_config, make_location
```

### Key Functions to Use
- `runner.invoke(app, ["users", "list"])` - Invoke CLI command
- `result.exit_code` - Check exit status
- `result.stdout` - Check output text
- `json.loads(result.stdout)` - Parse JSON output
- `mocks["user_service"].list_users.return_value = [...]` - Set mock return value
- `mocks["user_service"].resolve_user.side_effect = NotFoundError(...)` - Set mock to raise exception

### Common Patterns
- **Table output test**: `runner.invoke(app, ["--no-color", "users", "list"])` then check for strings in stdout
- **JSON output test**: `runner.invoke(app, ["--json", "users", "list"])` then `json.loads(result.stdout)`
- **Error test**: Set `side_effect` on mock, verify `result.exit_code == 4` (or 5)
- **Filter test**: Invoke with filter flag, verify mock was called with expected arguments via `mocks["user_service"].list_users.call_args`

### Exit Codes Reference
- 0: Success
- 2: Missing argument (Typer error)
- 4: NotFoundError
- 5: ValidationError (e.g., --limit and --all together)

---

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