quickbase-js 0.1.0-beta.8 → 0.1.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +822 -119
  4. package/dist/esm/quickbase.js +373 -236
  5. package/dist/esm/quickbase.js.map +1 -1
  6. package/dist/esm/quickbase.min.js +1 -1
  7. package/dist/esm/quickbase.min.js.map +1 -1
  8. package/dist/umd/quickbase.umd.js +373 -236
  9. package/dist/umd/quickbase.umd.js.map +1 -1
  10. package/dist/umd/quickbase.umd.min.js +1 -1
  11. package/dist/umd/quickbase.umd.min.js.map +1 -1
  12. package/open-api/generate-unified-interface.ts +97 -17
  13. package/open-api/utils/generateJsDoc.ts +96 -0
  14. package/package.json +3 -1
  15. package/src/ThrottleBucket.ts +17 -12
  16. package/src/authorizationStrategy.ts +246 -0
  17. package/src/generated-unified/QuickbaseClient.ts +809 -119
  18. package/src/invokeMethod.ts +150 -265
  19. package/src/quickbaseClient.ts +85 -58
  20. package/src/rateLimiter.ts +25 -0
  21. package/src/tokenCache.ts +10 -26
  22. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  23. package/tests/vitest/qb/auth/retryUserToken401.test.ts +59 -42
  24. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  25. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  26. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  27. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  28. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  29. package/tests/vitest/qb/tables/updateRelationship.test.ts +118 -145
  30. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  31. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  32. package/tests/vitest/unit/apps/updateApp.test.ts +42 -11
  33. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +3 -2
  34. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  35. package/tests/vitest/unit/auth/retryTempToken401.test.ts +46 -64
  36. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  37. package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -43
  38. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  39. package/tests/vitest/unit/fields/createField.test.ts +180 -116
  40. package/tests/vitest/unit/fields/getFieldUsage.test.ts +17 -3
  41. package/tests/vitest/unit/fields/updateField.test.ts +12 -7
  42. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  43. package/tests/vitest/unit/index.ts +2 -1
  44. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  45. package/tests/vitest/unit/rateLimit.test.ts +4 -2
  46. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  47. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  48. package/tests/vitest/unit/tables/updateRelationship.test.ts +12 -10
  49. package/tests/vitest/unit/tokenLifespan.test.ts +149 -83
  50. package/tools/log-gen.ts +14 -5
  51. package/tools/log-gen.yaml +710 -268
  52. package/tests/vitest/unit/auth/retryTempTokenTwo401.test.ts +0 -81
@@ -1,11 +1,19 @@
1
1
  // src/quickbaseClient.ts
2
+
2
3
  import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient";
3
4
  import { Configuration, HTTPHeaders } from "./generated/runtime";
4
5
  import * as apis from "./generated/apis";
5
6
  import { TokenCache } from "./tokenCache";
6
7
  import { simplifyName } from "./utils";
7
- import { invokeMethod, MethodInfo } from "./invokeMethod"; // Added import
8
+ import { invokeMethod, MethodInfo } from "./invokeMethod";
9
+ import {
10
+ TempTokenStrategy,
11
+ UserTokenStrategy,
12
+ AuthorizationStrategy,
13
+ SsoTokenStrategy,
14
+ } from "./authorizationStrategy";
8
15
  import { ThrottleBucket } from "./ThrottleBucket";
16
+ import { RateLimiter } from "./rateLimiter";
9
17
 
10
18
  export * from "./generated/models/index";
11
19
 
@@ -16,13 +24,17 @@ export interface QuickbaseConfig {
16
24
  userToken?: string;
17
25
  tempToken?: string;
18
26
  useTempTokens?: boolean;
27
+ useSso?: boolean;
28
+ samlToken?: string;
19
29
  debug?: boolean;
20
30
  fetchApi?: typeof fetch;
21
31
  convertDates?: boolean;
22
- tokenLifespan?: number;
32
+ tempTokenLifespan?: number;
23
33
  throttle?: { rate: number; burst: number };
24
34
  maxRetries?: number;
25
35
  retryDelay?: number;
36
+ tokenCache?: TokenCache;
37
+ baseUrl?: string;
26
38
  }
27
39
 
