# Task Recon: Implement switch_tenant with auto-login logic

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

---

## Task Summary

Add the `switch_tenant` method to TenantService that validates tenant existence, checks authentication status, and optionally triggers automatic login before updating the default tenant. This is the second subtask of the TenantService implementation.

### 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 service coordinates between ConfigManager for persistence and AuthService for authentication operations.
**This Subtask's Role:** Provides tenant switching functionality with auto-login capability, ensuring users can seamlessly switch between configured tenants without manually re-authenticating.

### Dependencies
- Depends on: 13.1 (list/get/add/remove tenant operations - DONE)
- Parent task depends on: Task 4 (ConfigManager - DONE), Task 6 (AuthService - DONE)

### Acceptance Criteria
From task details:
1. `switch_tenant(self, name: str) -> TenantProfile` method that:
   - Validates tenant exists via `config_manager.get_tenant(name)` (raises NotFoundError if not)
   - Checks authentication status via `auth_service.is_authenticated(name)`
   - If not authenticated, attempts auto-login via `auth_service.login(name)`
   - If auto-login fails, lets AuthenticationError propagate
   - Updates default tenant via `config_manager.set_default_tenant(name)`
   - Returns the TenantProfile for the switched tenant

2. Handle edge cases:
   - Switching to already-current tenant: still validate auth and update default (idempotent)
   - If auth_service.login fails, don't update default_tenant (atomic behavior)
   - Include logging/debug output for switch steps

3. Consider adding optional `auto_login: bool = True` parameter

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 4 | ConfigManager | Load-modify-save pattern: `config = load_config(); modify; save_config(config)`. All CRUD operations delegate to ConfigManager. |
| 4.3 | Default tenant management | `set_default_tenant` validates tenant existence before setting - prevents inconsistent config. |
| 6.4 | Auth login/logout/status | `is_authenticated()` never throws - always safe to call. `login()` returns CachedToken; uses `_resolve_tenant()` internally. |
| 13.1 | TenantService CRUD | Used TYPE_CHECKING import guards for AuthService/ConfigManager. Auth check is informational only in remove_tenant. |

### Reusable Patterns from Dependencies
- **Service pattern**: Follow `AuthService.__init__` pattern at `auth_service.py:53-63` for consistency
- **Exception handling**: Use `pytest.raises(ExceptionClass) as exc_info` then assert on `str(exc_info.value)` and `exc_info.value.remediation`
- **is_authenticated usage**: Never throws, returns False on any error - safe for conditional checks

### Warnings and Gotchas
- **From Task 6.4**: `is_authenticated(None)` returns `False` if no default tenant configured - doesn't throw
- **From Task 13.1**: ConfigManager's `set_tenant()` requires profile.name to match the key
- **Atomic behavior requirement**: Don't update default_tenant if login fails - must catch AuthenticationError before calling set_default_tenant

### Open Items Inherited
- None that affect this task

---

## Architecture Context

### Relevant Design Decisions
- **Service Layer Pattern**: Services abstract Graph API + cache access (repository pattern)
- **Exception Hierarchy**: All inherit from TeamsPhoneError with specific exit codes (2-8)
- **Pydantic Strict Mode**: fail-fast on API contract changes

### Module Structure
```
src/teams_phone/
  services/
    tenant_service.py   # Add switch_tenant here
    auth_service.py     # is_authenticated(), login() - dependencies
    __init__.py         # Already exports TenantService
  infrastructure/
    config_manager.py   # get_tenant(), set_default_tenant() - dependencies
  exceptions.py         # AuthenticationError, NotFoundError, ConfigurationError
  models/
    tenant.py           # TenantProfile model
```

### Conventions to Follow
- Type hints on all methods with `-> ReturnType`
- Docstrings with Args, Returns, Raises sections
- TYPE_CHECKING import guards for circular import prevention
- Pydantic models with `ConfigDict(extra="forbid")`

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/tenant_service.py` | 1-115 | Current TenantService implementation - add switch_tenant here |
| `src/teams_phone/services/auth_service.py` | 419-441 | `is_authenticated()` method - never throws, safe for conditionals |
| `src/teams_phone/services/auth_service.py` | 485-511 | `login()` method - returns CachedToken, raises AuthenticationError on failure |
| `src/teams_phone/infrastructure/config_manager.py` | 120-138 | `get_tenant()` - raises NotFoundError if not found |
| `src/teams_phone/infrastructure/config_manager.py` | 192-208 | `set_default_tenant()` - validates tenant exists first |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/exceptions.py` | 47-57 | AuthenticationError definition |
| `src/teams_phone/exceptions.py` | 72-82 | NotFoundError definition |
| `src/teams_phone/models/tenant.py` | 22-46 | TenantProfile model definition |
| `tests/unit/test_tenant_service.py` | 1-317 | Existing TenantService tests - follow patterns |

### Code Patterns to Follow

