refactor: 迁移本地 Supabase 到云端,使用 JWKS 进行 JWT 验证
- 新增 JwtVerifier 支持 RS256 + JWKS 验证 - 简化 docker-compose,删除本地 Supabase 服务(kong/auth/storage等) - 删除冗余的 Supabase 配置文件(volumes目录) - 适配测试用例以支持新配置方式 - 更新运行时文档和迁移计划
This commit is contained in:
@@ -1,45 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import httpx
|
||||
import jwt
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from core.config import config
|
||||
from core.db.session import AsyncSessionLocal
|
||||
from models.agent_chat_message import AgentChatMessage
|
||||
from models.agent_chat_session import AgentChatSession
|
||||
from models.profile import Profile
|
||||
|
||||
BASE_URL = os.getenv("AGENT_LIVE_BASE_URL", "http://localhost:5775")
|
||||
|
||||
|
||||
async def _owner_id() -> UUID:
|
||||
async with AsyncSessionLocal() as session:
|
||||
owner_id = (await session.execute(select(Profile.id).limit(1))).scalar_one_or_none()
|
||||
if owner_id is None:
|
||||
pytest.skip("profile owner not found")
|
||||
return owner_id
|
||||
async def _live_access_token(client: httpx.AsyncClient) -> str:
|
||||
email = os.getenv("AGENT_LIVE_EMAIL")
|
||||
password = os.getenv("AGENT_LIVE_PASSWORD")
|
||||
if not email or not password:
|
||||
pytest.fail(
|
||||
"AGENT_LIVE_INTEGRATION=1 requires AGENT_LIVE_EMAIL and AGENT_LIVE_PASSWORD"
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
f"{BASE_URL}/api/v1/auth/sessions",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
response_text = response.text.strip().replace("\n", " ")
|
||||
truncated_text = response_text[:200]
|
||||
if len(response_text) > 200:
|
||||
truncated_text += "..."
|
||||
|
||||
def _jwt_for(user_id: UUID) -> str:
|
||||
secret = config.supabase.jwt_secret
|
||||
if not secret:
|
||||
pytest.skip("JWT secret not configured")
|
||||
issuer = f"{config.supabase.public_url.rstrip('/')}/auth/v1"
|
||||
payload = {
|
||||
"sub": str(user_id),
|
||||
"role": "authenticated",
|
||||
"aud": "authenticated",
|
||||
"iss": issuer,
|
||||
"iat": datetime.now(timezone.utc),
|
||||
"exp": datetime.now(timezone.utc) + timedelta(minutes=30),
|
||||
}
|
||||
return jwt.encode(payload, secret, algorithm="HS256")
|
||||
assert response.status_code == 200, (
|
||||
f"live login failed: status={response.status_code}, response={truncated_text!r}"
|
||||
)
|
||||
|
||||
token = response.json().get("access_token")
|
||||
assert isinstance(token, str) and token
|
||||
return token
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -48,11 +46,10 @@ async def test_agent_sse_closed_loop_live() -> None:
|
||||
if os.getenv("AGENT_LIVE_INTEGRATION") != "1":
|
||||
pytest.skip("set AGENT_LIVE_INTEGRATION=1 to run live integration test")
|
||||
|
||||
owner_id = await _owner_id()
|
||||
token = _jwt_for(owner_id)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
token = await _live_access_token(client)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
run_resp = await client.post(
|
||||
f"{BASE_URL}/api/v1/agent/runs",
|
||||
headers=headers,
|
||||
@@ -76,9 +73,13 @@ async def test_agent_sse_closed_loop_live() -> None:
|
||||
|
||||
events_url = f"{BASE_URL}/api/v1/agent/runs/{thread_id}/events"
|
||||
event_names: list[str] = []
|
||||
async with client.stream("GET", events_url, headers=headers, timeout=20.0) as sse_resp:
|
||||
async with client.stream(
|
||||
"GET", events_url, headers=headers, timeout=20.0
|
||||
) as sse_resp:
|
||||
assert sse_resp.status_code == 200
|
||||
assert sse_resp.headers.get("content-type", "").startswith("text/event-stream")
|
||||
assert sse_resp.headers.get("content-type", "").startswith(
|
||||
"text/event-stream"
|
||||
)
|
||||
async for line in sse_resp.aiter_lines():
|
||||
if line.startswith("event:"):
|
||||
event_names.append(line.split(":", 1)[1].strip())
|
||||
@@ -94,6 +95,8 @@ async def test_agent_sse_closed_loop_live() -> None:
|
||||
assert session_row.total_cost >= 0
|
||||
|
||||
rows = await session.execute(
|
||||
select(AgentChatMessage).where(AgentChatMessage.session_id == UUID(thread_id))
|
||||
select(AgentChatMessage).where(
|
||||
AgentChatMessage.session_id == UUID(thread_id)
|
||||
)
|
||||
)
|
||||
assert len(list(rows.scalars().all())) >= 1
|
||||
|
||||
Reference in New Issue
Block a user