"""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 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 app.include_router(scim_router, prefix="/api/v1") 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( "/api/v1/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( "/api/v1/scim/v2/Users", json={"userName": "dup.user", "emails": [{"value": "dup@example.com"}]}, headers=_auth_headers(), ) resp = client.post( "/api/v1/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("/api/v1/scim/v2/Users", json={"userName": "x"}) assert resp.status_code == 401 def test_wrong_token_unauthorized(self, client: TestClient) -> None: resp = client.post( "/api/v1/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( "/api/v1/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"/api/v1/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( "/api/v1/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( "/api/v1/scim/v2/Users", json={"userName": "list.user1", "emails": [{"value": "l1@example.com"}]}, headers=_auth_headers(), ) client.post( "/api/v1/scim/v2/Users", json={"userName": "list.user2", "emails": [{"value": "l2@example.com"}]}, headers=_auth_headers(), ) resp = client.get("/api/v1/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( "/api/v1/scim/v2/Users", json={"userName": "filter.user", "emails": [{"value": "f@example.com"}]}, headers=_auth_headers(), ) resp = client.get( "/api/v1/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("/api/v1/scim/v2/Users") assert resp.status_code == 401