diff --git a/backend/load_tests/__init__.py b/backend/load_tests/__init__.py new file mode 100644 index 0000000..30356fd --- /dev/null +++ b/backend/load_tests/__init__.py @@ -0,0 +1 @@ +# Locust load-test package — not a pytest test target diff --git a/backend/load_tests/locustfile.py b/backend/load_tests/locustfile.py new file mode 100644 index 0000000..fbf44b6 --- /dev/null +++ b/backend/load_tests/locustfile.py @@ -0,0 +1,72 @@ +"""Locust load test for DocuVault — D-04, D-05, D-06. + +Run: + locust --headless --users 50 --spawn-rate 10 --run-time 5m \\ + --host http://localhost:8000 \\ + --csv backend/load_tests/results \\ + -f backend/load_tests/locustfile.py + +Prerequisites: a user with LOAD_TEST_EMAIL/LOAD_TEST_PASSWORD must exist in the +DB, or on_start will register one automatically (idempotent — 409 is ignored). + +Full implementation is in plan 06-03. This skeleton defines the class shape and +SLA listener so downstream CI tooling can import the file without errors. +""" +import os + +from locust import HttpUser, between, events, task + +TEST_EMAIL = os.environ.get("LOAD_TEST_EMAIL", "loadtest@example.com") +TEST_PASSWORD = os.environ.get("LOAD_TEST_PASSWORD", "Loadtest123!@#") + + +class DocuVaultUser(HttpUser): + """Simulated DocuVault end-user. + + Scenarios (weighted tasks): + - list_documents (weight 5) — GET /api/documents/ + - upload_document (weight 2) — POST /api/documents/upload-url flow + - refresh_token (weight 1) — POST /api/auth/refresh + """ + + wait_time = between(0.5, 2.0) + access_token: str = "" + + def on_start(self) -> None: + """Authenticate before running tasks. + + Registers the load-test user if it does not exist (409 is silently + ignored), then performs a login to obtain an access token. + """ + raise NotImplementedError("implementation in 06-03") + + def _auth_headers(self) -> dict: + """Return Authorization header dict for authenticated requests.""" + raise NotImplementedError("implementation in 06-03") + + @task(5) + def list_documents(self) -> None: + """GET /api/documents/ — highest-frequency task.""" + raise NotImplementedError("implementation in 06-03") + + @task(2) + def upload_document(self) -> None: + """Two-step presigned upload: POST /upload-url → PUT MinIO → POST /{id}/confirm.""" + raise NotImplementedError("implementation in 06-03") + + @task(1) + def refresh_token(self) -> None: + """POST /api/auth/refresh — rotates the httpOnly refresh cookie.""" + raise NotImplementedError("implementation in 06-03") + + +@events.quitting.add_listener +def check_sla(environment, **kwargs) -> None: + """Fail the Locust run if SLA thresholds are breached. + + Thresholds (from D-06): + - p95 response time > 200 ms → exit code 1 + - p99 response time > 500 ms → exit code 1 + - error ratio > 1% → exit code 1 + """ + raise NotImplementedError("implementation in 06-03")