-
Notifications
You must be signed in to change notification settings - Fork 0
Add Playwright auth interaction tests with 100% auth-suite coverage gate #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 --source="routes,models" -m pytest -q tests/test_auth_routes.py tests/test_oauth_routes.py | ||
| coverage report -m --fail-under=100 | ||
| ``` | ||
|
Comment on lines
+169
to
+172
|
||
|
|
||
| > Note: Playwright is included for interaction tests via API request contexts (no local browser binary required for these tests). | ||
|
|
||
| ## 🔧 Configuration | ||
|
|
||
| ### Environment Variables | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README gate command currently runs
coverage run --source="routes,models" ... tests/test_auth_routes.py tests/test_oauth_routes.pyand then enforces--fail-under=100, which makes the threshold apply to all measuredroutes/modelsfiles, not just the auth/OAuth suite. In practice this can fail even when the targeted auth tests are complete (for example, untouched modules likeroutes/api.pyor other model files drag total coverage below 100%), so the documented “100% auth/OAuth gate” is not reproducible as written.Useful? React with 👍 / 👎.