"""Unit tests for OAuthServerService with SQLite-backed database access.""" from __future__ import annotations import uuid from collections.abc import Iterator from typing import cast from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest from flask import Flask from sqlalchemy import Engine from sqlalchemy.orm import Session from werkzeug.exceptions import BadRequest from models.engine import db from models.model import OAuthProviderApp from services.oauth_server import ( OAUTH_ACCESS_TOKEN_EXPIRES_IN, OAUTH_ACCESS_TOKEN_REDIS_KEY, OAUTH_AUTHORIZATION_CODE_REDIS_KEY, OAUTH_REFRESH_TOKEN_EXPIRES_IN, OAUTH_REFRESH_TOKEN_REDIS_KEY, OAuthGrantType, OAuthServerService, ) @pytest.fixture def oauth_db() -> Iterator[Session]: """Provide the production database extension with an isolated SQLite provider table.""" app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" db.init_app(app) with app.app_context(): OAuthProviderApp.__table__.create(db.engine) with Session(db.engine, expire_on_commit=False) as session: yield session class TestOAuthServerServiceGetProviderApp: """Verify provider lookup against a real SQLAlchemy database.""" def test_get_oauth_provider_app_returns_app_when_exists(self, oauth_db: Session) -> None: client_id = f"client-{uuid4()}" app = OAuthProviderApp( app_icon="icon.png", client_id=client_id, client_secret=str(uuid4()), app_label={"en-US": "Test OAuth App"}, redirect_uris=["https://example.com/callback"], scope="read", ) oauth_db.add(app) oauth_db.commit() result = OAuthServerService.get_oauth_provider_app(client_id) assert result is not None assert result.client_id == client_id assert result.id == app.id def test_get_oauth_provider_app_returns_none_when_not_exists(self, oauth_db: Session) -> None: result = OAuthServerService.get_oauth_provider_app(f"nonexistent-{uuid4()}") assert result is None class TestOAuthServerServiceTokenOperations: """Verify Redis-backed token signing and validation branches.""" @pytest.fixture def mock_redis(self): with patch("services.oauth_server.redis_client") as mock: yield mock def test_sign_authorization_code_stores_and_returns_code(self, mock_redis) -> None: deterministic_uuid = uuid.UUID("00000000-0000-0000-0000-000000000111") with patch("services.oauth_server.uuid.uuid4", return_value=deterministic_uuid): code = OAuthServerService.sign_oauth_authorization_code("client-1", "user-1") assert code == str(deterministic_uuid) mock_redis.set.assert_called_once_with( OAUTH_AUTHORIZATION_CODE_REDIS_KEY.format(client_id="client-1", code=code), "user-1", ex=600, ) def test_sign_access_token_raises_bad_request_for_invalid_code(self, mock_redis) -> None: mock_redis.get.return_value = None with pytest.raises(BadRequest, match="invalid code"): OAuthServerService.sign_oauth_access_token( grant_type=OAuthGrantType.AUTHORIZATION_CODE, code="bad-code", client_id="client-1", ) def test_sign_access_token_issues_tokens_for_valid_code(self, mock_redis) -> None: token_uuids = [ uuid.UUID("00000000-0000-0000-0000-000000000201"), uuid.UUID("00000000-0000-0000-0000-000000000202"), ] with patch("services.oauth_server.uuid.uuid4", side_effect=token_uuids): mock_redis.get.return_value = b"user-1" access_token, refresh_token = OAuthServerService.sign_oauth_access_token( grant_type=OAuthGrantType.AUTHORIZATION_CODE, code="code-1", client_id="client-1", ) assert access_token == str(token_uuids[0]) assert refresh_token == str(token_uuids[1]) code_key = OAUTH_AUTHORIZATION_CODE_REDIS_KEY.format(client_id="client-1", code="code-1") mock_redis.delete.assert_called_once_with(code_key) mock_redis.set.assert_any_call( OAUTH_ACCESS_TOKEN_REDIS_KEY.format(client_id="client-1", token=access_token), b"user-1", ex=OAUTH_ACCESS_TOKEN_EXPIRES_IN, ) mock_redis.set.assert_any_call( OAUTH_REFRESH_TOKEN_REDIS_KEY.format(client_id="client-1", token=refresh_token), b"user-1", ex=OAUTH_REFRESH_TOKEN_EXPIRES_IN, ) def test_sign_access_token_raises_bad_request_for_invalid_refresh_token(self, mock_redis) -> None: mock_redis.get.return_value = None with pytest.raises(BadRequest, match="invalid refresh token"): OAuthServerService.sign_oauth_access_token( grant_type=OAuthGrantType.REFRESH_TOKEN, refresh_token="stale-token", client_id="client-1", ) def test_sign_access_token_issues_new_token_for_valid_refresh(self, mock_redis) -> None: deterministic_uuid = uuid.UUID("00000000-0000-0000-0000-000000000301") with patch("services.oauth_server.uuid.uuid4", return_value=deterministic_uuid): mock_redis.get.return_value = b"user-1" access_token, returned_refresh = OAuthServerService.sign_oauth_access_token( grant_type=OAuthGrantType.REFRESH_TOKEN, refresh_token="refresh-1", client_id="client-1", ) assert access_token == str(deterministic_uuid) assert returned_refresh == "refresh-1" def test_sign_access_token_returns_none_for_unknown_grant_type(self, mock_redis) -> None: grant_type = cast(OAuthGrantType, "invalid-grant-type") result = OAuthServerService.sign_oauth_access_token(grant_type=grant_type, client_id="client-1") assert result is None def test_sign_refresh_token_stores_with_expected_expiry(self, mock_redis) -> None: deterministic_uuid = uuid.UUID("00000000-0000-0000-0000-000000000401") with patch("services.oauth_server.uuid.uuid4", return_value=deterministic_uuid): refresh_token = OAuthServerService._sign_oauth_refresh_token("client-2", "user-2") assert refresh_token == str(deterministic_uuid) mock_redis.set.assert_called_once_with( OAUTH_REFRESH_TOKEN_REDIS_KEY.format(client_id="client-2", token=refresh_token), "user-2", ex=OAUTH_REFRESH_TOKEN_EXPIRES_IN, ) def test_validate_access_token_returns_none_when_not_found(self, mock_redis, sqlite_engine: Engine) -> None: mock_redis.get.return_value = None with Session(sqlite_engine) as session: result = OAuthServerService.validate_oauth_access_token("client-1", "missing-token", session) assert result is None def test_validate_access_token_loads_user_when_exists(self, mock_redis, sqlite_engine: Engine) -> None: mock_redis.get.return_value = b"user-88" expected_user = MagicMock() with Session(sqlite_engine) as session: with patch("services.oauth_server.AccountService.load_user", return_value=expected_user) as mock_load: result = OAuthServerService.validate_oauth_access_token("client-1", "access-token", session) mock_load.assert_called_once_with("user-88", session) assert result is expected_user