# Task Recon: Implement test_connection with permission validation

**Task ID:** 13.3
**Status:** pending
**Generated:** 2026-01-28T06:15:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Add the `test_connection` method to TenantService that validates connectivity to Microsoft Graph API for a given tenant. The method should:
1. Attempt token acquisition via AuthService
2. Make a test Graph API call to verify connectivity
3. Optionally extract/validate permissions from the token or API
4. Measure and return latency
5. Return a comprehensive `ConnectionTestResult` model with success status, latency, permissions list, and error message if failed

### Parent Task Context

**Parent Task:** Implement TenantService (ID: 13)
**Parent Goal:** Create the TenantService for managing tenant profiles including list, add, remove, switch, and connection testing. The complete TenantService coordinates with both ConfigManager (for profile persistence) and AuthService (for authentication).
**This Subtask's Role:** This is the final subtask (13.3) that completes TenantService by adding the `test_connection()` method with `ConnectionTestResult` model. Subtasks 13.1 and 13.2 established CRUD operations and switch_tenant - this subtask adds the connection testing capability.

### Dependencies
- Depends on: 13.1 (CRUD operations - DONE), 13.2 (switch_tenant - DONE)
- Parent task depends on: Task 4 (ConfigManager), Task 6 (AuthService)

### Acceptance Criteria
From task details:
1. Create `ConnectionTestResult` Pydantic model with: `success: bool`, `tenant_name: str`, `latency_ms: int | None`, `permissions: list[str]`, `error_message: str | None`
2. Implement `test_connection(self, name: str) -> ConnectionTestResult` method
3. Method should measure latency, attempt token acquisition, make a test Graph API call
4. On success: return ConnectionTestResult with success=True, latency, and permissions
5. On failure: return ConnectionTestResult with success=False and error_message (never raise)
6. Export `ConnectionTestResult` from `models/__init__.py`

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 13.1 | CRUD tenant operations | TenantService stores `config_manager` and `auth_service` as instance attributes |
| 13.2 | switch_tenant with auto-login | `auth_service.login(name, force=True)` gets fresh token; `is_authenticated` never throws |
| 4 | ConfigManager | `config_manager.get_tenant(name)` raises `NotFoundError` if tenant doesn't exist |
| 6 | AuthService | `login(tenant_name, force=True)` acquires fresh token via MSAL; returns `CachedToken` |

### Reusable Patterns from Dependencies
- **Service class pattern**: Follow existing TenantService structure at `tenant_service.py:15-147`
- **Pydantic model pattern**: Follow `CachedToken` and `AuthStatus` models at `auth.py:24-79` with `ConfigDict(extra="forbid")`
- **Exception swallowing pattern**: `is_authenticated()` catches all exceptions and returns False - use similar pattern for `test_connection()` to never raise
- **Timer pattern**: Use `time.perf_counter()` for high-precision latency measurement (not currently in codebase but standard Python)

### Warnings and Gotchas
- **From Task 13.2**: `is_authenticated()` never throws, safe to call even for non-existent tenants
- **From Task 6**: MSAL `ConfidentialClientApplication` makes network call in `__init__` - tests must mock it
- **From Task 6**: Error remediation varies by auth method (cert vs secret) for `invalid_client` errors
- **Important**: GraphClient is async but TenantService methods are synchronous - for test_connection, need to either use synchronous httpx call or run async code with asyncio.run()

### Open Items Inherited
- [ ] This is the final subtask for Task 13 - after completion, parent task 13 should be marked done

---

## Architecture Context

### Relevant Design Decisions
- **Services are synchronous**: All TenantService and AuthService methods are sync (not async)
- **GraphClient is async**: Uses `httpx.AsyncClient` with async context manager
- **Exception hierarchy**: All errors inherit from `TeamsPhoneError` with exit codes
- **Pydantic strict mode**: Use `ConfigDict(extra="forbid")` on all models

### Module Structure
```
src/teams_phone/
├── services/
│   ├── tenant_service.py    <- ADD test_connection here
│   └── auth_service.py      <- Use login(force=True) for token
├── models/
│   ├── auth.py              <- ADD ConnectionTestResult here (or new file)
│   └── __init__.py          <- EXPORT ConnectionTestResult
├── infrastructure/
│   └── graph_client.py      <- Reference for Graph API patterns
└── constants.py             <- GRAPH_API_V1_BASE for /organization endpoint
```

