Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from fastapi import FastAPI | |
| from fastapi.testclient import TestClient | |
| import pytest | |
| from src.api.oauth_routes import build_oauth_router | |
| from src.auth.jwt import decode_and_verify_jwt | |
| from src.auth.rate_limit import oauth_token_rate_limiter | |
| from src.config import settings | |
| from src.core.exceptions import register_exception_handlers | |
| def _oauth_settings(monkeypatch: pytest.MonkeyPatch): | |
| monkeypatch.setattr(settings, "oauth_enabled", True) | |
| monkeypatch.setattr(settings, "oauth_client_id", "my-client") | |
| monkeypatch.setattr(settings, "oauth_client_secret", "my-secret") | |
| monkeypatch.setattr( | |
| settings, | |
| "oauth_allowed_scopes", | |
| ("material:write", "lkpd:write", "lkpd:read"), | |
| ) | |
| monkeypatch.setattr( | |
| settings, | |
| "oauth_default_scopes", | |
| ("material:write", "lkpd:write", "lkpd:read"), | |
| ) | |
| monkeypatch.setattr(settings, "oauth_token_ttl_seconds", 300) | |
| monkeypatch.setattr(settings, "jwt_secret", "x" * 32) | |
| monkeypatch.setattr(settings, "jwt_issuer", "my-backend") | |
| monkeypatch.setattr(settings, "jwt_audience", "rtm-class-ai") | |
| monkeypatch.setattr(settings, "jwt_clock_skew_seconds", 0) | |
| oauth_token_rate_limiter.reset() | |
| yield | |
| oauth_token_rate_limiter.reset() | |
| def _build_client() -> TestClient: | |
| app = FastAPI() | |
| register_exception_handlers(app) | |
| app.include_router(build_oauth_router()) | |
| return TestClient(app) | |
| def test_oauth_token_success_default_scope() -> None: | |
| client = _build_client() | |
| response = client.post( | |
| "/api/oauth/token", | |
| data={ | |
| "grant_type": "client_credentials", | |
| "client_id": "my-client", | |
| "client_secret": "my-secret", | |
| }, | |
| ) | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert body["success"] is True | |
| assert body["message"] == "Access token issued." | |
| assert body["data"]["token_type"] == "Bearer" | |
| assert body["data"]["expires_in"] == 300 | |
| assert body["data"]["scope"] == "material:write lkpd:write lkpd:read" | |
| payload = decode_and_verify_jwt(body["data"]["access_token"]) | |
| assert payload["sub"] == "client:my-client" | |
| assert payload["scope"] == "material:write lkpd:write lkpd:read" | |
| assert isinstance(payload.get("jti"), str) and payload["jti"] | |
| def test_oauth_token_invalid_client() -> None: | |
| client = _build_client() | |
| response = client.post( | |
| "/api/oauth/token", | |
| data={ | |
| "grant_type": "client_credentials", | |
| "client_id": "my-client", | |
| "client_secret": "wrong-secret", | |
| }, | |
| ) | |
| assert response.status_code == 400 | |
| body = response.json() | |
| assert body["success"] is False | |
| assert body["error"]["code"] == "invalid_client" | |
| assert body["error"]["details"]["error"] == "invalid_client" | |
| def test_oauth_token_rejects_scope_outside_allowlist() -> None: | |
| client = _build_client() | |
| response = client.post( | |
| "/api/oauth/token", | |
| data={ | |
| "grant_type": "client_credentials", | |
| "client_id": "my-client", | |
| "client_secret": "my-secret", | |
| "scope": "material:write admin:all", | |
| }, | |
| ) | |
| assert response.status_code == 400 | |
| body = response.json() | |
| assert body["success"] is False | |
| assert body["error"]["code"] == "invalid_request" | |
| def test_oauth_token_compat_alias_path() -> None: | |
| client = _build_client() | |
| response = client.post( | |
| "/api/oauth/token", | |
| data={ | |
| "grant_type": "client_credentials", | |
| "client_id": "my-client", | |
| "client_secret": "my-secret", | |
| }, | |
| ) | |
| assert response.status_code == 200 | |
| body = response.json() | |
| assert body["success"] is True | |
| assert body["data"]["token_type"] == "Bearer" | |