# Task ID: 35

**Title:** Write CLI Integration Tests

**Status:** pending

**Dependencies:** 11, 12, 14, 16, 18, 22, 23, 25, 32

**Priority:** medium

**Description:** Create integration tests for CLI commands using CliRunner with mocked services.

**Details:**

Create tests in `tests/integration/cli/`:
```python
from typer.testing import CliRunner
from teams_phone.cli.main import app

runner = CliRunner()

def test_users_list(mock_user_service):
    result = runner.invoke(app, ["users", "list"])
    assert result.exit_code == 0
    assert "displayName" in result.output

def test_users_list_json(mock_user_service):
    result = runner.invoke(app, ["users", "list", "--json"])
    assert result.exit_code == 0
    data = json.loads(result.output)
    assert isinstance(data, list)
```

Test all commands with:
- Default (table) output
- JSON output
- Error cases (not found, validation)
- Global flags (--tenant, --verbose)

**Test Strategy:**

Run `pytest tests/integration/cli/ -v` and verify all CLI commands work correctly with mocked backends.

---

## Implementation Notes

### Session: 2026-01-30 15:30 - Agent: claude-sonnet-4-5-20250929

**Status:** Subtask 35.1 Implemented

#### What Was Done
- Created `tests/integration/cli/` directory structure with `__init__.py`
- Created `tests/integration/cli/conftest.py` with comprehensive fixtures for CLI integration testing

#### Files Created
- `tests/integration/cli/__init__.py` - Package marker
- `tests/integration/cli/conftest.py` - All CLI integration fixtures

#### Fixtures Provided

**CliRunner:**
- `runner` - Function-scoped CliRunner for invoking CLI commands

**Model Factory Functions:**
- `make_user_config()` - Create UserConfiguration with customizable fields
- `make_location()` - Create EmergencyLocation for testing
- `make_number()` - Create NumberAssignment for testing
- `make_policy()` - Create Policy for testing
- `make_tenant()` - Create TenantProfile for testing
- `make_auth_status()` - Create AuthStatus for testing
- `make_cached_token()` - Create CachedToken for testing
- `make_policy_assignment()` - Create EffectivePolicyAssignment for testing

**Individual Service Mocks:**
- `mock_cache_manager` - MagicMock for CacheManager
- `mock_auth_service` - MagicMock with common method stubs
- `mock_user_service` - MagicMock with AsyncMock methods
- `mock_number_service` - MagicMock with AsyncMock methods
- `mock_location_service` - MagicMock with sync methods (cache-based)
- `mock_policy_service` - MagicMock with sync methods (cache-based)
- `mock_tenant_service` - MagicMock with common stubs

**GraphClient Async Context Manager:**
- `mock_graph_client_context` - Pre-configured for `async with` usage

**Composite Fixtures (patches all dependencies for specific commands):**
- `mock_cli_services_users` - Patches CacheManager, AuthService, GraphClient, UserService
- `mock_cli_services_numbers` - Patches CacheManager, AuthService, GraphClient, NumberService
- `mock_cli_services_locations` - Patches CacheManager, LocationService
- `mock_cli_services_policies` - Patches via `_get_sync_service` helper
- `mock_cli_services_tenants` - Patches CacheManager, TenantService
- `mock_cli_services_auth` - Patches CacheManager, AuthService

#### Key Decisions Made
- Function-scoped fixtures for test isolation (matching unit test patterns)
- No autouse on composite fixtures - tests explicitly request what they need
- Factory functions as module-level helpers (not fixtures) for flexibility
- Composite fixtures return dict of mocks for easy access in tests

#### Patterns Discovered
- Users, Numbers CLI commands use GraphClient async context manager pattern
- Locations, Policies CLI commands use sync services (cache-based)
- Policies CLI uses `_get_sync_service` helper function to get service instance
- Auth CLI commands only need CacheManager and AuthService

#### Usage Examples