### Conventions to Follow
- Use `ConfigDict(extra="forbid")` on Pydantic models
- Method docstrings follow Google style (Args, Returns, Raises)
- Use `TYPE_CHECKING` import guards for type hints to avoid circular imports
- Model placement: Put `ConnectionTestResult` in `models/auth.py` alongside `AuthStatus` or create `models/tenant.py` (but tenant.py already has TenantProfile)

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/tenant_service.py` | 116-147 | Existing switch_tenant method pattern to follow |
| `src/teams_phone/services/auth_service.py` | 485-511 | login() method signature and return type |
| `src/teams_phone/models/auth.py` | 24-79 | CachedToken and AuthStatus models - pattern for ConnectionTestResult |
| `src/teams_phone/constants.py` | 15-19 | GRAPH_API_V1_BASE for /organization endpoint |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/graph_client.py` | 367-395 | get() method pattern for Graph API calls |
| `src/teams_phone/exceptions.py` | 47-57 | AuthenticationError definition |
| `.taskmaster/docs/graph-api-contracts/5-directory-operations-verified.md` | 18-31 | GET /organization endpoint (V1.0, Directory.Read.All) |

### Code Patterns to Follow

```python
# Pattern from auth.py:24-56 - Pydantic model with strict mode
class ConnectionTestResult(BaseModel):
    """Result of a tenant connection test."""

    model_config = ConfigDict(extra="forbid")

    success: bool
    tenant_name: str
    latency_ms: int | None = None
    permissions: list[str] = []
    error_message: str | None = None
```

```python
# Pattern from auth_service.py:419-441 - Exception swallowing
def is_authenticated(self, tenant_name: str | None = None) -> bool:
    try:
        # ... attempt operation
    except (ConfigurationError, NotFoundError, AuthenticationError):
        return False  # Never raise, return safe default
```

```python
# Pattern for latency measurement (standard Python, not in codebase yet)
import time

start = time.perf_counter()
# ... do operation
elapsed_ms = int((time.perf_counter() - start) * 1000)
```

---

## Dependencies

### Internal Modules
- `teams_phone.services.auth_service.AuthService` - Token acquisition via login(force=True)
- `teams_phone.infrastructure.config_manager.ConfigManager` - Get tenant profile
- `teams_phone.models.TenantProfile` - Tenant configuration
- `teams_phone.exceptions` - AuthenticationError, ConfigurationError, NotFoundError

### External Packages
- `httpx` (already in pyproject.toml) - For synchronous Graph API test call
- `pydantic` - For ConnectionTestResult model
- `time` (stdlib) - For perf_counter latency measurement

### New Dependencies Needed
- None required - httpx supports sync calls via `httpx.Client` (not just AsyncClient)

---

## Testing Guidance

### Test File Location
`tests/unit/test_tenant_service.py` - Add new test class `TestTestConnection`

### Test Patterns in Use
```python
# Example test structure from test_tenant_service.py:319-334
class TestSwitchTenant:
    @pytest.mark.unit
    def test_switch_to_valid_tenant_updates_default(self, tmp_path: Path) -> None:
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        auth_service = MagicMock(spec=AuthService)
        auth_service.is_authenticated.return_value = True
        service = TenantService(config_manager, auth_service)
        # ... test logic
```

### Fixtures Available
- `tmp_path` (pytest built-in) - Provides temporary directory for config files
- No custom fixtures in `conftest.py` - tests use real ConfigManager with tmp_path

### Test Utilities
- `unittest.mock.MagicMock(spec=AuthService)` - Mock AuthService with spec enforcement
- `pytest.raises()` - For exception testing (though test_connection should NOT raise)
- `monkeypatch` (pytest fixture) - For patching httpx calls

### Required Test Cases (from task testStrategy)
1. Test successful connection returns success=True with latency
2. Test failed auth returns success=False with error message
3. Test permission extraction from token/API response
4. Test network error handling returns appropriate error_message
5. Test latency measurement is reasonable (> 0)

---

## Risks and Blockers

### Technical Risks
- [ ] **Sync vs Async mismatch**: GraphClient is async, but TenantService is sync. For test_connection, use synchronous `httpx.Client` directly instead of GraphClient to keep the method simple. This avoids asyncio.run() complexity.
- [ ] **Permission extraction complexity**: Task mentions parsing token claims or querying `/oauth2PermissionGrants`. Token claims require JWT decoding; `/oauth2PermissionGrants` requires additional API call. RECOMMENDATION: Start simple - just validate connectivity works, and optionally return an empty permissions list or hardcoded expected permissions. Full permission validation can be a follow-up enhancement.
- [ ] **MSAL network call in tests**: Remember to mock `ConfidentialClientApplication` to avoid network calls during unit tests.

