| """ |
| Workflow API fuzzing harness for FastAPI endpoints. |
| |
| This module uses Atheris to fuzz workflow create, update, trigger, and schedule |
| endpoints to discover crashes, security vulnerabilities, and edge cases. |
| |
| Coverage: |
| - POST /api/workflows - Create workflow |
| - PUT /api/workflows/{id} - Update workflow |
| - POST /api/workflows/{id}/trigger - Trigger workflow execution |
| - POST /api/workflows/{id}/schedule - Schedule workflow |
| """ |
|
|
| import os |
| import sys |
| from typing import Dict, Any |
|
|
| import os |
| import sys |
|
|
| |
| backend_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| if backend_dir not in sys.path: |
| sys.path.insert(0, backend_dir) |
|
|
| import pytest |
| from fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
|
|
| |
| from tests.fuzzing.conftest import ATHERIS_AVAILABLE |
| from tests.e2e_ui.fixtures.database_fixtures import db_session |
| from tests.e2e_ui.fixtures.auth_fixtures import authenticated_user, test_user |
|
|
| from main_api_app import app |
| from core.database import get_db |
|
|
| |
| try: |
| import atheris |
| from atheris import fp |
| ATHERIS_AVAILABLE = True |
| except ImportError: |
| ATHERIS_AVAILABLE = False |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.fuzzing |
| @pytest.mark.slow |
| @pytest.mark.timeout(300) |
| def test_workflow_create_fuzzing(db_session: Session, authenticated_user): |
| """ |
| Fuzz workflow creation endpoint (POST /api/workflows). |
| |
| PROPERTY: Workflow create endpoint should not crash on malformed input |
| STRATEGY: Use FuzzedDataProvider to generate random workflow definitions |
| INVARIANT: Response status code always in [200, 400, 401, 422] (no 500 errors) |
| |
| RADII: 10000 iterations provides coverage of: |
| - SQL injection in workflow_name |
| - Empty/None values for required fields |
| - Malformed workflow_definition (nested, cyclical, huge) |
| - Invalid trigger configurations |
| |
| Args: |
| db_session: Database session with transaction rollback |
| authenticated_user: (user, token) tuple for JWT auth |
| """ |
| if not ATHERIS_AVAILABLE: |
| pytest.skip("Atheris not installed") |
|
|
| user, token = authenticated_user |
|
|
| |
| app.dependency_overrides[get_db] = lambda: db_session |
|
|
| |
| client = TestClient(app) |
|
|
| |
| headers = {"Authorization": f"Bearer {token}"} |
|
|
| def fuzz_one_input(data: bytes): |
| """Fuzz workflow create endpoint with random input.""" |
| try: |
| |
| fdp = fp.FuzzedDataProvider(data) |
|
|
| |
| workflow_name = fdp.ConsumeRandomLengthString(100) |
|
|
| |
| workflow_description = fdp.ConsumeRandomLengthString(1000) |
|
|
| |
| num_keys = fdp.ConsumeIntInRange(0, 20) |
| workflow_definition = {} |
| for i in range(num_keys): |
| key = fdp.ConsumeRandomLengthString(50) |
| value_type = fdp.ConsumeIntInRange(0, 3) |
| if value_type == 0: |
| value = fdp.ConsumeRandomLengthString(100) |
| elif value_type == 1: |
| value = fdp.ConsumeIntInRange(-1000000, 1000000) |
| elif value_type == 2: |
| |
| nested_keys = fdp.ConsumeIntInRange(0, 5) |
| value = {fdp.ConsumeRandomLengthString(20): fdp.ConsumeRandomLengthString(50) for _ in range(nested_keys)} |
| else: |
| value = None |
| workflow_definition[key] = value |
|
|
| |
| num_triggers = fdp.ConsumeIntInRange(0, 10) |
| triggers = [] |
| for i in range(num_triggers): |
| trigger = { |
| "type": fdp.ConsumeRandomLengthString(20), |
| "config": { |
| "cron": fdp.ConsumeRandomLengthString(50), |
| "enabled": fdp.ConsumeBool() |
| } |
| } |
| triggers.append(trigger) |
|
|
| |
| payload = { |
| "name": workflow_name if workflow_name else None, |
| "description": workflow_description if workflow_description else None, |
| "definition": workflow_definition, |
| "triggers": triggers |
| } |
|
|
| |
| response = client.post("/api/workflows", json=payload, headers=headers) |
|
|
| |
| assert response.status_code in [200, 400, 401, 422], \ |
| f"Unexpected status {response.status_code}: {response.text[:200]}" |
|
|
| except Exception as e: |
| |
| if "validation" not in str(e).lower() and "422" not in str(e): |
| raise |
|
|
| |
| atheris.Setup(sys.argv, [fuzz_one_input]) |
| atheris.Fuzz() |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.fuzzing |
| @pytest.mark.slow |
| @pytest.mark.timeout(300) |
| def test_workflow_update_fuzzing(db_session: Session, authenticated_user): |
| """ |
| Fuzz workflow update endpoint (PUT /api/workflows/{id}). |
| |
| PROPERTY: Workflow update endpoint should not crash on malformed input |
| STRATEGY: Use FuzzedDataProvider to generate random workflow IDs and update data |
| INVARIANT: Response status code always in [200, 400, 404, 422] (no 500 errors) |
| |
| RADII: 10000 iterations provides coverage of: |
| - Invalid workflow_id formats (None, empty, huge strings) |
| - Partial updates with null values |
| - Malformed workflow_definition |
| - SQL injection in update fields |
| |
| Args: |
| db_session: Database session with transaction rollback |
| authenticated_user: (user, token) tuple for JWT auth |
| """ |
| if not ATHERIS_AVAILABLE: |
| pytest.skip("Atheris not installed") |
|
|
| user, token = authenticated_user |
|
|
| app.dependency_overrides[get_db] = lambda: db_session |
| client = TestClient(app) |
| headers = {"Authorization": f"Bearer {token}"} |
|
|
| def fuzz_one_input(data: bytes): |
| """Fuzz workflow update endpoint with random input.""" |
| try: |
| fdp = fp.FuzzedDataProvider(data) |
|
|
| |
| workflow_id = fdp.ConsumeRandomLengthString(50) |
|
|
| |
| num_keys = fdp.ConsumeIntInRange(0, 10) |
| update_data = {} |
| for i in range(num_keys): |
| key = fdp.ConsumeRandomLengthString(50) |
| value_type = fdp.ConsumeIntInRange(0, 4) |
| if value_type == 0: |
| value = fdp.ConsumeRandomLengthString(1000) |
| elif value_type == 1: |
| value = None |
| elif value_type == 2: |
| value = fdp.ConsumeBool() |
| elif value_type == 3: |
| |
| nested_keys = fdp.ConsumeIntInRange(0, 5) |
| value = {fdp.ConsumeRandomLengthString(20): fdp.ConsumeRandomLengthString(100) for _ in range(nested_keys)} |
| else: |
| value = fdp.ConsumeIntInRange(-1000000, 1000000) |
| update_data[key] = value |
|
|
| |
| response = client.put(f"/api/workflows/{workflow_id}", json=update_data, headers=headers) |
|
|
| |
| assert response.status_code in [200, 400, 404, 422], \ |
| f"Unexpected status {response.status_code}: {response.text[:200]}" |
|
|
| except Exception as e: |
| if "validation" not in str(e).lower() and "422" not in str(e): |
| raise |
|
|
| atheris.Setup(sys.argv, [fuzz_one_input]) |
| atheris.Fuzz() |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.fuzzing |
| @pytest.mark.slow |
| @pytest.mark.timeout(300) |
| def test_workflow_trigger_fuzzing(db_session: Session, authenticated_user): |
| """ |
| Fuzz workflow trigger endpoint (POST /api/workflows/{id}/trigger). |
| |
| PROPERTY: Workflow trigger endpoint should not crash on malformed input |
| STRATEGY: Use FuzzedDataProvider to generate random workflow IDs and input data |
| INVARIANT: Response status code always in [200, 400, 404, 422] (no 500 errors) |
| |
| RADII: 10000 iterations provides coverage of: |
| - Invalid workflow_id formats |
| - Huge input_data payloads (DoS protection) |
| - Nested structures with cyclical references |
| - SQL injection in input fields |
| |
| Args: |
| db_session: Database session with transaction rollback |
| authenticated_user: (user, token) tuple for JWT auth |
| """ |
| if not ATHERIS_AVAILABLE: |
| pytest.skip("Atheris not installed") |
|
|
| user, token = authenticated_user |
|
|
| app.dependency_overrides[get_db] = lambda: db_session |
| client = TestClient(app) |
| headers = {"Authorization": f"Bearer {token}"} |
|
|
| def fuzz_one_input(data: bytes): |
| """Fuzz workflow trigger endpoint with random input.""" |
| try: |
| fdp = fp.FuzzedDataProvider(data) |
|
|
| |
| workflow_id = fdp.ConsumeRandomLengthString(50) |
|
|
| |
| num_keys = fdp.ConsumeIntInRange(0, 20) |
| input_data = {} |
| for i in range(num_keys): |
| key = fdp.ConsumeRandomLengthString(50) |
| value_type = fdp.ConsumeIntInRange(0, 3) |
| if value_type == 0: |
| |
| value = fdp.ConsumeRandomLengthString(10000) |
| elif value_type == 1: |
| |
| nested_keys = fdp.ConsumeIntInRange(0, 10) |
| value = {fdp.ConsumeRandomLengthString(30): fdp.ConsumeRandomLengthString(100) for _ in range(nested_keys)} |
| elif value_type == 2: |
| value = fdp.ConsumeIntInRange(-1000000, 1000000) |
| else: |
| value = None |
| input_data[key] = value |
|
|
| payload = {"input_data": input_data} |
|
|
| |
| response = client.post(f"/api/workflows/{workflow_id}/trigger", json=payload, headers=headers) |
|
|
| |
| assert response.status_code in [200, 400, 404, 422], \ |
| f"Unexpected status {response.status_code}: {response.text[:200]}" |
|
|
| except Exception as e: |
| if "validation" not in str(e).lower() and "422" not in str(e): |
| raise |
|
|
| atheris.Setup(sys.argv, [fuzz_one_input]) |
| atheris.Fuzz() |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.fuzzing |
| @pytest.mark.slow |
| @pytest.mark.timeout(300) |
| def test_workflow_schedule_fuzzing(db_session: Session, authenticated_user): |
| """ |
| Fuzz workflow schedule endpoint (POST /api/workflows/{id}/schedule). |
| |
| PROPERTY: Workflow schedule endpoint should not crash on malformed cron expressions |
| STRATEGY: Use FuzzedDataProvider to generate random schedule configurations |
| INVARIANT: Response status code always in [200, 400, 404, 422] (no 500 errors) |
| |
| RADII: 10000 iterations provides coverage of: |
| - Invalid cron expressions (syntax errors, out-of-range values) |
| - Special characters in cron fields |
| - Past dates in schedule config |
| - Huge intervals (DoS protection) |
| |
| Args: |
| db_session: Database session with transaction rollback |
| authenticated_user: (user, token) tuple for JWT auth |
| """ |
| if not ATHERIS_AVAILABLE: |
| pytest.skip("Atheris not installed") |
|
|
| user, token = authenticated_user |
|
|
| app.dependency_overrides[get_db] = lambda: db_session |
| client = TestClient(app) |
| headers = {"Authorization": f"Bearer {token}"} |
|
|
| def fuzz_one_input(data: bytes): |
| """Fuzz workflow schedule endpoint with random input.""" |
| try: |
| fdp = fp.FuzzedDataProvider(data) |
|
|
| |
| workflow_id = fdp.ConsumeRandomLengthString(50) |
|
|
| |
| schedule_type = fdp.ConsumeRandomLengthString(20) |
| schedule_config = { |
| "type": schedule_type, |
| "cron": fdp.ConsumeRandomLengthString(100), |
| "interval_seconds": fdp.ConsumeIntInRange(-1000000, 1000000), |
| "start_date": fdp.ConsumeRandomLengthString(50), |
| "enabled": fdp.ConsumeBool() |
| } |
|
|
| |
| response = client.post(f"/api/workflows/{workflow_id}/schedule", json=schedule_config, headers=headers) |
|
|
| |
| assert response.status_code in [200, 400, 404, 422], \ |
| f"Unexpected status {response.status_code}: {response.text[:200]}" |
|
|
| except Exception as e: |
| if "validation" not in str(e).lower() and "422" not in str(e): |
| raise |
|
|
| atheris.Setup(sys.argv, [fuzz_one_input]) |
| atheris.Fuzz() |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.fuzzing |
| @pytest.mark.slow |
| @pytest.mark.timeout(300) |
| def test_workflow_dag_validation_fuzz(db_session: Session, authenticated_user): |
| """ |
| Fuzz workflow DAG validation (cyclical dependencies, missing nodes). |
| |
| PROPERTY: Workflow DAG validation should not crash on malformed graphs |
| STRATEGY: Use FuzzedDataProvider to generate random node/edge configurations |
| INVARIANT: Response status code always in [200, 400, 422] (no 500 errors) |
| |
| RADII: 10000 iterations provides coverage of: |
| - Cyclical node dependencies (a -> b -> a) |
| - Missing node references (edge points to non-existent node) |
| - Empty or None node lists |
| - Self-referencing nodes |
| |
| Args: |
| db_session: Database session with transaction rollback |
| authenticated_user: (user, token) tuple for JWT auth |
| """ |
| if not ATHERIS_AVAILABLE: |
| pytest.skip("Atheris not installed") |
|
|
| user, token = authenticated_user |
|
|
| app.dependency_overrides[get_db] = lambda: db_session |
| client = TestClient(app) |
| headers = {"Authorization": f"Bearer {token}"} |
|
|
| def fuzz_one_input(data: bytes): |
| """Fuzz workflow DAG validation with random input.""" |
| try: |
| fdp = fp.FuzzedDataProvider(data) |
|
|
| |
| num_nodes = fdp.ConsumeIntInRange(0, 20) |
| nodes = [] |
| node_ids = [] |
| for i in range(num_nodes): |
| node_id = fdp.ConsumeRandomLengthString(30) |
| node_ids.append(node_id) |
| nodes.append({ |
| "id": node_id, |
| "type": fdp.ConsumeRandomLengthString(20), |
| "config": fdp.ConsumeRandomLengthString(100) |
| }) |
|
|
| |
| num_edges = fdp.ConsumeIntInRange(0, 30) |
| edges = [] |
| for i in range(num_edges): |
| |
| from_idx = fdp.ConsumeIntInRange(0, len(node_ids) + 5) |
| to_idx = fdp.ConsumeIntInRange(0, len(node_ids) + 5) |
|
|
| from_node = node_ids[from_idx] if from_idx < len(node_ids) else fdp.ConsumeRandomLengthString(30) |
| to_node = node_ids[to_idx] if to_idx < len(node_ids) else fdp.ConsumeRandomLengthString(30) |
|
|
| edges.append({ |
| "from": from_node, |
| "to": to_node, |
| "condition": fdp.ConsumeRandomLengthString(50) |
| }) |
|
|
| |
| payload = { |
| "name": fdp.ConsumeRandomLengthString(50), |
| "definition": { |
| "nodes": nodes, |
| "edges": edges |
| } |
| } |
|
|
| |
| response = client.post("/api/workflows", json=payload, headers=headers) |
|
|
| |
| assert response.status_code in [200, 400, 422], \ |
| f"Unexpected status {response.status_code}: {response.text[:200]}" |
|
|
| except Exception as e: |
| if "validation" not in str(e).lower() and "422" not in str(e): |
| raise |
|
|
| atheris.Setup(sys.argv, [fuzz_one_input]) |
| atheris.Fuzz() |
|
|