```python
# Using composite fixture
def test_users_list(runner, mock_cli_services_users):
    mocks = mock_cli_services_users
    mocks["user_service"].list_users.return_value = [make_user_config()]
    result = runner.invoke(app, ["users", "list"])
    assert result.exit_code == 0

# Using individual fixtures with manual patching
def test_with_custom_setup(runner, mock_user_service):
    mock_user_service.list_users.return_value = [
        make_user_config(upn="test@contoso.com")
    ]
    # Set up patches manually for more control
```

#### Open Items for Next Agent
- [ ] 35.2: Write users command integration tests
- [ ] 35.3: Write numbers command integration tests
- [ ] 35.4: Write locations command integration tests
- [ ] 35.5: Write policies command integration tests
- [ ] 35.6: Write tenants command integration tests
- [ ] 35.7: Write auth command integration tests

#### Gotchas and Warnings
- AsyncMock `__aenter__` must return self for context manager pattern
- Policies CLI patches `_get_sync_service` not the service class directly
- mypy errors about missing py.typed are project-wide, not fixture issues

---

### Session: 2026-01-30 21:45 - Agent: claude-sonnet-4-5-20250929

**Status:** Subtask 35.2 Implemented

#### What Was Done
- Created `tests/integration/cli/test_auth_integration.py` with TestAuthWorkflows class (7 tests)
- Created `tests/integration/cli/test_tenants_integration.py` with TestTenantsWorkflows class (8 tests)
- All 15 tests pass with correct exit codes and output validation

#### Files Created
- `tests/integration/cli/test_auth_integration.py` - Auth workflow integration tests
- `tests/integration/cli/test_tenants_integration.py` - Tenants workflow integration tests

#### Tests Implemented

**TestAuthWorkflows (7 tests):**
1. `test_login_then_status` - Login flow followed by status check verifying AUTHENTICATED state
2. `test_logout_then_status` - Logout followed by status showing UNAUTHENTICATED
3. `test_status_table_output` - Verify table format includes tenant, status, expiry columns
4. `test_status_json_output` - Verify JSON structure with json.loads() validation
5. `test_refresh_expired_token` - Test token refresh scenario with --force flag
6. `test_login_already_authenticated` - Test --force flag behavior when already logged in
7. `test_logout_all_tenants` - Test --all flag clears all tenant tokens

**TestTenantsWorkflows (8 tests):**
1. `test_add_then_list_tenant` - Add tenant and verify it appears in list
2. `test_add_then_show_tenant` - Add tenant and verify current displays all config
3. `test_switch_default_tenant` - Switch tenant and verify it becomes current
4. `test_remove_tenant` - Remove tenant with --force and verify it's gone from list
5. `test_test_connection_success` - Mock successful Graph API connection test
6. `test_test_connection_failure` - Mock connection failure with proper error
7. `test_list_marks_current_tenant` - Verify 'Current' indicator in list output
8. `test_tenants_json_output` - All commands produce valid JSON with --json flag

#### Key Decisions Made
- Used `@pytest.mark.integration` marker for all tests (matching project conventions)
- Used composite fixtures `mock_cli_services_auth` and `mock_cli_services_tenants` from conftest.py
- Used non-numeric tenant IDs in table output tests to avoid ANSI escape code issues with number highlighting
- For tenants remove tests, used `--force` flag to bypass interactive confirmation prompts

#### Patterns Discovered
- CLI output includes ANSI escape codes for number highlighting even with `--no-color` flag
- Factory functions from conftest.py (`make_auth_status`, `make_cached_token`, `make_tenant`) work seamlessly
- JSON output tests use `json.loads()` to validate output structure
- Connection test failure returns exit code 1 in table mode, exit code 0 in JSON mode (JSON mode always returns data)

#### Open Items for Next Agent
- [ ] 35.3: Write numbers command integration tests
- [ ] 35.4: Write locations command integration tests
- [ ] 35.5: Write policies command integration tests
- [ ] 35.6: Write users command integration tests