### Dependency Risks
- [ ] None - all dependency tasks (13.1, 13.2) are complete

### Open Questions
- [ ] **Where to place ConnectionTestResult model?** Task suggests `models/tenant.py` or `models/auth.py`. Recommendation: Place in `models/auth.py` since it's authentication-related (tests auth connectivity), similar to `AuthStatus`.
- [ ] **Which Graph API endpoint to use for connectivity test?** Task mentions `/me` or `/organization`. `/organization` (V1.0) with `Directory.Read.All` permission is better for service principal auth (no user context needed). See `5-directory-operations-verified.md`.
- [ ] **What permissions to return?** Options: (a) Parse JWT token claims for scopes, (b) Call `/oauth2PermissionGrants`, (c) Return hardcoded expected permissions, (d) Return empty list. Recommendation: Return the SCOPES used (["https://graph.microsoft.com/.default"]) or the expected app permissions from architecture docs.

---

## Implementation Recommendations

### Suggested Approach
1. **Create ConnectionTestResult model** in `models/auth.py` (alongside AuthStatus)
2. **Export from models/__init__.py**
3. **Add test_connection method** to TenantService:
   - Start timer with `time.perf_counter()`
   - Get tenant profile via `config_manager.get_tenant(name)`
   - Attempt token acquisition via `auth_service.login(name, force=True)`
   - Make sync Graph API call to `GET /organization` using `httpx.Client`
   - Calculate latency_ms
   - On success: return ConnectionTestResult(success=True, tenant_name=name, latency_ms=ms, permissions=[...])
   - On ANY exception: catch and return ConnectionTestResult(success=False, tenant_name=name, error_message=str(e))
4. **Write unit tests** with mocked AuthService and mocked httpx calls

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/models/auth.py` | Add ConnectionTestResult model |
| Modify | `src/teams_phone/models/__init__.py` | Export ConnectionTestResult |
| Modify | `src/teams_phone/services/tenant_service.py` | Add test_connection method |
| Modify | `tests/unit/test_tenant_service.py` | Add TestTestConnection class with tests |

### Quick Wins
- Start with ConnectionTestResult model - simple Pydantic class
- Implement basic test_connection that only checks token acquisition (no Graph call) first
- Add Graph API call second
- Add permission extraction last (or defer to enhancement)

---

## Cheat Sheet

### Key Imports
```python
# For tenant_service.py
import time
import httpx
from teams_phone.constants import GRAPH_API_V1_BASE
from teams_phone.exceptions import AuthenticationError, ConfigurationError, NotFoundError
from teams_phone.models import ConnectionTestResult

# For auth.py (model file)
from pydantic import BaseModel, ConfigDict
```

### Key Functions to Use
- `self.config_manager.get_tenant(name)` - Get TenantProfile (raises NotFoundError)
- `self.auth_service.login(name, force=True)` - Get fresh token, returns CachedToken
- `time.perf_counter()` - High-precision timer for latency
- `httpx.Client().get(url, headers=headers)` - Sync HTTP GET

### Common Patterns
- **Exception swallowing**: See `auth_service.py:419-441` - catch exceptions, return safe value
- **Pydantic model**: See `auth.py:24-56` - use ConfigDict(extra="forbid")
- **Test mocking**: See `test_tenant_service.py:291-301` - MagicMock(spec=AuthService)

### Graph API Endpoint for Test
```
GET https://graph.microsoft.com/v1.0/organization
Authorization: Bearer {token}
```
Returns organization info if token is valid and has Directory.Read.All permission.

### Required Permissions (for reference)
From `.taskmaster/docs/graph-api-contracts/required-permissions-summary-updated.md`:
- `User.Read.All` - User profiles
- `Directory.Read.All` - Tenant info (needed for /organization)
- `TeamsUserConfiguration.Read.All` - Read user voice config
- `TeamsTelephoneNumber.Read.All` - Read phone inventory
- `TeamsTelephoneNumber.ReadWrite.All` - Assign/unassign numbers
- `TeamsPolicyUserAssign.ReadWrite.All` - Assign policies

---

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