28
40
  type MethodMap = {
@@ -64,11 +76,20 @@ function transformDates(obj: any, convertStringsToDates: boolean = true): any {
64
76
  }
65
77
 
66
78
  function inferHttpMethod(methodSource: string, debug?: boolean): string {
67
- const methodMatch = methodSource.match(/method:\s*['"]?(\w+)['"]?/i);
68
- const method = methodMatch ? methodMatch[1].toUpperCase() : "GET";
79
+ const methodMatches = [
80
+ ...methodSource.matchAll(/method:\s*['"]?(\w+)['"]?/gi),
81
+ ];
82
+ const method =
83
+ methodMatches.length > 0
84
+ ? methodMatches[methodMatches.length - 1][1].toUpperCase()
85
+ : "GET";
69
86
  if (debug) {
70
- // console.log(`[inferHttpMethod] Source:`, methodSource);
71
- // console.log(`[inferHttpMethod] Extracted method:`, method);
87
+ console.log(`[inferHttpMethod] Source:`, methodSource);
88
+ console.log(
89
+ `[inferHttpMethod] All matches:`,
90
+ methodMatches.map((m) => m[0])
91
+ );
92
+ console.log(`[inferHttpMethod] Extracted method:`, method);
72
93
  }
73
94
  return method;
74
95
  }
@@ -77,50 +98,59 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
77
98
  const {
78
99
  realm,
79
100
  userToken,
80
- tempToken: initialTempToken,
101
+ tempToken,
81
102
  useTempTokens,
103
+ useSso,
104
+ samlToken,
82
105
  fetchApi,
83
106
  debug,
84
107
  convertDates = true,
85
- tokenLifespan,
108
+ tempTokenLifespan = 290000,
86
109
  throttle = { rate: 10, burst: 10 },
87
110
  maxRetries = 3,
88
111
  retryDelay = 1000,
112
+ tokenCache: providedTokenCache,
113
+ baseUrl = "https://api.quickbase.com/v1",
89
114
  } = config;
90
- const baseUrl = `https://api.quickbase.com/v1`;
91
115
 
92
- const tokenCache = new TokenCache(tokenLifespan);
116
+ const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
93
117
  const throttleBucket = throttle
94
118
  ? new ThrottleBucket(throttle.rate, throttle.burst)
95
119
  : null;
120
+ const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
121
+
122
+ const defaultFetch: typeof fetch | undefined =
123
+ typeof globalThis.window !== "undefined"
124
+ ? globalThis.window.fetch.bind(globalThis.window)
125
+ : undefined;
126
+ const effectiveFetch = fetchApi || defaultFetch;
127
+
128
+ if (!effectiveFetch) {
129
+ throw new Error("No fetch implementation available");
130
+ }
131
+
132
+ const authStrategy: AuthorizationStrategy = useSso
133
+ ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
134
+ : useTempTokens
135
+ ? new TempTokenStrategy(tokenCache, tempToken)
136
+ : new UserTokenStrategy(userToken || "");
96
137
 
97
138
  const baseHeaders: HTTPHeaders = {
98
139
  "QB-Realm-Hostname": `${realm}.quickbase.com`,
99
140
  "Content-Type": "application/json",
100
141
  };
101
142
 
102
- if (initialTempToken) {
103
- baseHeaders["Authorization"] = `QB-TEMP-TOKEN ${initialTempToken}`;
104
- } else if (userToken && !useTempTokens) {
105
- baseHeaders["Authorization"] = `QB-USER-TOKEN ${userToken}`;
106
- }
107
-
108
- const defaultFetch: typeof fetch | undefined =
109
- typeof globalThis.window !== "undefined"
110
- ? globalThis.window.fetch.bind(globalThis.window)
111
- : undefined;
112
143
  const configuration = new Configuration({
113
144
  basePath: baseUrl,
114
145
  headers: { ...baseHeaders },
115
- fetchApi: fetchApi || defaultFetch,
146
+ fetchApi: effectiveFetch,
116
147
  credentials: "omit",
117
148
  });
118
149
 
119
- if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
120
- throw new Error(
121
- "fetchApi must be provided in non-browser environments (e.g., Node.js)"
122
- );
123
- }
150
+ // Removed redundant check:
151
+ // if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
152
+ // throw new Error("fetchApi must be provided in non-browser environments (e.g., Node.js)");
153
+ // }
124
154
 
125
155
  const apiInstances = Object.fromEntries(
126
156
  Object.entries(apis)
@@ -131,8 +161,6 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
131
161
  ])
132
162
  );
133
163
 
134
- const methodMap = buildMethodMap();
135
-
136
164
  function buildMethodMap(): MethodMap {
137
165
  const methodMap: Partial<MethodMap> = {};
138
166
  const isValidMethod = (name: string) =>
@@ -159,32 +187,27 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
159
187
  const methodSource = method.toString();
160
188
  methodMap[simplifiedName] = {
161
189
  api,
162
- method: boundMethod as any, // Adjust typing as needed
190
+ method: boundMethod as any,
163
191
  paramMap: getParamNames(method),
164
192
  httpMethod: inferHttpMethod(methodSource, debug),
165
193
  };
166
194
  }
167
195
  });
