Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 +170 to +171

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow coverage command to auth modules

The README gate command currently runs coverage run --source="routes,models" ... tests/test_auth_routes.py tests/test_oauth_routes.py and then enforces --fail-under=100, which makes the threshold apply to all measured routes/models files, not just the auth/OAuth suite. In practice this can fail even when the targeted auth tests are complete (for example, untouched modules like routes/api.py or other model files drag total coverage below 100%), so the documented “100% auth/OAuth gate” is not reproducible as written.

Useful? React with 👍 / 👎.

```
Comment on lines +169 to +172
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README’s “100% coverage gate” command uses coverage run --source="routes,models" with --fail-under=100, which measures coverage of all imported code under routes/ and models/ (not just the auth/OAuth routes). This is inconsistent with the PR description’s scoped gate and is likely to be flaky/impossible to sustain. Consider scoping coverage to the intended files (e.g., --include for routes/auth.py / specific modules, or update the docs to match the actual gate you want enforced).

Copilot uses AI. Check for mistakes.

> Note: Playwright is included for interaction tests via API request contexts (no local browser binary required for these tests).

## 🔧 Configuration

### Environment Variables
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
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))
116 changes: 116 additions & 0 deletions tests/test_api.py
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
Loading