mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
- 新增 aether-billing、aether-crypto、aether-wallet 独立 crate - aether-data 扩展 repository 层:announcements、auth_modules、billing、 candidate_selection、gemini_file_mappings、global_models、management_tokens、 oauth_providers、proxy_nodes、quota、users、wallet 等模块 - aether-gateway 新增 api/auth/billing/control/middleware/scheduler/usage/ video_tasks/hooks/maintenance/model_fetch/provider_transport 等功能模块 - 重构 executor decision 和 gateway state 为模块目录结构 - 新增 gateway router、frontdoor 路由层及对应测试 - Python 侧 API 路由重构,新增 compat/support 模块 - 前端 Logo 组件更新及 Provider 管理页面调整
332 lines
10 KiB
Python
332 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from src.core.enums import AuthSource, UserRole
|
|
from src.models.database import Base, OAuthProvider, User, UserOAuthLink
|
|
from src.services.auth.oauth.models import OAuthFlowError
|
|
from src.services.auth.oauth.service import OAuthService
|
|
from src.services.auth.oauth.state import OAuthStateData
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_build_bind_authorize_url_includes_client_device_id(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
db = MagicMock()
|
|
user = SimpleNamespace(id="user-1", auth_source=AuthSource.LOCAL)
|
|
provider = SimpleNamespace(
|
|
get_authorization_url=MagicMock(return_value="https://provider.example/authorize")
|
|
)
|
|
config = SimpleNamespace()
|
|
create_state = AsyncMock(return_value="state-1")
|
|
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._require_module_active",
|
|
lambda _db: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_provider_impl",
|
|
lambda _provider_type: provider,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_enabled_provider_config",
|
|
lambda _db, _provider_type: config,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.get_redis_client",
|
|
AsyncMock(return_value=object()),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.create_oauth_state",
|
|
create_state,
|
|
)
|
|
|
|
url = await OAuthService.build_bind_authorize_url(
|
|
db,
|
|
user,
|
|
"github",
|
|
client_device_id="device-1",
|
|
)
|
|
|
|
assert url == "https://provider.example/authorize"
|
|
assert create_state.await_args.kwargs["client_device_id"] == "device-1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_callback_allows_bind_state_without_device_id(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
db = MagicMock()
|
|
provider = SimpleNamespace(
|
|
exchange_code=AsyncMock(return_value=SimpleNamespace(access_token="provider-access")),
|
|
get_user_info=AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
id="oauth-user",
|
|
username="tester",
|
|
email="user@example.com",
|
|
email_verified=True,
|
|
raw={},
|
|
)
|
|
),
|
|
)
|
|
config = SimpleNamespace(
|
|
frontend_callback_url="https://app.example.com/auth/callback",
|
|
display_name="GitHub",
|
|
is_enabled=True,
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._require_module_active",
|
|
lambda _db: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_provider_impl",
|
|
lambda _provider_type: provider,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_provider_config",
|
|
lambda _db, _provider_type: config,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.get_redis_client",
|
|
AsyncMock(return_value=object()),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.consume_oauth_state",
|
|
AsyncMock(
|
|
return_value=OAuthStateData(
|
|
nonce="state-1",
|
|
provider_type="github",
|
|
action="bind",
|
|
user_id="user-1",
|
|
client_device_id=None,
|
|
created_at=123,
|
|
)
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._handle_bind",
|
|
AsyncMock(return_value=SimpleNamespace()),
|
|
)
|
|
|
|
result = await OAuthService.handle_callback(
|
|
db=db,
|
|
provider_type="github",
|
|
state="state-1",
|
|
code="code-1",
|
|
error=None,
|
|
error_description=None,
|
|
client_ip=None,
|
|
user_agent="pytest-agent",
|
|
headers={},
|
|
)
|
|
|
|
parsed = urlparse(result.redirect_url)
|
|
assert result.refresh_token is None
|
|
assert parse_qs(parsed.query)["oauth_bound"] == ["GitHub"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_callback_redirects_when_provider_http_is_rust_only(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
db = MagicMock()
|
|
provider = SimpleNamespace(
|
|
exchange_code=AsyncMock(
|
|
side_effect=OAuthFlowError("provider_unavailable", "OAuth 仅支持 Rust executor")
|
|
),
|
|
get_user_info=AsyncMock(),
|
|
)
|
|
config = SimpleNamespace(
|
|
frontend_callback_url="https://app.example.com/auth/callback",
|
|
display_name="GitHub",
|
|
is_enabled=True,
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._require_module_active",
|
|
lambda _db: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_provider_impl",
|
|
lambda _provider_type: provider,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_provider_config",
|
|
lambda _db, _provider_type: config,
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.get_redis_client",
|
|
AsyncMock(return_value=object()),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.consume_oauth_state",
|
|
AsyncMock(
|
|
return_value=OAuthStateData(
|
|
nonce="state-1",
|
|
provider_type="github",
|
|
action="login",
|
|
user_id=None,
|
|
client_device_id="device-1",
|
|
created_at=123,
|
|
)
|
|
),
|
|
)
|
|
|
|
result = await OAuthService.handle_callback(
|
|
db=db,
|
|
provider_type="github",
|
|
state="state-1",
|
|
code="code-1",
|
|
error=None,
|
|
error_description=None,
|
|
client_ip=None,
|
|
user_agent="pytest-agent",
|
|
headers={},
|
|
)
|
|
|
|
parsed = urlparse(result.redirect_url)
|
|
params = parse_qs(parsed.query)
|
|
assert params["error_code"] == ["provider_unavailable"]
|
|
assert params["error_detail"] == ["OAuth 仅支持 Rust executor"]
|
|
provider.get_user_info.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_provider_config_returns_rust_only_failure_without_network(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
db = MagicMock()
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_provider_impl",
|
|
lambda _provider_type: SimpleNamespace(),
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_provider_config",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("Python OAuth config probe should not read DB config")
|
|
),
|
|
)
|
|
|
|
result = await OAuthService.test_provider_config(db, "github")
|
|
assert result == {
|
|
"authorization_url_reachable": False,
|
|
"token_url_reachable": False,
|
|
"secret_status": "unsupported",
|
|
"details": "OAuth 配置测试仅支持 Rust executor",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_test_provider_config_with_data_returns_rust_only_failure(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
provider = SimpleNamespace(
|
|
authorization_url="https://provider.example/authorize",
|
|
token_url="https://provider.example/token",
|
|
)
|
|
monkeypatch.setattr(
|
|
"src.services.auth.oauth.service.OAuthService._get_provider_impl",
|
|
lambda _provider_type: provider,
|
|
)
|
|
|
|
result = await OAuthService.test_provider_config_with_data(
|
|
provider_type="github",
|
|
client_id="client-id",
|
|
client_secret="secret",
|
|
authorization_url_override=None,
|
|
token_url_override=None,
|
|
redirect_uri="https://api.example.com/api/oauth/github/callback",
|
|
)
|
|
|
|
assert result == {
|
|
"authorization_url_reachable": False,
|
|
"token_url_reachable": False,
|
|
"secret_status": "unsupported",
|
|
"details": "OAuth 配置测试仅支持 Rust executor",
|
|
}
|
|
|
|
|
|
def test_handle_login_sync_returns_user_snapshot_outside_db_session(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(
|
|
engine,
|
|
tables=[User.__table__, OAuthProvider.__table__, UserOAuthLink.__table__],
|
|
)
|
|
SessionLocal = sessionmaker(bind=engine)
|
|
|
|
with SessionLocal() as db:
|
|
provider = OAuthProvider(
|
|
provider_type="linuxdo",
|
|
display_name="Linux Do",
|
|
client_id="client-id",
|
|
redirect_uri="https://api.example.com/api/oauth/linuxdo/callback",
|
|
frontend_callback_url="https://app.example.com/auth/callback",
|
|
is_enabled=True,
|
|
)
|
|
user = User(
|
|
email="user@example.com",
|
|
email_verified=True,
|
|
username="tester",
|
|
auth_source=AuthSource.OAUTH,
|
|
role=UserRole.USER,
|
|
is_active=True,
|
|
is_deleted=False,
|
|
)
|
|
db.add(provider)
|
|
db.add(user)
|
|
db.flush()
|
|
user_id = str(user.id)
|
|
db.add(
|
|
UserOAuthLink(
|
|
user_id=user_id,
|
|
provider_type="linuxdo",
|
|
provider_user_id="linuxdo-user-1",
|
|
provider_username="tester",
|
|
provider_email="user@example.com",
|
|
extra_data={},
|
|
)
|
|
)
|
|
db.commit()
|
|
|
|
@contextmanager
|
|
def _fake_get_db_context():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
db.commit()
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
finally:
|
|
db.close()
|
|
|
|
monkeypatch.setattr("src.services.auth.oauth.service.get_db_context", _fake_get_db_context)
|
|
|
|
snapshot = OAuthService._handle_login_sync(
|
|
"linuxdo",
|
|
SimpleNamespace(
|
|
id="linuxdo-user-1",
|
|
username="tester",
|
|
email="user@example.com",
|
|
email_verified=True,
|
|
raw={},
|
|
),
|
|
)
|
|
|
|
assert snapshot.user_id == user_id
|
|
assert snapshot.email == "user@example.com"
|
|
assert snapshot.role == UserRole.USER
|
|
|
|
engine.dispose()
|