diff --git a/test/development/.gitignore b/test/development/.gitignore new file mode 100644 index 0000000..14a277e --- /dev/null +++ b/test/development/.gitignore @@ -0,0 +1,66 @@ +# Python virtual environment +.venv/ +venv/ +env/ +ENV/ + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec + +# Test reports and artifacts +test_report.html +test_report.json +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Temporary files +*.tmp +*.temp +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/test/development/README.md b/test/development/README.md new file mode 100644 index 0000000..a8d00df --- /dev/null +++ b/test/development/README.md @@ -0,0 +1,373 @@ +# RedisGate Development Test Suite + +This directory contains a comprehensive test suite designed for development workflow testing of RedisGate. The test suite verifies all server APIs work correctly during local development. + +## Overview + +The development test suite is designed to be run by developers on their local machines after: +1. Running the setup script (`./setup-dev.sh`) +2. Building the project (`cargo build`) +3. Starting the server (`cargo run`) + +The test suite verifies: +- **Public API endpoints** (health, version, stats) - no authentication required +- **Authentication endpoints** (register, login) - user management +- **Protected API endpoints** (organizations, api-keys, redis-instances) - JWT authentication required +- **Redis HTTP API endpoints** - API key authentication required + +## Test Structure + +``` +test/development/ +├── conftest.py # Test fixtures and configuration +├── pytest.ini # Pytest configuration +├── requirements.txt # Python dependencies +├── run_tests.py # Main test runner script +├── test_public_endpoints.py # Public API tests +├── test_auth_endpoints.py # Authentication tests +├── test_protected_endpoints.py # Protected API tests +├── test_redis_endpoints.py # Redis HTTP API tests +└── README.md # This file +``` + +## Prerequisites + +### System Requirements +- **Python**: 3.8 or higher +- **RedisGate server**: Running on localhost:8080 (default) +- **PostgreSQL**: For RedisGate's metadata storage (configured via setup script) + +### RedisGate Server +The tests assume the RedisGate server is already running. Follow the development workflow: + +```bash +# 1. One-time setup (install dependencies and start services) +./setup-dev.sh + +# 2. Build the application +cargo build + +# 3. Run the application (migrations run automatically) +cargo run +``` + +## Quick Start + +### Automatic Setup (Recommended) +```bash +# Navigate to the development test directory +cd test/development + +# Install dependencies and run all tests +python run_tests.py --install-deps + +# Or if you prefer verbose output +python run_tests.py --install-deps -v +``` + +### Manual Setup +```bash +# Navigate to the development test directory +cd test/development + +# Create virtual environment +python -m venv .venv + +# Activate virtual environment +# On Linux/macOS: +source .venv/bin/activate +# On Windows: +.venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt + +# Run tests +python run_tests.py +``` + +## Usage Examples + +### Run All Tests +```bash +python run_tests.py +``` + +### Run Specific Test Categories +```bash +# Test only public endpoints +python run_tests.py -m public + +# Test only authentication +python run_tests.py -m auth + +# Test only protected endpoints +python run_tests.py -m protected + +# Test only Redis HTTP API +python run_tests.py -m redis +``` + +### Run Specific Test Files +```bash +# Run only public endpoint tests +python run_tests.py test_public_endpoints.py + +# Run multiple test files +python run_tests.py test_public_endpoints.py test_auth_endpoints.py +``` + +### Verbose Output and Reporting +```bash +# Verbose output +python run_tests.py -v + +# Generate HTML report +python run_tests.py --report + +# Generate both HTML and JSON reports +python run_tests.py --report --json-report +``` + +### Custom Server Configuration +```bash +# Test against custom host/port +python run_tests.py --host 192.168.1.100 --port 9000 + +# With verbose output +python run_tests.py --host localhost --port 8080 -v +``` + +## Test Categories + +### Public Endpoints (`test_public_endpoints.py`) +Tests endpoints that don't require authentication: +- `GET /health` - Server health check +- `GET /version` - Server version information +- `GET /stats` - Database statistics + +### Authentication (`test_auth_endpoints.py`) +Tests user authentication and registration: +- `POST /auth/register` - User registration +- `POST /auth/login` - User login +- JWT token validation +- Error handling for invalid credentials + +### Protected Endpoints (`test_protected_endpoints.py`) +Tests endpoints that require JWT authentication: + +**Organizations:** +- `POST /api/organizations` - Create organization +- `GET /api/organizations` - List organizations +- `GET /api/organizations/{id}` - Get organization +- `PUT /api/organizations/{id}` - Update organization +- `DELETE /api/organizations/{id}` - Delete organization + +**API Keys:** +- `POST /api/organizations/{org_id}/api-keys` - Create API key +- `GET /api/organizations/{org_id}/api-keys` - List API keys +- `GET /api/organizations/{org_id}/api-keys/{key_id}` - Get API key +- `DELETE /api/organizations/{org_id}/api-keys/{key_id}` - Revoke API key + +**Redis Instances:** +- `POST /api/organizations/{org_id}/redis-instances` - Create Redis instance +- `GET /api/organizations/{org_id}/redis-instances` - List Redis instances +- `GET /api/organizations/{org_id}/redis-instances/{id}` - Get Redis instance +- `PUT /api/organizations/{org_id}/redis-instances/{id}/status` - Update status +- `DELETE /api/organizations/{org_id}/redis-instances/{id}` - Delete Redis instance + +### Redis HTTP API (`test_redis_endpoints.py`) +Tests Redis operations via HTTP API (requires API key authentication): +- `GET /redis/{instance_id}/ping` - PING command +- `GET /redis/{instance_id}/set/{key}/{value}` - SET command +- `GET /redis/{instance_id}/get/{key}` - GET command +- `GET /redis/{instance_id}/del/{key}` - DEL command +- `GET /redis/{instance_id}/incr/{key}` - INCR command +- `GET /redis/{instance_id}/hset/{key}/{field}/{value}` - HSET command +- `GET /redis/{instance_id}/hget/{key}/{field}` - HGET command +- `GET /redis/{instance_id}/lpush/{key}/{value}` - LPUSH command +- `GET /redis/{instance_id}/lpop/{key}` - LPOP command +- `POST /redis/{instance_id}` - Generic command execution + +## Configuration + +### Environment Variables +The tests use the following environment variables: + +```bash +# Server configuration (set automatically by test runner) +REDISGATE_TEST_HOST=127.0.0.1 +REDISGATE_TEST_PORT=8080 + +# Python environment +PYTHONPATH=./test/development +``` + +### Test Markers +Tests are categorized using pytest markers: + +- `@pytest.mark.public` - Public API tests +- `@pytest.mark.auth` - Authentication tests +- `@pytest.mark.protected` - Protected API tests +- `@pytest.mark.redis` - Redis HTTP API tests +- `@pytest.mark.integration` - Full integration tests + +## Development Workflow Integration + +This test suite is designed to fit into the typical development workflow: + +1. **Setup Development Environment:** + ```bash + ./setup-dev.sh + ``` + +2. **Build Application:** + ```bash + cargo build + ``` + +3. **Start Server:** + ```bash + cargo run + ``` + +4. **Run Development Tests:** + ```bash + cd test/development + python run_tests.py --install-deps + ``` + +5. **Make Code Changes** (repeat steps 2-4 as needed) + +## Troubleshooting + +### Common Issues + +**Server Not Running:** +``` +✗ Server not available at http://127.0.0.1:8080 +``` +- Make sure you've run `cargo run` and the server started successfully +- Check that the server is running on the expected port (8080 by default) +- Verify no firewall is blocking the connection + +**Dependency Issues:** +``` +✗ Required dependencies not found +``` +- Run with `--install-deps` flag to install dependencies automatically +- Or manually install: `pip install -r requirements.txt` + +**Database Connection Errors:** +``` +Database connection failed +``` +- Ensure PostgreSQL is running (check with `./scripts/dev-services.sh status`) +- Verify the database configuration in `.env.development` +- Run database migrations: The server should do this automatically + +**Test Failures:** +- Check server logs for errors +- Verify all external services (PostgreSQL) are running +- Run tests with `-v` flag for more detailed output + +### Debug Mode +```bash +# Run with maximum verbosity +python run_tests.py -v -s + +# Run single test file with debug output +python run_tests.py test_public_endpoints.py -v -s + +# Generate detailed reports +python run_tests.py --report --json-report -v +``` + +## Integration with CI/CD + +This test suite can be integrated into CI/CD pipelines. Example GitHub Actions workflow: + +```yaml +name: Development Tests +on: [push, pull_request] + +jobs: + dev-tests: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:14 + env: + POSTGRES_PASSWORD: password + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Setup Development Environment + run: ./setup-dev.sh + + - name: Build Application + run: cargo build + + - name: Start Server + run: cargo run & + + - name: Run Development Tests + run: | + cd test/development + python run_tests.py --install-deps --report + + - name: Upload Test Results + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results + path: test/development/test_report.html +``` + +## Contributing + +When adding new API endpoints to RedisGate: + +1. **Add corresponding tests** to the appropriate test file +2. **Update test markers** if creating new endpoint categories +3. **Update this README** with new endpoint documentation +4. **Run the full test suite** to ensure no regressions + +### Adding New Tests + +1. **Choose the appropriate test file:** + - Public endpoints → `test_public_endpoints.py` + - Authentication → `test_auth_endpoints.py` + - Protected endpoints → `test_protected_endpoints.py` + - Redis operations → `test_redis_endpoints.py` + +2. **Use appropriate pytest markers:** + ```python + @pytest.mark.public + async def test_new_public_endpoint(self, api_client: ApiClient): + # Test implementation + ``` + +3. **Follow existing test patterns:** + - Use fixtures for authentication and test data + - Test both success and error cases + - Use descriptive test names and docstrings + +4. **Test the new functionality:** + ```bash + python run_tests.py test_new_file.py -v + ``` \ No newline at end of file diff --git a/test/development/conftest.py b/test/development/conftest.py new file mode 100644 index 0000000..d1dd8df --- /dev/null +++ b/test/development/conftest.py @@ -0,0 +1,205 @@ +""" +Pytest configuration and fixtures for RedisGate development test suite. + +This module provides: +- Server URL configuration +- HTTP client setup +- Test data generation and cleanup +- Authentication helpers +""" + +import asyncio +import os +import time +from pathlib import Path +from typing import AsyncGenerator, Dict, Any, Optional +from uuid import uuid4 + +import httpx +import pytest +import pytest_asyncio +from rich.console import Console + +# Configure rich console for better test output +console = Console() + +# Test configuration +TEST_HOST = os.getenv("REDISGATE_TEST_HOST", "127.0.0.1") +TEST_PORT = int(os.getenv("REDISGATE_TEST_PORT", "8080")) +BASE_URL = f"http://{TEST_HOST}:{TEST_PORT}" +CLIENT_TIMEOUT = 10 # seconds for client operations + + +class ApiClient: + """Simple HTTP client wrapper for RedisGate API testing.""" + + def __init__(self, base_url: str, timeout: int = CLIENT_TIMEOUT): + self.base_url = base_url + self.timeout = timeout + self._client = None + + async def __aenter__(self): + self._client = httpx.AsyncClient( + base_url=self.base_url, + timeout=self.timeout, + follow_redirects=True + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self._client: + await self._client.aclose() + + async def get(self, url: str, headers: Optional[Dict] = None, params: Optional[Dict] = None) -> httpx.Response: + """Make GET request.""" + return await self._client.get(url, headers=headers, params=params) + + async def post(self, url: str, json: Optional[Dict] = None, headers: Optional[Dict] = None, params: Optional[Dict] = None) -> httpx.Response: + """Make POST request.""" + return await self._client.post(url, json=json, headers=headers, params=params) + + async def put(self, url: str, json: Optional[Dict] = None, headers: Optional[Dict] = None) -> httpx.Response: + """Make PUT request.""" + return await self._client.put(url, json=json, headers=headers) + + async def delete(self, url: str, headers: Optional[Dict] = None) -> httpx.Response: + """Make DELETE request.""" + return await self._client.delete(url, headers=headers) + + +@pytest_asyncio.fixture +async def api_client() -> AsyncGenerator[ApiClient, None]: + """Provide an HTTP client for API testing.""" + async with ApiClient(BASE_URL) as client: + yield client + + +@pytest_asyncio.fixture +async def wait_for_server(): + """Wait for RedisGate server to be available.""" + max_attempts = 30 + for attempt in range(max_attempts): + try: + async with httpx.AsyncClient() as client: + response = await client.get(f"{BASE_URL}/health", timeout=5.0) + if response.status_code == 200: + console.print(f"[green]Server is ready at {BASE_URL}[/green]") + return + except (httpx.ConnectError, httpx.TimeoutException): + if attempt < max_attempts - 1: + console.print(f"[yellow]Waiting for server... (attempt {attempt + 1}/{max_attempts})[/yellow]") + await asyncio.sleep(2) + else: + raise ConnectionError(f"Server not available at {BASE_URL} after {max_attempts} attempts") + + +@pytest_asyncio.fixture +async def auth_user(api_client: ApiClient): + """Create a test user and return authentication data.""" + # Generate unique test data + username = f"testuser_{uuid4().hex[:8]}" + email = f"{username}@example.com" + password = "TestPassword123!" + + # Register user + register_data = { + "username": username, + "email": email, + "password": password + } + + register_response = await api_client.post("/auth/register", json=register_data) + assert register_response.status_code == 200, f"Registration failed: {register_response.text}" + + # Login to get JWT token + login_data = { + "email": email, + "password": password + } + + login_response = await api_client.post("/auth/login", json=login_data) + assert login_response.status_code == 200, f"Login failed: {login_response.text}" + + login_result = login_response.json() + + return { + "user_id": login_result["data"]["user"]["id"], + "username": username, + "email": email, + "jwt_token": login_result["data"]["token"], + "auth_headers": {"Authorization": f"Bearer {login_result['data']['token']}"} + } + + +@pytest_asyncio.fixture +async def test_organization(api_client: ApiClient, auth_user: Dict[str, Any]): + """Create a test organization and return its data.""" + org_data = { + "name": f"Test Organization {uuid4().hex[:8]}", + "slug": f"test-org-{int(time.time() * 1000000)}", # Use microsecond timestamp for uniqueness + "description": "Test organization for development testing" + } + + response = await api_client.post( + "/api/organizations", + json=org_data, + headers=auth_user["auth_headers"] + ) + assert response.status_code == 200, f"Organization creation failed: {response.text}" + + return response.json()["data"] + + +@pytest_asyncio.fixture +async def test_api_key(api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any]): + """Create a test API key and return its data.""" + org_id = test_organization["id"] + + api_key_data = { + "name": f"Test API Key {uuid4().hex[:8]}", + "organization_id": org_id, + "permissions": ["read", "write"], + "expires_at": None # No expiration for testing + } + + response = await api_client.post( + f"/api/organizations/{org_id}/api-keys", + json=api_key_data, + headers=auth_user["auth_headers"] + ) + assert response.status_code == 200, f"API key creation failed: {response.text}" + + return response.json()["data"] + + +@pytest_asyncio.fixture +async def test_redis_instance(api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any]): + """Create a test Redis instance and return its data.""" + org_id = test_organization["id"] + + instance_data = { + "name": f"Test Redis Instance {uuid4().hex[:8]}", + "redis_url": "redis://localhost:6379/0", # Assuming local Redis for development + "port": 6379, + "database": 0, + "max_connections": 10 + } + + response = await api_client.post( + f"/api/organizations/{org_id}/redis-instances", + json=instance_data, + headers=auth_user["auth_headers"] + ) + assert response.status_code == 200, f"Redis instance creation failed: {response.text}" + + return response.json()["data"] + + +def generate_test_key(prefix: str = "test") -> str: + """Generate a unique test key.""" + return f"{prefix}_{uuid4().hex[:8]}" + + +def generate_test_value() -> str: + """Generate a test value.""" + return f"value_{uuid4().hex[:8]}" \ No newline at end of file diff --git a/test/development/pytest.ini b/test/development/pytest.ini new file mode 100644 index 0000000..1893109 --- /dev/null +++ b/test/development/pytest.ini @@ -0,0 +1,44 @@ +[pytest] +# Test configuration for RedisGate development test suite + +# Test discovery +testpaths = . +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +# Markers for test categorization +markers = + public: Public API tests (no authentication required) + auth: Authentication tests + protected: Protected API tests (JWT authentication required) + redis: Redis HTTP API tests (API key authentication required) + integration: Full integration tests + slow: Slow running tests + +# Asyncio configuration +asyncio_mode = auto + +# Test timeout (in seconds) +timeout = 30 +timeout_method = thread + +# Output configuration +addopts = + -v + --tb=short + --strict-markers + --strict-config + --color=yes + --durations=10 + +# Logging +log_cli = true +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S + +# Filters +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning \ No newline at end of file diff --git a/test/development/requirements.txt b/test/development/requirements.txt new file mode 100644 index 0000000..898f6b4 --- /dev/null +++ b/test/development/requirements.txt @@ -0,0 +1,23 @@ +# RedisGate Development Test Suite - Requirements +# +# Core testing framework +pytest==7.4.3 +pytest-asyncio==0.21.1 +pytest-timeout==2.1.0 + +# HTTP client +httpx==0.27.0 + +# Utilities +psutil==5.9.6 +colorama==0.4.6 +rich==13.7.0 +python-dotenv==1.0.0 + +# Data generation and validation +Faker==20.1.0 +pydantic==2.5.3 + +# Test reporting +pytest-html==4.1.1 +pytest-json-report==1.5.0 \ No newline at end of file diff --git a/test/development/run_tests.py b/test/development/run_tests.py new file mode 100755 index 0000000..66448be --- /dev/null +++ b/test/development/run_tests.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +""" +RedisGate Development Test Suite Runner + +This script runs the complete development test suite for RedisGate. +It is designed to be used during development workflow: + +1. Developer runs setup script (./setup-dev.sh) +2. Developer builds the project (cargo build) +3. Developer starts the server (cargo run) +4. Developer runs this test suite to verify all APIs work + +Usage: + python run_tests.py [options] + +Examples: + # Run all tests + python run_tests.py + + # Run only public API tests + python run_tests.py -m public + + # Run tests with verbose output + python run_tests.py -v + + # Install dependencies and run tests + python run_tests.py --install-deps + + # Run tests and generate HTML report + python run_tests.py --report +""" + +import argparse +import asyncio +import os +import subprocess +import sys +import time +from pathlib import Path +from typing import List, Optional + +try: + from rich.console import Console + from rich.panel import Panel + from rich.progress import Progress, SpinnerColumn, TextColumn + from rich.table import Table + RICH_AVAILABLE = True +except ImportError: + RICH_AVAILABLE = False + class Console: + def print(self, *args, **kwargs): + print(*args) + console = Console() + +if RICH_AVAILABLE: + console = Console() +else: + console = Console() + + +class TestRunner: + """Main test runner for RedisGate development test suite.""" + + def __init__(self, args): + self.args = args + self.test_dir = Path(__file__).parent + self.project_root = self.test_dir.parent.parent + self.venv_dir = self.test_dir / ".venv" + self.results = {} + + def run_tests(self) -> bool: + """Run the development test suite based on the selected options.""" + if RICH_AVAILABLE: + console.print(Panel.fit("🚀 RedisGate Development Test Suite", style="bold blue")) + else: + console.print("🚀 RedisGate Development Test Suite") + + # Check server is running + if not self._wait_for_server(): + return False + + # Install dependencies if requested + if self.args.install_deps and not self._install_dependencies(): + return False + + # Check dependencies + if not self._check_dependencies(): + return False + + # Run the tests + start_time = time.time() + success = self._execute_tests() + end_time = time.time() + + # Generate report if requested + if self.args.report: + self._generate_report(start_time, end_time) + + return success + + def _wait_for_server(self) -> bool: + """Wait for RedisGate server to be available.""" + console.print("[blue]Checking if RedisGate server is running...[/blue]") + + import httpx + + max_attempts = 10 + for attempt in range(max_attempts): + try: + with httpx.Client() as client: + response = client.get(f"http://{self.args.host}:{self.args.port}/health", timeout=5.0) + if response.status_code == 200: + console.print(f"[green]✓ Server is ready at http://{self.args.host}:{self.args.port}[/green]") + return True + except (httpx.ConnectError, httpx.TimeoutException): + if attempt < max_attempts - 1: + console.print(f"[yellow]Waiting for server... (attempt {attempt + 1}/{max_attempts})[/yellow]") + time.sleep(2) + else: + console.print(f"[red]✗ Server not available at http://{self.args.host}:{self.args.port}[/red]") + console.print("[yellow]Please make sure the RedisGate server is running:[/yellow]") + console.print(" 1. Run: cargo build") + console.print(" 2. Run: cargo run") + console.print(" 3. Wait for server to start on port 8080") + return False + + return False + + def _install_dependencies(self) -> bool: + """Install Python dependencies in a virtual environment.""" + console.print("[blue]Installing Python dependencies...[/blue]") + + try: + # Create virtual environment if it doesn't exist + if not self.venv_dir.exists(): + console.print("[blue]Creating virtual environment...[/blue]") + subprocess.run([ + sys.executable, "-m", "venv", str(self.venv_dir) + ], check=True, cwd=self.test_dir) + + # Determine python executable in venv + if os.name == 'nt': + python_cmd = self.venv_dir / "Scripts" / "python.exe" + pip_cmd = self.venv_dir / "Scripts" / "pip.exe" + else: + python_cmd = self.venv_dir / "bin" / "python" + pip_cmd = self.venv_dir / "bin" / "pip" + + # Upgrade pip + subprocess.run([ + str(pip_cmd), "install", "--upgrade", "pip" + ], check=True, cwd=self.test_dir) + + # Install requirements + subprocess.run([ + str(pip_cmd), "install", "-r", "requirements.txt" + ], check=True, cwd=self.test_dir) + + console.print("[green]✓ Dependencies installed successfully[/green]") + return True + + except subprocess.CalledProcessError as e: + console.print(f"[red]✗ Failed to install dependencies: {e}[/red]") + return False + + def _check_dependencies(self) -> bool: + """Check if required dependencies are available.""" + console.print("[blue]Checking dependencies...[/blue]") + + # Determine python executable + if self.venv_dir.exists(): + if os.name == 'nt': + python_cmd = self.venv_dir / "Scripts" / "python.exe" + else: + python_cmd = self.venv_dir / "bin" / "python" + else: + python_cmd = "python3" + + # Check if pytest is available + try: + result = subprocess.run([ + str(python_cmd), "-c", "import pytest; import httpx; print('OK')" + ], capture_output=True, text=True, cwd=self.test_dir) + + if result.returncode != 0: + console.print("[red]✗ Required dependencies not found[/red]") + console.print("Run with --install-deps to install them automatically") + return False + + console.print("[green]✓ Dependencies are available[/green]") + return True + + except FileNotFoundError: + console.print("[red]✗ Python executable not found[/red]") + return False + + def _execute_tests(self) -> bool: + """Execute the tests using pytest.""" + console.print("[blue]Running development tests...[/blue]") + + # Determine python executable + if self.venv_dir.exists(): + if os.name == 'nt': + python_cmd = self.venv_dir / "Scripts" / "python.exe" + else: + python_cmd = self.venv_dir / "bin" / "python" + else: + python_cmd = "python3" + + # Build pytest command + pytest_args = self._build_pytest_args() + cmd = [str(python_cmd), "-m", "pytest"] + pytest_args + + console.print(f"[green]Running: {' '.join(cmd)}[/green]") + + # Set environment + env = os.environ.copy() + env["PYTHONPATH"] = str(self.test_dir) + env["REDISGATE_TEST_HOST"] = self.args.host + env["REDISGATE_TEST_PORT"] = str(self.args.port) + + # Run tests + try: + result = subprocess.run( + cmd, + cwd=self.test_dir, + env=env, + timeout=self.args.timeout + ) + + if result.returncode == 0: + console.print("[green]✓ All tests passed![/green]") + return True + else: + console.print(f"[red]✗ Tests failed with exit code {result.returncode}[/red]") + return False + + except subprocess.TimeoutExpired: + console.print(f"[red]✗ Tests timed out after {self.args.timeout} seconds[/red]") + return False + except KeyboardInterrupt: + console.print("[yellow]Tests interrupted by user[/yellow]") + return False + + def _build_pytest_args(self) -> List[str]: + """Build pytest command line arguments.""" + args = [] + + # Test selection by marker + if self.args.marker: + args.extend(["-m", self.args.marker]) + + # Verbosity + if self.args.verbose: + args.append("-v") + else: + args.append("-q") + + # Show output + if self.args.capture == "no": + args.append("-s") + + # Parallel execution + if self.args.workers and self.args.workers > 1: + args.extend(["-n", str(self.args.workers)]) + + # Test file selection + if self.args.test_files: + args.extend(self.args.test_files) + else: + args.append(".") # Run all tests in current directory + + # HTML report + if self.args.report: + report_file = self.test_dir / "test_report.html" + args.extend(["--html", str(report_file), "--self-contained-html"]) + + # JSON report + if self.args.json_report: + json_file = self.test_dir / "test_report.json" + args.extend(["--json-report", "--json-report-file", str(json_file)]) + + return args + + def _generate_report(self, start_time: float, end_time: float): + """Generate a detailed test report.""" + duration = end_time - start_time + + if RICH_AVAILABLE: + # Create report table + table = Table(title="RedisGate Development Test Report") + table.add_column("Metric", style="cyan") + table.add_column("Value", style="magenta") + + table.add_row("Test Mode", self.args.marker or "all") + table.add_row("Duration", f"{duration:.2f}s") + table.add_row("Python Version", sys.version.split()[0]) + table.add_row("Test Directory", str(self.test_dir)) + table.add_row("Server URL", f"http://{self.args.host}:{self.args.port}") + + console.print(table) + else: + console.print(f"Test Mode: {self.args.marker or 'all'}") + console.print(f"Duration: {duration:.2f}s") + console.print(f"Python Version: {sys.version.split()[0]}") + + # Save report to file if HTML report was generated + if self.args.report: + report_file = self.test_dir / "test_report.html" + if report_file.exists(): + console.print(f"[green]HTML report saved to: {report_file}[/green]") + + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="RedisGate Development Test Suite Runner", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__ + ) + + # Server configuration + parser.add_argument("--host", default="127.0.0.1", + help="RedisGate server host (default: 127.0.0.1)") + parser.add_argument("--port", type=int, default=8080, + help="RedisGate server port (default: 8080)") + + # Test selection + parser.add_argument("-m", "--marker", + choices=["public", "auth", "protected", "redis", "integration"], + help="Run tests with specific marker") + parser.add_argument("test_files", nargs="*", + help="Specific test files to run") + + # Test execution + parser.add_argument("-v", "--verbose", action="store_true", + help="Verbose output") + parser.add_argument("-s", "--capture", choices=["yes", "no"], default="yes", + help="Capture output (default: yes)") + parser.add_argument("-n", "--workers", type=int, default=1, + help="Number of parallel test workers") + parser.add_argument("--timeout", type=int, default=300, + help="Test timeout in seconds (default: 300)") + + # Dependencies and setup + parser.add_argument("--install-deps", action="store_true", + help="Install Python dependencies before running tests") + + # Reporting + parser.add_argument("--report", action="store_true", + help="Generate HTML test report") + parser.add_argument("--json-report", action="store_true", + help="Generate JSON test report") + + return parser.parse_args() + + +def main(): + """Main entry point for the test runner.""" + args = parse_args() + + runner = TestRunner(args) + success = runner.run_tests() + + # Exit with appropriate code + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test/development/test_auth_endpoints.py b/test/development/test_auth_endpoints.py new file mode 100644 index 0000000..1d99cc2 --- /dev/null +++ b/test/development/test_auth_endpoints.py @@ -0,0 +1,149 @@ +""" +Test authentication endpoints. + +This module tests: +- User registration +- User login +- JWT token validation +""" + +import pytest +from uuid import uuid4 +from conftest import ApiClient, generate_test_key + + +class TestAuthentication: + """Test authentication related endpoints.""" + + @pytest.mark.auth + async def test_user_registration(self, api_client: ApiClient, wait_for_server): + """Test user registration.""" + username = f"testuser_{generate_test_key()}" + email = f"{username}@example.com" + password = "TestPassword123!" + + register_data = { + "username": username, + "email": email, + "password": password + } + + response = await api_client.post("/auth/register", json=register_data) + + assert response.status_code == 200 + data = response.json() + assert data["success"] == True + assert data["data"]["username"] == username + assert data["data"]["email"] == email + assert "id" in data["data"] + + @pytest.mark.auth + async def test_user_registration_duplicate_email(self, api_client: ApiClient, wait_for_server): + """Test registration with duplicate email fails.""" + username = f"testuser_{generate_test_key()}" + email = f"{username}@example.com" + password = "TestPassword123!" + + register_data = { + "username": username, + "email": email, + "password": password + } + + # First registration should succeed + response1 = await api_client.post("/auth/register", json=register_data) + assert response1.status_code == 200 + + # Second registration with same email should fail + username2 = f"testuser2_{generate_test_key()}" + register_data2 = { + "username": username2, + "email": email, # Same email + "password": password + } + + response2 = await api_client.post("/auth/register", json=register_data2) + assert response2.status_code == 409 + error_data = response2.json() + assert error_data["success"] == False + + @pytest.mark.auth + async def test_user_login(self, api_client: ApiClient, wait_for_server): + """Test user login.""" + username = f"testuser_{generate_test_key()}" + email = f"{username}@example.com" + password = "TestPassword123!" + + # First register a user + register_data = { + "username": username, + "email": email, + "password": password + } + + register_response = await api_client.post("/auth/register", json=register_data) + assert register_response.status_code == 200 + + # Then login + login_data = { + "email": email, + "password": password + } + + login_response = await api_client.post("/auth/login", json=login_data) + + assert login_response.status_code == 200 + data = login_response.json() + assert data["success"] == True + assert data["data"]["user"]["username"] == username + assert data["data"]["user"]["email"] == email + assert "id" in data["data"]["user"] + assert "token" in data["data"] + assert isinstance(data["data"]["token"], str) + assert len(data["data"]["token"]) > 0 + + @pytest.mark.auth + async def test_user_login_invalid_credentials(self, api_client: ApiClient, wait_for_server): + """Test login with invalid credentials fails.""" + login_data = { + "email": "nonexistent@example.com", + "password": "wrongpassword" + } + + response = await api_client.post("/auth/login", json=login_data) + + assert response.status_code == 401 + error_data = response.json() + assert error_data["success"] == False + + @pytest.mark.auth + async def test_user_registration_invalid_data(self, api_client: ApiClient, wait_for_server): + """Test registration with invalid data fails.""" + # Test missing required fields + invalid_data = { + "username": "testuser" + # Missing email and password + } + + response = await api_client.post("/auth/register", json=invalid_data) + assert response.status_code == 422 + + # Test invalid email format + invalid_email_data = { + "username": "testuser", + "email": "invalid-email", + "password": "TestPassword123!" + } + + response = await api_client.post("/auth/register", json=invalid_email_data) + assert response.status_code in [400, 422] # Either is acceptable for validation errors + + # Test weak password + weak_password_data = { + "username": "testuser", + "email": "test@example.com", + "password": "123" # Too weak + } + + response = await api_client.post("/auth/register", json=weak_password_data) + assert response.status_code in [400, 422] # Either is acceptable for validation errors \ No newline at end of file diff --git a/test/development/test_protected_endpoints.py b/test/development/test_protected_endpoints.py new file mode 100644 index 0000000..2c9e9e4 --- /dev/null +++ b/test/development/test_protected_endpoints.py @@ -0,0 +1,395 @@ +""" +Test protected API endpoints that require JWT authentication. + +This module tests: +- Organization management endpoints +- API key management endpoints +- Redis instance management endpoints +""" + +import pytest +import time +from uuid import uuid4 +from typing import Dict, Any +from conftest import ApiClient, generate_test_key + + +class TestOrganizations: + """Test organization management endpoints.""" + + @pytest.mark.protected + async def test_create_organization(self, api_client: ApiClient, auth_user: Dict[str, Any], wait_for_server): + """Test creating an organization.""" + org_data = { + "name": f"Test Organization {generate_test_key()}", + "slug": f"test-org-{int(time.time() * 1000000)}", + "description": "Test organization for development testing" + } + + response = await api_client.post( + "/api/organizations", + json=org_data, + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert data["success"] == True + assert data["data"]["name"] == org_data["name"] + assert data["data"]["description"] == org_data["description"] + assert data["data"]["slug"] == org_data["slug"] + assert "id" in data["data"] + assert "owner_id" in data["data"] + assert data["data"]["owner_id"] == auth_user["user_id"] + + @pytest.mark.protected + async def test_list_organizations(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], wait_for_server): + """Test listing organizations.""" + response = await api_client.get( + "/api/organizations", + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert data["success"] == True + assert isinstance(data["data"]["items"], list) + assert len(data["data"]["items"]) >= 1 + + # Check if our test organization is in the list + org_ids = [org["id"] for org in data["data"]["items"]] + assert test_organization["id"] in org_ids + + @pytest.mark.protected + async def test_get_organization(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], wait_for_server): + """Test getting a specific organization.""" + org_id = test_organization["id"] + + response = await api_client.get( + f"/api/organizations/{org_id}", + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert data["success"] == True + assert data["data"]["id"] == org_id + assert data["data"]["name"] == test_organization["name"] + assert data["data"]["description"] == test_organization["description"] + + @pytest.mark.protected + async def test_update_organization(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], wait_for_server): + """Test updating an organization.""" + org_id = test_organization["id"] + + update_data = { + "name": f"Updated Organization {generate_test_key()}", + "slug": f"updated-org-{int(time.time() * 1000000)}", + "description": "Updated description" + } + + response = await api_client.put( + f"/api/organizations/{org_id}", + json=update_data, + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert data["success"] == True + assert data["data"]["name"] == update_data["name"] + assert data["data"]["description"] == update_data["description"] + + @pytest.mark.protected + async def test_delete_organization(self, api_client: ApiClient, auth_user: Dict[str, Any], wait_for_server): + """Test deleting an organization.""" + # Create a temporary organization for deletion + org_data = { + "name": f"Temp Organization {generate_test_key()}", + "slug": f"temp-org-{int(time.time() * 1000000)}", + "description": "Temporary organization for deletion test" + } + + create_response = await api_client.post( + "/api/organizations", + json=org_data, + headers=auth_user["auth_headers"] + ) + assert create_response.status_code == 200 + temp_org = create_response.json()["data"] + + # Delete the organization + delete_response = await api_client.delete( + f"/api/organizations/{temp_org['id']}", + headers=auth_user["auth_headers"] + ) + + assert delete_response.status_code == 200 # API returns 200, not 204 + + # Verify it's deleted by trying to get it + get_response = await api_client.get( + f"/api/organizations/{temp_org['id']}", + headers=auth_user["auth_headers"] + ) + assert get_response.status_code == 404 + + +class TestApiKeys: + """Test API key management endpoints.""" + + @pytest.mark.protected + async def test_create_api_key(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], wait_for_server): + """Test creating an API key.""" + org_id = test_organization["id"] + + api_key_data = { + "name": f"Test API Key {generate_test_key()}", + "permissions": ["read", "write"] + } + + response = await api_client.post( + f"/api/organizations/{org_id}/api-keys", + json=api_key_data, + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 201 + data = response.json() + assert data["name"] == api_key_data["name"] + assert data["permissions"] == api_key_data["permissions"] + assert "id" in data + assert "key" in data + assert "organization_id" in data + assert data["organization_id"] == org_id + + @pytest.mark.protected + async def test_list_api_keys(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test listing API keys.""" + org_id = test_organization["id"] + + response = await api_client.get( + f"/api/organizations/{org_id}/api-keys", + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) >= 1 + + # Check if our test API key is in the list + key_ids = [key["id"] for key in data] + assert test_api_key["id"] in key_ids + + @pytest.mark.protected + async def test_get_api_key(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test getting a specific API key.""" + org_id = test_organization["id"] + key_id = test_api_key["id"] + + response = await api_client.get( + f"/api/organizations/{org_id}/api-keys/{key_id}", + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert data["id"] == key_id + assert data["name"] == test_api_key["name"] + assert data["organization_id"] == org_id + + @pytest.mark.protected + async def test_revoke_api_key(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], wait_for_server): + """Test revoking an API key.""" + org_id = test_organization["id"] + + # Create a temporary API key for revocation + api_key_data = { + "name": f"Temp API Key {generate_test_key()}", + "permissions": ["read"] + } + + create_response = await api_client.post( + f"/api/organizations/{org_id}/api-keys", + json=api_key_data, + headers=auth_user["auth_headers"] + ) + assert create_response.status_code == 201 + temp_key = create_response.json() + + # Revoke the API key + revoke_response = await api_client.delete( + f"/api/organizations/{org_id}/api-keys/{temp_key['id']}", + headers=auth_user["auth_headers"] + ) + + assert revoke_response.status_code == 204 + + # Verify it's revoked by trying to get it + get_response = await api_client.get( + f"/api/organizations/{org_id}/api-keys/{temp_key['id']}", + headers=auth_user["auth_headers"] + ) + assert get_response.status_code == 404 + + +class TestRedisInstances: + """Test Redis instance management endpoints.""" + + @pytest.mark.protected + async def test_create_redis_instance(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], wait_for_server): + """Test creating a Redis instance.""" + org_id = test_organization["id"] + + instance_data = { + "name": f"Test Redis Instance {generate_test_key()}", + "redis_url": "redis://localhost:6379/0", + "port": 6379, + "database": 0, + "max_connections": 10 + } + + response = await api_client.post( + f"/api/organizations/{org_id}/redis-instances", + json=instance_data, + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 201 + data = response.json() + assert data["name"] == instance_data["name"] + assert data["redis_url"] == instance_data["redis_url"] + assert data["port"] == instance_data["port"] + assert data["database"] == instance_data["database"] + assert "id" in data + assert "organization_id" in data + assert data["organization_id"] == org_id + + @pytest.mark.protected + async def test_list_redis_instances(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], test_redis_instance: Dict[str, Any], wait_for_server): + """Test listing Redis instances.""" + org_id = test_organization["id"] + + response = await api_client.get( + f"/api/organizations/{org_id}/redis-instances", + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) >= 1 + + # Check if our test Redis instance is in the list + instance_ids = [instance["id"] for instance in data] + assert test_redis_instance["id"] in instance_ids + + @pytest.mark.protected + async def test_get_redis_instance(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], test_redis_instance: Dict[str, Any], wait_for_server): + """Test getting a specific Redis instance.""" + org_id = test_organization["id"] + instance_id = test_redis_instance["id"] + + response = await api_client.get( + f"/api/organizations/{org_id}/redis-instances/{instance_id}", + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert data["id"] == instance_id + assert data["name"] == test_redis_instance["name"] + assert data["organization_id"] == org_id + + @pytest.mark.protected + async def test_update_redis_instance_status(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], test_redis_instance: Dict[str, Any], wait_for_server): + """Test updating Redis instance status.""" + org_id = test_organization["id"] + instance_id = test_redis_instance["id"] + + status_data = { + "status": "active" + } + + response = await api_client.put( + f"/api/organizations/{org_id}/redis-instances/{instance_id}/status", + json=status_data, + headers=auth_user["auth_headers"] + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == status_data["status"] + + @pytest.mark.protected + async def test_delete_redis_instance(self, api_client: ApiClient, auth_user: Dict[str, Any], test_organization: Dict[str, Any], wait_for_server): + """Test deleting a Redis instance.""" + org_id = test_organization["id"] + + # Create a temporary Redis instance for deletion + instance_data = { + "name": f"Temp Redis Instance {generate_test_key()}", + "redis_url": "redis://localhost:6379/1", + "port": 6379, + "database": 1, + "max_connections": 5 + } + + create_response = await api_client.post( + f"/api/organizations/{org_id}/redis-instances", + json=instance_data, + headers=auth_user["auth_headers"] + ) + assert create_response.status_code == 201 + temp_instance = create_response.json() + + # Delete the Redis instance + delete_response = await api_client.delete( + f"/api/organizations/{org_id}/redis-instances/{temp_instance['id']}", + headers=auth_user["auth_headers"] + ) + + assert delete_response.status_code == 204 + + # Verify it's deleted by trying to get it + get_response = await api_client.get( + f"/api/organizations/{org_id}/redis-instances/{temp_instance['id']}", + headers=auth_user["auth_headers"] + ) + assert get_response.status_code == 404 + + +class TestUnauthorizedAccess: + """Test that protected endpoints require authentication.""" + + @pytest.mark.protected + async def test_organizations_require_auth(self, api_client: ApiClient, wait_for_server): + """Test that organization endpoints require authentication.""" + # Test without any headers + response = await api_client.get("/api/organizations") + assert response.status_code == 401 + + response = await api_client.post("/api/organizations", json={"name": "test"}) + assert response.status_code == 401 + + @pytest.mark.protected + async def test_api_keys_require_auth(self, api_client: ApiClient, wait_for_server): + """Test that API key endpoints require authentication.""" + fake_org_id = str(uuid4()) + + response = await api_client.get(f"/api/organizations/{fake_org_id}/api-keys") + assert response.status_code == 401 + + response = await api_client.post(f"/api/organizations/{fake_org_id}/api-keys", json={"name": "test"}) + assert response.status_code == 401 + + @pytest.mark.protected + async def test_redis_instances_require_auth(self, api_client: ApiClient, wait_for_server): + """Test that Redis instance endpoints require authentication.""" + fake_org_id = str(uuid4()) + + response = await api_client.get(f"/api/organizations/{fake_org_id}/redis-instances") + assert response.status_code == 401 + + response = await api_client.post(f"/api/organizations/{fake_org_id}/redis-instances", json={"name": "test"}) + assert response.status_code == 401 \ No newline at end of file diff --git a/test/development/test_public_endpoints.py b/test/development/test_public_endpoints.py new file mode 100644 index 0000000..2f9639e --- /dev/null +++ b/test/development/test_public_endpoints.py @@ -0,0 +1,51 @@ +""" +Test public API endpoints that don't require authentication. + +This module tests: +- Health check endpoint +- Version endpoint +- Database stats endpoint +""" + +import pytest +from conftest import ApiClient + + +class TestPublicEndpoints: + """Test public API endpoints that don't require authentication.""" + + @pytest.mark.public + async def test_health_check(self, api_client: ApiClient, wait_for_server): + """Test the health check endpoint.""" + response = await api_client.get("/health") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "ok" + assert "timestamp" in data + assert data["database"] == "healthy" + + @pytest.mark.public + async def test_version(self, api_client: ApiClient, wait_for_server): + """Test the version endpoint.""" + response = await api_client.get("/version") + + assert response.status_code == 200 + data = response.json() + assert "version" in data + assert "name" in data + assert "description" in data + assert data["name"] == "redisgate" + + @pytest.mark.public + async def test_database_stats(self, api_client: ApiClient, wait_for_server): + """Test the database statistics endpoint.""" + response = await api_client.get("/stats") + + assert response.status_code == 200 + data = response.json() + assert "tables" in data + assert "timestamp" in data + assert isinstance(data["tables"], dict) + assert "users" in data["tables"] + assert "organizations" in data["tables"] \ No newline at end of file diff --git a/test/development/test_redis_endpoints.py b/test/development/test_redis_endpoints.py new file mode 100644 index 0000000..7fd424e --- /dev/null +++ b/test/development/test_redis_endpoints.py @@ -0,0 +1,276 @@ +""" +Test Redis HTTP API endpoints that require API key authentication. + +This module tests: +- Basic Redis operations (PING, GET, SET, DEL, INCR) +- Hash operations (HGET, HSET) +- List operations (LPUSH, LPOP) +- Generic command execution +- API key authentication +""" + +import pytest +from typing import Dict, Any +from conftest import ApiClient, generate_test_key, generate_test_value + + +class TestRedisHttpApi: + """Test Redis HTTP API endpoints.""" + + @pytest.mark.redis + async def test_redis_ping(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test Redis PING command.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + + response = await api_client.get( + f"/redis/{instance_id}/ping", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert response.status_code == 200 + data = response.json() + assert data["result"] == "PONG" + + @pytest.mark.redis + async def test_redis_set_get(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test Redis SET and GET commands.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + key = generate_test_key("test_set_get") + value = generate_test_value() + + # Test SET + set_response = await api_client.get( + f"/redis/{instance_id}/set/{key}/{value}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert set_response.status_code == 200 + set_data = set_response.json() + assert set_data["result"] == "OK" + + # Test GET + get_response = await api_client.get( + f"/redis/{instance_id}/get/{key}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert get_response.status_code == 200 + get_data = get_response.json() + assert get_data["result"] == value + + @pytest.mark.redis + async def test_redis_del(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test Redis DEL command.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + key = generate_test_key("test_del") + value = generate_test_value() + + # First set a value + await api_client.get( + f"/redis/{instance_id}/set/{key}/{value}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + # Then delete it + del_response = await api_client.get( + f"/redis/{instance_id}/del/{key}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert del_response.status_code == 200 + del_data = del_response.json() + assert del_data["result"] == 1 # Number of keys deleted + + # Verify it's deleted + get_response = await api_client.get( + f"/redis/{instance_id}/get/{key}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert get_response.status_code == 200 + get_data = get_response.json() + assert get_data["result"] is None + + @pytest.mark.redis + async def test_redis_incr(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test Redis INCR command.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + key = generate_test_key("test_incr") + + # Test INCR on non-existent key (should start from 0) + incr_response = await api_client.get( + f"/redis/{instance_id}/incr/{key}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert incr_response.status_code == 200 + incr_data = incr_response.json() + assert incr_data["result"] == 1 + + # Test INCR again + incr_response2 = await api_client.get( + f"/redis/{instance_id}/incr/{key}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert incr_response2.status_code == 200 + incr_data2 = incr_response2.json() + assert incr_data2["result"] == 2 + + @pytest.mark.redis + async def test_redis_hset_hget(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test Redis HSET and HGET commands.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + key = generate_test_key("test_hash") + field = "test_field" + value = generate_test_value() + + # Test HSET + hset_response = await api_client.get( + f"/redis/{instance_id}/hset/{key}/{field}/{value}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert hset_response.status_code == 200 + hset_data = hset_response.json() + assert hset_data["result"] == 1 # Number of fields added + + # Test HGET + hget_response = await api_client.get( + f"/redis/{instance_id}/hget/{key}/{field}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert hget_response.status_code == 200 + hget_data = hget_response.json() + assert hget_data["result"] == value + + @pytest.mark.redis + async def test_redis_lpush_lpop(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test Redis LPUSH and LPOP commands.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + key = generate_test_key("test_list") + value = generate_test_value() + + # Test LPUSH + lpush_response = await api_client.get( + f"/redis/{instance_id}/lpush/{key}/{value}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert lpush_response.status_code == 200 + lpush_data = lpush_response.json() + assert lpush_data["result"] == 1 # Length of list after push + + # Test LPOP + lpop_response = await api_client.get( + f"/redis/{instance_id}/lpop/{key}", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert lpop_response.status_code == 200 + lpop_data = lpop_response.json() + assert lpop_data["result"] == value + + @pytest.mark.redis + async def test_redis_generic_command(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test generic Redis command execution via POST.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + key = generate_test_key("test_generic") + value = generate_test_value() + + # Test generic SET command + set_command = ["SET", key, value] + + set_response = await api_client.post( + f"/redis/{instance_id}", + json=set_command, + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert set_response.status_code == 200 + set_data = set_response.json() + assert set_data["result"] == "OK" + + # Test generic GET command + get_command = ["GET", key] + + get_response = await api_client.post( + f"/redis/{instance_id}", + json=get_command, + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert get_response.status_code == 200 + get_data = get_response.json() + assert get_data["result"] == value + + @pytest.mark.redis + async def test_redis_api_key_query_param(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test Redis API with API key as query parameter.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + + response = await api_client.get( + f"/redis/{instance_id}/ping", + params={"_token": api_key} + ) + + assert response.status_code == 200 + data = response.json() + assert data["result"] == "PONG" + + @pytest.mark.redis + async def test_redis_unauthorized_access(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], wait_for_server): + """Test that Redis endpoints require API key authentication.""" + instance_id = test_redis_instance["id"] + + # Test without API key + response = await api_client.get(f"/redis/{instance_id}/ping") + assert response.status_code == 401 + + # Test with invalid API key + response = await api_client.get( + f"/redis/{instance_id}/ping", + headers={"Authorization": "Bearer invalid_key"} + ) + assert response.status_code == 401 + + @pytest.mark.redis + async def test_redis_nonexistent_instance(self, api_client: ApiClient, test_api_key: Dict[str, Any], wait_for_server): + """Test Redis operations with non-existent instance ID.""" + fake_instance_id = "00000000-0000-0000-0000-000000000000" + api_key = test_api_key["key"] + + response = await api_client.get( + f"/redis/{fake_instance_id}/ping", + headers={"Authorization": f"Bearer {api_key}"} + ) + + assert response.status_code == 404 + + @pytest.mark.redis + async def test_redis_debug_endpoint(self, api_client: ApiClient, test_redis_instance: Dict[str, Any], test_api_key: Dict[str, Any], wait_for_server): + """Test Redis debug endpoint.""" + instance_id = test_redis_instance["id"] + api_key = test_api_key["key"] + + response = await api_client.get( + f"/redis/{instance_id}/debug/test", + headers={"Authorization": f"Bearer {api_key}"} + ) + + # Debug endpoint should return information about the request + assert response.status_code == 200 + data = response.json() + assert "method" in data + assert "path" in data + assert "instance_id" in data + assert data["instance_id"] == instance_id \ No newline at end of file