```python
# Pattern 1: AuthService.login() signature (auth_service.py:485)
def login(self, tenant_name: str | None = None, force: bool = False) -> CachedToken:
    """Login and acquire an authentication token.
    ...
    """

# Pattern 2: is_authenticated() never-throw pattern (auth_service.py:419)
def is_authenticated(self, tenant_name: str | None = None) -> bool:
    """Check if currently authenticated with a valid token.
    This method never raises exceptions - it returns False for any error.
    """
    try:
        profile = self._resolve_tenant(tenant_name)
        ...
    except (ConfigurationError, NotFoundError, AuthenticationError):
        return False

# Pattern 3: Test pattern with MagicMock (test_tenant_service.py:288-300)
@pytest.mark.unit
def test_checks_auth_status_before_removal(self, tmp_path: Path) -> None:
    """remove_tenant checks if tenant is authenticated before removal."""
    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)
    ...
    auth_service.is_authenticated.assert_called_once_with("auth-tenant")
```

---

## Dependencies

### Internal Modules
- `teams_phone.infrastructure.ConfigManager` - `get_tenant()`, `set_default_tenant()`
- `teams_phone.services.AuthService` - `is_authenticated()`, `login()`
- `teams_phone.exceptions` - `AuthenticationError`, `NotFoundError`
- `teams_phone.models.TenantProfile` - Return type

### External Packages
- No new external packages needed

### New Dependencies Needed
- None required

---

## Testing Guidance

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

### Test Patterns in Use
```python
# Test setup pattern (test_tenant_service.py:31-40)
@pytest.mark.unit
def test_accepts_dependencies(self, tmp_path: Path) -> None:
    config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
    cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))
    auth_service = AuthService(config_manager, cache_manager)
    service = TenantService(config_manager, auth_service)

# Test with mocked AuthService (test_tenant_service.py:288-300)
auth_service = MagicMock(spec=AuthService)
auth_service.is_authenticated.return_value = True
service = TenantService(config_manager, auth_service)
```

### Fixtures Available
- `tmp_path` (pytest builtin) - Temporary directory for config/token files
- `create_test_profile(name)` helper at line 15-24

### Test Utilities
- `from unittest.mock import MagicMock` for mocking AuthService
- `pytest.raises(ExceptionClass)` for exception testing

### Required Test Cases (from testStrategy)
1. Test switch to valid tenant updates default
2. Test switch to nonexistent tenant raises NotFoundError
3. Test auto-login triggers when not authenticated
4. Test auto-login failure propagates exception without changing default
5. Test switch to already-authenticated tenant skips login
6. (Additional) Test with auto_login=False if parameter is added

---

## Risks and Blockers

### Technical Risks
- [x] **Atomic behavior**: Must ensure default_tenant is NOT updated if login fails. Implement by calling login() before set_default_tenant().

### Dependency Risks
- None - all dependencies (Task 4, 6, 13.1) are complete

### Open Questions
- [ ] Should switch_tenant include debug/logging output? Task details mention it but no logging pattern established yet. Recommend: Skip logging for now, add in future task.
- [ ] Should auto_login parameter be added now or deferred? Recommendation: Add `auto_login: bool = True` parameter as mentioned in task details.

---

## Implementation Recommendations

### Suggested Approach
1. Add `switch_tenant` method to TenantService class after `remove_tenant`
2. Implement validation: call `config_manager.get_tenant(name)` first
3. Check auth status: call `auth_service.is_authenticated(name)`
4. If not authenticated and `auto_login=True`: call `auth_service.login(name)`
5. Only after successful auth (or if already authenticated): call `config_manager.set_default_tenant(name)`
6. Return the TenantProfile
7. Add comprehensive unit tests in `TestSwitchTenant` class

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/services/tenant_service.py` | Add switch_tenant method |
| Modify | `tests/unit/test_tenant_service.py` | Add TestSwitchTenant test class |

### Quick Wins
- Start by adding the basic switch_tenant without auto_login parameter
- Add happy path test first to validate approach
- Then add auto_login parameter and remaining tests

---

## Cheat Sheet

### Key Imports
```python
# Already in tenant_service.py:
from teams_phone.exceptions import ConfigurationError
from teams_phone.models import TenantProfile

# May need to add for AuthenticationError (if catching explicitly):
from teams_phone.exceptions import AuthenticationError
```

### Key Functions to Use
- `self.config_manager.get_tenant(name) -> TenantProfile` - validates tenant exists, raises NotFoundError
- `self.auth_service.is_authenticated(name) -> bool` - checks auth status, never throws
- `self.auth_service.login(name) -> CachedToken` - performs login, raises AuthenticationError on failure
- `self.config_manager.set_default_tenant(name) -> None` - updates default tenant

### Common Patterns
- **Atomic operation order**: Validate -> Auth check -> Login (if needed) -> Update default
- **Exception propagation**: Let AuthenticationError from login() propagate naturally
- **Test mocking**: Use `MagicMock(spec=AuthService)` for controlled auth behavior

### Method Signature
```python
def switch_tenant(self, name: str, *, auto_login: bool = True) -> TenantProfile:
    """Switch to a different tenant profile.

    Validates the tenant exists, checks authentication status,
    optionally performs auto-login, and updates the default tenant.

    Args:
        name: Name of the tenant profile to switch to.
        auto_login: If True (default), automatically login if not authenticated.

    Returns:
        The TenantProfile that was switched to.

    Raises:
        NotFoundError: If the tenant does not exist.
        AuthenticationError: If auto_login is True and login fails.
    """
```

---

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