From af5607b7977b51fe76ebf199fb9dfe38584d31a4 Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 10 Dec 2024 18:52:41 +0100 Subject: [PATCH 1/3] e2etests --- e2e-tests/.env-example | 41 ++++ e2e-tests/config.py | 83 ++++++++ e2e-tests/e2e-test.py | 441 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 565 insertions(+) create mode 100644 e2e-tests/.env-example create mode 100644 e2e-tests/config.py create mode 100644 e2e-tests/e2e-test.py diff --git a/e2e-tests/.env-example b/e2e-tests/.env-example new file mode 100644 index 00000000..5be229c1 --- /dev/null +++ b/e2e-tests/.env-example @@ -0,0 +1,41 @@ +# OPAL Test Environment Configuration + +# Server Configuration +SERVER_PORT=7002 +SERVER_HOST=0.0.0.0 +SERVER_WORKERS=4 +SERVER_LOG_LEVEL=DEBUG +SERVER_MASTER_TOKEN=master-token-for-testing + +# Client Configuration +CLIENT_PORT=7000 +CLIENT_HOST=0.0.0.0 +CLIENT_TOKEN=default-token-for-testing +CLIENT_LOG_LEVEL=DEBUG + +# Database Configuration +POSTGRES_PORT=5432 +POSTGRES_HOST=broadcast_channel +POSTGRES_DB=postgres +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres + +# Policy Configuration +POLICY_REPO_URL=https://github.com/permitio/opal-example-policy-repo +POLICY_REPO_POLLING_INTERVAL=30 + +# Network Configuration +NETWORK_NAME=opal_test_network + +# Authentication Configuration +AUTH_JWT_AUDIENCE=https://api.opal.ac/v1/ +AUTH_JWT_ISSUER=https://opal.ac/ + +# Test Configuration +TEST_TIMEOUT=300 +TEST_RETRY_INTERVAL=2 +TEST_MAX_RETRIES=30 + +# Statistics Configuration +STATISTICS_ENABLED=false +STATISTICS_CHECK_TIMEOUT=10 \ No newline at end of file diff --git a/e2e-tests/config.py b/e2e-tests/config.py new file mode 100644 index 00000000..6fc3ef0b --- /dev/null +++ b/e2e-tests/config.py @@ -0,0 +1,83 @@ +from pydantic import BaseSettings, Field +from typing import Optional +from pathlib import Path +import os + +class OPALEnvironment(BaseSettings): + """Environment configuration for OPAL tests with support for .env file""" + # Server Configuration + SERVER_PORT: int = Field(7002, description="OPAL server port") + SERVER_HOST: str = Field("0.0.0.0", description="OPAL server host") + SERVER_WORKERS: int = Field(4, description="Number of server workers") + SERVER_LOG_LEVEL: str = Field("DEBUG", description="Server log level") + SERVER_MASTER_TOKEN: str = Field("master-token-for-testing", description="Server master token") + + # Client Configuration + CLIENT_PORT: int = Field(7000, description="OPAL client port") + CLIENT_HOST: str = Field("0.0.0.0", description="OPAL client host") + CLIENT_TOKEN: str = Field("default-token-for-testing", description="Client auth token") + CLIENT_LOG_LEVEL: str = Field("DEBUG", description="Client log level") + + # Database Configuration + POSTGRES_PORT: int = Field(5432, description="PostgreSQL port") + POSTGRES_HOST: str = Field("broadcast_channel", description="PostgreSQL host") + POSTGRES_DB: str = Field("postgres", description="PostgreSQL database") + POSTGRES_USER: str = Field("postgres", description="PostgreSQL user") + POSTGRES_PASSWORD: str = Field("postgres", description="PostgreSQL password") + + + # Statistics Configuration + STATISTICS_ENABLED: bool = Field(True, description="Enable statistics collection") + STATISTICS_CHECK_TIMEOUT: int = Field(10, description="Timeout for statistics checks in seconds") + + # Policy Configuration + POLICY_REPO_URL: str = Field( + "https://github.com/permitio/opal-example-policy-repo", + description="Git repository URL for policies" + ) + POLICY_REPO_POLLING_INTERVAL: int = Field(30, description="Policy repo polling interval in seconds") + + # Network Configuration + NETWORK_NAME: str = Field("opal_test_network", description="Docker network name") + + # Authentication Configuration + AUTH_JWT_AUDIENCE: str = Field("https://api.opal.ac/v1/", description="JWT audience") + AUTH_JWT_ISSUER: str = Field("https://opal.ac/", description="JWT issuer") + + # Test Configuration + TEST_TIMEOUT: int = Field(300, description="Test timeout in seconds") + TEST_RETRY_INTERVAL: int = Field(2, description="Retry interval in seconds") + TEST_MAX_RETRIES: int = Field(30, description="Maximum number of retries") + + class Config: + env_file = '.env' + env_file_encoding = 'utf-8' + case_sensitive = True + + @property + def postgres_dsn(self) -> str: + """Get PostgreSQL connection string""" + return f"postgres://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" + + @classmethod + def load_from_env_file(cls, env_file: str = '.env') -> 'OPALEnvironment': + """Load configuration from specific env file""" + if not os.path.exists(env_file): + raise FileNotFoundError(f"Environment file not found: {env_file}") + + return cls(_env_file=env_file) + +def get_environment() -> OPALEnvironment: + """Get environment configuration, with support for local development overrides""" + # Try local dev config first + local_env = Path('.env.local') + if local_env.exists(): + return OPALEnvironment.load_from_env_file('.env.local') + + # Fallback to default .env + default_env = Path('.env') + if default_env.exists(): + return OPALEnvironment.load_from_env_file('.env') + + # Use defaults/environment variables + return OPALEnvironment() \ No newline at end of file diff --git a/e2e-tests/e2e-test.py b/e2e-tests/e2e-test.py new file mode 100644 index 00000000..f7c167cd --- /dev/null +++ b/e2e-tests/e2e-test.py @@ -0,0 +1,441 @@ +from typing import Dict +import pytest +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for_logs +import docker +import requests +import time +import json +from config import OPALEnvironment, get_environment + +class OPALTestEnvironment: + """Main test environment manager""" + def __init__(self, config: OPALEnvironment): + self.config = config + self.containers: Dict[str, DockerContainer] = {} + self.docker_client = docker.from_env() + self.network = None + self.network_id = None + + def setup(self): + """Setup the complete test environment""" + try: + # Create network + self._create_network() + print(f"\nCreated network: {self.config.NETWORK_NAME}") + + #Start Postgres broadcast channel + print("\n=== Starting Postgres Container ===") + self.containers['postgres'] = DockerContainer("postgres:alpine") + self._configure_postgres(self.containers['postgres']) + self.containers['postgres'].with_name("broadcast_channel") + self.containers['postgres'].start() + + # Wait for Postgres to be ready + self._wait_for_postgres() + + #Start OPAL Server + print("\n=== Starting OPAL Server Container ===") + self.containers['server'] = DockerContainer("permitio/opal-server:latest") + self._configure_server(self.containers['server']) + self.containers['server'].with_name("opal_server") + self.containers['server'].start() + + # Wait for server to be healthy + self._wait_for_server() + + #Start OPAL Client + print("\n=== Starting OPAL Client Container ===") + self.containers['client'] = DockerContainer("permitio/opal-client:latest") + self._configure_client(self.containers['client']) + self.containers['client'].with_name("opal_client") + self.containers['client'].with_command( + f"sh -c 'exec ./wait-for.sh opal_server:{self.config.SERVER_PORT} --timeout=20 -- ./start.sh'" + ) + self.containers['client'].start() + + # Wait for client + self._wait_for_client() + + except Exception as e: + print(f"\n❌ Error during setup: {str(e)}") + self._print_container_logs() + self.teardown() + raise + + def _create_network(self): + """Create Docker network for test environment""" + try: + # Remove network if it exists + try: + existing_network = self.docker_client.networks.get(self.config.NETWORK_NAME) + existing_network.remove() + except docker.errors.NotFound: + pass + + # Create new network + self.network = self.docker_client.networks.create( + name=self.config.NETWORK_NAME, + driver="bridge", + check_duplicate=True + ) + self.network_id = self.network.id + except Exception as e: + raise Exception(f"Failed to create network: {str(e)}") + + def _configure_postgres(self, container: DockerContainer): + """Configure Postgres container""" + container.with_env("POSTGRES_DB", self.config.POSTGRES_DB) + container.with_env("POSTGRES_USER", self.config.POSTGRES_USER) + container.with_env("POSTGRES_PASSWORD", self.config.POSTGRES_PASSWORD) + container.with_exposed_ports(self.config.POSTGRES_PORT) + container.with_kwargs(network=self.config.NETWORK_NAME) + + def _configure_server(self, container: DockerContainer): + """Configure OPAL server container""" + # Core settings + container.with_env("PORT", str(self.config.SERVER_PORT)) + container.with_env("HOST", self.config.SERVER_HOST) + container.with_env("UVICORN_NUM_WORKERS", str(self.config.SERVER_WORKERS)) + container.with_env("LOG_LEVEL", self.config.SERVER_LOG_LEVEL) + + # Enable Statistics + container.with_env("OPAL_STATISTICS_ENABLED", "true") + + # Broadcast configuration + container.with_env("OPAL_BROADCAST_URI", self.config.postgres_dsn) + container.with_env("BROADCAST_CHANNEL_NAME", "opal_updates") + + # Policy repository configuration + container.with_env("OPAL_POLICY_REPO_URL", self.config.POLICY_REPO_URL) + container.with_env("OPAL_POLICY_REPO_POLLING_INTERVAL", str(self.config.POLICY_REPO_POLLING_INTERVAL)) + + # Data configuration + container.with_env( + "OPAL_DATA_CONFIG_SOURCES", + '{"config":{"entries":[{"url":"http://opal_server:7002/policy-data","topics":["policy_data"],"dst_path":"/static"}]}}' + ) + + # Authentication + container.with_env("AUTH_JWT_AUDIENCE", self.config.AUTH_JWT_AUDIENCE) + container.with_env("AUTH_JWT_ISSUER", self.config.AUTH_JWT_ISSUER) + container.with_env("AUTH_MASTER_TOKEN", self.config.SERVER_MASTER_TOKEN) + + # Logging + container.with_env("OPAL_LOG_FORMAT_INCLUDE_PID", "true") + container.with_env("LOG_FORMAT", "text") + container.with_env("LOG_TRACEBACK", "true") + + container.with_exposed_ports(self.config.SERVER_PORT) + container.with_kwargs(network=self.config.NETWORK_NAME) + + def _configure_client(self, container: DockerContainer): + """Configure OPAL client container""" + # Core settings + container.with_env("OPAL_SERVER_URL", f"http://opal_server:{self.config.SERVER_PORT}") + container.with_env("PORT", str(self.config.CLIENT_PORT)) + container.with_env("HOST", self.config.CLIENT_HOST) + container.with_env("LOG_LEVEL", self.config.CLIENT_LOG_LEVEL) + + # Authentication + container.with_env("OPAL_CLIENT_TOKEN", self.config.CLIENT_TOKEN) + container.with_env("AUTH_JWT_AUDIENCE", self.config.AUTH_JWT_AUDIENCE) + container.with_env("AUTH_JWT_ISSUER", self.config.AUTH_JWT_ISSUER) + + # Features + container.with_env("POLICY_UPDATER_ENABLED", "true") + container.with_env("DATA_UPDATER_ENABLED", "true") + container.with_env("INLINE_OPA_ENABLED", "true") + container.with_env("OPA_HEALTH_CHECK_POLICY_ENABLED", "true") + + # OPA Configuration + container.with_env("OPAL_INLINE_OPA_LOG_FORMAT", "http") + container.with_env("INLINE_OPA_CONFIG", "{}") + + # Statistics + container.with_env("OPAL_STATISTICS_ENABLED", "true") + + # Logging + container.with_env("OPAL_LOG_FORMAT_INCLUDE_PID", "true") + container.with_env("LOG_FORMAT", "text") + container.with_env("LOG_TRACEBACK", "true") + + container.with_exposed_ports(self.config.CLIENT_PORT) + container.with_exposed_ports(8181) # OPA port + container.with_kwargs(network=self.config.NETWORK_NAME) + + def _wait_for_postgres(self): + """Wait for Postgres to be ready""" + print("\nWaiting for Postgres to be ready...") + wait_for_logs(self.containers['postgres'], "database system is ready to accept connections", timeout=30) + print("✅ Postgres is ready!") + time.sleep(2) + + def _wait_for_server(self): + """Wait for server to be ready""" + print("\nWaiting for OPAL server to be ready...") + + for retry in range(self.config.TEST_MAX_RETRIES): + try: + server_url = f"http://{self.containers['server'].get_container_host_ip()}:{self.containers['server'].get_exposed_port(self.config.SERVER_PORT)}" + response = requests.get(f"{server_url}/healthcheck", timeout=5) + if response.status_code == 200: + print("✅ OPAL server is ready!") + time.sleep(5) # Give it more time to stabilize + return + except Exception as e: + print(f"⚠️ Server not ready (attempt {retry + 1}/{self.config.TEST_MAX_RETRIES}): {str(e)}") + if retry == self.config.TEST_MAX_RETRIES - 1: + self._print_container_logs() + raise TimeoutError("❌ OPAL server failed to become healthy") + time.sleep(self.config.TEST_RETRY_INTERVAL) + + def _wait_for_client(self): + """Wait for client to be ready""" + print("\nWaiting for OPAL client to be ready...") + + for retry in range(self.config.TEST_MAX_RETRIES): + try: + client_url = f"http://{self.containers['client'].get_container_host_ip()}:{self.containers['client'].get_exposed_port(self.config.CLIENT_PORT)}" + response = requests.get(f"{client_url}/healthcheck", timeout=5) + if response.status_code == 200: + print("✅ OPAL client is ready!") + time.sleep(5) # Give extra time for OPA to initialize + return + except Exception as e: + print(f"⚠️ Client not ready (attempt {retry + 1}/{self.config.TEST_MAX_RETRIES}): {str(e)}") + time.sleep(self.config.TEST_RETRY_INTERVAL) + if retry == self.config.TEST_MAX_RETRIES - 1: + self._print_container_logs() + raise TimeoutError("❌ OPAL client failed to become healthy") + + def check_client_server_connection(self) -> bool: + """Verify client-server connection using Statistics API""" + try: + server_url = f"http://{self.containers['server'].get_container_host_ip()}:{self.containers['server'].get_exposed_port(self.config.SERVER_PORT)}" + stats_url = f"{server_url}/statistics" + headers = {"Authorization": f"Bearer {self.config.SERVER_MASTER_TOKEN}"} + + print(f"\nChecking statistics at: {stats_url}") + response = requests.get(stats_url, headers=headers) + + if response.status_code != 200: + print(f"\n❌ Failed to get statistics: HTTP {response.status_code}") + print(f"Response: {response.text}") + return False + + stats = response.json() + print(f"\n📊 Server Statistics:") + print(json.dumps(stats, indent=2)) + + # Verify required fields + required_fields = ["clients", "uptime", "version"] + for field in required_fields: + if field not in stats: + print(f"\n❌ Missing required field in statistics: {field}") + return False + + # Check for connected clients + if not stats["clients"]: + print("\n❌ No clients found in statistics") + return False + + # Verify client subscriptions + found_client = False + expected_topics = ["policy_data"] + + for client_id, client_data in stats["clients"].items(): + print(f"\n📊 Client {client_id} Data:") + print(json.dumps(client_data, indent=2)) + + if isinstance(client_data, list): + for conn in client_data: + client_topics = conn.get("topics", []) + if any(topic in client_topics for topic in expected_topics): + found_client = True + print(f"✅ Found client with expected topics: {client_topics}") + break + + if not found_client: + print("\n❌ No client found with expected topic subscriptions") + return False + + print("\n✅ Client-server connection verified successfully") + return True + + except Exception as e: + print(f"\n❌ Error checking client-server connection: {str(e)}") + return False + + def check_container_logs_for_errors(self) -> bool: + """Check container logs for critical errors""" + error_keywords = ["ERROR", "CRITICAL", "FATAL", "Exception"] + found_errors = False + + print("\n=== Container Logs Analysis ===") + for name, container in self.containers.items(): + try: + logs = container.get_logs()[0].decode() + container_errors = [] + + for line in logs.split('\n'): + if any(keyword in line for keyword in error_keywords): + container_errors.append(line.strip()) + + if container_errors: + print(f"\n⚠️ Found errors in {name} logs:") + for error in container_errors[:5]: # Show first 5 errors + print(f" - {error}") + if len(container_errors) > 5: + print(f" ... and {len(container_errors) - 5} more errors") + found_errors = True + else: + print(f"✅ {name}: No critical errors found") + + except Exception as e: + print(f"❌ Error getting logs for {name}: {str(e)}") + found_errors = True + + return not found_errors + + def _print_container_logs(self): + """Print logs from all containers for debugging""" + print("\n=== Debug: Container Logs ===") + for name, container in self.containers.items(): + try: + print(f"\n📋 {name.upper()} LOGS:") + print("=== STDOUT ===") + print(container.get_logs()[0].decode()) + print("\n=== STDERR ===") + print(container.get_logs()[1].decode()) + print("=" * 80) + except Exception as e: + print(f"❌ Could not get logs for {name}: {str(e)}") + + def teardown(self): + """Cleanup all resources""" + print("\n=== Cleaning Up Test Environment ===") + for container in reversed(list(self.containers.values())): + try: + container.stop() + except Exception as e: + print(f"❌ Error stopping container: {str(e)}") + + if self.network: + try: + self.network.remove() + print(f"✅ Removed network: {self.config.NETWORK_NAME}") + except Exception as e: + print(f"❌ Error removing network: {str(e)}") + +@pytest.fixture(scope="module") +def opal_config(): + """Fixture that provides environment configuration""" + return get_environment() + +@pytest.fixture(scope="module") +def opal_env(opal_config): + """Main fixture that provides the test environment""" + env = OPALTestEnvironment(opal_config) + env.setup() + yield env + env.teardown() + +def test_opal_baseline(opal_env, opal_config): + """Test basic OPAL functionality with clear output formatting""" + print("\n" + "="*50) + print("Starting OPAL Environment Tests") + print("="*50) + + time.sleep(opal_config.TEST_RETRY_INTERVAL * 5) # Give services extra time to stabilize + + # Test server health + print("\n=== Testing Server Health ===") + try: + server_url = f"http://{opal_env.containers['server'].get_container_host_ip()}:{opal_env.containers['server'].get_exposed_port(opal_config.SERVER_PORT)}" + server_health = requests.get(f"{server_url}/healthcheck") + assert server_health.status_code == 200, "Server health check failed" + print("✅ Server Health Check: PASSED") + + # Get server statistics + stats_url = f"{server_url}/statistics" + headers = {"Authorization": f"Bearer {opal_config.SERVER_MASTER_TOKEN}"} + stats_response = requests.get(stats_url, headers=headers) + if stats_response.status_code == 200: + print("\n📊 Server Statistics:") + print(json.dumps(stats_response.json(), indent=2)) + else: + print("⚠️ Could not fetch server statistics") + + except Exception as e: + print(f"❌ Server Health Check: FAILED - {str(e)}") + raise + + # Test client health + print("\n=== Testing Client Health ===") + try: + client_url = f"http://{opal_env.containers['client'].get_container_host_ip()}:{opal_env.containers['client'].get_exposed_port(opal_config.CLIENT_PORT)}" + client_health = requests.get(f"{client_url}/healthcheck") #/ready + assert client_health.status_code == 200, "Client health check failed" + print("✅ Client Health Check: PASSED") + + # Test OPA endpoint + opa_url = f"http://{opal_env.containers['client'].get_container_host_ip()}:{opal_env.containers['client'].get_exposed_port(8181)}" + opa_health = requests.get(f"{opa_url}/health") + if opa_health.status_code == 200: + print("✅ OPA Health Check: PASSED") + print("\n📊 OPA Status:") + print(json.dumps(opa_health.json(), indent=2)) + else: + print("⚠️ OPA Health Check: WARNING - Unexpected status code") + + except Exception as e: + print(f"❌ Client Health Check: FAILED - {str(e)}") + raise + + # Check logs for errors + print("\n=== Analyzing Container Logs ===") + for name, container in opal_env.containers.items(): + try: + logs = container.get_logs()[0].decode() + error_count = sum(1 for line in logs.split('\n') if "ERROR" in line) + critical_count = sum(1 for line in logs.split('\n') if "CRITICAL" in line) + + print(f"\n📋 {name.title()} Log Analysis:") + if error_count == 0 and critical_count == 0: + print(f"✅ No errors or critical issues found") + else: + print(f"⚠️ Found issues:") + print(f" - {error_count} ERRORs") + print(f" - {critical_count} CRITICALs") + + # Print first few errors if any found + if error_count > 0 or critical_count > 0: + print("\nSample of issues found:") + for line in logs.split('\n'): + if "ERROR" in line or "CRITICAL" in line: + print(f" {line.strip()}") + break # Just show first error as example + + except Exception as e: + print(f"❌ {name.title()} Log Analysis Failed: {str(e)}") + + # Check client-server connection + print("\n=== Testing Client-Server Connection ===") + connection_ok = opal_env.check_client_server_connection() + if connection_ok: + print("✅ Client-Server Connection: PASSED") + else: + print("❌ Client-Server Connection: FAILED") + assert False, "Client-Server connection check failed" + + # Summary + print("\n" + "="*50) + print("Test Summary") + print("="*50) + print("✅ Server Health: PASSED") + print("✅ Client Health: PASSED") + print("✅ OPA Integration: PASSED") + print("✅ Log Analysis: Complete") + print("✅ Client-Server Connection: PASSED") + print("\n✨ All Tests Completed Successfully ✨") \ No newline at end of file From c01ccae04edfd5087e80e4ca09175629df68fb0c Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 10 Dec 2024 23:52:42 +0100 Subject: [PATCH 2/3] Added loggers and refactored code --- e2e-tests/container_config.py | 75 ++++++ e2e-tests/e2e-test.py | 441 ---------------------------------- e2e-tests/pytest.ini | 10 + e2e-tests/test_cases.py | 76 ++++++ e2e-tests/test_environment.py | 167 +++++++++++++ e2e-tests/test_validation.py | 99 ++++++++ 6 files changed, 427 insertions(+), 441 deletions(-) create mode 100644 e2e-tests/container_config.py delete mode 100644 e2e-tests/e2e-test.py create mode 100644 e2e-tests/pytest.ini create mode 100644 e2e-tests/test_cases.py create mode 100644 e2e-tests/test_environment.py create mode 100644 e2e-tests/test_validation.py diff --git a/e2e-tests/container_config.py b/e2e-tests/container_config.py new file mode 100644 index 00000000..3db0cbe7 --- /dev/null +++ b/e2e-tests/container_config.py @@ -0,0 +1,75 @@ +"""Container configuration helpers""" +import json +from testcontainers.core.container import DockerContainer +from config import OPALEnvironment + +def configure_postgres(container: DockerContainer, config: OPALEnvironment): + """Configure Postgres container""" + container.with_env("POSTGRES_DB", config.POSTGRES_DB) + container.with_env("POSTGRES_USER", config.POSTGRES_USER) + container.with_env("POSTGRES_PASSWORD", config.POSTGRES_PASSWORD) + container.with_exposed_ports(config.POSTGRES_PORT) + container.with_kwargs(network=config.NETWORK_NAME) + +def configure_server(container: DockerContainer, config: OPALEnvironment): + """Configure OPAL server container with all required environment variables""" + env_vars = { + "PORT": str(config.SERVER_PORT), + "HOST": config.SERVER_HOST, + "UVICORN_NUM_WORKERS": str(config.SERVER_WORKERS), + "LOG_LEVEL": config.SERVER_LOG_LEVEL, + "OPAL_STATISTICS_ENABLED": "true", + "OPAL_BROADCAST_URI": config.postgres_dsn, + "BROADCAST_CHANNEL_NAME": "opal_updates", + "OPAL_POLICY_REPO_URL": config.POLICY_REPO_URL, + "OPAL_POLICY_REPO_POLLING_INTERVAL": str(config.POLICY_REPO_POLLING_INTERVAL), + "OPAL_DATA_CONFIG_SOURCES": json.dumps({ + "config": { + "entries": [{ + "url": "http://opal_server:7002/policy-data", + "topics": ["policy_data"], + "dst_path": "/static" + }] + } + }), + "AUTH_JWT_AUDIENCE": config.AUTH_JWT_AUDIENCE, + "AUTH_JWT_ISSUER": config.AUTH_JWT_ISSUER, + "AUTH_MASTER_TOKEN": config.SERVER_MASTER_TOKEN, + "OPAL_LOG_FORMAT_INCLUDE_PID": "true", + "LOG_FORMAT": "text", + "LOG_TRACEBACK": "true" + } + + for key, value in env_vars.items(): + container.with_env(key, value) + + container.with_exposed_ports(config.SERVER_PORT) + container.with_kwargs(network=config.NETWORK_NAME) + +def configure_client(container: DockerContainer, config: OPALEnvironment): + """Configure OPAL client container with all required environment variables""" + env_vars = { + "OPAL_SERVER_URL": f"http://opal_server:{config.SERVER_PORT}", + "PORT": str(config.CLIENT_PORT), + "HOST": config.CLIENT_HOST, + "LOG_LEVEL": config.CLIENT_LOG_LEVEL, + "OPAL_CLIENT_TOKEN": config.CLIENT_TOKEN, + "AUTH_JWT_AUDIENCE": config.AUTH_JWT_AUDIENCE, + "AUTH_JWT_ISSUER": config.AUTH_JWT_ISSUER, + "POLICY_UPDATER_ENABLED": "true", + "DATA_UPDATER_ENABLED": "true", + "INLINE_OPA_ENABLED": "true", + "OPA_HEALTH_CHECK_POLICY_ENABLED": "true", + "OPAL_INLINE_OPA_LOG_FORMAT": "http", + "INLINE_OPA_CONFIG": "{}", + "OPAL_STATISTICS_ENABLED": "true", + "OPAL_LOG_FORMAT_INCLUDE_PID": "true", + "LOG_FORMAT": "text", + "LOG_TRACEBACK": "true" + } + + for key, value in env_vars.items(): + container.with_env(key, value) + + container.with_exposed_ports(config.CLIENT_PORT, 8181) + container.with_kwargs(network=config.NETWORK_NAME) \ No newline at end of file diff --git a/e2e-tests/e2e-test.py b/e2e-tests/e2e-test.py deleted file mode 100644 index f7c167cd..00000000 --- a/e2e-tests/e2e-test.py +++ /dev/null @@ -1,441 +0,0 @@ -from typing import Dict -import pytest -from testcontainers.core.container import DockerContainer -from testcontainers.core.waiting_utils import wait_for_logs -import docker -import requests -import time -import json -from config import OPALEnvironment, get_environment - -class OPALTestEnvironment: - """Main test environment manager""" - def __init__(self, config: OPALEnvironment): - self.config = config - self.containers: Dict[str, DockerContainer] = {} - self.docker_client = docker.from_env() - self.network = None - self.network_id = None - - def setup(self): - """Setup the complete test environment""" - try: - # Create network - self._create_network() - print(f"\nCreated network: {self.config.NETWORK_NAME}") - - #Start Postgres broadcast channel - print("\n=== Starting Postgres Container ===") - self.containers['postgres'] = DockerContainer("postgres:alpine") - self._configure_postgres(self.containers['postgres']) - self.containers['postgres'].with_name("broadcast_channel") - self.containers['postgres'].start() - - # Wait for Postgres to be ready - self._wait_for_postgres() - - #Start OPAL Server - print("\n=== Starting OPAL Server Container ===") - self.containers['server'] = DockerContainer("permitio/opal-server:latest") - self._configure_server(self.containers['server']) - self.containers['server'].with_name("opal_server") - self.containers['server'].start() - - # Wait for server to be healthy - self._wait_for_server() - - #Start OPAL Client - print("\n=== Starting OPAL Client Container ===") - self.containers['client'] = DockerContainer("permitio/opal-client:latest") - self._configure_client(self.containers['client']) - self.containers['client'].with_name("opal_client") - self.containers['client'].with_command( - f"sh -c 'exec ./wait-for.sh opal_server:{self.config.SERVER_PORT} --timeout=20 -- ./start.sh'" - ) - self.containers['client'].start() - - # Wait for client - self._wait_for_client() - - except Exception as e: - print(f"\n❌ Error during setup: {str(e)}") - self._print_container_logs() - self.teardown() - raise - - def _create_network(self): - """Create Docker network for test environment""" - try: - # Remove network if it exists - try: - existing_network = self.docker_client.networks.get(self.config.NETWORK_NAME) - existing_network.remove() - except docker.errors.NotFound: - pass - - # Create new network - self.network = self.docker_client.networks.create( - name=self.config.NETWORK_NAME, - driver="bridge", - check_duplicate=True - ) - self.network_id = self.network.id - except Exception as e: - raise Exception(f"Failed to create network: {str(e)}") - - def _configure_postgres(self, container: DockerContainer): - """Configure Postgres container""" - container.with_env("POSTGRES_DB", self.config.POSTGRES_DB) - container.with_env("POSTGRES_USER", self.config.POSTGRES_USER) - container.with_env("POSTGRES_PASSWORD", self.config.POSTGRES_PASSWORD) - container.with_exposed_ports(self.config.POSTGRES_PORT) - container.with_kwargs(network=self.config.NETWORK_NAME) - - def _configure_server(self, container: DockerContainer): - """Configure OPAL server container""" - # Core settings - container.with_env("PORT", str(self.config.SERVER_PORT)) - container.with_env("HOST", self.config.SERVER_HOST) - container.with_env("UVICORN_NUM_WORKERS", str(self.config.SERVER_WORKERS)) - container.with_env("LOG_LEVEL", self.config.SERVER_LOG_LEVEL) - - # Enable Statistics - container.with_env("OPAL_STATISTICS_ENABLED", "true") - - # Broadcast configuration - container.with_env("OPAL_BROADCAST_URI", self.config.postgres_dsn) - container.with_env("BROADCAST_CHANNEL_NAME", "opal_updates") - - # Policy repository configuration - container.with_env("OPAL_POLICY_REPO_URL", self.config.POLICY_REPO_URL) - container.with_env("OPAL_POLICY_REPO_POLLING_INTERVAL", str(self.config.POLICY_REPO_POLLING_INTERVAL)) - - # Data configuration - container.with_env( - "OPAL_DATA_CONFIG_SOURCES", - '{"config":{"entries":[{"url":"http://opal_server:7002/policy-data","topics":["policy_data"],"dst_path":"/static"}]}}' - ) - - # Authentication - container.with_env("AUTH_JWT_AUDIENCE", self.config.AUTH_JWT_AUDIENCE) - container.with_env("AUTH_JWT_ISSUER", self.config.AUTH_JWT_ISSUER) - container.with_env("AUTH_MASTER_TOKEN", self.config.SERVER_MASTER_TOKEN) - - # Logging - container.with_env("OPAL_LOG_FORMAT_INCLUDE_PID", "true") - container.with_env("LOG_FORMAT", "text") - container.with_env("LOG_TRACEBACK", "true") - - container.with_exposed_ports(self.config.SERVER_PORT) - container.with_kwargs(network=self.config.NETWORK_NAME) - - def _configure_client(self, container: DockerContainer): - """Configure OPAL client container""" - # Core settings - container.with_env("OPAL_SERVER_URL", f"http://opal_server:{self.config.SERVER_PORT}") - container.with_env("PORT", str(self.config.CLIENT_PORT)) - container.with_env("HOST", self.config.CLIENT_HOST) - container.with_env("LOG_LEVEL", self.config.CLIENT_LOG_LEVEL) - - # Authentication - container.with_env("OPAL_CLIENT_TOKEN", self.config.CLIENT_TOKEN) - container.with_env("AUTH_JWT_AUDIENCE", self.config.AUTH_JWT_AUDIENCE) - container.with_env("AUTH_JWT_ISSUER", self.config.AUTH_JWT_ISSUER) - - # Features - container.with_env("POLICY_UPDATER_ENABLED", "true") - container.with_env("DATA_UPDATER_ENABLED", "true") - container.with_env("INLINE_OPA_ENABLED", "true") - container.with_env("OPA_HEALTH_CHECK_POLICY_ENABLED", "true") - - # OPA Configuration - container.with_env("OPAL_INLINE_OPA_LOG_FORMAT", "http") - container.with_env("INLINE_OPA_CONFIG", "{}") - - # Statistics - container.with_env("OPAL_STATISTICS_ENABLED", "true") - - # Logging - container.with_env("OPAL_LOG_FORMAT_INCLUDE_PID", "true") - container.with_env("LOG_FORMAT", "text") - container.with_env("LOG_TRACEBACK", "true") - - container.with_exposed_ports(self.config.CLIENT_PORT) - container.with_exposed_ports(8181) # OPA port - container.with_kwargs(network=self.config.NETWORK_NAME) - - def _wait_for_postgres(self): - """Wait for Postgres to be ready""" - print("\nWaiting for Postgres to be ready...") - wait_for_logs(self.containers['postgres'], "database system is ready to accept connections", timeout=30) - print("✅ Postgres is ready!") - time.sleep(2) - - def _wait_for_server(self): - """Wait for server to be ready""" - print("\nWaiting for OPAL server to be ready...") - - for retry in range(self.config.TEST_MAX_RETRIES): - try: - server_url = f"http://{self.containers['server'].get_container_host_ip()}:{self.containers['server'].get_exposed_port(self.config.SERVER_PORT)}" - response = requests.get(f"{server_url}/healthcheck", timeout=5) - if response.status_code == 200: - print("✅ OPAL server is ready!") - time.sleep(5) # Give it more time to stabilize - return - except Exception as e: - print(f"⚠️ Server not ready (attempt {retry + 1}/{self.config.TEST_MAX_RETRIES}): {str(e)}") - if retry == self.config.TEST_MAX_RETRIES - 1: - self._print_container_logs() - raise TimeoutError("❌ OPAL server failed to become healthy") - time.sleep(self.config.TEST_RETRY_INTERVAL) - - def _wait_for_client(self): - """Wait for client to be ready""" - print("\nWaiting for OPAL client to be ready...") - - for retry in range(self.config.TEST_MAX_RETRIES): - try: - client_url = f"http://{self.containers['client'].get_container_host_ip()}:{self.containers['client'].get_exposed_port(self.config.CLIENT_PORT)}" - response = requests.get(f"{client_url}/healthcheck", timeout=5) - if response.status_code == 200: - print("✅ OPAL client is ready!") - time.sleep(5) # Give extra time for OPA to initialize - return - except Exception as e: - print(f"⚠️ Client not ready (attempt {retry + 1}/{self.config.TEST_MAX_RETRIES}): {str(e)}") - time.sleep(self.config.TEST_RETRY_INTERVAL) - if retry == self.config.TEST_MAX_RETRIES - 1: - self._print_container_logs() - raise TimeoutError("❌ OPAL client failed to become healthy") - - def check_client_server_connection(self) -> bool: - """Verify client-server connection using Statistics API""" - try: - server_url = f"http://{self.containers['server'].get_container_host_ip()}:{self.containers['server'].get_exposed_port(self.config.SERVER_PORT)}" - stats_url = f"{server_url}/statistics" - headers = {"Authorization": f"Bearer {self.config.SERVER_MASTER_TOKEN}"} - - print(f"\nChecking statistics at: {stats_url}") - response = requests.get(stats_url, headers=headers) - - if response.status_code != 200: - print(f"\n❌ Failed to get statistics: HTTP {response.status_code}") - print(f"Response: {response.text}") - return False - - stats = response.json() - print(f"\n📊 Server Statistics:") - print(json.dumps(stats, indent=2)) - - # Verify required fields - required_fields = ["clients", "uptime", "version"] - for field in required_fields: - if field not in stats: - print(f"\n❌ Missing required field in statistics: {field}") - return False - - # Check for connected clients - if not stats["clients"]: - print("\n❌ No clients found in statistics") - return False - - # Verify client subscriptions - found_client = False - expected_topics = ["policy_data"] - - for client_id, client_data in stats["clients"].items(): - print(f"\n📊 Client {client_id} Data:") - print(json.dumps(client_data, indent=2)) - - if isinstance(client_data, list): - for conn in client_data: - client_topics = conn.get("topics", []) - if any(topic in client_topics for topic in expected_topics): - found_client = True - print(f"✅ Found client with expected topics: {client_topics}") - break - - if not found_client: - print("\n❌ No client found with expected topic subscriptions") - return False - - print("\n✅ Client-server connection verified successfully") - return True - - except Exception as e: - print(f"\n❌ Error checking client-server connection: {str(e)}") - return False - - def check_container_logs_for_errors(self) -> bool: - """Check container logs for critical errors""" - error_keywords = ["ERROR", "CRITICAL", "FATAL", "Exception"] - found_errors = False - - print("\n=== Container Logs Analysis ===") - for name, container in self.containers.items(): - try: - logs = container.get_logs()[0].decode() - container_errors = [] - - for line in logs.split('\n'): - if any(keyword in line for keyword in error_keywords): - container_errors.append(line.strip()) - - if container_errors: - print(f"\n⚠️ Found errors in {name} logs:") - for error in container_errors[:5]: # Show first 5 errors - print(f" - {error}") - if len(container_errors) > 5: - print(f" ... and {len(container_errors) - 5} more errors") - found_errors = True - else: - print(f"✅ {name}: No critical errors found") - - except Exception as e: - print(f"❌ Error getting logs for {name}: {str(e)}") - found_errors = True - - return not found_errors - - def _print_container_logs(self): - """Print logs from all containers for debugging""" - print("\n=== Debug: Container Logs ===") - for name, container in self.containers.items(): - try: - print(f"\n📋 {name.upper()} LOGS:") - print("=== STDOUT ===") - print(container.get_logs()[0].decode()) - print("\n=== STDERR ===") - print(container.get_logs()[1].decode()) - print("=" * 80) - except Exception as e: - print(f"❌ Could not get logs for {name}: {str(e)}") - - def teardown(self): - """Cleanup all resources""" - print("\n=== Cleaning Up Test Environment ===") - for container in reversed(list(self.containers.values())): - try: - container.stop() - except Exception as e: - print(f"❌ Error stopping container: {str(e)}") - - if self.network: - try: - self.network.remove() - print(f"✅ Removed network: {self.config.NETWORK_NAME}") - except Exception as e: - print(f"❌ Error removing network: {str(e)}") - -@pytest.fixture(scope="module") -def opal_config(): - """Fixture that provides environment configuration""" - return get_environment() - -@pytest.fixture(scope="module") -def opal_env(opal_config): - """Main fixture that provides the test environment""" - env = OPALTestEnvironment(opal_config) - env.setup() - yield env - env.teardown() - -def test_opal_baseline(opal_env, opal_config): - """Test basic OPAL functionality with clear output formatting""" - print("\n" + "="*50) - print("Starting OPAL Environment Tests") - print("="*50) - - time.sleep(opal_config.TEST_RETRY_INTERVAL * 5) # Give services extra time to stabilize - - # Test server health - print("\n=== Testing Server Health ===") - try: - server_url = f"http://{opal_env.containers['server'].get_container_host_ip()}:{opal_env.containers['server'].get_exposed_port(opal_config.SERVER_PORT)}" - server_health = requests.get(f"{server_url}/healthcheck") - assert server_health.status_code == 200, "Server health check failed" - print("✅ Server Health Check: PASSED") - - # Get server statistics - stats_url = f"{server_url}/statistics" - headers = {"Authorization": f"Bearer {opal_config.SERVER_MASTER_TOKEN}"} - stats_response = requests.get(stats_url, headers=headers) - if stats_response.status_code == 200: - print("\n📊 Server Statistics:") - print(json.dumps(stats_response.json(), indent=2)) - else: - print("⚠️ Could not fetch server statistics") - - except Exception as e: - print(f"❌ Server Health Check: FAILED - {str(e)}") - raise - - # Test client health - print("\n=== Testing Client Health ===") - try: - client_url = f"http://{opal_env.containers['client'].get_container_host_ip()}:{opal_env.containers['client'].get_exposed_port(opal_config.CLIENT_PORT)}" - client_health = requests.get(f"{client_url}/healthcheck") #/ready - assert client_health.status_code == 200, "Client health check failed" - print("✅ Client Health Check: PASSED") - - # Test OPA endpoint - opa_url = f"http://{opal_env.containers['client'].get_container_host_ip()}:{opal_env.containers['client'].get_exposed_port(8181)}" - opa_health = requests.get(f"{opa_url}/health") - if opa_health.status_code == 200: - print("✅ OPA Health Check: PASSED") - print("\n📊 OPA Status:") - print(json.dumps(opa_health.json(), indent=2)) - else: - print("⚠️ OPA Health Check: WARNING - Unexpected status code") - - except Exception as e: - print(f"❌ Client Health Check: FAILED - {str(e)}") - raise - - # Check logs for errors - print("\n=== Analyzing Container Logs ===") - for name, container in opal_env.containers.items(): - try: - logs = container.get_logs()[0].decode() - error_count = sum(1 for line in logs.split('\n') if "ERROR" in line) - critical_count = sum(1 for line in logs.split('\n') if "CRITICAL" in line) - - print(f"\n📋 {name.title()} Log Analysis:") - if error_count == 0 and critical_count == 0: - print(f"✅ No errors or critical issues found") - else: - print(f"⚠️ Found issues:") - print(f" - {error_count} ERRORs") - print(f" - {critical_count} CRITICALs") - - # Print first few errors if any found - if error_count > 0 or critical_count > 0: - print("\nSample of issues found:") - for line in logs.split('\n'): - if "ERROR" in line or "CRITICAL" in line: - print(f" {line.strip()}") - break # Just show first error as example - - except Exception as e: - print(f"❌ {name.title()} Log Analysis Failed: {str(e)}") - - # Check client-server connection - print("\n=== Testing Client-Server Connection ===") - connection_ok = opal_env.check_client_server_connection() - if connection_ok: - print("✅ Client-Server Connection: PASSED") - else: - print("❌ Client-Server Connection: FAILED") - assert False, "Client-Server connection check failed" - - # Summary - print("\n" + "="*50) - print("Test Summary") - print("="*50) - print("✅ Server Health: PASSED") - print("✅ Client Health: PASSED") - print("✅ OPA Integration: PASSED") - print("✅ Log Analysis: Complete") - print("✅ Client-Server Connection: PASSED") - print("\n✨ All Tests Completed Successfully ✨") \ No newline at end of file diff --git a/e2e-tests/pytest.ini b/e2e-tests/pytest.ini new file mode 100644 index 00000000..786e18bd --- /dev/null +++ b/e2e-tests/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +asyncio_mode = strict +# Set the default fixture loop scope to function +asyncio_default_fixture_loop_scope = function + +# Optional but recommended settings +log_cli = true +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +log_cli_date_format = %Y-%m-%d %H:%M:%S \ No newline at end of file diff --git a/e2e-tests/test_cases.py b/e2e-tests/test_cases.py new file mode 100644 index 00000000..8ff74015 --- /dev/null +++ b/e2e-tests/test_cases.py @@ -0,0 +1,76 @@ +"""Test cases for OPAL integration tests""" +import logging +import json +import time +import pytest +import requests +from config import OPALEnvironment, get_environment +from test_environment import OPALTestEnvironment + +logger = logging.getLogger(__name__) + +@pytest.fixture(scope="module") +def opal_config(): + """Fixture that provides environment configuration""" + return get_environment() + +@pytest.fixture(scope="module") +def opal_env(opal_config): + """Main fixture that provides the test environment""" + env = OPALTestEnvironment(opal_config) + env.setup() + yield env + env.teardown() + +def test_opal_baseline(opal_env, opal_config): + """Test basic OPAL functionality""" + logger.info("Starting OPAL Environment Tests") + + time.sleep(opal_config.TEST_RETRY_INTERVAL * 5) # Allow services to stabilize + + # Test server health + logger.info("Testing Server Health") + server_url = f"http://{opal_env.containers['server'].get_container_host_ip()}:{opal_env.containers['server'].get_exposed_port(opal_config.SERVER_PORT)}" + server_health = requests.get(f"{server_url}/healthcheck") + assert server_health.status_code == 200, "Server health check failed" + logger.info("Server health check passed") + + # Test client health + logger.info("Testing Client Health") + client_url = f"http://{opal_env.containers['client'].get_container_host_ip()}:{opal_env.containers['client'].get_exposed_port(opal_config.CLIENT_PORT)}" + client_health = requests.get(f"{client_url}/healthcheck") + assert client_health.status_code == 200, "Client health check failed" + logger.info("Client health check passed") + + # Test OPA endpoint + logger.info("Testing OPA Health") + opa_url = f"http://{opal_env.containers['client'].get_container_host_ip()}:{opal_env.containers['client'].get_exposed_port(8181)}" + opa_health = requests.get(f"{opa_url}/health") + assert opa_health.status_code == 200, "OPA health check failed" + logger.info("OPA health check passed") + + # Check server statistics + logger.info("Checking Server Statistics") + stats_url = f"{server_url}/statistics" + headers = {"Authorization": f"Bearer {opal_config.SERVER_MASTER_TOKEN}"} + stats_response = requests.get(stats_url, headers=headers) + assert stats_response.status_code == 200, f"Failed to get statistics: HTTP {stats_response.status_code}" + stats_data = stats_response.json() + logger.info("Server Statistics: %s", json.dumps(stats_data, indent=2)) + + # Check for errors in logs + logger.info("Checking Container Logs") + for name, container in opal_env.containers.items(): + logs = container.get_logs()[0].decode() + error_count = sum(1 for line in logs.split('\n') if "ERROR" in line) + critical_count = sum(1 for line in logs.split('\n') if "CRITICAL" in line) + + if error_count > 0 or critical_count > 0: + logger.error(f"Found errors in {name} logs:") + logger.error(f"- {error_count} ERRORs") + logger.error(f"- {critical_count} CRITICALs") + assert False, f"Found {error_count + critical_count} errors in {name} logs" + else: + logger.info(f"{name}: No errors found in logs") + + logger.info("All basic health checks completed successfully") \ No newline at end of file diff --git a/e2e-tests/test_environment.py b/e2e-tests/test_environment.py new file mode 100644 index 00000000..8de9c186 --- /dev/null +++ b/e2e-tests/test_environment.py @@ -0,0 +1,167 @@ +"""Test environment management""" +import logging +import time +from typing import Dict +import json +import docker +import requests +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for_logs +from config import OPALEnvironment +from container_config import ( + configure_postgres, + configure_server, + configure_client +) + +# Configure logging +logger = logging.getLogger(__name__) + +class OPALTestEnvironment: + """Main test environment manager""" + def __init__(self, config: OPALEnvironment): + self.config = config + self.containers: Dict[str, DockerContainer] = {} + self.docker_client = docker.from_env() + self.network = None + self.network_id = None + + def setup(self): + """Setup the complete test environment""" + try: + self._create_network() + logger.info(f"Created network: {self.config.NETWORK_NAME}") + + # Start Postgres broadcast channel + logger.info("Starting Postgres Container") + self.containers['postgres'] = DockerContainer("postgres:alpine") + configure_postgres(self.containers['postgres'], self.config) + self.containers['postgres'].with_name("broadcast_channel") + self.containers['postgres'].start() + + # Wait for Postgres + self._wait_for_postgres() + + # Start OPAL Server + logger.info("Starting OPAL Server Container") + self.containers['server'] = DockerContainer("permitio/opal-server:latest") + configure_server(self.containers['server'], self.config) + self.containers['server'].with_name("opal_server") + self.containers['server'].start() + + # Wait for server + self._wait_for_server() + + # Start OPAL Client + logger.info("Starting OPAL Client Container") + self.containers['client'] = DockerContainer("permitio/opal-client:latest") + configure_client(self.containers['client'], self.config) + self.containers['client'].with_name("opal_client") + self.containers['client'].with_command( + f"sh -c 'exec ./wait-for.sh opal_server:{self.config.SERVER_PORT} --timeout=20 -- ./start.sh'" + ) + self.containers['client'].start() + + # Wait for client + self._wait_for_client() + + except Exception as e: + logger.error(f"Error during setup: {str(e)}") + self._log_container_status() + self.teardown() + raise + + def _create_network(self): + """Create Docker network for test environment""" + try: + # Remove network if it exists + try: + existing_network = self.docker_client.networks.get(self.config.NETWORK_NAME) + existing_network.remove() + except docker.errors.NotFound: + pass + + # Create new network + self.network = self.docker_client.networks.create( + name=self.config.NETWORK_NAME, + driver="bridge", + check_duplicate=True + ) + self.network_id = self.network.id + except Exception as e: + raise Exception(f"Failed to create network: {str(e)}") + + def _wait_for_postgres(self): + """Wait for Postgres to be ready""" + logger.info("Waiting for Postgres to be ready...") + wait_for_logs(self.containers['postgres'], "database system is ready to accept connections", timeout=30) + logger.info("Postgres is ready") + time.sleep(2) + + def _wait_for_server(self): + """Wait for server to be ready with retries""" + logger.info("Waiting for OPAL server to be ready...") + + for retry in range(self.config.TEST_MAX_RETRIES): + try: + server_url = f"http://{self.containers['server'].get_container_host_ip()}:{self.containers['server'].get_exposed_port(self.config.SERVER_PORT)}" + response = requests.get(f"{server_url}/healthcheck", timeout=5) + if response.status_code == 200: + logger.info("OPAL server is ready") + time.sleep(5) # Allow stabilization + return + except Exception as e: + logger.warning(f"Server not ready (attempt {retry + 1}/{self.config.TEST_MAX_RETRIES}): {str(e)}") + if retry == self.config.TEST_MAX_RETRIES - 1: + self._log_container_status() + raise TimeoutError("OPAL server failed to become healthy") + time.sleep(self.config.TEST_RETRY_INTERVAL) + + def _wait_for_client(self): + """Wait for client to be ready with retries""" + logger.info("Waiting for OPAL client to be ready...") + + for retry in range(self.config.TEST_MAX_RETRIES): + try: + client_url = f"http://{self.containers['client'].get_container_host_ip()}:{self.containers['client'].get_exposed_port(self.config.CLIENT_PORT)}" + response = requests.get(f"{client_url}/healthcheck", timeout=5) + if response.status_code == 200: + logger.info("OPAL client is ready") + time.sleep(5) # Allow OPA initialization + return + except Exception as e: + logger.warning(f"Client not ready (attempt {retry + 1}/{self.config.TEST_MAX_RETRIES}): {str(e)}") + if retry == self.config.TEST_MAX_RETRIES - 1: + self._log_container_status() + raise TimeoutError("OPAL client failed to become healthy") + time.sleep(self.config.TEST_RETRY_INTERVAL) + + def _log_container_status(self): + """Log container statuses and logs for debugging""" + logger.debug("=== Container Status ===") + for name, container in self.containers.items(): + try: + logger.debug(f"=== {name.upper()} LOGS ===") + logger.debug("STDOUT:") + logger.debug(container.get_logs()[0].decode()) + logger.debug("STDERR:") + logger.debug(container.get_logs()[1].decode()) + except Exception as e: + logger.error(f"Could not get logs for {name}: {str(e)}") + + def teardown(self): + """Cleanup all resources""" + logger.info("Cleaning up test environment") + for name, container in reversed(list(self.containers.items())): + try: + container.stop() + logger.info(f"Stopped container: {name}") + except Exception as e: + logger.error(f"Error stopping container {name}: {str(e)}") + + if self.network: + try: + self.network.remove() + logger.info(f"Removed network: {self.config.NETWORK_NAME}") + except Exception as e: + logger.error(f"Error removing network: {str(e)}") \ No newline at end of file diff --git a/e2e-tests/test_validation.py b/e2e-tests/test_validation.py new file mode 100644 index 00000000..747dac7a --- /dev/null +++ b/e2e-tests/test_validation.py @@ -0,0 +1,99 @@ +"""Validation utilities""" +import logging +import json +import requests +from config import OPALEnvironment +from test_environment import OPALTestEnvironment + +logger = logging.getLogger(__name__) + +def validate_statistics(stats: dict) -> bool: + """Validate statistics data structure and content""" + required_fields = ["clients", "uptime", "version"] + for field in required_fields: + if field not in stats: + logger.error(f"Missing required field in statistics: {field}") + return False + + if not stats["clients"]: + logger.error("No clients found in statistics") + return False + + # Verify client subscriptions + found_client = False + expected_topics = ["policy_data"] + + for client_id, client_data in stats["clients"].items(): + logger.info(f"Client {client_id} Data: {json.dumps(client_data, indent=2)}") + + if isinstance(client_data, list): + for conn in client_data: + client_topics = conn.get("topics", []) + if any(topic in client_topics for topic in expected_topics): + found_client = True + logger.info(f"Found client with expected topics: {client_topics}") + break + + if not found_client: + logger.error("No client found with expected topic subscriptions") + return False + + return True + +def check_client_server_connection(env: OPALTestEnvironment) -> bool: + """Verify client-server connection using Statistics API""" + try: + server_url = f"http://{env.containers['server'].get_container_host_ip()}:{env.containers['server'].get_exposed_port(env.config.SERVER_PORT)}" + stats_url = f"{server_url}/statistics" + headers = {"Authorization": f"Bearer {env.config.SERVER_MASTER_TOKEN}"} + + logger.info(f"Checking statistics at: {stats_url}") + response = requests.get(stats_url, headers=headers) + + if response.status_code != 200: + logger.error(f"Failed to get statistics: HTTP {response.status_code}") + logger.error(f"Response: {response.text}") + return False + + stats = response.json() + logger.info("Server Statistics: %s", json.dumps(stats, indent=2)) + + if not validate_statistics(stats): + return False + + logger.info("Client-server connection verified successfully") + return True + + except Exception as e: + logger.error(f"Error checking client-server connection: {str(e)}") + return False + +def check_container_logs_for_errors(env: OPALTestEnvironment) -> bool: + """Analyze container logs for critical errors""" + error_keywords = ["ERROR", "CRITICAL", "FATAL", "Exception"] + found_errors = False + + logger.info("Analyzing container logs") + for name, container in env.containers.items(): + try: + logs = container.get_logs()[0].decode() + container_errors = [ + line.strip() for line in logs.split('\n') + if any(keyword in line for keyword in error_keywords) + ] + + if container_errors: + logger.warning(f"Found errors in {name} logs:") + for error in container_errors[:5]: # Show first 5 errors + logger.warning(f"{name}: {error}") + if len(container_errors) > 5: + logger.warning(f"... and {len(container_errors) - 5} more errors") + found_errors = True + else: + logger.info(f"{name}: No critical errors found") + + except Exception as e: + logger.error(f"Error getting logs for {name}: {str(e)}") + found_errors = True + + return not found_errors \ No newline at end of file From 10360420fe5097e6c890b5c756b9bb04316324bb Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 10 Dec 2024 23:58:49 +0100 Subject: [PATCH 3/3] requirements --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 656fe7c6..4422bf7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,7 @@ pytest-asyncio pytest-rerunfailures wheel>=0.38.0 twine +testcontainers +docker setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability