


























@@ -0,0 +1,110 @@
1+import { describe, expect, it, vi } from "vitest";
2+import {
3+type OAuthCredential,
4+buildXaiOAuthAuthorizeUrl,
5+fetchXaiOAuthDiscovery,
6+isTrustedXaiOAuthEndpoint,
7+refreshXaiOAuthCredential,
8+XAI_OAUTH_CALLBACK_PORT,
9+XAI_OAUTH_CLIENT_ID,
10+XAI_OAUTH_REDIRECT_URI,
11+XAI_OAUTH_SCOPE,
12+} from "./xai-oauth.js";
13+14+function jsonResponse(value: unknown, init?: ResponseInit): Response {
15+return new Response(JSON.stringify(value), {
16+status: 200,
17+headers: { "Content-Type": "application/json" },
18+ ...init,
19+});
20+}
21+22+describe("xAI OAuth", () => {
23+it("accepts only trusted xAI OAuth endpoints", () => {
24+expect(isTrustedXaiOAuthEndpoint("https://auth.x.ai/oauth2/token")).toBe(true);
25+expect(isTrustedXaiOAuthEndpoint("https://accounts.x.ai/oauth2/token")).toBe(true);
26+expect(isTrustedXaiOAuthEndpoint("http://auth.x.ai/oauth2/token")).toBe(false);
27+expect(isTrustedXaiOAuthEndpoint("https://x.ai.evil.test/oauth2/token")).toBe(false);
28+expect(isTrustedXaiOAuthEndpoint("not a url")).toBe(false);
29+});
30+31+it("builds the Hermes-compatible authorize URL for OpenClaw", () => {
32+const url = new URL(
33+buildXaiOAuthAuthorizeUrl({
34+authorizationEndpoint: "https://auth.x.ai/oauth2/authorize",
35+state: "state-1",
36+nonce: "nonce-1",
37+challenge: "challenge-1",
38+}),
39+);
40+41+expect(url.origin + url.pathname).toBe("https://auth.x.ai/oauth2/authorize");
42+expect(url.searchParams.get("response_type")).toBe("code");
43+expect(url.searchParams.get("client_id")).toBe(XAI_OAUTH_CLIENT_ID);
44+expect(url.searchParams.get("redirect_uri")).toBe(XAI_OAUTH_REDIRECT_URI);
45+expect(url.searchParams.get("scope")).toBe(XAI_OAUTH_SCOPE);
46+expect(url.searchParams.get("code_challenge")).toBe("challenge-1");
47+expect(url.searchParams.get("code_challenge_method")).toBe("S256");
48+expect(url.searchParams.get("state")).toBe("state-1");
49+expect(url.searchParams.get("nonce")).toBe("nonce-1");
50+expect(url.searchParams.get("plan")).toBe("generic");
51+expect(url.searchParams.get("referrer")).toBe("openclaw");
52+expect(XAI_OAUTH_REDIRECT_URI).toContain(`:${XAI_OAUTH_CALLBACK_PORT}/`);
53+});
54+55+it("validates discovered endpoints before using them", async () => {
56+const fetchImpl = vi.fn(async () =>
57+jsonResponse({
58+authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
59+token_endpoint: "https://auth.x.ai/oauth2/token",
60+}),
61+) as unknown as typeof fetch;
62+63+await expect(fetchXaiOAuthDiscovery({ fetchImpl })).resolves.toEqual({
64+authorizationEndpoint: "https://auth.x.ai/oauth2/authorize",
65+tokenEndpoint: "https://auth.x.ai/oauth2/token",
66+});
67+68+const poisonedFetch = vi.fn(async () =>
69+jsonResponse({
70+authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
71+token_endpoint: "https://evil.test/oauth2/token",
72+}),
73+) as unknown as typeof fetch;
74+75+await expect(fetchXaiOAuthDiscovery({ fetchImpl: poisonedFetch })).rejects.toThrow(
76+"untrusted token endpoint",
77+);
78+});
79+80+it("refreshes with the cached token endpoint and preserves refresh fallback", async () => {
81+const fetchImpl = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
82+expect(init?.method).toBe("POST");
83+const body = String(init?.body);
84+expect(body).toContain("grant_type=refresh_token");
85+expect(body).toContain(`client_id=${encodeURIComponent(XAI_OAUTH_CLIENT_ID)}`);
86+expect(body).toContain("refresh_token=refresh-1");
87+return jsonResponse({
88+access_token: "access-2",
89+expires_in: 120,
90+});
91+}) as unknown as typeof fetch;
92+93+const refreshed = await refreshXaiOAuthCredential(
94+{
95+type: "oauth",
96+provider: "xai",
97+access: "access-1",
98+refresh: "refresh-1",
99+expires: 100,
100+tokenEndpoint: "https://auth.x.ai/oauth2/token",
101+} as OAuthCredential & { tokenEndpoint: string },
102+{ fetchImpl, now: () => 1_000 },
103+);
104+105+expect(fetchImpl).toHaveBeenCalledWith("https://auth.x.ai/oauth2/token", expect.any(Object));
106+expect(refreshed.access).toBe("access-2");
107+expect(refreshed.refresh).toBe("refresh-1");
108+expect(refreshed.expires).toBe(121_000);
109+});
110+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。