quickbase-js 1.0.1-beta.0
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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/build.js +66 -0
- package/dist/quickbaseClient.d.ts +3888 -0
- package/dist/quickbaseClient.js +5812 -0
- package/dist/quickbaseClient.js.map +1 -0
- package/examples/react.js +22 -0
- package/examples/vanillajs.html +23 -0
- package/logGen.yaml +1820 -0
- package/package.json +72 -0
- package/playwright.config.ts +37 -0
- package/rollup.config.js +70 -0
- package/src/code-generation/fix-spec-definitions.ts +136 -0
- package/src/code-generation/fix-spec-main.ts +176 -0
- package/src/code-generation/fix-spec-paths.ts +163 -0
- package/src/code-generation/generate-unified-interface.ts +152 -0
- package/src/code-generation/output/quickbase-fixed.json +8435 -0
- package/src/code-generation/regenerate-client.ts +110 -0
- package/src/generated-unified/QuickbaseClient.ts +59 -0
- package/src/index.ts +1 -0
- package/src/log-gen.ts +122 -0
- package/src/quickbaseClient.ts +320 -0
- package/src/specs/QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json +46878 -0
- package/src/tokenCache.ts +51 -0
- package/src/utils.ts +5 -0
- package/test-artifacts/.last-run.json +4 -0
- package/test-artifacts/test-results.json +1801 -0
- package/test-results.json +2050 -0
- package/tests/playwright/qb/auth/cacheSwitching.test.ts +189 -0
- package/tests/playwright/qb/auth/fetchTempToken401.test.ts +217 -0
- package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +151 -0
- package/tests/playwright/qb/auth/getTempToken.test.ts +180 -0
- package/tests/playwright/qb/auth/tokenPrefetch.test.ts +192 -0
- package/tests/playwright/qb/auth/tokenRenewal.test.ts +110 -0
- package/tests/setup.ts +19 -0
- package/tests/vitest/qb/apps/getApp.test.ts +46 -0
- package/tests/vitest/qb/fields/getFields.test.ts +234 -0
- package/tests/vitest/qb/index.ts +4 -0
- package/tests/vitest/qb/tables/getAppTables.test.ts +67 -0
- package/tests/vitest/unit/apps/getApp.test.ts +81 -0
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +59 -0
- package/tests/vitest/unit/auth/getTempToken.test.ts +83 -0
- package/tests/vitest/unit/auth/retryOn401.test.ts +72 -0
- package/tests/vitest/unit/auth/two401s.test.ts +77 -0
- package/tests/vitest/unit/fields/getFields.test.ts +115 -0
- package/tests/vitest/unit/index.ts +4 -0
- package/tests/vitest/unit/tables/getAppTables.test.ts +72 -0
- package/text.txt +104 -0
- package/tsconfig.json +28 -0
- package/versionrc.json +9 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createClient } from "../../../setup.ts";
|
|
3
|
+
|
|
4
|
+
describe("QuickbaseClient Integration - getFields", () => {
|
|
5
|
+
const client = createClient();
|
|
6
|
+
|
|
7
|
+
it(
|
|
8
|
+
"fetches real field data from QuickBase",
|
|
9
|
+
async () => {
|
|
10
|
+
const tableId = "buwai2z3s";
|
|
11
|
+
if (!tableId) throw new Error("Table ID is not defined");
|
|
12
|
+
if (!process.env.QB_REALM)
|
|
13
|
+
throw new Error("QB_REALM is not defined in .env");
|
|
14
|
+
if (!process.env.QB_USER_TOKEN)
|
|
15
|
+
throw new Error("QB_USER_TOKEN is not defined in .env");
|
|
16
|
+
|
|
17
|
+
console.log("Config used:", {
|
|
18
|
+
realm: process.env.QB_REALM,
|
|
19
|
+
userToken: process.env.QB_USER_TOKEN,
|
|
20
|
+
tableId,
|
|
21
|
+
});
|
|
22
|
+
const result = await client.getFields({
|
|
23
|
+
tableId,
|
|
24
|
+
includeFieldPerms: true,
|
|
25
|
+
});
|
|
26
|
+
console.log("Real API response:", result);
|
|
27
|
+
expect(result).toEqual(
|
|
28
|
+
expect.arrayContaining([
|
|
29
|
+
expect.objectContaining({
|
|
30
|
+
id: 1,
|
|
31
|
+
label: "Date Created",
|
|
32
|
+
fieldType: "timestamp",
|
|
33
|
+
noWrap: true,
|
|
34
|
+
bold: false,
|
|
35
|
+
required: false,
|
|
36
|
+
appearsByDefault: false,
|
|
37
|
+
findEnabled: false,
|
|
38
|
+
unique: false,
|
|
39
|
+
doesDataCopy: false,
|
|
40
|
+
fieldHelp: "",
|
|
41
|
+
audited: false,
|
|
42
|
+
properties: expect.objectContaining({
|
|
43
|
+
primaryKey: false,
|
|
44
|
+
foreignKey: false,
|
|
45
|
+
sortAsGiven: true,
|
|
46
|
+
carryChoices: true,
|
|
47
|
+
allowNewChoices: false,
|
|
48
|
+
formula: "",
|
|
49
|
+
defaultValue: "",
|
|
50
|
+
}),
|
|
51
|
+
permissions: expect.arrayContaining([
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
permissionType: "Modify",
|
|
54
|
+
role: "Viewer",
|
|
55
|
+
roleId: 10,
|
|
56
|
+
}),
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
permissionType: "Modify",
|
|
59
|
+
role: "Participant",
|
|
60
|
+
roleId: 11,
|
|
61
|
+
}),
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
permissionType: "Modify",
|
|
64
|
+
role: "Administrator",
|
|
65
|
+
roleId: 12,
|
|
66
|
+
}),
|
|
67
|
+
]),
|
|
68
|
+
}),
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
id: 2,
|
|
71
|
+
label: "Date Modified",
|
|
72
|
+
fieldType: "timestamp",
|
|
73
|
+
noWrap: true,
|
|
74
|
+
bold: false,
|
|
75
|
+
required: false,
|
|
76
|
+
appearsByDefault: false,
|
|
77
|
+
findEnabled: false,
|
|
78
|
+
unique: false,
|
|
79
|
+
doesDataCopy: false,
|
|
80
|
+
fieldHelp: "",
|
|
81
|
+
audited: false,
|
|
82
|
+
properties: expect.objectContaining({
|
|
83
|
+
primaryKey: false,
|
|
84
|
+
foreignKey: false,
|
|
85
|
+
sortAsGiven: true,
|
|
86
|
+
carryChoices: true,
|
|
87
|
+
allowNewChoices: false,
|
|
88
|
+
formula: "",
|
|
89
|
+
defaultValue: "",
|
|
90
|
+
}),
|
|
91
|
+
permissions: expect.arrayContaining([
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
permissionType: "Modify",
|
|
94
|
+
role: "Viewer",
|
|
95
|
+
roleId: 10,
|
|
96
|
+
}),
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
permissionType: "Modify",
|
|
99
|
+
role: "Participant",
|
|
100
|
+
roleId: 11,
|
|
101
|
+
}),
|
|
102
|
+
expect.objectContaining({
|
|
103
|
+
permissionType: "Modify",
|
|
104
|
+
role: "Administrator",
|
|
105
|
+
roleId: 12,
|
|
106
|
+
}),
|
|
107
|
+
]),
|
|
108
|
+
}),
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
id: 3,
|
|
111
|
+
label: "Record ID#",
|
|
112
|
+
fieldType: "recordid",
|
|
113
|
+
noWrap: true,
|
|
114
|
+
bold: false,
|
|
115
|
+
required: false,
|
|
116
|
+
appearsByDefault: false,
|
|
117
|
+
findEnabled: false,
|
|
118
|
+
unique: true,
|
|
119
|
+
doesDataCopy: false,
|
|
120
|
+
fieldHelp: "",
|
|
121
|
+
audited: false,
|
|
122
|
+
properties: expect.objectContaining({
|
|
123
|
+
primaryKey: true,
|
|
124
|
+
foreignKey: false,
|
|
125
|
+
sortAsGiven: true,
|
|
126
|
+
carryChoices: true,
|
|
127
|
+
allowNewChoices: false,
|
|
128
|
+
formula: "",
|
|
129
|
+
defaultValue: "",
|
|
130
|
+
}),
|
|
131
|
+
permissions: expect.arrayContaining([
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
permissionType: "Modify",
|
|
134
|
+
role: "Viewer",
|
|
135
|
+
roleId: 10,
|
|
136
|
+
}),
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
permissionType: "Modify",
|
|
139
|
+
role: "Participant",
|
|
140
|
+
roleId: 11,
|
|
141
|
+
}),
|
|
142
|
+
expect.objectContaining({
|
|
143
|
+
permissionType: "Modify",
|
|
144
|
+
role: "Administrator",
|
|
145
|
+
roleId: 12,
|
|
146
|
+
}),
|
|
147
|
+
]),
|
|
148
|
+
}),
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
id: 4,
|
|
151
|
+
label: "Record Owner",
|
|
152
|
+
fieldType: "user",
|
|
153
|
+
noWrap: true,
|
|
154
|
+
bold: false,
|
|
155
|
+
required: false,
|
|
156
|
+
appearsByDefault: false,
|
|
157
|
+
findEnabled: true,
|
|
158
|
+
unique: false,
|
|
159
|
+
doesDataCopy: false,
|
|
160
|
+
fieldHelp: "",
|
|
161
|
+
audited: false,
|
|
162
|
+
properties: expect.objectContaining({
|
|
163
|
+
primaryKey: false,
|
|
164
|
+
foreignKey: false,
|
|
165
|
+
sortAsGiven: true,
|
|
166
|
+
carryChoices: true,
|
|
167
|
+
allowNewChoices: true,
|
|
168
|
+
formula: "",
|
|
169
|
+
defaultValue: "",
|
|
170
|
+
}),
|
|
171
|
+
permissions: expect.arrayContaining([
|
|
172
|
+
expect.objectContaining({
|
|
173
|
+
permissionType: "Modify",
|
|
174
|
+
role: "Viewer",
|
|
175
|
+
roleId: 10,
|
|
176
|
+
}),
|
|
177
|
+
expect.objectContaining({
|
|
178
|
+
permissionType: "Modify",
|
|
179
|
+
role: "Participant",
|
|
180
|
+
roleId: 11,
|
|
181
|
+
}),
|
|
182
|
+
expect.objectContaining({
|
|
183
|
+
permissionType: "Modify",
|
|
184
|
+
role: "Administrator",
|
|
185
|
+
roleId: 12,
|
|
186
|
+
}),
|
|
187
|
+
]),
|
|
188
|
+
}),
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
id: 5,
|
|
191
|
+
label: "Last Modified By",
|
|
192
|
+
fieldType: "user",
|
|
193
|
+
noWrap: true,
|
|
194
|
+
bold: false,
|
|
195
|
+
required: false,
|
|
196
|
+
appearsByDefault: false,
|
|
197
|
+
findEnabled: true,
|
|
198
|
+
unique: false,
|
|
199
|
+
doesDataCopy: false,
|
|
200
|
+
fieldHelp: "",
|
|
201
|
+
audited: false,
|
|
202
|
+
properties: expect.objectContaining({
|
|
203
|
+
primaryKey: false,
|
|
204
|
+
foreignKey: false,
|
|
205
|
+
sortAsGiven: true,
|
|
206
|
+
carryChoices: true,
|
|
207
|
+
allowNewChoices: true,
|
|
208
|
+
formula: "",
|
|
209
|
+
defaultValue: "",
|
|
210
|
+
}),
|
|
211
|
+
permissions: expect.arrayContaining([
|
|
212
|
+
expect.objectContaining({
|
|
213
|
+
permissionType: "Modify",
|
|
214
|
+
role: "Viewer",
|
|
215
|
+
roleId: 10,
|
|
216
|
+
}),
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
permissionType: "Modify",
|
|
219
|
+
role: "Participant",
|
|
220
|
+
roleId: 11,
|
|
221
|
+
}),
|
|
222
|
+
expect.objectContaining({
|
|
223
|
+
permissionType: "Modify",
|
|
224
|
+
role: "Administrator",
|
|
225
|
+
roleId: 12,
|
|
226
|
+
}),
|
|
227
|
+
]),
|
|
228
|
+
}),
|
|
229
|
+
])
|
|
230
|
+
);
|
|
231
|
+
},
|
|
232
|
+
{ timeout: 10000 }
|
|
233
|
+
);
|
|
234
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createClient } from "../../../setup.ts";
|
|
3
|
+
|
|
4
|
+
describe("QuickbaseClient Integration - getAppTables", () => {
|
|
5
|
+
const client = createClient();
|
|
6
|
+
|
|
7
|
+
it(
|
|
8
|
+
"fetches real table data from QuickBase",
|
|
9
|
+
async () => {
|
|
10
|
+
const tablesAppId = process.env.QB_APP_ID;
|
|
11
|
+
if (!tablesAppId) throw new Error("QB_APP_ID is not defined in .env");
|
|
12
|
+
if (!process.env.QB_REALM)
|
|
13
|
+
throw new Error("QB_REALM is not defined in .env");
|
|
14
|
+
if (!process.env.QB_USER_TOKEN)
|
|
15
|
+
throw new Error("QB_USER_TOKEN is not defined in .env");
|
|
16
|
+
|
|
17
|
+
console.log("Config used:", {
|
|
18
|
+
realm: process.env.QB_REALM,
|
|
19
|
+
userToken: process.env.QB_USER_TOKEN,
|
|
20
|
+
appId: tablesAppId,
|
|
21
|
+
});
|
|
22
|
+
const result = await client.getAppTables({ appId: tablesAppId });
|
|
23
|
+
console.log("Real API response:", result);
|
|
24
|
+
expect(result).toEqual(
|
|
25
|
+
expect.arrayContaining([
|
|
26
|
+
expect.objectContaining({
|
|
27
|
+
alias: "_DBID_ROOT",
|
|
28
|
+
created: new Date("2025-02-13T18:22:33Z"),
|
|
29
|
+
defaultSortFieldId: 2,
|
|
30
|
+
defaultSortOrder: "DESC",
|
|
31
|
+
description: "",
|
|
32
|
+
id: "buwai2zr4",
|
|
33
|
+
keyFieldId: 3,
|
|
34
|
+
name: "Root",
|
|
35
|
+
nextFieldId: 6,
|
|
36
|
+
nextRecordId: 1,
|
|
37
|
+
pluralRecordName: "Roots",
|
|
38
|
+
singleRecordName: "Root",
|
|
39
|
+
sizeLimit: "500 MB",
|
|
40
|
+
spaceRemaining: "500 MB",
|
|
41
|
+
spaceUsed: "0 KB",
|
|
42
|
+
updated: new Date("2025-02-13T18:22:34Z"),
|
|
43
|
+
}),
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
alias: "_DBID_ROLES",
|
|
46
|
+
created: new Date("2025-02-13T18:22:33Z"),
|
|
47
|
+
defaultSortFieldId: 2,
|
|
48
|
+
defaultSortOrder: "DESC",
|
|
49
|
+
description: "",
|
|
50
|
+
id: "buwai2z3s",
|
|
51
|
+
keyFieldId: 3,
|
|
52
|
+
name: "Roles",
|
|
53
|
+
nextFieldId: 6,
|
|
54
|
+
nextRecordId: 1,
|
|
55
|
+
pluralRecordName: "Roles",
|
|
56
|
+
singleRecordName: "Role",
|
|
57
|
+
sizeLimit: "500 MB",
|
|
58
|
+
spaceRemaining: "500 MB",
|
|
59
|
+
spaceUsed: "0 KB",
|
|
60
|
+
updated: new Date("2025-02-13T18:22:34Z"),
|
|
61
|
+
}),
|
|
62
|
+
])
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
{ timeout: 10000 }
|
|
66
|
+
);
|
|
67
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createClient, mockFetch } from "@/tests/setup.ts";
|
|
3
|
+
|
|
4
|
+
describe("QuickbaseClient - getApp (Unit)", () => {
|
|
5
|
+
const client = createClient(mockFetch);
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockFetch.mockClear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("initializes without errors", () => {
|
|
12
|
+
expect(client).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("has getApp method", () => {
|
|
16
|
+
expect(typeof client.getApp).toBe("function");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("calls getApp successfully", async () => {
|
|
20
|
+
mockFetch.mockImplementation((url: string, options: any) => {
|
|
21
|
+
console.log("Mock fetch:", url, options);
|
|
22
|
+
return Promise.resolve({
|
|
23
|
+
ok: true,
|
|
24
|
+
status: 200,
|
|
25
|
+
json: () =>
|
|
26
|
+
Promise.resolve({
|
|
27
|
+
id: process.env.QB_APP_ID,
|
|
28
|
+
name: "qb-copy",
|
|
29
|
+
created: "2025-02-13T18:22:33Z",
|
|
30
|
+
updated: "2025-03-04T04:25:51Z",
|
|
31
|
+
description: "",
|
|
32
|
+
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
33
|
+
dateFormat: "MM-DD-YYYY",
|
|
34
|
+
hasEveryoneOnTheInternet: false,
|
|
35
|
+
memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
|
|
36
|
+
securityProperties: {
|
|
37
|
+
allowClone: false,
|
|
38
|
+
allowExport: true,
|
|
39
|
+
enableAppTokens: true,
|
|
40
|
+
hideFromPublic: false,
|
|
41
|
+
mustBeRealmApproved: false,
|
|
42
|
+
useIPFilter: false,
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
} as Response);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const getAppId = process.env.QB_APP_ID;
|
|
49
|
+
if (!getAppId) throw new Error("QB_APP_ID is not defined in .env");
|
|
50
|
+
console.log("Test appId:", getAppId);
|
|
51
|
+
const result = await client.getApp({ appId: getAppId });
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
id: getAppId,
|
|
54
|
+
name: "qb-copy",
|
|
55
|
+
created: new Date("2025-02-13T18:22:33Z"),
|
|
56
|
+
updated: new Date("2025-03-04T04:25:51Z"),
|
|
57
|
+
description: "",
|
|
58
|
+
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
59
|
+
dateFormat: "MM-DD-YYYY",
|
|
60
|
+
hasEveryoneOnTheInternet: false,
|
|
61
|
+
memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
|
|
62
|
+
securityProperties: {
|
|
63
|
+
allowClone: false,
|
|
64
|
+
allowExport: true,
|
|
65
|
+
enableAppTokens: true,
|
|
66
|
+
hideFromPublic: false,
|
|
67
|
+
mustBeRealmApproved: false,
|
|
68
|
+
useIPFilter: false,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
72
|
+
`https://api.quickbase.com/v1/apps/${getAppId}`,
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
headers: expect.objectContaining({
|
|
75
|
+
Authorization: `QB-USER-TOKEN ${process.env.QB_USER_TOKEN}`,
|
|
76
|
+
"QB-Realm-Hostname": `${process.env.QB_REALM}.quickbase.com`,
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { createClient, mockFetch } from "@/tests/setup.ts";
|
|
3
|
+
|
|
4
|
+
describe("QuickbaseClient - 401 with fetchTempToken 401", () => {
|
|
5
|
+
let client: ReturnType<typeof createClient>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockFetch.mockClear();
|
|
9
|
+
client = createClient(mockFetch, { useTempTokens: true, debug: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("fails without infinite loop when fetchTempToken returns 401 after initial 401", async () => {
|
|
13
|
+
const mockDbid = "mockDbid123";
|
|
14
|
+
let callCount = 0;
|
|
15
|
+
|
|
16
|
+
mockFetch.mockImplementation((url) => {
|
|
17
|
+
callCount++;
|
|
18
|
+
console.log(`Mock fetch call ${callCount}: ${url}`);
|
|
19
|
+
if (url.includes("auth/temporary") && callCount === 1) {
|
|
20
|
+
return Promise.resolve({
|
|
21
|
+
ok: true,
|
|
22
|
+
status: 200,
|
|
23
|
+
json: () =>
|
|
24
|
+
Promise.resolve({ temporaryAuthorization: "initial_token" }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (url.includes("fields") && callCount === 2) {
|
|
28
|
+
return Promise.resolve({
|
|
29
|
+
ok: false,
|
|
30
|
+
status: 401,
|
|
31
|
+
text: () => Promise.resolve("Unauthorized"),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (url.includes("auth/temporary") && callCount === 3) {
|
|
35
|
+
return Promise.resolve({
|
|
36
|
+
ok: false,
|
|
37
|
+
status: 401,
|
|
38
|
+
text: () => Promise.resolve("Unauthorized in fetchTempToken"),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return Promise.reject(new Error(`Unexpected fetch call: ${url}`));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
45
|
+
await expect(client.getFields({ tableId: mockDbid })).rejects.toThrow(
|
|
46
|
+
"API Error: Unauthorized in fetchTempToken (Status: 401)"
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
50
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
51
|
+
"Authorization error for getFields, refreshing token:",
|
|
52
|
+
expect.any(String)
|
|
53
|
+
);
|
|
54
|
+
expect(consoleSpy).not.toHaveBeenCalledWith(
|
|
55
|
+
"Retrying getFields with new token"
|
|
56
|
+
);
|
|
57
|
+
consoleSpy.mockRestore();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createClient, mockFetch } from "@/tests/setup.ts";
|
|
3
|
+
|
|
4
|
+
describe("QuickbaseClient - getTempTokenDBID (Unit)", () => {
|
|
5
|
+
let client: ReturnType<typeof createClient>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockFetch.mockClear();
|
|
9
|
+
client = createClient(mockFetch, { useTempTokens: true, debug: false }); // Keep debug off for unit test clarity
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("initializes without errors", () => {
|
|
13
|
+
expect(client).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("has getTempTokenDBID method", () => {
|
|
17
|
+
expect(typeof client.getTempTokenDBID).toBe("function");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("fetches and caches temp token on first call", async () => {
|
|
21
|
+
const mockDbid = "mockDbid123";
|
|
22
|
+
const mockToken = "b123xyz_temp_token";
|
|
23
|
+
|
|
24
|
+
mockFetch.mockResolvedValueOnce({
|
|
25
|
+
ok: true,
|
|
26
|
+
status: 200,
|
|
27
|
+
json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
|
|
28
|
+
} as Response);
|
|
29
|
+
|
|
30
|
+
const result = await client.getTempTokenDBID({ dbid: mockDbid });
|
|
31
|
+
expect(result).toEqual({ temporaryAuthorization: mockToken });
|
|
32
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
33
|
+
`https://api.quickbase.com/v1/auth/temporary/${mockDbid}`,
|
|
34
|
+
expect.objectContaining({
|
|
35
|
+
method: "GET",
|
|
36
|
+
headers: expect.objectContaining({
|
|
37
|
+
"QB-Realm-Hostname": `${process.env.QB_REALM}.quickbase.com`,
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
}),
|
|
40
|
+
credentials: "include",
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("reuses cached temp token on second call", async () => {
|
|
46
|
+
const mockDbid = "mockDbid123";
|
|
47
|
+
const mockToken = "b123xyz_temp_token";
|
|
48
|
+
|
|
49
|
+
mockFetch.mockResolvedValueOnce({
|
|
50
|
+
ok: true,
|
|
51
|
+
status: 200,
|
|
52
|
+
json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
|
|
53
|
+
} as Response);
|
|
54
|
+
|
|
55
|
+
const firstResult = await client.getTempTokenDBID({ dbid: mockDbid });
|
|
56
|
+
expect(firstResult).toEqual({ temporaryAuthorization: mockToken });
|
|
57
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
58
|
+
|
|
59
|
+
mockFetch.mockClear();
|
|
60
|
+
const secondResult = await client.getTempTokenDBID({ dbid: mockDbid });
|
|
61
|
+
expect(secondResult).toEqual({ temporaryAuthorization: mockToken });
|
|
62
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("handles API error", async () => {
|
|
66
|
+
const mockDbid = "mockDbid123";
|
|
67
|
+
const errorResponse = {
|
|
68
|
+
ok: false,
|
|
69
|
+
status: 401,
|
|
70
|
+
text: () => Promise.resolve("Unauthorized"),
|
|
71
|
+
} as Response;
|
|
72
|
+
|
|
73
|
+
mockFetch.mockResolvedValueOnce(errorResponse);
|
|
74
|
+
|
|
75
|
+
await expect(client.getTempTokenDBID({ dbid: mockDbid })).rejects.toSatisfy(
|
|
76
|
+
(error: Error) => {
|
|
77
|
+
expect(error).toBeInstanceOf(Error);
|
|
78
|
+
expect(error.message).toBe("API Error: Unauthorized (Status: 401)");
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { createClient, mockFetch } from "@/tests/setup.ts";
|
|
3
|
+
|
|
4
|
+
describe("QuickbaseClient - 401 Retry Creates New Token", () => {
|
|
5
|
+
let client: ReturnType<typeof createClient>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockFetch.mockClear();
|
|
9
|
+
client = createClient(mockFetch, { useTempTokens: true, debug: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("creates a new token on 401 and retries successfully", async () => {
|
|
13
|
+
const mockDbid = "mockDbid123";
|
|
14
|
+
const mockToken = "new_token_456";
|
|
15
|
+
const mockFields = [{ id: 1, label: "Field1" }];
|
|
16
|
+
let callCount = 0;
|
|
17
|
+
|
|
18
|
+
mockFetch.mockImplementation((url) => {
|
|
19
|
+
callCount++;
|
|
20
|
+
console.log(`Mock fetch call ${callCount}: ${url}`);
|
|
21
|
+
if (url.includes("auth/temporary") && callCount === 1) {
|
|
22
|
+
return Promise.resolve({
|
|
23
|
+
ok: true,
|
|
24
|
+
status: 200,
|
|
25
|
+
json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (url.includes("fields") && callCount === 2) {
|
|
29
|
+
return Promise.resolve({
|
|
30
|
+
ok: false,
|
|
31
|
+
status: 401,
|
|
32
|
+
text: () => Promise.resolve("Unauthorized"),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (url.includes("auth/temporary") && callCount === 3) {
|
|
36
|
+
return Promise.resolve({
|
|
37
|
+
ok: true,
|
|
38
|
+
status: 200,
|
|
39
|
+
json: () =>
|
|
40
|
+
Promise.resolve({ temporaryAuthorization: mockToken + "_retry" }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (url.includes("fields") && callCount === 4) {
|
|
44
|
+
return Promise.resolve({
|
|
45
|
+
ok: true,
|
|
46
|
+
status: 200,
|
|
47
|
+
json: () => Promise.resolve(mockFields),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return Promise.reject(new Error(`Unexpected fetch call: ${url}`));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
54
|
+
const result = await client.getFields({ tableId: mockDbid });
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual(mockFields);
|
|
57
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
58
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
59
|
+
"Authorization error for getFields, refreshing token:",
|
|
60
|
+
expect.any(String)
|
|
61
|
+
);
|
|
62
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
63
|
+
"Fetched and cached new token for dbid: mockDbid123",
|
|
64
|
+
mockToken + "_retry",
|
|
65
|
+
expect.any(String)
|
|
66
|
+
);
|
|
67
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
68
|
+
"Retrying getFields with new token"
|
|
69
|
+
);
|
|
70
|
+
consoleSpy.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { createClient, mockFetch } from "@/tests/setup.ts";
|
|
3
|
+
|
|
4
|
+
describe("QuickbaseClient - Two 401s in a Row", () => {
|
|
5
|
+
let client: ReturnType<typeof createClient>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockFetch.mockClear();
|
|
9
|
+
client = createClient(mockFetch, { useTempTokens: true, debug: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("logs an error after two 401s in a row", async () => {
|
|
13
|
+
const mockDbid = "mockDbid123";
|
|
14
|
+
const mockToken = "new_token_456";
|
|
15
|
+
let callCount = 0;
|
|
16
|
+
|
|
17
|
+
mockFetch.mockImplementation((url) => {
|
|
18
|
+
callCount++;
|
|
19
|
+
console.log(`Mock fetch call ${callCount}: ${url}`);
|
|
20
|
+
if (url.includes("auth/temporary") && callCount === 1) {
|
|
21
|
+
return Promise.resolve({
|
|
22
|
+
ok: true,
|
|
23
|
+
status: 200,
|
|
24
|
+
json: () => Promise.resolve({ temporaryAuthorization: mockToken }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (url.includes("fields") && callCount === 2) {
|
|
28
|
+
return Promise.resolve({
|
|
29
|
+
ok: false,
|
|
30
|
+
status: 401,
|
|
31
|
+
text: () => Promise.resolve("Unauthorized"),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (url.includes("auth/temporary") && callCount === 3) {
|
|
35
|
+
return Promise.resolve({
|
|
36
|
+
ok: true,
|
|
37
|
+
status: 200,
|
|
38
|
+
json: () =>
|
|
39
|
+
Promise.resolve({ temporaryAuthorization: mockToken + "_retry" }),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (url.includes("fields") && callCount === 4) {
|
|
43
|
+
return Promise.resolve({
|
|
44
|
+
ok: false,
|
|
45
|
+
status: 401,
|
|
46
|
+
json: () => Promise.resolve({ message: "Unauthorized again" }), // Changed to json()
|
|
47
|
+
text: () => Promise.resolve("Unauthorized again"), // Keep text() for fallback
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return Promise.reject(new Error(`Unexpected fetch call: ${url}`));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
54
|
+
await expect(client.getFields({ tableId: mockDbid })).rejects.toThrow(
|
|
55
|
+
"API Error: Unauthorized again (Status: 401)"
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
59
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
60
|
+
"Authorization error for getFields, refreshing token:",
|
|
61
|
+
expect.any(String)
|
|
62
|
+
);
|
|
63
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
64
|
+
"Fetched and cached new token for dbid: mockDbid123",
|
|
65
|
+
mockToken + "_retry",
|
|
66
|
+
expect.any(String)
|
|
67
|
+
);
|
|
68
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
69
|
+
"Retrying getFields with new token"
|
|
70
|
+
);
|
|
71
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
72
|
+
"Error response body for getFields:",
|
|
73
|
+
{ message: "Unauthorized again" }
|
|
74
|
+
);
|
|
75
|
+
consoleSpy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
});
|