#### Gotchas and Warnings
- Table output with numeric values may contain ANSI escape codes - use non-numeric test data or check for presence rather than exact match
- Connection test JSON mode doesn't fail with exit code 1 even on failure - it returns the full result object
- Interactive prompts (like `typer.confirm()`) can be bypassed with `--force` flag or `input="y\n"` parameter

---

### Session: 2026-01-30 22:00 - Agent: claude-sonnet-4-5-20250929

**Status:** Subtask 35.3 Implemented

#### What Was Done
- Created `tests/integration/cli/test_users_integration.py` with 3 test classes (20 tests)
- Created `tests/integration/cli/test_locations_integration.py` with 3 test classes (19 tests)
- All 39 tests pass with correct exit codes and output validation

#### Files Created
- `tests/integration/cli/test_users_integration.py` - Users CLI integration tests
- `tests/integration/cli/test_locations_integration.py` - Locations CLI integration tests

#### Tests Implemented

**TestUsersListIntegration (11 tests):**
1. `test_list_all_users_table_output` - List without filters returns users in table format
2. `test_list_users_json_output` - List with --json produces valid JSON array
3. `test_list_users_licensed_filter` - --licensed filter passes correct param to service
4. `test_list_users_unlicensed_filter` - --unlicensed passes licensed=False to service
5. `test_list_users_assigned_filter` - --assigned filter passes correct param to service
6. `test_list_users_unassigned_filter` - --unassigned passes assigned=False to service
7. `test_list_users_account_type_filter` - --account-type filter passes correct param
8. `test_list_users_limit_option` - --limit passes max_items to service
9. `test_list_users_all_option` - --all passes max_items=None to service
10. `test_list_users_limit_and_all_mutually_exclusive` - ValidationError exit code 5
11. `test_list_users_empty_result` - Empty result shows info message

**TestUsersShowIntegration (3 tests):**
1. `test_show_user_table_output` - Show displays detailed info in table format
2. `test_show_user_json_output` - Show with --json produces valid JSON object
3. `test_show_user_not_found` - NotFoundError results in exit code 4

**TestUsersSearchIntegration (6 tests):**
1. `test_search_users_table_output` - Search returns matching users in table format
2. `test_search_users_json_output` - Search with --json produces valid JSON array
3. `test_search_users_with_field_filter` - --field restricts search to specific field
4. `test_search_users_with_limit` - --limit passes max_results to service
5. `test_search_users_empty_result` - Empty search shows info message
6. `test_search_users_phone_field` - --field phone searches by phone number

**TestLocationsListIntegration (7 tests):**
1. `test_list_all_locations_table_output` - List without filters returns locations
2. `test_list_locations_json_output` - List with --json produces valid JSON array
3. `test_list_locations_city_filter` - --city filter returns only matching city
4. `test_list_locations_limit_option` - --limit restricts number of results
5. `test_list_locations_limit_and_all_mutually_exclusive` - ValidationError exit code 5
6. `test_list_locations_staleness_warning` - Staleness warning is displayed
7. `test_list_locations_empty_result` - Empty result shows info message

**TestLocationsShowIntegration (4 tests):**
1. `test_show_location_table_output` - Show displays detailed information
2. `test_show_location_json_output` - Show with --json produces valid JSON
3. `test_show_location_not_found` - NotFoundError results in exit code 4
4. `test_show_location_staleness_warning` - Staleness warning on show command

**TestLocationsSearchIntegration (8 tests):**
1. `test_search_locations_table_output` - Search returns matching locations
2. `test_search_locations_json_output` - Search with --json produces valid JSON
3. `test_search_locations_with_field_filter` - --field restricts search
4. `test_search_locations_by_name` - --field name searches by description
5. `test_search_locations_with_limit` - --limit restricts number of results
6. `test_search_locations_empty_result` - Empty search shows info message
7. `test_search_locations_by_company` - --field company searches by company name
8. `test_search_locations_staleness_warning` - Staleness warning on search command

