LetsBeBiz-Redesign/letsbe-orchestrator/tests/test_local_mode.py

315 lines
12 KiB
Python
Raw Normal View History

"""
Tests for LOCAL_MODE behavior.
Verifies:
1. Multi-tenant mode (LOCAL_MODE=false) is unchanged
2. Local mode (LOCAL_MODE=true) bootstrap works correctly
3. Meta endpoint is stable in all scenarios
"""
import pytest
from unittest.mock import patch, AsyncMock
from uuid import uuid4
from fastapi.testclient import TestClient
from sqlalchemy import select
from app.config import Settings
from app.main import app
from app.models import Tenant
from app.services.local_bootstrap import LocalBootstrapService
class TestMultiTenantModeUnchanged:
"""
Tests that multi-tenant mode (LOCAL_MODE=false, the default) remains unchanged.
Key assertions:
- No automatic tenant creation on startup
- Tenants must be created manually via API
- Bootstrap service does nothing
"""
def test_local_mode_default_is_false(self):
"""Verify LOCAL_MODE defaults to False."""
settings = Settings()
assert settings.LOCAL_MODE is False
def test_bootstrap_service_skips_when_local_mode_false(self):
"""Verify bootstrap does nothing when LOCAL_MODE=false."""
# Reset class state
LocalBootstrapService._local_tenant_id = None
LocalBootstrapService._bootstrap_attempted = False
LocalBootstrapService._bootstrap_error = None
# Patch settings in the bootstrap module where it's used
with patch('app.services.local_bootstrap.settings') as mock_settings:
mock_settings.LOCAL_MODE = False
import asyncio
asyncio.get_event_loop().run_until_complete(LocalBootstrapService.run())
# Should not have attempted anything
assert LocalBootstrapService._bootstrap_attempted is False
assert LocalBootstrapService._local_tenant_id is None
def test_meta_endpoint_in_multi_tenant_mode(self):
"""Verify meta endpoint returns correct values in multi-tenant mode."""
# Reset bootstrap state
LocalBootstrapService._local_tenant_id = None
LocalBootstrapService._bootstrap_attempted = False
LocalBootstrapService._bootstrap_error = None
with patch('app.config.settings') as mock_settings:
mock_settings.LOCAL_MODE = False
mock_settings.INSTANCE_ID = None
mock_settings.APP_VERSION = "0.1.0"
client = TestClient(app)
response = client.get("/api/v1/meta/instance")
assert response.status_code == 200
data = response.json()
assert data["local_mode"] is False
assert data["tenant_id"] is None
# instance_id may or may not be set depending on config
class TestLocalModeBootstrap:
"""
Tests for LOCAL_MODE=true bootstrap behavior.
"""
def test_bootstrap_requires_instance_id(self):
"""Verify bootstrap fails gracefully without INSTANCE_ID."""
# Reset class state
LocalBootstrapService._local_tenant_id = None
LocalBootstrapService._bootstrap_attempted = False
LocalBootstrapService._bootstrap_error = None
with patch('app.services.local_bootstrap.settings') as mock_settings:
mock_settings.LOCAL_MODE = True
mock_settings.INSTANCE_ID = None
import asyncio
asyncio.get_event_loop().run_until_complete(LocalBootstrapService.run())
assert LocalBootstrapService._bootstrap_attempted is True
assert LocalBootstrapService._local_tenant_id is None
assert "INSTANCE_ID is required" in LocalBootstrapService._bootstrap_error
def test_bootstrap_status_tracking(self):
"""Verify bootstrap status is correctly tracked."""
# Reset class state
LocalBootstrapService._local_tenant_id = None
LocalBootstrapService._bootstrap_attempted = False
LocalBootstrapService._bootstrap_error = None
status = LocalBootstrapService.get_bootstrap_status()
assert status["attempted"] is False
assert status["success"] is False
assert status["tenant_id"] is None
assert status["error"] is None
class TestMetaEndpointStability:
"""
Tests that /api/v1/meta/instance is stable in all scenarios.
Key assertions:
- Endpoint works before tenant exists
- Endpoint works after tenant bootstrap fails
- Endpoint always returns required fields
"""
def test_meta_endpoint_returns_required_fields(self):
"""Verify meta endpoint always returns required fields."""
# Reset bootstrap state
LocalBootstrapService._local_tenant_id = None
LocalBootstrapService._bootstrap_attempted = False
LocalBootstrapService._bootstrap_error = None
client = TestClient(app)
response = client.get("/api/v1/meta/instance")
assert response.status_code == 200
data = response.json()
# These fields must always be present
assert "local_mode" in data
assert "version" in data
assert "bootstrap_status" in data
# These fields can be null but must be present
assert "instance_id" in data
assert "tenant_id" in data
def test_meta_endpoint_after_failed_bootstrap(self):
"""Verify meta endpoint works even after bootstrap failure."""
# Simulate failed bootstrap
LocalBootstrapService._local_tenant_id = None
LocalBootstrapService._bootstrap_attempted = True
LocalBootstrapService._bootstrap_error = "Test error"
client = TestClient(app)
response = client.get("/api/v1/meta/instance")
assert response.status_code == 200
data = response.json()
# Should still return all fields
assert data["bootstrap_status"]["attempted"] is True
assert data["bootstrap_status"]["success"] is False
assert data["bootstrap_status"]["error"] == "Test error"
def test_meta_endpoint_with_successful_bootstrap(self):
"""Verify meta endpoint reflects successful bootstrap."""
tenant_id = uuid4()
# Simulate successful bootstrap
LocalBootstrapService._local_tenant_id = tenant_id
LocalBootstrapService._bootstrap_attempted = True
LocalBootstrapService._bootstrap_error = None
with patch('app.routes.meta.settings') as mock_settings:
mock_settings.LOCAL_MODE = True
mock_settings.INSTANCE_ID = "test-instance-123"
mock_settings.APP_VERSION = "0.1.0"
client = TestClient(app)
response = client.get("/api/v1/meta/instance")
assert response.status_code == 200
data = response.json()
assert data["local_mode"] is True
assert data["instance_id"] == "test-instance-123"
assert data["tenant_id"] == str(tenant_id)
assert data["bootstrap_status"]["success"] is True
class TestConfigDefaults:
"""
Tests for configuration defaults related to LOCAL_MODE.
"""
def test_all_local_mode_settings_have_safe_defaults(self):
"""Verify all LOCAL_MODE settings have safe defaults that don't break multi-tenant mode."""
settings = Settings()
# Core setting defaults to False (multi-tenant)
assert settings.LOCAL_MODE is False
# Optional settings default to None/False
assert settings.INSTANCE_ID is None
assert settings.HUB_URL is None
assert settings.HUB_API_KEY is None
assert settings.HUB_TELEMETRY_ENABLED is False
# Local tenant domain has a default
assert settings.LOCAL_TENANT_DOMAIN == "local.letsbe.cloud"
# Phase 2: LOCAL_AGENT_KEY defaults to None
assert settings.LOCAL_AGENT_KEY is None
class TestLocalAgentRegistration:
"""
Tests for /api/v1/agents/register-local endpoint (Phase 2).
HTTP Status Semantics:
- 404: Endpoint hidden when LOCAL_MODE=false
- 401: Invalid or missing LOCAL_AGENT_KEY
- 503: Local tenant not bootstrapped
- 201: New agent created
- 200: Existing agent returned (idempotent)
"""
def test_register_local_hidden_when_local_mode_false(self):
"""Verify /register-local returns 404 when LOCAL_MODE=false (security by obscurity)."""
# Reset bootstrap state
LocalBootstrapService._local_tenant_id = None
LocalBootstrapService._bootstrap_attempted = False
LocalBootstrapService._bootstrap_error = None
with patch('app.dependencies.local_agent_auth.get_settings') as mock_get_settings:
mock_settings = Settings()
mock_settings.LOCAL_MODE = False
mock_get_settings.return_value = mock_settings
client = TestClient(app)
response = client.post(
"/api/v1/agents/register-local",
json={"hostname": "test-agent", "version": "1.0.0"},
headers={"X-Local-Agent-Key": "any-key"},
)
assert response.status_code == 404
def test_register_local_requires_local_agent_key(self):
"""Verify /register-local returns 401 without X-Local-Agent-Key header."""
# Reset bootstrap state
LocalBootstrapService._local_tenant_id = uuid4()
LocalBootstrapService._bootstrap_attempted = True
LocalBootstrapService._bootstrap_error = None
with patch('app.dependencies.local_agent_auth.get_settings') as mock_get_settings:
mock_settings = Settings()
mock_settings.LOCAL_MODE = True
mock_settings.LOCAL_AGENT_KEY = "test-key-12345"
mock_get_settings.return_value = mock_settings
client = TestClient(app)
# Missing header
response = client.post(
"/api/v1/agents/register-local",
json={"hostname": "test-agent", "version": "1.0.0"},
)
assert response.status_code == 422 # FastAPI validation error for missing header
def test_register_local_rejects_invalid_key(self):
"""Verify /register-local returns 401 with wrong LOCAL_AGENT_KEY."""
# Reset bootstrap state
LocalBootstrapService._local_tenant_id = uuid4()
LocalBootstrapService._bootstrap_attempted = True
LocalBootstrapService._bootstrap_error = None
with patch('app.dependencies.local_agent_auth.get_settings') as mock_get_settings:
mock_settings = Settings()
mock_settings.LOCAL_MODE = True
mock_settings.LOCAL_AGENT_KEY = "correct-key"
mock_get_settings.return_value = mock_settings
client = TestClient(app)
response = client.post(
"/api/v1/agents/register-local",
json={"hostname": "test-agent", "version": "1.0.0"},
headers={"X-Local-Agent-Key": "wrong-key"},
)
assert response.status_code == 401
def test_register_local_503_when_not_bootstrapped(self):
"""Verify /register-local returns 503 if local tenant not bootstrapped."""
# Bootstrap not complete
LocalBootstrapService._local_tenant_id = None
LocalBootstrapService._bootstrap_attempted = False
LocalBootstrapService._bootstrap_error = None
with patch('app.dependencies.local_agent_auth.get_settings') as mock_get_settings:
mock_settings = Settings()
mock_settings.LOCAL_MODE = True
mock_settings.LOCAL_AGENT_KEY = "test-key"
mock_get_settings.return_value = mock_settings
client = TestClient(app)
response = client.post(
"/api/v1/agents/register-local",
json={"hostname": "test-agent", "version": "1.0.0"},
headers={"X-Local-Agent-Key": "test-key"},
)
assert response.status_code == 503
assert "Retry-After" in response.headers