Skip to content

Commit 5dc92de

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(quickbooks): enforce response and token invariants
1 parent 117f122 commit 5dc92de

11 files changed

Lines changed: 98 additions & 17 deletions

apps/sim/tools/quickbooks/create_record.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ export const quickBooksCreateRecordTool: ToolConfig<
9191
record: {
9292
type: 'json',
9393
description: 'Created entity-specific QuickBooks record',
94-
optional: true,
9594
},
9695
entity: { type: 'string', description: 'QuickBooks entity name' },
9796
time: {

apps/sim/tools/quickbooks/delete_record.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ export const quickBooksDeleteRecordTool: ToolConfig<
104104
record: {
105105
type: 'json',
106106
description: 'Deleted entity-specific QuickBooks record',
107-
optional: true,
108107
},
109108
entity: { type: 'string', description: 'QuickBooks entity name' },
110109
time: {

apps/sim/tools/quickbooks/generic_operations.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
buildQuickBooksCdcUrl,
2121
buildQuickBooksDocumentUrl,
2222
buildQuickBooksExchangeRateUrl,
23+
buildQuickBooksHeaders,
2324
buildQuickBooksListRecordsQuery,
2425
buildQuickBooksPreferencesUrl,
2526
buildQuickBooksRecordUrl,
@@ -81,6 +82,16 @@ describe('QuickBooks generic operations', () => {
8182
).toThrow('QuickBooks entity "Vendor" cannot be deleted')
8283
})
8384

85+
it('rejects invalid access tokens before constructing QuickBooks headers', () => {
86+
expect(() => buildQuickBooksHeaders('')).toThrow('QuickBooks access token is required')
87+
expect(() => buildQuickBooksHeaders(`token\r\nX-Injected: true`)).toThrow(
88+
'QuickBooks access token contains invalid characters'
89+
)
90+
expect(() => buildQuickBooksHeaders('x'.repeat(4097))).toThrow(
91+
'QuickBooks access token must be 4096 characters or less'
92+
)
93+
})
94+
8495
it('normalizes create, update, and delete request payloads', () => {
8596
const createBody = quickBooksCreateRecordTool.request.body
8697
const updateBody = quickBooksUpdateRecordTool.request.body
@@ -428,6 +439,65 @@ describe('QuickBooks generic operations', () => {
428439
})
429440
})
430441

442+
it('rejects successful record responses that omit the entity record', async () => {
443+
const emptyResponse = () =>
444+
new Response(JSON.stringify({ time: '2026-01-20T10:00:00-08:00' }), { status: 200 })
445+
446+
await expect(
447+
quickBooksCreateRecordTool.transformResponse?.(emptyResponse(), {
448+
accessToken: 'token',
449+
realmId: '123145',
450+
entity: 'Vendor',
451+
payload: { DisplayName: 'Acme Supplies' },
452+
})
453+
).rejects.toThrow('QuickBooks API response did not include Vendor')
454+
455+
await expect(
456+
quickBooksGetRecordTool.transformResponse?.(emptyResponse(), {
457+
accessToken: 'token',
458+
realmId: '123145',
459+
entity: 'PurchaseOrder',
460+
recordId: '42',
461+
})
462+
).rejects.toThrow('QuickBooks API response did not include PurchaseOrder')
463+
464+
await expect(
465+
quickBooksUpdateRecordTool.transformResponse?.(emptyResponse(), {
466+
accessToken: 'token',
467+
realmId: '123145',
468+
entity: 'Vendor',
469+
recordId: '7',
470+
syncToken: '1',
471+
payload: { DisplayName: 'Acme Industrial' },
472+
})
473+
).rejects.toThrow('QuickBooks API response did not include Vendor')
474+
475+
await expect(
476+
quickBooksDeleteRecordTool.transformResponse?.(emptyResponse(), {
477+
accessToken: 'token',
478+
realmId: '123145',
479+
entity: 'Bill',
480+
recordId: '17',
481+
syncToken: '2',
482+
})
483+
).rejects.toThrow('QuickBooks API response did not include Bill')
484+
485+
await expect(
486+
quickBooksGetPreferencesTool.transformResponse?.(emptyResponse(), {
487+
accessToken: 'token',
488+
realmId: '123145',
489+
})
490+
).rejects.toThrow('QuickBooks API response did not include Preferences')
491+
492+
await expect(
493+
quickBooksGetExchangeRateTool.transformResponse?.(emptyResponse(), {
494+
accessToken: 'token',
495+
realmId: '123145',
496+
sourceCurrencyCode: 'EUR',
497+
})
498+
).rejects.toThrow('QuickBooks API response did not include ExchangeRate')
499+
})
500+
431501
it('transforms QuickBooks report sections', async () => {
432502
const response = new Response(
433503
JSON.stringify({

apps/sim/tools/quickbooks/get_exchange_rate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
import {
66
buildQuickBooksExchangeRateUrl,
77
buildQuickBooksHeaders,
8+
extractQuickBooksRecord,
89
parseQuickBooksJson,
910
} from '@/tools/quickbooks/utils'
1011
import type { ToolConfig } from '@/tools/types'
@@ -66,7 +67,7 @@ export const quickBooksGetExchangeRateTool: ToolConfig<
6667
return {
6768
success: true,
6869
output: {
69-
record: data.ExchangeRate ?? null,
70+
record: extractQuickBooksRecord(data, 'ExchangeRate'),
7071
entity: 'ExchangeRate',
7172
time: typeof data.time === 'string' ? data.time : null,
7273
},
@@ -76,7 +77,6 @@ export const quickBooksGetExchangeRateTool: ToolConfig<
7677
record: {
7778
type: 'json',
7879
description: 'QuickBooks exchange rate',
79-
optional: true,
8080
},
8181
entity: { type: 'string', description: 'QuickBooks entity name' },
8282
time: {

apps/sim/tools/quickbooks/get_preferences.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
import {
66
buildQuickBooksHeaders,
77
buildQuickBooksPreferencesUrl,
8+
extractQuickBooksRecord,
89
parseQuickBooksJson,
910
} from '@/tools/quickbooks/utils'
1011
import type { ToolConfig } from '@/tools/types'
@@ -54,7 +55,7 @@ export const quickBooksGetPreferencesTool: ToolConfig<
5455
return {
5556
success: true,
5657
output: {
57-
record: data.Preferences ?? null,
58+
record: extractQuickBooksRecord(data, 'Preferences'),
5859
entity: 'Preferences',
5960
time: typeof data.time === 'string' ? data.time : null,
6061
},
@@ -64,7 +65,6 @@ export const quickBooksGetPreferencesTool: ToolConfig<
6465
record: {
6566
type: 'json',
6667
description: 'QuickBooks company preferences',
67-
optional: true,
6868
},
6969
entity: { type: 'string', description: 'QuickBooks entity name' },
7070
time: {

apps/sim/tools/quickbooks/get_record.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export const quickBooksGetRecordTool: ToolConfig<
8686
record: {
8787
type: 'json',
8888
description: 'Entity-specific QuickBooks record',
89-
optional: true,
9089
},
9190
entity: { type: 'string', description: 'QuickBooks entity name' },
9291
time: {

apps/sim/tools/quickbooks/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export interface QuickBooksQueryResponse extends ToolResponse {
324324
}
325325

326326
export interface QuickBooksRecordOutput {
327-
record: QuickBooksRecord | null
327+
record: QuickBooksRecord
328328
entity: string
329329
time: string | null
330330
}

apps/sim/tools/quickbooks/update_exchange_rate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
buildQuickBooksExchangeRateBody,
77
buildQuickBooksExchangeRateUpdateUrl,
88
buildQuickBooksHeaders,
9+
extractQuickBooksRecord,
910
parseQuickBooksJson,
1011
} from '@/tools/quickbooks/utils'
1112
import type { ToolConfig } from '@/tools/types'
@@ -63,7 +64,7 @@ export const quickBooksUpdateExchangeRateTool: ToolConfig<
6364
return {
6465
success: true,
6566
output: {
66-
record: data.ExchangeRate ?? null,
67+
record: extractQuickBooksRecord(data, 'ExchangeRate'),
6768
entity: 'ExchangeRate',
6869
time: typeof data.time === 'string' ? data.time : null,
6970
},
@@ -73,7 +74,6 @@ export const quickBooksUpdateExchangeRateTool: ToolConfig<
7374
record: {
7475
type: 'json',
7576
description: 'Updated QuickBooks exchange rate',
76-
optional: true,
7777
},
7878
entity: { type: 'string', description: 'QuickBooks entity name' },
7979
time: {

apps/sim/tools/quickbooks/update_preferences.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
buildQuickBooksHeaders,
77
buildQuickBooksPreferencesBody,
88
buildQuickBooksPreferencesUrl,
9+
extractQuickBooksRecord,
910
parseQuickBooksJson,
1011
} from '@/tools/quickbooks/utils'
1112
import type { ToolConfig } from '@/tools/types'
@@ -62,7 +63,7 @@ export const quickBooksUpdatePreferencesTool: ToolConfig<
6263
return {
6364
success: true,
6465
output: {
65-
record: data.Preferences ?? null,
66+
record: extractQuickBooksRecord(data, 'Preferences'),
6667
entity: 'Preferences',
6768
time: typeof data.time === 'string' ? data.time : null,
6869
},
@@ -72,7 +73,6 @@ export const quickBooksUpdatePreferencesTool: ToolConfig<
7273
record: {
7374
type: 'json',
7475
description: 'Updated QuickBooks company preferences',
75-
optional: true,
7676
},
7777
entity: { type: 'string', description: 'QuickBooks entity name' },
7878
time: {

apps/sim/tools/quickbooks/update_record.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ export const quickBooksUpdateRecordTool: ToolConfig<
109109
record: {
110110
type: 'json',
111111
description: 'Updated entity-specific QuickBooks record',
112-
optional: true,
113112
},
114113
entity: { type: 'string', description: 'QuickBooks entity name' },
115114
time: {

0 commit comments

Comments
 (0)