""" 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