@@ -211,6 +211,67 @@ describe("xAI OAuth", () => {
|
211 | 211 | expect(refreshed.expires).toBe(121_000); |
212 | 212 | }); |
213 | 213 | |
| 214 | +it("rediscovers the current token endpoint for stale xAI OAuth credentials", async () => { |
| 215 | +const fetchImpl = vi.fn<typeof fetch>(async (url, init) => { |
| 216 | +if (requestUrl(url) === XAI_OAUTH_DISCOVERY_URL) { |
| 217 | +expect(init?.method).toBeUndefined(); |
| 218 | +return jsonResponse({ |
| 219 | +authorization_endpoint: "https://auth.x.ai/oauth2/authorize", |
| 220 | +token_endpoint: "https://auth.x.ai/oauth2/token", |
| 221 | +}); |
| 222 | +} |
| 223 | +expect(requestUrl(url)).toBe("https://auth.x.ai/oauth2/token"); |
| 224 | +expect(init?.method).toBe("POST"); |
| 225 | +expect(requireStringBody(init)).toContain("refresh_token=refresh-1"); |
| 226 | +return jsonResponse({ |
| 227 | +access_token: "access-2", |
| 228 | +refresh_token: "refresh-2", |
| 229 | +expires_in: 120, |
| 230 | +}); |
| 231 | +}); |
| 232 | +const credential = { |
| 233 | +type: "oauth", |
| 234 | +provider: "xai", |
| 235 | +access: "access-1", |
| 236 | +refresh: "refresh-1", |
| 237 | +expires: 100, |
| 238 | +tokenEndpoint: "https://auth.x.ai/oauth/token", |
| 239 | +} satisfies OAuthCredential & { tokenEndpoint: string }; |
| 240 | + |
| 241 | +const refreshed = await refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 }); |
| 242 | + |
| 243 | +expect(fetchImpl).toHaveBeenCalledTimes(2); |
| 244 | +expect(fetchImpl.mock.calls.map(([url]) => requestUrl(url))).toEqual([ |
| 245 | +XAI_OAUTH_DISCOVERY_URL, |
| 246 | +"https://auth.x.ai/oauth2/token", |
| 247 | +]); |
| 248 | +expect(refreshed).toMatchObject({ |
| 249 | +access: "access-2", |
| 250 | +refresh: "refresh-2", |
| 251 | +tokenEndpoint: "https://auth.x.ai/oauth2/token", |
| 252 | +}); |
| 253 | +}); |
| 254 | + |
| 255 | +it("does not reuse the stale xAI OAuth token endpoint when discovery fails", async () => { |
| 256 | +const fetchImpl = vi.fn<typeof fetch>(async (url) => { |
| 257 | +expect(requestUrl(url)).toBe(XAI_OAUTH_DISCOVERY_URL); |
| 258 | +throw new Error("discovery unavailable"); |
| 259 | +}); |
| 260 | +const credential = { |
| 261 | +type: "oauth", |
| 262 | +provider: "xai", |
| 263 | +access: "access-1", |
| 264 | +refresh: "refresh-1", |
| 265 | +expires: 100, |
| 266 | +tokenEndpoint: "https://auth.x.ai/oauth/token", |
| 267 | +} satisfies OAuthCredential & { tokenEndpoint: string }; |
| 268 | + |
| 269 | +await expect(refreshXaiOAuthCredential(credential, { fetchImpl })).rejects.toThrow( |
| 270 | +"discovery unavailable", |
| 271 | +); |
| 272 | +expect(fetchImpl).toHaveBeenCalledTimes(1); |
| 273 | +}); |
| 274 | + |
214 | 275 | it("does not coerce partial xAI expires_in values", async () => { |
215 | 276 | const fetchImpl = vi.fn<typeof fetch>(async () => |
216 | 277 | jsonResponse({ |
|