168
196
  }
197
+ if (debug) {
198
+ console.log("[buildMethodMap] Methods:", Object.keys(methodMap));
199
+ }
169
200
  return methodMap as MethodMap;
170
201
  }
171
202
 
172
- const fetchTempToken = async (dbid: string): Promise<string> => {
173
- const effectiveFetch = fetchApi || defaultFetch;
174
- if (!effectiveFetch) {
175
- throw new Error(
176
- "No fetch implementation available for fetching temp token"
177
- );
178
- }
203
+ const methodMap = buildMethodMap();
179
204
 
180
- const response = await effectiveFetch(
181
- `https://api.quickbase.com/v1/auth/temporary/${dbid}`,
182
- {
183
- method: "GET",
184
- headers: { ...baseHeaders },
185
- credentials: "include",
186
- }
187
- );
205
+ const fetchTempToken = async (dbid: string): Promise<string> => {
206
+ const response = await effectiveFetch(`${baseUrl}/auth/temporary/${dbid}`, {
207
+ method: "GET",
208
+ headers: { ...baseHeaders },
209
+ credentials: "include",
210
+ });
188
211
 
189
212
  if (!response.ok) {
190
213
  const errorBody: { message?: string } = await response.json();
@@ -196,19 +219,26 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
196
219
  }
197
220
 
198
221
  const tokenResult = await response.json();
222
+ console.log(`[fetchTempToken] Response:`, tokenResult);
199
223
  const token = tokenResult.temporaryAuthorization;
200
224
  if (!token) {
201
225
  throw new Error("No temporary token returned from API");
202
226
  }
203
- tokenCache.set(dbid, token);
227
+ tokenCache.set(dbid, token, tempTokenLifespan);
204
228
  if (debug) {
205
229
  console.log(`Fetched and cached new token for dbid: ${dbid}`, token);
206
230
  }
207
231
  return token;
208
232
  };
209
233
 
210
- const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, {
211
- get: (_, prop: string): ((params: any) => Promise<any>) | undefined => {
234
+ const proxyHandler: ProxyHandler<QuickbaseClient> = {
235
+ get: (_, prop: string) => {
236
+ console.log(
237
+ "[proxy] Accessing:",
238
+ prop,
239
+ "in methodMap:",
240
+ prop in methodMap
241
+ );
212
242
  if (prop in methodMap) {
213
243
  const methodName = prop as keyof QuickbaseClient;
214
244
  return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) =>
@@ -217,27 +247,24 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
217
247
  params,
218
248
  methodMap,
219
249
  baseHeaders,
220
- tokenCache,
250
+ authStrategy,
251
+ rateLimiter,
221
252
  fetchTempToken,
222
253
  transformDates,
223
- initialTempToken,
224
- userToken,
225
- useTempTokens,
226
254
  debug,
227
- convertDates,
228
- 0, // retryCount
229
- throttleBucket,
230
- maxRetries,
231
- retryDelay
255
+ convertDates
232
256
  );
233
257
  }
258
+ console.log("[proxy] Method not found:", prop);
234
259
  return undefined;
235
260
  },
236
- });
261
+ };
262
+
263
+ const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, proxyHandler);
237
264
 
238
265
  if (debug) {
239
266
  console.log("[createClient] Config:", config);
240
- console.log("[createClient] Returning:", proxy);
267
+ console.log("[createClient] Proxy created:", proxy);
241
268
  }
242
269
 
243
270
  return proxy;