#### Key Decisions Made
- Used short, unique display names (e.g., "JohnDoe", "HasPhone") to avoid table column truncation issues
- Avoided numeric values in test data where possible to prevent ANSI escape code complications
- Staleness warning checks use partial matching (e.g., "days old") to avoid numeric ANSI codes
- Used `mock_cli_services_users` and `mock_cli_services_locations` composite fixtures

#### Patterns Discovered
- Users CLI uses async operations - mocks already configured with AsyncMock
- Locations CLI is synchronous (cache-based) - simpler mocking
- Rich table columns truncate long values with ellipsis - use short test data
- Staleness warnings contain numeric values that get ANSI-highlighted

#### Gotchas and Warnings
- ANSI escape codes appear even with `--no-color` for some numeric highlighting
- Rich table columns have fixed width and truncate with ellipsis (…)
- For table output tests, use short unique strings that fit in column width
- Exit codes: 4 = NotFoundError, 5 = ValidationError

---

### Session: 2026-01-30 22:30 - Agent: claude-sonnet-4-5-20250929

**Status:** Subtask 35.4 Implemented

#### What Was Done
- Created `tests/integration/cli/test_numbers_integration.py` with 5 test classes (26 tests)
- Created `tests/integration/cli/test_policies_integration.py` with 3 test classes (12 tests)
- Created `tests/integration/cli/test_global_options.py` with 3 test classes (19 tests)
- All 57 tests pass, completing the CLI integration test suite (111 total tests)

#### Files Created
- `tests/integration/cli/test_numbers_integration.py` - Numbers CLI integration tests
- `tests/integration/cli/test_policies_integration.py` - Policies CLI integration tests
- `tests/integration/cli/test_global_options.py` - Global flags and error scenario tests

#### Tests Implemented

**TestNumbersListIntegration (9 tests):**
1. `test_list_all_numbers_table_output` - List without filters returns numbers in table format
2. `test_list_numbers_json_output` - List with --json produces valid JSON array
3. `test_list_numbers_status_filter` - --status filter passes correct param to service
4. `test_list_numbers_type_filter` - --type filter passes correct param to service
5. `test_list_numbers_city_filter` - --city filter passes correct param to service
6. `test_list_numbers_limit_option` - --limit passes max_items to service
7. `test_list_numbers_all_option` - --all passes max_items=None to service
8. `test_list_numbers_limit_and_all_mutually_exclusive` - Exit code 5
9. `test_list_numbers_empty_result` - Empty result shows info message

**TestNumbersShowIntegration (3 tests):**
1. `test_show_number_table_output` - Show displays detailed number info
2. `test_show_number_json_output` - Show with --json produces valid JSON
3. `test_show_number_not_found` - NotFoundError results in exit code 4

**TestNumbersSearchIntegration (4 tests):**
1. `test_search_numbers_table_output` - Search returns matching numbers
2. `test_search_numbers_json_output` - Search with --json produces valid JSON
3. `test_search_numbers_with_limit` - --limit passes max_results to service
4. `test_search_numbers_empty_result` - Empty search shows info message

**TestNumbersAssignIntegration (5 tests):**
1. `test_assign_number_success` - Successful assignment with --force
2. `test_assign_number_dry_run` - --dry-run previews without changes
3. `test_assign_number_dry_run_json` - --dry-run with --json produces JSON preview
4. `test_assign_number_missing_location_validation_error` - Exit code 5 for missing location
5. `test_assign_number_user_not_found` - Exit code 4 for unknown user

**TestNumbersUnassignIntegration (5 tests):**
1. `test_unassign_number_success` - Successful unassignment with --force
2. `test_unassign_number_dry_run` - --dry-run previews without changes
3. `test_unassign_number_dry_run_json` - --dry-run with --json produces JSON preview
4. `test_unassign_number_not_assigned_validation_error` - Exit code 5
5. `test_unassign_number_not_found` - Exit code 4 for unknown number

