quickbase-js 0.1.0-beta.7

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 (123) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/LICENSE +21 -0
  3. package/README.md +204 -0
  4. package/build-common.js +131 -0
  5. package/build-esm.js +71 -0
  6. package/build-umd.js +69 -0
  7. package/dist/esm/quickbase.d.ts +7594 -0
  8. package/dist/esm/quickbase.js +9752 -0
  9. package/dist/esm/quickbase.js.map +1 -0
  10. package/dist/esm/quickbase.min.js +2 -0
  11. package/dist/esm/quickbase.min.js.map +1 -0
  12. package/dist/umd/quickbase.umd.js +10502 -0
  13. package/dist/umd/quickbase.umd.js.map +1 -0
  14. package/dist/umd/quickbase.umd.min.js +2 -0
  15. package/dist/umd/quickbase.umd.min.js.map +1 -0
  16. package/examples/quicbase-jsCDN.html +36 -0
  17. package/examples/react.js +48 -0
  18. package/examples/vanillajs.html +47 -0
  19. package/notes.txt +158 -0
  20. package/open-api/fix-spec-main.ts +96 -0
  21. package/open-api/generate-unified-interface.ts +277 -0
  22. package/open-api/output/override-report.json +58 -0
  23. package/open-api/output/quickbase-fixed.json +10610 -0
  24. package/open-api/regenerate-client.ts +116 -0
  25. package/open-api/schema/enhance-general.ts +100 -0
  26. package/open-api/schema/fix-arrays.ts +69 -0
  27. package/open-api/schema/tags/apps.ts +90 -0
  28. package/open-api/schema/tags/fields.ts +156 -0
  29. package/open-api/schema/tags/groups.ts +51 -0
  30. package/open-api/schema/tags/index.ts +68 -0
  31. package/open-api/schema/tags/records.ts +115 -0
  32. package/open-api/schema/tags/tables.ts +211 -0
  33. package/open-api/schema/tags/users.ts +47 -0
  34. package/open-api/types/spec.ts +41 -0
  35. package/open-api/utils/infer-schema.ts +40 -0
  36. package/open-api/utils/naming.ts +10 -0
  37. package/package.json +91 -0
  38. package/playwright.config.ts +37 -0
  39. package/rollup.config.js +114 -0
  40. package/specs/QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json +46878 -0
  41. package/specs/split/apps.json +1315 -0
  42. package/specs/split/auth.json +191 -0
  43. package/specs/split/fields.json +3869 -0
  44. package/specs/split/records.json +754 -0
  45. package/specs/split/reports.json +619 -0
  46. package/specs/split/tables.json +1605 -0
  47. package/src/generated-unified/QuickbaseClient.ts +359 -0
  48. package/src/generated-unified/missing-types-report.json +3 -0
  49. package/src/index.ts +1 -0
  50. package/src/quickbaseClient.ts +402 -0
  51. package/src/tokenCache.ts +51 -0
  52. package/src/utils.ts +7 -0
  53. package/test-esm.js +1 -0
  54. package/test-results.json +2050 -0
  55. package/test-umd.js +1 -0
  56. package/test.ts +1 -0
  57. package/tests/playwright/qb/auth/cacheSwitching.test.ts +159 -0
  58. package/tests/playwright/qb/auth/fetchTempToken401.test.ts +219 -0
  59. package/tests/playwright/qb/auth/getAppWithTempToken.test.ts +162 -0
  60. package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +153 -0
  61. package/tests/playwright/qb/auth/getTempToken.test.ts +152 -0
  62. package/tests/playwright/qb/auth/tokenPrefetch.test.ts +179 -0
  63. package/tests/playwright/qb/auth/tokenRenewal.test.ts +133 -0
  64. package/tests/setup.ts +51 -0
  65. package/tests/vitest/qb/apps/copyApp.test.ts +76 -0
  66. package/tests/vitest/qb/apps/createApp.test.ts +64 -0
  67. package/tests/vitest/qb/apps/deleteApp.test.ts +76 -0
  68. package/tests/vitest/qb/apps/getApp.test.ts +40 -0
  69. package/tests/vitest/qb/fields/createField.test.ts +78 -0
  70. package/tests/vitest/qb/fields/deleteFields.test.ts +89 -0
  71. package/tests/vitest/qb/fields/getField.test.ts +85 -0
  72. package/tests/vitest/qb/fields/getFieldUsage.test.ts +174 -0
  73. package/tests/vitest/qb/fields/getFields.test.ts +232 -0
  74. package/tests/vitest/qb/fields/getFieldsUsage.test.ts +77 -0
  75. package/tests/vitest/qb/index.ts +28 -0
  76. package/tests/vitest/qb/records/deleteRecords.test.ts +110 -0
  77. package/tests/vitest/qb/records/runQuery.test.ts +146 -0
  78. package/tests/vitest/qb/records/upsert.test.ts +102 -0
  79. package/tests/vitest/qb/tables/createTable.test.ts +55 -0
  80. package/tests/vitest/qb/tables/deleteTable.test.ts +50 -0
  81. package/tests/vitest/qb/tables/getAppTables.test.ts +46 -0
  82. package/tests/vitest/qb/tables/getRelationships.test.ts +80 -0
  83. package/tests/vitest/qb/tables/getTable.test.ts +26 -0
  84. package/tests/vitest/qb/tables/updateTable.test.ts +48 -0
  85. package/tests/vitest/unit/apps/copyApp.test.ts +488 -0
  86. package/tests/vitest/unit/apps/createApp.test.ts +186 -0
  87. package/tests/vitest/unit/apps/deleteApp.test.ts +216 -0
  88. package/tests/vitest/unit/apps/getApp.test.ts +128 -0
  89. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +82 -0
  90. package/tests/vitest/unit/auth/getTempToken.test.ts +95 -0
  91. package/tests/vitest/unit/auth/retryOn401.test.ts +116 -0
  92. package/tests/vitest/unit/auth/two401s.test.ts +86 -0
  93. package/tests/vitest/unit/fields/createField.test.ts +391 -0
  94. package/tests/vitest/unit/fields/deleteFields.test.ts +141 -0
  95. package/tests/vitest/unit/fields/getField.test.ts +189 -0
  96. package/tests/vitest/unit/fields/getFieldUsage.test.ts +147 -0
  97. package/tests/vitest/unit/fields/getFields.test.ts +122 -0
  98. package/tests/vitest/unit/fields/getFieldsUsage.test.ts +160 -0
  99. package/tests/vitest/unit/index.ts +33 -0
  100. package/tests/vitest/unit/records/deleteRecords.test.ts +112 -0
  101. package/tests/vitest/unit/records/runQuery.test.ts +102 -0
  102. package/tests/vitest/unit/records/upsert.test.ts +234 -0
  103. package/tests/vitest/unit/tables/createTable.test.ts +70 -0
  104. package/tests/vitest/unit/tables/deleteTable.test.ts +77 -0
  105. package/tests/vitest/unit/tables/getAppTables.test.ts +85 -0
  106. package/tests/vitest/unit/tables/getRelationships.test.ts +259 -0
  107. package/tests/vitest/unit/tables/getTable.test.ts +233 -0
  108. package/tests/vitest/unit/tables/updateTable.test.ts +80 -0
  109. package/tests/vitest/unit/umd.test.ts +8 -0
  110. package/text.txt +104 -0
  111. package/tools/generate-types.ts +29 -0
  112. package/tools/log-gen.ts +231 -0
  113. package/tools/log-gen.yaml +3826 -0
  114. package/tools/openapi-jars/openapi-generator-cli-7.12.0.jar +0 -0
  115. package/tools/openapi-schema-validator.ts +38 -0
  116. package/tools/spec-analyzer.ts +163 -0
  117. package/tools/split-spec.ts +148 -0
  118. package/tools/test-import.ts +3 -0
  119. package/tools/validate-spec.ts +40 -0
  120. package/tsconfig.build.json +17 -0
  121. package/tsconfig.json +29 -0
  122. package/versionrc.json +9 -0
  123. package/vitest.config.ts +27 -0