@@ -0,0 +1,25 @@
1
+ // src/rateLimiter.ts
2
+ import { ThrottleBucket } from "./ThrottleBucket";
3
+ import { ResponseError } from "./generated/runtime";
4
+
5
+ export class RateLimiter {
6
+ constructor(
7
+ private throttleBucket: ThrottleBucket | null,
8
+ public maxRetries: number = 3,
9
+ private retryDelay: number = 1000
10
+ ) {}
11
+
12
+ async throttle(): Promise<void> {
13
+ if (this.throttleBucket) {
14
+ await this.throttleBucket.acquire();
15
+ }
16
+ }
17
+
18
+ async handle429(error: ResponseError, attempt: number): Promise<number> {
19
+ // Remove the throw here; let invokeMethod handle exhaustion
20
+ const retryAfter = error.response.headers.get("Retry-After");
21
+ return retryAfter
22
+ ? parseInt(retryAfter, 10) * 1000
23
+ : this.retryDelay * Math.pow(2, attempt - 1);
24
+ }
25
+ }
package/src/tokenCache.ts CHANGED
@@ -1,50 +1,34 @@
1
+ // src/tokenCache.ts
1
2
  interface CachedToken {
2
3
  token: string;
3
- expiresAt: number; // Timestamp in milliseconds
4
+ expiresAt: number;
4
5
  }
5
6
 