**TestPoliciesListIntegration (5 tests):**
1. `test_list_all_policies_table_output` - List returns policies in table format
2. `test_list_policies_json_output` - List with --json produces valid JSON
3. `test_list_policies_type_filter` - Policy type argument filters results
4. `test_list_policies_staleness_warning` - Staleness warning is displayed
5. `test_list_policies_empty_result` - Empty result shows info message

**TestPoliciesShowIntegration (3 tests):**
1. `test_show_user_policies_table_output` - Show displays policy assignments
2. `test_show_user_policies_json_output` - Show with --json produces valid JSON
3. `test_show_user_policies_user_not_found` - Exit code 4 for unknown user

**TestPoliciesAssignIntegration (7 tests):**
1. `test_assign_policy_success` - Successful assignment with --force
2. `test_assign_policy_dry_run` - --dry-run previews without changes
3. `test_assign_policy_dry_run_json` - --dry-run with --json produces JSON preview
4. `test_assign_policy_json_output` - Successful assignment with --json
5. `test_assign_policy_invalid_type` - Exit code 4 (unknown type goes to service)
6. `test_assign_policy_user_not_found` - Exit code 4 for unknown user
7. `test_assign_policy_not_found` - Exit code 4 for unknown policy

**TestGlobalOptions (5 tests):**
1. `test_json_flag_position_before_command` - --json flag before command works
2. `test_no_color_flag_disables_ansi` - --no-color suppresses colors
3. `test_verbose_flag_increases_output` - --verbose enables verbose mode
4. `test_combined_flags` - Multiple global flags work together
5. `test_json_flag_on_empty_result` - --json with empty result returns empty array

**TestErrorScenarios (11 tests):**
1. `test_not_found_error_exit_code_4` - NotFoundError produces exit code 4
2. `test_validation_error_exit_code_5` - ValidationError produces exit code 5
3. `test_authentication_error_exit_code_2` - AuthenticationError produces exit code 2
4. `test_configuration_error_exit_code_7` - ConfigurationError produces exit code 7
5. `test_contract_error_exit_code_8` - ContractError produces exit code 8
6. `test_error_exit_code_in_json_mode` - Errors in JSON mode return correct exit code
7. `test_remediation_message_displayed` - Remediation message shown with error
8. `test_mutually_exclusive_options_validation_error` - Exit code 5
9. `test_numbers_list_validation_error` - Numbers list conflicting options
10. `test_locations_list_validation_error` - Locations list conflicting options

**TestGlobalTenantFlag (1 test):**
1. `test_tenant_flag_is_passed_to_context` - --tenant flag captured in context

#### Key Decisions Made
- Created custom fixtures for assign commands requiring multiple services (UserService, LocationService, PolicyService)
- PolicyService async methods (get_user_policies, assign_policy) need AsyncMock setup
- Phone numbers in output may have ANSI bold codes around digits - use partial matching
- Invalid policy types pass through to service layer for validation (not caught at CLI level)
- In JSON mode, error messages are suppressed - exit code is the only indicator

#### Patterns Discovered
- Numbers assign command uses `_run_assign_async` which creates multiple services internally
- Policies show/assign use `_run_async` with PolicyService + UserService
- OutputFormatter.error() returns early in JSON mode (no output)
- Assign commands require proper AsyncMock setup for async operations

#### Gotchas and Warnings
- Phone numbers in table output have ANSI bold codes splitting the digits (e.g., `+\x1b[1m14255551234\x1b[0m`)
- PolicyService.get_user_policies and assign_policy must be AsyncMock (not regular MagicMock)
- In JSON mode, errors don't produce any output - check exit code only
- Invalid policy types don't trigger exit code 5 at CLI level - they get NotFoundError (4) from service

---
