""" tests/test_server.py Test the FastAPI server endpoints. Run with: python tests/test_server.py """ import sys import os import time import threading sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import httpx import uvicorn import pytest from feature_flag_env.server.app import app # Global server reference server = None server_thread = None def start_test_server(): """Start server in background thread for testing""" global server, server_thread def run_server(): uvicorn.run(app, host="127.0.0.1", port=8001, log_level="error") server_thread = threading.Thread(target=run_server, daemon=True) server_thread.start() # Wait for server to start for _ in range(20): try: with httpx.Client() as client: response = client.get("http://127.0.0.1:8001/health", timeout=1.0) if response.status_code == 200: return except Exception: time.sleep(0.5) @pytest.fixture(scope="module", autouse=True) def server_fixture(): """Ensure the test server is running for the whole module.""" start_test_server() yield def stop_test_server(): """Stop the test server""" global server, server_thread # Server runs in daemon thread, will stop when main process exits def test_health_endpoint(): """Test /health endpoint""" print("๐Ÿงช Testing /health endpoint...") with httpx.Client() as client: response = client.get("http://127.0.0.1:8001/health") assert response.status_code == 200, f"Health check failed: {response.status_code}" data = response.json() assert data["status"] == "healthy", "Server should be healthy" assert data["environment_ready"] == True, "Environment should be ready" print(f" โœ… Health check passed: {data['status']}") return True def test_reset_endpoint(): """Test /reset endpoint""" print("\n๐Ÿงช Testing /reset endpoint...") with httpx.Client() as client: response = client.post("http://127.0.0.1:8001/reset") assert response.status_code == 200, f"Reset failed: {response.status_code}" data = response.json() assert "observation" in data, "Response should have observation" assert "info" in data, "Response should have info" obs = data["observation"] assert obs["current_rollout_percentage"] == 0.0, "Initial rollout should be 0%" assert "error_rate" in obs, "Observation should have error_rate" assert "feature_name" in obs, "Observation should have feature_name" print(f" โœ… Reset successful") print(f" ๐Ÿ“Š Feature: {obs['feature_name']}") print(f" ๐Ÿ“Š Initial rollout: {obs['current_rollout_percentage']}%") return True def test_step_endpoint(): """Test /step endpoint""" print("\n๐Ÿงช Testing /step endpoint...") with httpx.Client() as client: # First reset client.post("http://127.0.0.1:8001/reset") # Take a step action = { "action_type": "INCREASE_ROLLOUT", "target_percentage": 10.0, "reason": "Starting rollout" } response = client.post( "http://127.0.0.1:8001/step", json=action ) assert response.status_code == 200, f"Step failed: {response.status_code}" data = response.json() assert "observation" in data, "Response should have observation" assert "reward" in data, "Response should have reward" assert "done" in data, "Response should have done flag" obs = data["observation"] assert obs["current_rollout_percentage"] == 10.0, "Rollout should be 10%" print(f" โœ… Step successful") print(f" ๐Ÿ“Š Rollout: {obs['current_rollout_percentage']}%") print(f" ๐Ÿ“Š Reward: {data['reward']:+.2f}") print(f" ๐Ÿ“Š Done: {data['done']}") return True def test_state_endpoint(): """Test /state endpoint""" print("\n๐Ÿงช Testing /state endpoint...") with httpx.Client() as client: # Reset and take a few steps first client.post("http://127.0.0.1:8001/reset") for i in range(3): action = { "action_type": "INCREASE_ROLLOUT", "target_percentage": float((i + 1) * 10), "reason": f"Step {i+1}" } client.post("http://127.0.0.1:8001/step", json=action) # Get state response = client.get("http://127.0.0.1:8001/state") assert response.status_code == 200, f"State failed: {response.status_code}" data = response.json() assert "episode_id" in data, "State should have episode_id" assert "step_count" in data, "State should have step_count" assert data["step_count"] == 3, f"Step count should be 3, got {data['step_count']}" print(f" โœ… State retrieved successfully") print(f" ๐Ÿ“Š Episode ID: {data['episode_id'][:8]}...") print(f" ๐Ÿ“Š Step count: {data['step_count']}") print(f" ๐Ÿ“Š Total reward: {data['total_reward']:+.2f}") return True def test_invalid_action(): """Test that invalid actions are rejected""" print("\n๐Ÿงช Testing Invalid Action Handling...") with httpx.Client() as client: # Reset first client.post("http://127.0.0.1:8001/reset") # Try invalid action type - should get HTTP 422 from Pydantic validation action = { "action_type": "INVALID_ACTION", "target_percentage": 50.0, "reason": "This should fail" } response = client.post( "http://127.0.0.1:8001/step", json=action ) # โœ… FIXED: Now expects 422 (Pydantic validation error) instead of 400 assert response.status_code in [400, 422], f"Should reject invalid action type, got {response.status_code}" print(f" โœ… Invalid action_type rejected: HTTP {response.status_code}") # Try invalid percentage - should get HTTP 422 from Pydantic Field validation action = { "action_type": "INCREASE_ROLLOUT", "target_percentage": 150.0, # Invalid! "reason": "This should fail" } response = client.post( "http://127.0.0.1:8001/step", json=action ) # โœ… FIXED: Now expects 422 (Pydantic validation error) instead of 400 assert response.status_code in [400, 422], f"Should reject invalid percentage, got {response.status_code}" print(f" โœ… Invalid percentage rejected: HTTP {response.status_code}") print(f" โœ… Invalid actions correctly rejected") return True def test_info_endpoint(): """Test /info endpoint""" print("\n๐Ÿงช Testing /info endpoint...") with httpx.Client() as client: response = client.get("http://127.0.0.1:8001/info") assert response.status_code == 200, f"Info failed: {response.status_code}" data = response.json() assert "name" in data, "Info should have name" assert "action_space" in data, "Info should have action_space" assert "observation_space" in data, "Info should have observation_space" print(f" โœ… Info retrieved successfully") print(f" ๐Ÿ“Š Environment: {data['name']}") print(f" ๐Ÿ“Š Actions: {len(data['action_space'])} available") return True def main(): """Run all server tests""" print("=" * 60) print("๐Ÿš€ FEATURE FLAG ENVIRONMENT - SERVER TESTS") print("=" * 60) print() # Start server print("๐Ÿ”ง Starting test server...") start_test_server() print("โœ… Server started on http://127.0.0.1:8001") print() results = [] results.append(test_health_endpoint()) results.append(test_reset_endpoint()) results.append(test_step_endpoint()) results.append(test_state_endpoint()) results.append(test_invalid_action()) results.append(test_info_endpoint()) # Stop server stop_test_server() print() print("=" * 60) if all(results): print("โœ… ALL SERVER TESTS PASSED!") print("๐ŸŽ‰ FastAPI server is working correctly!") else: print("โŒ SOME TESTS FAILED. Review and fix errors.") print("=" * 60) if __name__ == "__main__": main()