@@ -0,0 +1,216 @@
1
+ // tests/vitest/unit/apps/deleteApp.test.ts
2
+
3
+ import { describe, it, expect, beforeEach } from "vitest";
4
+ import {
5
+ createClient,
6
+ mockFetch,
7
+ QB_APP_ID,
8
+ QB_REALM,
9
+ QB_USER_TOKEN,
10
+ } from "@tests/setup.ts";
11
+ import { DeleteAppRequest, DeleteApp200Response } from "@/generated/models";
12
+
13
+ describe("QuickbaseClient Unit - deleteApp", () => {
14
+ let client: ReturnType<typeof createClient>;
15
+
16
+ beforeEach(() => {
17
+ mockFetch.mockClear();
18
+ });
19
+
20
+ it("initializes without errors", () => {
21
+ client = createClient(mockFetch, { debug: true });
22
+ expect(client).toBeDefined();
23
+ });
24
+
25
+ it("has deleteApp method", () => {
26
+ client = createClient(mockFetch, { debug: true });
27
+ expect(typeof client.deleteApp).toBe("function");
28
+ });
29
+
30
+ it("calls deleteApp successfully with user token", async () => {
31
+ client = createClient(mockFetch, { debug: true });
32
+
33
+ const request: DeleteAppRequest = {
34
+ name: "App to Delete",
35
+ };
36
+
37
+ const mockResponse: DeleteApp200Response = {
38
+ deletedAppId: QB_APP_ID,
39
+ };
40
+
41
+ mockFetch.mockResolvedValueOnce({
42
+ ok: true,
43
+ status: 200,
44
+ json: () => Promise.resolve(mockResponse),
45
+ });
46
+
47
+ const result = await client.deleteApp({ appId: QB_APP_ID, body: request });
48
+
49
+ expect(result).toEqual(mockResponse);
50
+ expect(mockFetch).toHaveBeenCalledWith(
51
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
52
+ expect.objectContaining({
53
+ method: "DELETE",
54
+ headers: expect.objectContaining({
55
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
56
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
57
+ "Content-Type": "application/json",
58
+ }),
59
+ body: JSON.stringify(request),
60
+ })
61
+ );
62
+ });
63
+
64
+ it("calls deleteApp successfully with temp token", async () => {
65
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
66
+
67
+ const request: DeleteAppRequest = {
68
+ name: "App to Delete with Temp Token",
69
+ };
70
+
71
+ const mockResponse: DeleteApp200Response = {
72
+ deletedAppId: QB_APP_ID,
73
+ };
74
+
75
+ mockFetch
76
+ .mockResolvedValueOnce({
77
+ ok: true,
78
+ status: 200,
79
+ json: () => Promise.resolve({ temporaryAuthorization: "temp_token" }),
80
+ })
81
+ .mockResolvedValueOnce({
82
+ ok: true,
83
+ status: 200,
84
+ json: () => Promise.resolve(mockResponse),
85
+ });
86
+
87
+ const result = await client.deleteApp({ appId: QB_APP_ID, body: request });
88
+
89
+ expect(result).toEqual(mockResponse);
90
+ expect(mockFetch).toHaveBeenCalledTimes(2);
91
+ expect(mockFetch).toHaveBeenNthCalledWith(
92
+ 1,
93
+ `https://api.quickbase.com/v1/auth/temporary/${QB_APP_ID}`,
94
+ expect.objectContaining({
95
+ method: "GET",
96
+ headers: expect.objectContaining({
97
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
98
+ "Content-Type": "application/json",
99
+ }),
100
+ credentials: "include",
101
+ })
102
+ );
103
+ expect(mockFetch).toHaveBeenNthCalledWith(
104
+ 2,
105
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
106
+ expect.objectContaining({
107
+ method: "DELETE",
108
+ headers: expect.objectContaining({
109
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
110
+ Authorization: "QB-TEMP-TOKEN temp_token",
111
+ "Content-Type": "application/json",
112
+ }),
113
+ body: JSON.stringify(request),
114
+ })
115
+ );
116
+ });
117
+
118
+ it("retries successfully after 401 with temp token", async () => {
119
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
120
+
121
+ const request: DeleteAppRequest = {
122
+ name: "Retry Delete App",
123
+ };
124
+
125
+ const mockResponse: DeleteApp200Response = {
126
+ deletedAppId: QB_APP_ID,
127
+ };
128
+
129
+ mockFetch
130
+ .mockResolvedValueOnce({
131
+ ok: true,
132
+ status: 200,
133
+ json: () =>
134
+ Promise.resolve({ temporaryAuthorization: "initial_token" }),
135
+ })
136
+ .mockResolvedValueOnce({
137
+ ok: false,
138
+ status: 401,
139
+ json: () => Promise.resolve({ message: "Unauthorized" }),
140
+ })
141
+ .mockResolvedValueOnce({
142
+ ok: true,
143
+ status: 200,
144
+ json: () => Promise.resolve({ temporaryAuthorization: "new_token" }),
145
+ })
146
+ .mockResolvedValueOnce({
147
+ ok: true,
148
+ status: 200,
149
+ json: () => Promise.resolve(mockResponse),
150
+ });
151
+
152
+ const result = await client.deleteApp({ appId: QB_APP_ID, body: request });
153
+
154
+ expect(result).toEqual(mockResponse);
155
+ expect(mockFetch).toHaveBeenCalledTimes(4);
156
+ expect(mockFetch).toHaveBeenNthCalledWith(
157
+ 1,
158
+ `https://api.quickbase.com/v1/auth/temporary/${QB_APP_ID}`,
159
+ expect.any(Object)
160
+ );
161
+ expect(mockFetch).toHaveBeenNthCalledWith(
162
+ 2,
163
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
164
+ expect.objectContaining({
165
+ headers: expect.objectContaining({
166
+ Authorization: "QB-TEMP-TOKEN initial_token",
167
+ }),
168
+ })
169
+ );
170
+ expect(mockFetch).toHaveBeenNthCalledWith(
171
+ 3,
172
+ `https://api.quickbase.com/v1/auth/temporary/${QB_APP_ID}`,
173
+ expect.any(Object)
174
+ );
175
+ expect(mockFetch).toHaveBeenNthCalledWith(
176
+ 4,
177
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
178
+ expect.objectContaining({
179
+ headers: expect.objectContaining({
180
+ Authorization: "QB-TEMP-TOKEN new_token",
181
+ }),
182
+ })
183
+ );
184
+ });
185
+
186
+ it("handles 400 error for invalid request", async () => {
187
+ client = createClient(mockFetch, { debug: true });
188
+
189
+ const request: DeleteAppRequest = {
190
+ name: "Wrong Name",
191
+ };
192
+
193
+ mockFetch.mockResolvedValueOnce({
194
+ ok: false,
195
+ status: 400,
196
+ json: () => Promise.resolve({ message: "Invalid application name" }),
197
+ });
198
+
199
+ await expect(
200
+ client.deleteApp({ appId: QB_APP_ID, body: request })
201
+ ).rejects.toThrow("API Error: Invalid application name (Status: 400)");
202
+
203
+ expect(mockFetch).toHaveBeenCalledWith(
204
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
205
+ expect.objectContaining({
206
+ method: "DELETE",
207
+ headers: expect.objectContaining({
208
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
209
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
210
+ "Content-Type": "application/json",
211
+ }),
212
+ body: JSON.stringify(request),
213
+ })
214
+ );
215
+ });
216
+ });
@@ -0,0 +1,128 @@
1
+ // tests/vitest/unit/apps/getApp.test.ts
2
+
3
+ import { describe, it, expect, beforeEach } from "vitest";
4
+ import {
5
+ createClient,
6
+ mockFetch,
7
+ QB_APP_ID,
8
+ QB_REALM,
9
+ QB_USER_TOKEN,
10
+ } from "@tests/setup.ts";
11
+
12
+ describe("QuickbaseClient Unit - getApp", () => {
13
+ let client: ReturnType<typeof createClient>;
14
+
15
+ beforeEach(() => {
16
+ mockFetch.mockClear();
17
+ });
18
+
19
+ it("initializes without errors", () => {
20
+ client = createClient(mockFetch, { debug: true });
21
+ expect(client).toBeDefined();
22
+ });
23
+
24
+ it("has getApp method", () => {
25
+ client = createClient(mockFetch, { debug: true });
26
+ expect(typeof client.getApp).toBe("function");
27
+ });
28
+
29
+ it("calls getApp successfully with dates as Date objects", async () => {
30
+ client = createClient(mockFetch, { debug: true });
31
+
32
+ const mockResponse = {
33
+ id: QB_APP_ID,
34
+ name: "qb-copy",
35
+ created: new Date("2025-02-13T18:22:33Z"), // Expect Date object
36
+ updated: new Date("2025-03-04T04:25:51Z"), // Expect Date object
37
+ description: "",
38
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
39
+ dateFormat: "MM-DD-YYYY",
40
+ hasEveryoneOnTheInternet: false,
41
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
42
+ securityProperties: {
43
+ allowClone: false,
44
+ allowExport: true,
45
+ enableAppTokens: true,
46
+ hideFromPublic: false,
47
+ mustBeRealmApproved: false,
48
+ useIPFilter: false,
49
+ },
50
+ ancestorId: undefined,
51
+ variables: undefined,
52
+ dataClassification: undefined,
53
+ };
54
+
55
+ mockFetch.mockResolvedValueOnce({
56
+ ok: true,
57
+ status: 200,
58
+ json: () => Promise.resolve(mockResponse),
59
+ });
60
+
61
+ const result = await client.getApp({ appId: QB_APP_ID });
62
+
63
+ expect(result).toEqual(mockResponse);
64
+ expect(mockFetch).toHaveBeenCalledWith(
65
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
66
+ expect.objectContaining({
67
+ method: "GET",
68
+ headers: expect.objectContaining({
69
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
70
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
71
+ "Content-Type": "application/json",
72
+ }),
73
+ body: undefined,
74
+ credentials: "omit",
75
+ })
76
+ );
77
+ });
78
+
79
+ it("calls getApp successfully with dates as strings", async () => {
80
+ client = createClient(mockFetch, { debug: true, convertDates: false });
81
+
82
+ const mockResponse = {
83
+ id: QB_APP_ID,
84
+ name: "qb-copy",
85
+ created: "2025-02-13T18:22:33Z", // Expect string
86
+ updated: "2025-03-04T04:25:51Z", // Expect string
87
+ description: "",
88
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
89
+ dateFormat: "MM-DD-YYYY",
90
+ hasEveryoneOnTheInternet: false,
91
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
92
+ securityProperties: {
93
+ allowClone: false,
94
+ allowExport: true,
95
+ enableAppTokens: true,
96
+ hideFromPublic: false,
97
+ mustBeRealmApproved: false,
98
+ useIPFilter: false,
99
+ },
100
+ ancestorId: undefined,
101
+ variables: undefined,
102
+ dataClassification: undefined,
103
+ };
104
+
105
+ mockFetch.mockResolvedValueOnce({
106
+ ok: true,
107
+ status: 200,
108
+ json: () => Promise.resolve(mockResponse),
109
+ });
110
+
111
+ const result = await client.getApp({ appId: QB_APP_ID });
112
+
113
+ expect(result).toEqual(mockResponse);
114
+ expect(mockFetch).toHaveBeenCalledWith(
115
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
116
+ expect.objectContaining({
117
+ method: "GET",
118
+ headers: expect.objectContaining({
119
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
120
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
121
+ "Content-Type": "application/json",
122
+ }),
123
+ body: undefined,
124
+ credentials: "omit",
125
+ })
126
+ );
127
+ });
128
+ });
@@ -0,0 +1,82 @@
1
+ // tests/vitest/unit/auth/fetchTempToken401.test.ts
2
+ import { describe, it, expect, beforeEach, vi } from "vitest";
3
+ import {
4
+ createClient,
5
+ mockFetch,
6
+ QB_REALM,
7
+ QB_TABLE_ID_1,
8
+ } from "@tests/setup.ts";
9
+
10
+ describe("QuickbaseClient Unit - 401 with fetchTempToken 401", () => {
11
+ let client: ReturnType<typeof createClient>;
12
+
13
+ beforeEach(() => {
14
+ mockFetch.mockClear();
15
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
16
+ });
17
+
18
+ it("fails without infinite loop when fetchTempToken returns 401 after initial 401", async () => {
19
+ let callCount = 0;
20
+
21
+ mockFetch.mockImplementation((url) => {
22
+ callCount++;
23
+ if (url.includes("auth/temporary") && callCount === 1) {
24
+ return Promise.resolve({
25
+ ok: true,
26
+ status: 200,
27
+ json: () =>
28
+ Promise.resolve({ temporaryAuthorization: "initial_token" }),
29
+ });
30
+ }
31
+ if (url.includes("fields") && callCount === 2) {
32
+ return Promise.resolve({
33
+ ok: false,
34
+ status: 401,
35
+ json: () => Promise.resolve({ message: "Unauthorized" }),
36
+ });
37
+ }
38
+ if (url.includes("auth/temporary") && callCount === 3) {
39
+ return Promise.resolve({
40
+ ok: false,
41
+ status: 401,
42
+ json: () =>
43
+ Promise.resolve({ message: "Unauthorized in fetchTempToken" }),
44
+ });
45
+ }
46
+ return Promise.reject(new Error(`Unexpected fetch call: ${url}`));
47
+ });
48
+
49
+ const consoleSpy = vi.spyOn(console, "log");
50
+
51
+ await expect(client.getFields({ tableId: QB_TABLE_ID_1 })).rejects.toThrow(
52
+ "API Error: Unauthorized in fetchTempToken (Status: 401)"
53
+ );
54
+
55
+ expect(mockFetch).toHaveBeenCalledTimes(3);
56
+ expect(mockFetch).toHaveBeenNthCalledWith(
57
+ 1,
58
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
59
+ expect.any(Object)
60
+ );
61
+ expect(mockFetch).toHaveBeenNthCalledWith(
62
+ 2,
63
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
64
+ expect.objectContaining({
65
+ headers: expect.objectContaining({
66
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
67
+ }),
68
+ })
69
+ );
70
+ expect(mockFetch).toHaveBeenNthCalledWith(
71
+ 3,
72
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
73
+ expect.any(Object)
74
+ );
75
+
76
+ expect(consoleSpy).toHaveBeenCalledWith(
77
+ "Authorization error for getFields, refreshing token:",
78
+ expect.any(String)
79
+ );
80
+ consoleSpy.mockRestore();
81
+ });
82
+ });
@@ -0,0 +1,95 @@
1
+ // tests/vitest/unit/auth/getTempTokenDBID.test.ts
2
+ import { describe, it, expect, beforeEach } from "vitest";
3
+ import {
4
+ createClient,
5
+ mockFetch,
6
+ QB_REALM,
7
+ QB_TABLE_ID_1,
8
+ } from "@tests/setup.ts";
9
+
10
+ describe("QuickbaseClient Unit - getTempTokenDBID", () => {
11
+ let client: ReturnType<typeof createClient>;
12
+
13
+ beforeEach(() => {
14
+ mockFetch.mockClear();
15
+ client = createClient(mockFetch, { useTempTokens: true, debug: false });
16
+ });
17
+
18
+ it("initializes without errors", () => {
19
+ expect(client).toBeDefined();
20
+ });
21
+
22
+ it("has getTempTokenDBID method", () => {
23
+ expect(typeof client.getTempTokenDBID).toBe("function");
24
+ });
25
+
26
+ it("fetches and caches temp token on first call", async () => {
27
+ const mockToken = "b123xyz_temp_token";
28
+
29
+ mockFetch.mockResolvedValueOnce({
30
+ ok: true,
31
+ status: 200,
32
+ json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
33
+ });
34
+
35
+ const result = await client.getTempTokenDBID({ dbid: QB_TABLE_ID_1 });
36
+ expect(result).toEqual({ temporaryAuthorization: mockToken });
37
+ expect(mockFetch).toHaveBeenCalledWith(
38
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
39
+ expect.objectContaining({
40
+ method: "GET",
41
+ headers: expect.objectContaining({
42
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
43
+ "Content-Type": "application/json",
44
+ }),
45
+ credentials: "include",
46
+ })
47
+ );
48
+ });
49
+
50
+ it("reuses cached temp token on second call", async () => {
51
+ const mockToken = "b123xyz_temp_token";
52
+
53
+ mockFetch.mockResolvedValueOnce({
54
+ ok: true,
55
+ status: 200,
56
+ json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
57
+ });
58
+
59
+ const firstResult = await client.getTempTokenDBID({ dbid: QB_TABLE_ID_1 });
60
+ expect(firstResult).toEqual({ temporaryAuthorization: mockToken });
61
+ expect(mockFetch).toHaveBeenCalledTimes(1);
62
+
63
+ mockFetch.mockClear();
64
+ const secondResult = await client.getTempTokenDBID({ dbid: QB_TABLE_ID_1 });
65
+ expect(secondResult).toEqual({ temporaryAuthorization: mockToken });
66
+ expect(mockFetch).not.toHaveBeenCalled();
67
+ });
68
+
69
+ it("handles API error", async () => {
70
+ mockFetch.mockResolvedValueOnce({
71
+ ok: false,
72
+ status: 401,
73
+ json: () => Promise.resolve({ message: "Unauthorized" }),
74
+ });
75
+
76
+ await expect(
77
+ client.getTempTokenDBID({ dbid: QB_TABLE_ID_1 })
78
+ ).rejects.toSatisfy((error: Error) => {
79
+ expect(error).toBeInstanceOf(Error);
80
+ expect(error.message).toBe("API Error: Unauthorized (Status: 401)");
81
+ return true;
82
+ });
83
+ expect(mockFetch).toHaveBeenCalledWith(
84
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
85
+ expect.objectContaining({
86
+ method: "GET",
87
+ headers: expect.objectContaining({
88
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
89
+ "Content-Type": "application/json",
90
+ }),
91
+ credentials: "include",
92
+ })
93
+ );
94
+ });
95
+ });
@@ -0,0 +1,116 @@
1
+ // tests/vitest/unit/auth/retryOn401.test.ts
2
+ import { describe, it, expect, beforeEach, vi } from "vitest";
3
+ import {
4
+ createClient,
5
+ mockFetch,
6
+ QB_REALM,
7
+ QB_TABLE_ID_1,
8
+ } from "@tests/setup.ts";
9
+
10
+ describe("QuickbaseClient Unit - 401 Retry Creates New Token", () => {
11
+ let client: ReturnType<typeof createClient>;
12
+
13
+ beforeEach(() => {
14
+ mockFetch.mockClear();
15
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
16
+ });
17
+
18
+ it("creates a new token on 401 and retries successfully", async () => {
19
+ const mockToken = "new_token_456";
20
+ const mockFields = [{ id: 1, label: "Field1" }];
21
+ let callCount = 0;
22
+
23
+ // Mock fetch without console.log interference
24
+ mockFetch.mockImplementation((url) => {
25
+ callCount++;
26
+ if (url.includes("auth/temporary") && callCount === 1) {
27
+ return Promise.resolve({
28
+ ok: true,
29
+ status: 200,
30
+ json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
31
+ });
32
+ }
33
+ if (url.includes("fields") && callCount === 2) {
34
+ return Promise.resolve({
35
+ ok: false,
36
+ status: 401,
37
+ json: () => Promise.resolve({ message: "Unauthorized" }),
38
+ });
39
+ }
40
+ if (url.includes("auth/temporary") && callCount === 3) {
41
+ return Promise.resolve({
42
+ ok: true,
43
+ status: 200,
44
+ json: () =>
45
+ Promise.resolve({ temporaryAuthorization: mockToken + "_retry" }),
46
+ });
47
+ }
48
+ if (url.includes("fields") && callCount === 4) {
49
+ return Promise.resolve({
50
+ ok: true,
51
+ status: 200,
52
+ json: () => Promise.resolve(mockFields),
53
+ });
54
+ }
55
+ return Promise.reject(new Error(`Unexpected fetch call: ${url}`));
56
+ });
57
+
58
+ const consoleSpy = vi.spyOn(console, "log");
59
+
60
+ const result = await client.getFields({ tableId: QB_TABLE_ID_1 });
61
+
62
+ expect(result).toEqual(mockFields);
63
+ expect(mockFetch).toHaveBeenCalledTimes(4);
64
+ expect(mockFetch).toHaveBeenNthCalledWith(
65
+ 1,
66
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
67
+ expect.any(Object)
68
+ );
69
+ expect(mockFetch).toHaveBeenNthCalledWith(
70
+ 2,
71
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
72
+ expect.objectContaining({
73
+ headers: expect.objectContaining({
74
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
75
+ }),
76
+ })
77
+ );
78
+ expect(mockFetch).toHaveBeenNthCalledWith(
79
+ 3,
80
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
81
+ expect.any(Object)
82
+ );
83
+ expect(mockFetch).toHaveBeenNthCalledWith(
84
+ 4,
85
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
86
+ expect.objectContaining({
87
+ headers: expect.objectContaining({
88
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
89
+ }),
90
+ })
91
+ );
92
+
93
+ // Filter console logs to only those we care about
94
+ const tokenLogs = consoleSpy.mock.calls.filter((call) =>
95
+ call[0].includes("Fetched and cached new token")
96
+ );
97
+ expect(tokenLogs).toContainEqual([
98
+ `Fetched and cached new token for dbid: ${QB_TABLE_ID_1}`,
99
+ mockToken,
100
+ ]);
101
+ expect(tokenLogs).toContainEqual([
102
+ `Fetched and cached new token for dbid: ${QB_TABLE_ID_1}`,
103
+ mockToken + "_retry",
104
+ ]);
105
+
106
+ expect(consoleSpy).toHaveBeenCalledWith(
107
+ "Authorization error for getFields, refreshing token:",
108
+ expect.any(String)
109
+ );
110
+ expect(consoleSpy).toHaveBeenCalledWith(
111
+ "Retrying getFields with new token"
112
+ );
113
+
114
+ consoleSpy.mockRestore();
115
+ });
116
+ });