6
7
  export class TokenCache {
7
8
  private cache: Map<string, CachedToken>;
8
- private readonly lifespan: number; // Token lifespan in milliseconds
9
+ private readonly tempTokenLifespan: number;
9
10
 
10
- constructor(lifespan: number = 4 * 60 * 1000 + 50 * 1000) {
11
- // Default 4:50
11
+ constructor(tempTokenLifespan: number = 4 * 60 * 1000 + 50 * 1000) {
12
12
  this.cache = new Map<string, CachedToken>();
13
- this.lifespan = lifespan;
13
+ this.tempTokenLifespan = tempTokenLifespan;
14
14
  }
15
15
 
16
16
  get(dbid: string): string | undefined {
17
17
  const entry = this.cache.get(dbid);
18
18
  const now = Date.now();
19
- if (entry && entry.expiresAt > now) {
20
- return entry.token;
21
- }
22
- return undefined; // Expired or not found
19
+ if (entry && entry.expiresAt > now) return entry.token;
20
+ if (entry) this.cache.delete(dbid); // Clean up expired temp tokens
21
+ return undefined;
23
22
  }
24
23
 
25
- // New method to get full entry
26
- getEntry(dbid: string): CachedToken | undefined {
27
- const entry = this.cache.get(dbid);
28
- const now = Date.now();
29
- if (entry && entry.expiresAt > now) {
30
- return entry;
31
- }
32
- return undefined; // Expired or not found
33
- }
34
-
35
- set(dbid: string, token: string): void {
24
+ set(dbid: string, token: string, lifespan?: number): void {
36
25
  const now = Date.now();
37
26
  this.cache.set(dbid, {
38
27
  token,
39
- expiresAt: now + this.lifespan,
28
+ expiresAt: now + (lifespan || this.tempTokenLifespan),
40
29
  });
41
30
  }
42
31
 
43
- dump(): [string, CachedToken][] {
44
- return Array.from(this.cache.entries());
45
- }
46
-
47
- // New method to clear the cache
48
32
  clear(): void {
49
33
  this.cache.clear();
50
34
  }
@@ -0,0 +1,60 @@
1
+ // tests/vitest/qb/auth/mockQuickbaseServer.ts
2
+ import express from "express";
3
+
4
+ export function startMockQuickbaseServer(port: number = 3000) {
5
+ const app = express();
6
+ app.use(express.json());
7
+ let failNextCall = false;
8
+
9
+ app.post("/v1/auth/oauth/token", (req, res) => {
10
+ const { subject_token } = req.body;
11
+ console.log("[Mock Server] Received SSO token request:", req.body);
12
+ if (!subject_token) {
13
+ return res.status(400).json({ message: "Missing SAML token" });
14
+ }
15
+ res.json({
16
+ access_token: "mock-temp-token",
17
+ token_type: "Bearer",
18
+ });
19
+ });
20
+
21
+ app.get("/v1/apps/:appId", (req, res) => {
22
+ const authHeader = req.headers.authorization;
23
+ console.log(
24
+ "[Mock Server] Received getApp request:",
25
+ req.params,
26
+ "Auth:",
27
+ authHeader
28
+ );
29
+ if (!authHeader || !authHeader.startsWith("QB-TEMP-TOKEN")) {
30
+ return res.status(401).json({ message: "Unauthorized" });
31
+ }
32
+ if (failNextCall) {
33
+ failNextCall = false;
34
+ return res.status(401).json({ message: "Token expired" });
35
+ }
36
+ res.json({
37
+ id: req.params.appId,
38
+ name: "Mock App",
39
+ created: "2025-03-19T12:00:00Z",
40
+ updated: "2025-03-19T12:00:00Z",
41
+ });
42
+ });
43
+
44
+ app.post("/mock/toggle-fail", (req, res) => {
45
+ failNextCall = true;
46
+ console.log("[Mock Server] Toggling next getApp call to fail with 401");
47
+ res.sendStatus(200);
48
+ });
49
+
50
+ const server = app.listen(port, () => {
51
+ console.log(`Mock QuickBase server running on port ${port}`);
52
+ });
53
+
54
+ return {
55
+ server,
56
+ close: () => server.close(),
57
+ toggleFail: () =>
58
+ fetch(`http://localhost:${port}/mock/toggle-fail`, { method: "POST" }),
59
+ };
60
+ }
@@ -1,56 +1,73 @@
1
1
  import { describe, it, vi, expect, beforeEach } from "vitest";
2
2
  import { quickbase, QuickbaseClient } from "../../../../src/quickbaseClient";
3
- import { ResponseError } from "../../../../src/generated/runtime";
4
3
 
5
4
  describe("QuickbaseClient Integration - User Token Retry on 401", () => {
6
5
  let client: QuickbaseClient;
7
- const mockFetch = vi.fn();
8
6
 
9
7
  beforeEach(() => {
8
+ const userToken = process.env.QB_USER_TOKEN;
9
+ const realm = process.env.QB_REALM;
10
+ const tableId = process.env.QB_TABLE_ID_1; // Consistent env var name
11
+
12
+ if (!userToken) {
13
+ throw new Error(
14
+ "QB_USER_TOKEN environment variable is required for integration test"
15
+ );
16
+ }
17
+ if (!tableId) {
18
+ throw new Error(
19
+ "QB_TABLE_ID_1 environment variable is required for integration test"
20
+ );
21
+ }
22
+
10
23
  const config = {
11
- realm: process.env.QB_REALM || "builderprogram-dbradford6815",
12
- userToken: "mock-user-token",
24
+ realm,
25
+ userToken,
13
26
  debug: true,
14
- fetchApi: mockFetch,
15
27
  };
16
- console.log("[quickbaseTest] Config:", config);
28
+ console.log("[quickbaseTest] Config:", {
29
+ realm,
30
+ userToken: userToken.substring(0, 10) + "...",
31
+ tableId, // Log to confirm
32
+ debug: true,
33
+ });
17
34
  client = quickbase(config);
18
35
  });
19
36
 
20
- it("retries on transient 401 with user token and succeeds", async () => {
21
- const consoleSpy = vi.spyOn(console, "log");
22
-
23
- mockFetch
24
- .mockImplementationOnce(() => {
25
- const response = new Response(null, {
26
- status: 401,
27
- statusText: "Unauthorized",
28
- });
29
- console.log("[mockFetch] Throwing 401 ResponseError");
30
- return Promise.reject(new ResponseError(response));
31
- })
32
- .mockResolvedValueOnce({
33
- ok: true,
34
- status: 200,
35
- json: async () => [{ id: 1, label: "Mock Field", fieldType: "text" }],
36
- });
37
-
38
- const fields = await client.getFields({ tableId: "mock-table-id" });
39
-
40
- expect(fields).toBeInstanceOf(Array);
41
- expect(fields.length).toBeGreaterThan(0);
42
- expect(fields[0]).toHaveProperty("id");
43
- expect(fields[0]).toHaveProperty("label");
44
-
45
- expect(consoleSpy).toHaveBeenCalledWith(
46
- "Authorization error for getFields (user token), retrying with same token:",
47
- "" // Matches empty ResponseError message from mock
48
- );
49
- expect(consoleSpy).toHaveBeenCalledWith(
50
- "Retrying getFields with user token"
51
- );
52
- expect(mockFetch).toHaveBeenCalledTimes(2);
53
-
54
- consoleSpy.mockRestore();
55
- });
37
+ it(
38
+ "successfully retrieves fields with user token, retrying on transient 401 if needed",
39
+ async () => {
40
+ const consoleSpy = vi.spyOn(console, "log");
41
+
42
+ const tableId = process.env.QB_TABLE_ID_1!; // Match beforeEach
43
+ let fields;
44
+ try {
45
+ fields = await client.getFields({ tableId });
46
+ } catch (error) {
47
+ console.error("[integrationTest] Failed to retrieve fields:", error);
48
+ throw error;
49
+ }
50
+
51
+ expect(fields).toBeInstanceOf(Array);
52
+ expect(fields.length).toBeGreaterThan(0);
53
+ expect(fields[0]).toHaveProperty("id");
54
+ expect(fields[0]).toHaveProperty("label");
55
+
56
+ console.log("[integrationTest] Fields retrieved:", fields.length);
57
+
58
+ const retryLog = consoleSpy.mock.calls.some((call) =>
59
+ call[0].includes("Retrying getFields with existing user token")
60
+ );
61
+ if (retryLog) {
62
+ console.log("[integrationTest] Retry on 401 detected");
63
+ } else {
64
+ console.log(
65
+ "[integrationTest] No 401 retries observed (expected if no transient failure)"
66
+ );
67
+ }
68
+
69
+ consoleSpy.mockRestore();
70
+ },
71
+ { timeout: 10000 }
72
+ );
56
73
  });
@@ -0,0 +1,65 @@
1
+ // tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts
2
+
3
+ import { describe, it, expect, beforeAll, afterAll, vi } from "vitest"; // Add 'vi' to imports
4
+ import { quickbase } from "../../../../src/quickbaseClient";
5
+ import { startMockQuickbaseServer } from "./mockQuickbaseServer";
6
+
7
+ describe("QuickbaseClient Integration - SSO with Mock Server", () => {
8
+ let mockServer: ReturnType<typeof startMockQuickbaseServer>;
9
+ const mockBaseUrl = "http://localhost:3000";
10
+
11
+ beforeAll(async () => {
12
+ mockServer = startMockQuickbaseServer(3000);
13
+ await new Promise((resolve) => setTimeout(resolve, 500));
14
+ console.log("Server setup complete, port 3000 should be listening");
15
+ });
16
+
17
+ afterAll(() => {
18
+ mockServer.close();
19
+ console.log("Server closed");
20
+ });
21
+
22
+ it(
23
+ "uses SSO token from mock server and retries on 401",
24
+ { timeout: 10000 }, // Move timeout to second argument
25
+ async () => {
26
+ const consoleSpy = vi.spyOn(console, "log"); // Spy on console.log to capture logs
27
+ const client = quickbase({
28
+ realm: "testrealm",
29
+ samlToken: "mock-saml-token",
30
+ useSso: true,
31
+ fetchApi: (url, options) => {
32
+ const rewrittenUrl = url.replace(
33
+ "https://api.quickbase.com/v1",
34
+ mockBaseUrl + "/v1"
35
+ );
36
+ console.log(
37
+ "[fetchApi] Requesting:",
38
+ rewrittenUrl,
39
+ "Options:",
40
+ options
41
+ );
42
+ return fetch(rewrittenUrl, options);
43
+ },
44
+ debug: true,
45
+ });
46
+
47
+ await mockServer.toggleFail();
48
+ const response = await client.getApp({ appId: "mockappid" });
49
+
50
+ expect(response).toBeDefined();
51
+ expect(response.id).toBe("mockappid");
52
+ expect(response.name).toBe("Mock App");
53
+
54
+ // Verify retry happened
55
+ const retryLog = consoleSpy.mock.calls.some((call) =>
56
+ call[0].includes(
57
+ "Authorization error for getApp (SSO), refreshing token"
58
+ )
59
+ );
60
+ expect(retryLog).toBe(true); // Ensure retry logic was triggered
61
+
62
+ consoleSpy.mockRestore();
63
+ }
64
+ );
65
+ });