From 2ccbe97784ef2a37871c3007cf1bd3c6dd8b69bb Mon Sep 17 00:00:00 2001 From: Adam Xu Date: Tue, 17 Feb 2026 23:54:56 -0800 Subject: [PATCH 1/3] add playwright interaction tests --- README.md | 18 +++ requirements.txt | 3 + routes/auth.py | 6 +- tests/conftest.py | 11 ++ tests/test_api.py | 116 +++++++++++++++++++ tests/test_auth_routes.py | 205 ++++++++++++++++++++++++++++++++++ tests/test_data_deletion.py | 79 +++++++++++++ tests/test_oauth_routes.py | 198 ++++++++++++++++++++++++++++++++ tests/test_playwright_auth.py | 103 +++++++++++++++++ 9 files changed, 738 insertions(+), 1 deletion(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_api.py create mode 100644 tests/test_auth_routes.py create mode 100644 tests/test_data_deletion.py create mode 100644 tests/test_oauth_routes.py create mode 100644 tests/test_playwright_auth.py diff --git a/README.md b/README.md index c66fb4a..801d0c7 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,24 @@ hack-id/ └── permissions.json # API permissions ``` + +## ✅ Testing + +Run the full automated suite (unit + route + Playwright API interaction tests): + +```bash +python -m pytest -q +``` + +Run the auth/OAuth interaction coverage gate at 100%: + +```bash +coverage run --include="tests/test_auth_routes.py,tests/test_oauth_routes.py" -m pytest -q tests/test_auth_routes.py tests/test_oauth_routes.py +coverage report -m --fail-under=100 +``` + +> Note: Playwright is included for interaction tests via API request contexts (no local browser binary required for these tests). + ## 🔧 Configuration ### Environment Variables diff --git a/requirements.txt b/requirements.txt index bdb0283..df3b7a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -71,3 +71,6 @@ workos==5.32.0 wrapt==1.17.2 WTForms==3.2.1 yarl==1.20.0 +playwright==1.56.0 +pytest==8.3.5 +pytest-cov==6.0.0 diff --git a/routes/auth.py b/routes/auth.py index b2c7970..256886d 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -403,7 +403,11 @@ def oauth_token(): client_secret = request.form.get("client_secret") if DEBUG_MODE: - print(f"OAuth token request: grant_type={grant_type}, code={code[:20]}..., client_id={client_id}, redirect_uri={redirect_uri}") + code_preview = (code[:20] + "...") if code else "None" + print( + f"OAuth token request: grant_type={grant_type}, code={code_preview}, " + f"client_id={client_id}, redirect_uri={redirect_uri}" + ) # Validate grant_type if grant_type != "authorization_code": diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9a0e9a0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +import os +import sys +from pathlib import Path + +os.environ.setdefault("SECRET_KEY", "test-secret") +os.environ.setdefault("WORKOS_API_KEY", "test-workos-key") +os.environ.setdefault("WORKOS_CLIENT_ID", "test-workos-client") + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..cf7951a --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,116 @@ +import os + +os.environ.setdefault("SECRET_KEY", "test-secret") + +from flask import Flask, jsonify +import routes.api as api_routes + + +def create_test_app(): + app = Flask(__name__) + app.register_blueprint(api_routes.api_bp) + return app + + +def test_require_api_key_rejects_missing_authorization_header(): + app = Flask(__name__) + + @app.route("/protected") + @api_routes.require_api_key("users.read") + def protected(): + return jsonify({"success": True}) + + client = app.test_client() + response = client.get("/protected") + + assert response.status_code == 401 + assert response.get_json()["error"] == "Missing or invalid Authorization header" + + +def test_require_api_key_rejects_insufficient_permissions(monkeypatch): + app = Flask(__name__) + + @app.route("/protected") + @api_routes.require_api_key("users.read") + def protected(): + return jsonify({"success": True}) + + monkeypatch.setattr(api_routes, "get_key_permissions", lambda _api_key: ["events.register"]) + + client = app.test_client() + response = client.get( + "/protected", headers={"Authorization": "Bearer test-key"} + ) + + assert response.status_code == 403 + assert response.get_json()["error"] == "Insufficient permissions" + + +def test_require_api_key_allows_valid_key_and_logs_usage(monkeypatch): + app = Flask(__name__) + + @app.route("/protected") + @api_routes.require_api_key("users.read") + def protected(): + return jsonify({"success": True}) + + log_calls = [] + + monkeypatch.setattr(api_routes, "get_key_permissions", lambda _api_key: ["users.read"]) + monkeypatch.setattr( + api_routes, + "log_api_key_usage", + lambda api_key, action, metadata: log_calls.append( + {"api_key": api_key, "action": action, "metadata": metadata} + ), + ) + + client = app.test_client() + response = client.get( + "/protected", headers={"Authorization": "Bearer test-key"} + ) + + assert response.status_code == 200 + assert response.get_json()["success"] is True + assert len(log_calls) == 1 + assert log_calls[0]["action"] == "protected" + + +def test_api_current_event_returns_404_when_no_current_event(monkeypatch): + app = create_test_app() + monkeypatch.setattr(api_routes, "get_current_event", lambda: None) + + client = app.test_client() + response = client.get("/api/current-event") + + assert response.status_code == 404 + payload = response.get_json() + assert payload["success"] is False + + +def test_api_user_status_returns_data_for_valid_request(monkeypatch): + app = create_test_app() + + monkeypatch.setattr(api_routes, "get_key_permissions", lambda _api_key: ["users.read"]) + monkeypatch.setattr(api_routes, "log_api_key_usage", lambda *_args, **_kwargs: None) + monkeypatch.setattr( + api_routes, + "get_user_event_status", + lambda user_email, event_id: { + "success": True, + "user_email": user_email, + "event_id": event_id, + "registered": True, + }, + ) + + client = app.test_client() + response = client.get( + "/api/user-status?user_email=test@example.com&event_id=hackathon-1", + headers={"Authorization": "Bearer valid-key"}, + ) + + assert response.status_code == 200 + payload = response.get_json() + assert payload["success"] is True + assert payload["registered"] is True diff --git a/tests/test_auth_routes.py b/tests/test_auth_routes.py new file mode 100644 index 0000000..50731dd --- /dev/null +++ b/tests/test_auth_routes.py @@ -0,0 +1,205 @@ +import os +from pathlib import Path + +os.environ.setdefault("SECRET_KEY", "test-secret") + +from flask import Flask +import routes.auth as auth_routes + + +def create_auth_app(): + template_dir = Path(__file__).resolve().parents[1] / "templates" + app = Flask(__name__, template_folder=str(template_dir)) + app.secret_key = "test-secret" + app.jinja_env.globals["csrf_token"] = lambda: "test-csrf" + app.register_blueprint(auth_routes.auth_bp) + return app + + +def test_index_unauthenticated_renders_login_page(): + app = create_auth_app() + client = app.test_client() + + response = client.get("/") + + assert response.status_code == 200 + assert b"Sign in" in response.data + + +def test_auth_google_redirects_to_google_provider(monkeypatch): + app = create_auth_app() + client = app.test_client() + monkeypatch.setattr( + auth_routes, + "get_google_auth_url", + lambda: "https://accounts.google.com/o/oauth2/auth?state=test", + ) + + response = client.get("/auth/google") + + assert response.status_code == 302 + assert response.location.startswith("https://accounts.google.com/o/oauth2/auth") + + +def test_auth_google_callback_rejects_invalid_state(): + app = create_auth_app() + client = app.test_client() + + with client.session_transaction() as sess: + sess["oauth_state"] = "expected-state" + + response = client.get("/auth/google/callback?state=wrong-state&code=abc") + + assert response.status_code == 200 + assert b"Invalid OAuth state" in response.data + + +def test_send_code_json_requires_email(): + app = create_auth_app() + client = app.test_client() + + response = client.post("/send-code", json={}) + + assert response.status_code == 200 + assert response.get_json()["success"] is False + assert response.get_json()["error"] == "Email is required" + + +def test_send_code_json_success(monkeypatch): + app = create_auth_app() + client = app.test_client() + + monkeypatch.setattr(auth_routes, "send_email_verification", lambda _email: True) + + response = client.post("/send-code", json={"email": "test@example.com"}) + + assert response.status_code == 200 + payload = response.get_json() + assert payload["success"] is True + assert "Magic link sent" in payload["message"] + + +def test_email_callback_without_code_returns_error(): + app = create_auth_app() + client = app.test_client() + + response = client.get("/auth/email/callback") + + assert response.status_code == 200 + assert b"No code provided" in response.data + + +def test_email_callback_logs_in_existing_user(monkeypatch): + app = create_auth_app() + client = app.test_client() + + monkeypatch.setattr( + auth_routes, + "verify_email_code", + lambda _code: {"success": True, "email": "test@example.com", "name": "Test User"}, + ) + monkeypatch.setattr( + auth_routes, + "get_user_by_email", + lambda _email: {"email": "test@example.com", "legal_name": "Test User", "preferred_name": ""}, + ) + + response = client.get("/auth/email/callback?code=valid-code") + + assert response.status_code == 302 + assert response.location.endswith("/") + with client.session_transaction() as sess: + assert sess["user_email"] == "test@example.com" + + +def test_oauth_authorize_missing_required_params_shows_error(): + app = create_auth_app() + client = app.test_client() + + response = client.get("/oauth/authorize") + + assert response.status_code == 200 + assert b"Missing required OAuth parameters" in response.data + + +def test_oauth_authorize_unauthenticated_user_sees_login(monkeypatch): + app = create_auth_app() + client = app.test_client() + + monkeypatch.setattr( + auth_routes, + "get_app_by_client_id", + lambda _client_id: { + "id": "app_1", + "is_active": True, + "redirect_uris": '["https://example.com/callback"]', + "allowed_scopes": '["profile", "email"]', + "allow_anyone": True, + }, + ) + monkeypatch.setattr(auth_routes, "validate_redirect_uri", lambda _uri, _allowed: True) + + response = client.get( + "/oauth/authorize?client_id=client_1&redirect_uri=https://example.com/callback&scope=profile%20email&response_type=code" + ) + + assert response.status_code == 200 + assert b"Sign in" in response.data + + +def test_oauth_authorize_logged_in_with_skip_consent_redirects_with_code(monkeypatch): + app = create_auth_app() + client = app.test_client() + + monkeypatch.setattr( + auth_routes, + "get_app_by_client_id", + lambda _client_id: { + "id": "app_1", + "is_active": True, + "redirect_uris": '["https://example.com/callback"]', + "allowed_scopes": '["profile", "email"]', + "allow_anyone": True, + "skip_consent_screen": True, + }, + ) + monkeypatch.setattr(auth_routes, "validate_redirect_uri", lambda _uri, _allowed: True) + monkeypatch.setattr( + auth_routes, + "get_user_by_email", + lambda _email: {"email": "test@example.com", "legal_name": "Test User"}, + ) + monkeypatch.setattr(auth_routes, "create_authorization_code", lambda **_kwargs: "auth-code-123") + + with client.session_transaction() as sess: + sess["user_email"] = "test@example.com" + + response = client.get( + "/oauth/authorize?client_id=client_1&redirect_uri=https://example.com/callback&scope=profile%20email&state=abc&response_type=code" + ) + + assert response.status_code == 302 + assert response.location == "https://example.com/callback?code=auth-code-123&state=abc" + + +def test_oauth_authorize_consent_denied_redirects_with_access_denied(monkeypatch): + app = create_auth_app() + client = app.test_client() + + monkeypatch.setattr( + auth_routes, + "get_app_by_client_id", + lambda _client_id: {"id": "app_1", "is_active": True, "allow_anyone": True}, + ) + + with client.session_transaction() as sess: + sess["user_email"] = "test@example.com" + sess["oauth2_client_id"] = "client_1" + sess["oauth2_redirect_uri"] = "https://example.com/callback" + sess["oauth2_scope"] = "profile email" + sess["oauth2_state"] = "xyz" + + response = client.post("/oauth/authorize", data={"action": "deny"}) + + assert response.status_code == 302 + assert response.location == "https://example.com/callback?error=access_denied&state=xyz" diff --git a/tests/test_data_deletion.py b/tests/test_data_deletion.py new file mode 100644 index 0000000..5d030ff --- /dev/null +++ b/tests/test_data_deletion.py @@ -0,0 +1,79 @@ +import os + +os.environ.setdefault("SECRET_KEY", "test-secret") + +import services.data_deletion as data_deletion + + +class FakeCursor: + def __init__(self, rowcount): + self.rowcount = rowcount + + +class FakeConnection: + def __init__(self, rowcount=0): + self.rowcount = rowcount + self.committed = False + self.closed = False + + def execute(self, _query, _params): + return FakeCursor(self.rowcount) + + def commit(self): + self.committed = True + + def close(self): + self.closed = True + + +def test_delete_user_data_returns_error_when_user_not_found(monkeypatch): + monkeypatch.setattr( + data_deletion, + "get_user_data_summary", + lambda _email: {"user_found": False, "discord_linked": False}, + ) + + result = data_deletion.delete_user_data("missing@example.com") + + assert result["success"] is False + assert "User not found" in result["errors"] + + +def test_delete_user_data_deletes_opt_out_and_user_record(monkeypatch): + fake_conn = FakeConnection(rowcount=2) + deleted_user_ids = [] + + monkeypatch.setattr( + data_deletion, + "get_user_data_summary", + lambda _email: {"user_found": True, "discord_linked": False}, + ) + monkeypatch.setattr(data_deletion, "get_db_connection", lambda: fake_conn) + monkeypatch.setattr( + data_deletion, + "delete_subscriber_by_email", + lambda _email: {"success": True}, + ) + monkeypatch.setattr( + data_deletion, + "get_user_by_email", + lambda _email: {"id": "user_123", "email": "test@example.com"}, + ) + + import models.user as user_model + + monkeypatch.setattr( + user_model, "delete_user", lambda user_id: deleted_user_ids.append(user_id) + ) + + result = data_deletion.delete_user_data( + "test@example.com", include_discord=False, include_listmonk=True + ) + + assert result["success"] is True + assert result["deletion_counts"]["opt_out_tokens"] == 2 + assert result["deletion_counts"]["users"] == 1 + assert result["total_records_deleted"] == 3 + assert deleted_user_ids == ["user_123"] + assert fake_conn.committed is True + assert fake_conn.closed is True diff --git a/tests/test_oauth_routes.py b/tests/test_oauth_routes.py new file mode 100644 index 0000000..28ffb9e --- /dev/null +++ b/tests/test_oauth_routes.py @@ -0,0 +1,198 @@ +import os +from pathlib import Path + +os.environ.setdefault("SECRET_KEY", "test-secret") + +from flask import Flask +import routes.auth as auth_routes + + +def create_oauth_app(): + template_dir = Path(__file__).resolve().parents[1] / "templates" + app = Flask(__name__, template_folder=str(template_dir)) + app.secret_key = "test-secret" + app.jinja_env.globals["csrf_token"] = lambda: "test-csrf" + app.register_blueprint(auth_routes.auth_bp) + app.register_blueprint(auth_routes.oauth_bp) + return app + + +def test_oauth_token_rejects_unsupported_grant_type(): + app = create_oauth_app() + client = app.test_client() + + response = client.post( + "/oauth/token", + data={ + "grant_type": "client_credentials", + "code": "abc", + "redirect_uri": "https://example.com/callback", + "client_id": "client_1", + "client_secret": "secret_1", + }, + ) + + assert response.status_code == 400 + payload = response.get_json() + assert payload["error"] == "unsupported_grant_type" + + +def test_oauth_token_requires_all_parameters(): + app = create_oauth_app() + client = app.test_client() + + response = client.post("/oauth/token", data={"grant_type": "authorization_code"}) + + assert response.status_code == 400 + payload = response.get_json() + assert payload["error"] == "invalid_request" + + +def test_oauth_token_returns_access_token_payload(monkeypatch): + app = create_oauth_app() + client = app.test_client() + + monkeypatch.setattr( + auth_routes, + "exchange_code_for_token", + lambda code, client_id, client_secret, redirect_uri: { + "success": True, + "access_token": "access_123", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "profile email", + }, + ) + + response = client.post( + "/oauth/token", + data={ + "grant_type": "authorization_code", + "code": "code_123", + "redirect_uri": "https://example.com/callback", + "client_id": "client_1", + "client_secret": "secret_1", + }, + ) + + assert response.status_code == 200 + payload = response.get_json() + assert payload["access_token"] == "access_123" + assert payload["token_type"] == "Bearer" + + +def test_oauth_token_maps_invalid_grant_error(monkeypatch): + app = create_oauth_app() + client = app.test_client() + + monkeypatch.setattr( + auth_routes, + "exchange_code_for_token", + lambda code, client_id, client_secret, redirect_uri: { + "success": False, + "error": "invalid_grant", + }, + ) + + response = client.post( + "/oauth/token", + data={ + "grant_type": "authorization_code", + "code": "expired_code", + "redirect_uri": "https://example.com/callback", + "client_id": "client_1", + "client_secret": "secret_1", + }, + ) + + assert response.status_code == 400 + payload = response.get_json() + assert payload["error"] == "invalid_grant" + assert "expired" in payload["error_description"] + + +def test_oauth_revoke_requires_token(): + app = create_oauth_app() + client = app.test_client() + + response = client.post("/oauth/revoke", data={}) + + assert response.status_code == 400 + assert response.get_json()["error"] == "invalid_request" + + +def test_oauth_revoke_returns_success_even_for_invalid_tokens(monkeypatch): + app = create_oauth_app() + client = app.test_client() + + monkeypatch.setattr(auth_routes, "revoke_access_token", lambda _token: False) + + response = client.post("/oauth/revoke", data={"token": "already-invalid"}) + + assert response.status_code == 200 + assert response.get_json()["success"] is True + + +def test_oauth_legacy_requires_redirect_parameter(): + app = create_oauth_app() + client = app.test_client() + + response = client.get("/oauth") + + assert response.status_code == 200 + assert b"Missing redirect parameter" in response.data + + +def test_oauth_legacy_rejects_unregistered_redirect(monkeypatch): + app = create_oauth_app() + client = app.test_client() + + monkeypatch.setattr(auth_routes, "validate_app_redirect", lambda _redirect: None) + + response = client.get("/oauth?redirect=https://example.com/callback") + + assert response.status_code == 200 + assert b"Invalid redirect URL" in response.data + + +def test_oauth_legacy_logged_in_user_redirects_with_token(monkeypatch): + app = create_oauth_app() + client = app.test_client() + + monkeypatch.setattr( + auth_routes, + "validate_app_redirect", + lambda _redirect: {"id": "app_1", "is_active": True, "allow_anyone": True}, + ) + monkeypatch.setattr( + auth_routes, + "get_user_by_email", + lambda _email: {"email": "test@example.com", "legal_name": "Test User"}, + ) + monkeypatch.setattr(auth_routes, "create_oauth_token", lambda _email, expires_in_seconds: "oauth_token_123") + + with client.session_transaction() as sess: + sess["user_email"] = "test@example.com" + + response = client.get("/oauth?redirect=https://example.com/callback") + + assert response.status_code == 302 + assert response.location == "https://example.com/callback?token=oauth_token_123" + + +def test_logout_clears_session_and_redirects_home(): + app = create_oauth_app() + client = app.test_client() + + with client.session_transaction() as sess: + sess["user_email"] = "test@example.com" + sess["user_name"] = "Tester" + + response = client.get("/logout") + + assert response.status_code == 302 + assert response.location.endswith("/") + + with client.session_transaction() as sess: + assert "user_email" not in sess + assert "user_name" not in sess diff --git a/tests/test_playwright_auth.py b/tests/test_playwright_auth.py new file mode 100644 index 0000000..73f30db --- /dev/null +++ b/tests/test_playwright_auth.py @@ -0,0 +1,103 @@ +import os +from pathlib import Path +from threading import Thread + +os.environ.setdefault("SECRET_KEY", "test-secret") + +import pytest +from flask import Flask, session +from playwright.sync_api import sync_playwright +from werkzeug.serving import make_server + +import routes.auth as auth_routes + + +@pytest.fixture +def auth_server(monkeypatch): + """Run a minimal auth app with deterministic mocked integrations.""" + template_dir = Path(__file__).resolve().parents[1] / "templates" + app = Flask(__name__, template_folder=str(template_dir)) + app.secret_key = "test-secret" + app.jinja_env.globals["csrf_token"] = lambda: "test-csrf" + + monkeypatch.setattr( + auth_routes, + "validate_app_redirect", + lambda _redirect: {"id": "app_1", "is_active": True, "allow_anyone": True}, + ) + monkeypatch.setattr( + auth_routes, + "get_user_by_email", + lambda _email: {"email": "playwright@example.com", "legal_name": "Playwright User"}, + ) + monkeypatch.setattr( + auth_routes, + "create_oauth_token", + lambda _email, expires_in_seconds: "pw_oauth_token", + ) + monkeypatch.setattr(auth_routes, "send_email_verification", lambda _email: True) + + app.register_blueprint(auth_routes.auth_bp) + + @app.get("/_test/login") + def _test_login(): + session["user_email"] = "playwright@example.com" + return "ok", 200 + + server = make_server("127.0.0.1", 0, app) + port = server.server_port + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + + try: + yield f"http://127.0.0.1:{port}" + finally: + server.shutdown() + thread.join(timeout=2) + + +@pytest.fixture +def pw_request(): + """Playwright API request context (no browser binary required).""" + with sync_playwright() as playwright: + request_context = playwright.request.new_context(ignore_https_errors=True) + try: + yield request_context + finally: + request_context.dispose() + + +def test_playwright_login_page_renders(auth_server, pw_request): + response = pw_request.get(f"{auth_server}/") + + assert response.status == 200 + assert "Sign in" in response.text() + + +def test_playwright_send_code_json_flow(auth_server, pw_request): + response = pw_request.post( + f"{auth_server}/send-code", + data={"email": "playwright@example.com"}, + ) + + assert response.status == 200 + payload = response.json() + assert payload["success"] is True + assert "Magic link sent" in payload["message"] + + +def test_playwright_oauth_login_and_redirect_flow(auth_server, pw_request): + oauth_url = ( + f"{auth_server}/oauth?redirect=https://example.com/callback" + ) + + unauthenticated = pw_request.get(oauth_url) + assert unauthenticated.status == 200 + assert "Sign in" in unauthenticated.text() + + login_response = pw_request.get(f"{auth_server}/_test/login") + assert login_response.status == 200 + + authenticated = pw_request.get(oauth_url, max_redirects=0) + assert authenticated.status == 302 + assert authenticated.headers["location"] == "https://example.com/callback?token=pw_oauth_token" From 54f1e93844666258980c5f92449f7914583753d4 Mon Sep 17 00:00:00 2001 From: Adam Xu Date: Wed, 18 Feb 2026 00:06:09 -0800 Subject: [PATCH 2/3] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 801d0c7..6b6029c 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ python -m pytest -q Run the auth/OAuth interaction coverage gate at 100%: ```bash -coverage run --include="tests/test_auth_routes.py,tests/test_oauth_routes.py" -m pytest -q tests/test_auth_routes.py tests/test_oauth_routes.py +coverage run --source="routes,models" -m pytest -q tests/test_auth_routes.py tests/test_oauth_routes.py coverage report -m --fail-under=100 ``` From f7e1036f3c21f58159bc1a6872660903c0eef2bd Mon Sep 17 00:00:00 2001 From: Adam Xu Date: Wed, 18 Feb 2026 15:51:16 -0800 Subject: [PATCH 3/3] add tests --- .coverage | Bin 0 -> 106496 bytes README.md | 2 +- tests/test_api_routes_comprehensive.py | 1088 +++++++++++++ tests/test_auth_routes_comprehensive.py | 1723 +++++++++++++++++++++ tests/test_data_deletion_comprehensive.py | 431 ++++++ 5 files changed, 3243 insertions(+), 1 deletion(-) create mode 100644 .coverage create mode 100644 tests/test_api_routes_comprehensive.py create mode 100644 tests/test_auth_routes_comprehensive.py create mode 100644 tests/test_data_deletion_comprehensive.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..45e1207d580e425f5c8f9d49d54abcb89eb4f886 GIT binary patch literal 106496 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