""" Integration tests for Retrieval Strategy API. [AC-AISVC-RES-01~15] End-to-end tests for strategy management endpoints. Tests the full API flow: - GET /strategy/retrieval/current - POST /strategy/retrieval/switch - POST /strategy/retrieval/validate - POST /strategy/retrieval/rollback """ import json import pytest from unittest.mock import patch, MagicMock, AsyncMock from fastapi.testclient import TestClient from app.main import app @pytest.fixture(autouse=True) def mock_api_key_service(): """ Mock API key service to bypass authentication in tests. """ mock_service = MagicMock() mock_service._initialized = True mock_service._keys_cache = {"test-api-key": MagicMock()} mock_validation = MagicMock() mock_validation.ok = True mock_validation.reason = None mock_service.validate_key_with_context.return_value = mock_validation with patch("app.services.api_key.get_api_key_service", return_value=mock_service): yield mock_service @pytest.fixture(autouse=True) def reset_strategy_state(): """ Reset strategy state before and after each test. """ from app.services.retrieval.strategy.strategy_router import get_strategy_router, set_strategy_router from app.services.retrieval.strategy.config import RetrievalStrategyConfig set_strategy_router(None) router = get_strategy_router() router.update_config(RetrievalStrategyConfig()) yield set_strategy_router(None) router = get_strategy_router() router.update_config(RetrievalStrategyConfig()) class TestRetrievalStrategyAPIIntegration: """ [AC-AISVC-RES-01~15] Integration tests for retrieval strategy API. """ @pytest.fixture def client(self): return TestClient(app) @pytest.fixture def valid_headers(self): return { "X-Tenant-Id": "test@ash@2026", "X-API-Key": "test-api-key", } def test_get_current_strategy(self, client, valid_headers): """ [AC-AISVC-RES-01] GET /current should return strategy status. """ response = client.get( "/strategy/retrieval/current", headers=valid_headers, ) assert response.status_code == 200 data = response.json() assert "active_strategy" in data assert "grayscale" in data assert data["active_strategy"] in ["default", "enhanced"] def test_switch_strategy_to_enhanced(self, client, valid_headers): """ [AC-AISVC-RES-02] POST /switch should switch to enhanced strategy. """ response = client.post( "/strategy/retrieval/switch", json={ "active_strategy": "enhanced", }, headers=valid_headers, ) assert response.status_code == 200 data = response.json() assert "success" in data assert data["success"] is True assert data["current_strategy"] == "enhanced" def test_switch_strategy_with_grayscale_percentage(self, client, valid_headers): """ [AC-AISVC-RES-03] POST /switch should accept grayscale percentage. """ response = client.post( "/strategy/retrieval/switch", json={ "active_strategy": "enhanced", "grayscale": { "enabled": True, "percentage": 30.0, }, }, headers=valid_headers, ) assert response.status_code == 200 data = response.json() assert data["success"] is True def test_switch_strategy_with_allowlist(self, client, valid_headers): """ [AC-AISVC-RES-03] POST /switch should accept allowlist. """ response = client.post( "/strategy/retrieval/switch", json={ "active_strategy": "enhanced", "grayscale": { "enabled": True, "allowlist": ["tenant_a", "tenant_b"], }, }, headers=valid_headers, ) assert response.status_code == 200 data = response.json() assert data["success"] is True def test_validate_strategy(self, client, valid_headers): """ [AC-AISVC-RES-06] POST /validate should validate strategy. """ response = client.post( "/strategy/retrieval/validate", json={ "active_strategy": "enhanced", }, headers=valid_headers, ) assert response.status_code == 200 data = response.json() assert "valid" in data assert "errors" in data assert isinstance(data["valid"], bool) def test_validate_default_strategy(self, client, valid_headers): """ [AC-AISVC-RES-06] Default strategy should pass validation. """ response = client.post( "/strategy/retrieval/validate", json={ "active_strategy": "default", }, headers=valid_headers, ) assert response.status_code == 200 data = response.json() assert data["valid"] is True def test_rollback_strategy(self, client, valid_headers): """ [AC-AISVC-RES-07] POST /rollback should rollback to default. """ client.post( "/strategy/retrieval/switch", json={ "active_strategy": "enhanced", }, headers=valid_headers, ) response = client.post( "/strategy/retrieval/rollback", headers=valid_headers, ) assert response.status_code == 200 data = response.json() assert "success" in data assert data["current_strategy"] == "default" class TestRetrievalStrategyAPIValidation: """ [AC-AISVC-RES-03] Tests for API request validation. """ @pytest.fixture def client(self): return TestClient(app) @pytest.fixture def valid_headers(self): return { "X-Tenant-Id": "test@ash@2026", "X-API-Key": "test-api-key", } def test_switch_invalid_strategy(self, client, valid_headers): """ [AC-AISVC-RES-03] Invalid strategy value should return error. """ response = client.post( "/strategy/retrieval/switch", json={ "active_strategy": "invalid_strategy", }, headers=valid_headers, ) assert response.status_code in [400, 422, 500] def test_switch_percentage_out_of_range(self, client, valid_headers): """ [AC-AISVC-RES-03] Percentage > 100 should return validation error. """ response = client.post( "/strategy/retrieval/switch", json={ "active_strategy": "enhanced", "grayscale": { "percentage": 150.0, }, }, headers=valid_headers, ) assert response.status_code in [400, 422] class TestRetrievalStrategyAPIFlow: """ [AC-AISVC-RES-01~15] Tests for complete API flow scenarios. """ @pytest.fixture def client(self): return TestClient(app) @pytest.fixture def valid_headers(self): return { "X-Tenant-Id": "test@ash@2026", "X-API-Key": "test-api-key", } def test_complete_strategy_lifecycle(self, client, valid_headers): """ [AC-AISVC-RES-01~07] Test complete strategy lifecycle: 1. Get current strategy 2. Switch to enhanced 3. Validate 4. Rollback 5. Verify back to default """ current = client.get( "/strategy/retrieval/current", headers=valid_headers, ) assert current.status_code == 200 assert current.json()["active_strategy"] == "default" switch = client.post( "/strategy/retrieval/switch", json={ "active_strategy": "enhanced", "grayscale": {"enabled": True, "percentage": 50.0}, }, headers=valid_headers, ) assert switch.status_code == 200 assert switch.json()["current_strategy"] == "enhanced" validate = client.post( "/strategy/retrieval/validate", json={"active_strategy": "enhanced"}, headers=valid_headers, ) assert validate.status_code == 200 rollback = client.post( "/strategy/retrieval/rollback", headers=valid_headers, ) assert rollback.status_code == 200 assert rollback.json()["current_strategy"] == "default" final = client.get( "/strategy/retrieval/current", headers=valid_headers, ) assert final.status_code == 200 assert final.json()["active_strategy"] == "default" class TestRetrievalStrategyAPIMissingTenant: """ Tests for API behavior without tenant ID. """ @pytest.fixture def client(self): return TestClient(app) @pytest.fixture def api_key_headers(self): return {"X-API-Key": "test-api-key"} def test_current_without_tenant(self, client, api_key_headers): """ Missing X-Tenant-Id should return 400. """ response = client.get( "/strategy/retrieval/current", headers=api_key_headers, ) assert response.status_code == 400 def test_switch_without_tenant(self, client, api_key_headers): """ Missing X-Tenant-Id should return 400. """ response = client.post( "/strategy/retrieval/switch", json={"active_strategy": "enhanced"}, headers=api_key_headers, ) assert response.status_code == 400