168 lines
5.5 KiB
Python
168 lines
5.5 KiB
Python
"""SCIM 2.0 router 单元测试 (U4 SSO 集成)。
|
||
|
||
验证 POST / PATCH / GET /scim/v2/Users:
|
||
- POST 创建用户
|
||
- PATCH 禁用用户 (active=false)
|
||
- GET 列表 / 过滤
|
||
- Bearer token 认证(缺 token / 错误 token → 401)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from fastapi import FastAPI
|
||
from fastapi.testclient import TestClient
|
||
|
||
from agentkit.server.auth.models import init_auth_db
|
||
from agentkit.server.auth.scim.router import (
|
||
register_scim_exception_handlers,
|
||
router as scim_router,
|
||
)
|
||
|
||
|
||
SCIM_TOKEN = "test-scim-token-secret"
|
||
|
||
|
||
@pytest.fixture
|
||
async def scim_app(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> FastAPI:
|
||
"""创建含 SCIM router 的 TestApp + 初始化 auth DB。"""
|
||
db_path = tmp_path / "auth.db"
|
||
await init_auth_db(db_path)
|
||
monkeypatch.setenv("AGENTKIT_AUTH_DB", str(db_path))
|
||
monkeypatch.setenv("AGENTKIT_SCIM_TOKEN", SCIM_TOKEN)
|
||
|
||
app = FastAPI()
|
||
app.state.auth_db_path = str(db_path)
|
||
app.state.scim_token = SCIM_TOKEN
|
||
# STAND-3: 按 RFC 7644 挂载在 /scim/v2(非 /api/v1)
|
||
app.include_router(scim_router, prefix="/scim/v2")
|
||
register_scim_exception_handlers(app)
|
||
return app
|
||
|
||
|
||
@pytest.fixture
|
||
def client(scim_app: FastAPI) -> TestClient:
|
||
return TestClient(scim_app)
|
||
|
||
|
||
def _auth_headers(token: str = SCIM_TOKEN) -> dict[str, str]:
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestCreateUser:
|
||
def test_creates_user(self, client: TestClient) -> None:
|
||
resp = client.post(
|
||
"/scim/v2/Users",
|
||
json={
|
||
"userName": "scim.user1",
|
||
"active": True,
|
||
"emails": [{"value": "scim1@example.com", "primary": True}],
|
||
},
|
||
headers=_auth_headers(),
|
||
)
|
||
assert resp.status_code == 201
|
||
data = resp.json()
|
||
assert data["userName"] == "scim.user1"
|
||
assert data["active"] is True
|
||
assert data["emails"][0]["value"] == "scim1@example.com"
|
||
assert data["schemas"] == ["urn:ietf:params:scim:schemas:core:2.0:User"]
|
||
|
||
def test_duplicate_username_conflict(self, client: TestClient) -> None:
|
||
client.post(
|
||
"/scim/v2/Users",
|
||
json={"userName": "dup.user", "emails": [{"value": "dup@example.com"}]},
|
||
headers=_auth_headers(),
|
||
)
|
||
resp = client.post(
|
||
"/scim/v2/Users",
|
||
json={"userName": "dup.user", "emails": [{"value": "other@example.com"}]},
|
||
headers=_auth_headers(),
|
||
)
|
||
assert resp.status_code == 409
|
||
|
||
def test_no_token_unauthorized(self, client: TestClient) -> None:
|
||
resp = client.post("/scim/v2/Users", json={"userName": "x"})
|
||
assert resp.status_code == 401
|
||
|
||
def test_wrong_token_unauthorized(self, client: TestClient) -> None:
|
||
resp = client.post(
|
||
"/scim/v2/Users",
|
||
json={"userName": "x"},
|
||
headers={"Authorization": "Bearer wrong-token"},
|
||
)
|
||
assert resp.status_code == 401
|
||
|
||
|
||
class TestPatchUser:
|
||
def test_disable_user(self, client: TestClient) -> None:
|
||
# 先创建
|
||
create = client.post(
|
||
"/scim/v2/Users",
|
||
json={"userName": "patch.user", "active": True, "emails": [{"value": "p@example.com"}]},
|
||
headers=_auth_headers(),
|
||
)
|
||
user_id = create.json()["id"]
|
||
|
||
# PATCH 禁用
|
||
resp = client.patch(
|
||
f"/scim/v2/Users/{user_id}",
|
||
json={"Operations": [{"op": "replace", "path": "active", "value": False}]},
|
||
headers=_auth_headers(),
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["active"] is False
|
||
|
||
def test_patch_unknown_user_404(self, client: TestClient) -> None:
|
||
resp = client.patch(
|
||
"/scim/v2/Users/nonexistent-id",
|
||
json={"Operations": [{"op": "replace", "path": "active", "value": False}]},
|
||
headers=_auth_headers(),
|
||
)
|
||
assert resp.status_code == 404
|
||
|
||
|
||
class TestListUsers:
|
||
def test_list_all(self, client: TestClient) -> None:
|
||
client.post(
|
||
"/scim/v2/Users",
|
||
json={"userName": "list.user1", "emails": [{"value": "l1@example.com"}]},
|
||
headers=_auth_headers(),
|
||
)
|
||
client.post(
|
||
"/scim/v2/Users",
|
||
json={"userName": "list.user2", "emails": [{"value": "l2@example.com"}]},
|
||
headers=_auth_headers(),
|
||
)
|
||
resp = client.get("/scim/v2/Users", headers=_auth_headers())
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["totalResults"] >= 2
|
||
assert len(data["Resources"]) >= 2
|
||
|
||
def test_filter_by_username(self, client: TestClient) -> None:
|
||
client.post(
|
||
"/scim/v2/Users",
|
||
json={"userName": "filter.user", "emails": [{"value": "f@example.com"}]},
|
||
headers=_auth_headers(),
|
||
)
|
||
resp = client.get(
|
||
"/scim/v2/Users",
|
||
params={"filter": 'userName eq "filter.user"'},
|
||
headers=_auth_headers(),
|
||
)
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["totalResults"] == 1
|
||
assert data["Resources"][0]["userName"] == "filter.user"
|
||
|
||
def test_no_token_unauthorized(self, client: TestClient) -> None:
|
||
resp = client.get("/scim/v2/Users")
|
||
assert resp.status_code == 401
|