mirror of
https://github.com/langgenius/dify.git
synced 2026-07-28 23:59:34 +08:00
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Byron.wang <byron@dify.ai>
189 lines
7.4 KiB
Python
189 lines
7.4 KiB
Python
"""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
|