Skip to content

Commit a0486f8

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(quickbooks): validate delete and document payloads
1 parent 4985764 commit a0486f8

7 files changed

Lines changed: 110 additions & 14 deletions

File tree

apps/sim/blocks/blocks/quickbooks.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,30 @@ describe('QuickBooksBlock', () => {
8585
file,
8686
})
8787
})
88+
89+
it('requires full entity payloads for non-simplified deletes', () => {
90+
const payload = QuickBooksBlock.subBlocks.find((subBlock) => subBlock.id === 'payload')
91+
if (!payload || typeof payload.required !== 'function') {
92+
throw new Error('Expected QuickBooks payload to use conditional required state')
93+
}
94+
95+
expect(
96+
payload.required({
97+
operation: 'delete_record',
98+
deleteEntity: 'InventoryAdjustment',
99+
})
100+
).toEqual({
101+
field: 'deleteEntity',
102+
value: ['Attachable', 'Deposit', 'InventoryAdjustment', 'Transfer'],
103+
})
104+
expect(
105+
payload.required({
106+
operation: 'create_record',
107+
createEntity: 'Vendor',
108+
})
109+
).toEqual({
110+
field: 'operation',
111+
value: ['create_record', 'update_record', 'update_exchange_rate', 'update_preferences'],
112+
})
113+
})
88114
})

apps/sim/blocks/blocks/quickbooks.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { QuickBooksResponse } from '@/tools/quickbooks/types'
77
import {
88
QUICKBOOKS_CREATABLE_ENTITIES,
99
QUICKBOOKS_DELETABLE_ENTITIES,
10+
QUICKBOOKS_FULL_DELETE_ENTITIES,
1011
QUICKBOOKS_PDF_ENTITIES,
1112
QUICKBOOKS_QUERYABLE_ENTITIES,
1213
QUICKBOOKS_READABLE_ENTITIES,
@@ -199,10 +200,18 @@ export const QuickBooksBlock: BlockConfig<QuickBooksResponse> = {
199200
'update_preferences',
200201
],
201202
},
202-
required: {
203-
field: 'operation',
204-
value: ['create_record', 'update_record', 'update_exchange_rate', 'update_preferences'],
205-
},
203+
required: (values) =>
204+
values?.operation === 'delete_record'
205+
? { field: 'deleteEntity', value: [...QUICKBOOKS_FULL_DELETE_ENTITIES] }
206+
: {
207+
field: 'operation',
208+
value: [
209+
'create_record',
210+
'update_record',
211+
'update_exchange_rate',
212+
'update_preferences',
213+
],
214+
},
206215
wandConfig: {
207216
enabled: true,
208217
prompt:

apps/sim/tools/quickbooks/download_document.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import type {
44
QuickBooksDownloadDocumentParams,
55
QuickBooksFileResponse,
66
} from '@/tools/quickbooks/types'
7-
import { buildQuickBooksDocumentUrl, buildQuickBooksHeaders } from '@/tools/quickbooks/utils'
7+
import {
8+
assertQuickBooksPdfResponse,
9+
buildQuickBooksDocumentUrl,
10+
buildQuickBooksHeaders,
11+
} from '@/tools/quickbooks/utils'
812
import type { ToolConfig } from '@/tools/types'
913

1014
const QUICKBOOKS_MAX_PDF_BYTES = 25 * 1024 * 1024
@@ -76,6 +80,7 @@ export const quickBooksDownloadDocumentTool: ToolConfig<
7680
`QuickBooks API error (${response.status}): ${truncate(buffer.toString('utf8'), 500) || response.statusText || 'Request failed'}`
7781
)
7882
}
83+
assertQuickBooksPdfResponse(response, buffer, 'PDF download')
7984
return {
8085
success: true,
8186
output: {

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ describe('QuickBooks generic operations', () => {
580580
})
581581

582582
const documentResult = await quickBooksDownloadDocumentTool.transformResponse?.(
583-
new Response(new Uint8Array([37, 80, 68, 70]), {
583+
new Response(new Uint8Array([37, 80, 68, 70, 45]), {
584584
headers: { 'content-type': 'application/pdf' },
585585
status: 200,
586586
}),
@@ -595,15 +595,15 @@ describe('QuickBooks generic operations', () => {
595595
file: {
596596
name: 'PurchaseOrder-42.pdf',
597597
mimeType: 'application/pdf',
598-
data: 'JVBERg==',
599-
size: 4,
598+
data: 'JVBERi0=',
599+
size: 5,
600600
},
601601
entity: 'PurchaseOrder',
602602
recordId: '42',
603603
})
604604

605605
const sentDocumentResult = await quickBooksSendDocumentTool.transformResponse?.(
606-
new Response(new Uint8Array([37, 80, 68, 70]), {
606+
new Response(new Uint8Array([37, 80, 68, 70, 45]), {
607607
headers: { 'content-type': 'application/octet-stream' },
608608
status: 200,
609609
}),
@@ -619,8 +619,8 @@ describe('QuickBooks generic operations', () => {
619619
file: {
620620
name: 'PurchaseOrder-42.pdf',
621621
mimeType: 'application/pdf',
622-
data: 'JVBERg==',
623-
size: 4,
622+
data: 'JVBERi0=',
623+
size: 5,
624624
},
625625
entity: 'PurchaseOrder',
626626
recordId: '42',
@@ -642,6 +642,37 @@ describe('QuickBooks generic operations', () => {
642642
})
643643
})
644644

645+
it('rejects successful document responses that are not PDFs', async () => {
646+
const params = {
647+
accessToken: 'token',
648+
realmId: '123145',
649+
entity: 'PurchaseOrder' as const,
650+
recordId: '42',
651+
}
652+
653+
await expect(
654+
quickBooksDownloadDocumentTool.transformResponse?.(
655+
new Response(JSON.stringify({ Fault: { Error: [{ Message: 'Unexpected response' }] } }), {
656+
headers: { 'content-type': 'application/json' },
657+
status: 200,
658+
}),
659+
params
660+
)
661+
).rejects.toThrow('QuickBooks PDF download returned a non-PDF response (application/json)')
662+
663+
await expect(
664+
quickBooksSendDocumentTool.transformResponse?.(
665+
new Response('', {
666+
headers: { 'content-type': 'text/plain' },
667+
status: 200,
668+
}),
669+
params
670+
)
671+
).rejects.toThrow(
672+
'QuickBooks sent document returned a non-PDF response (text/plain): Empty response'
673+
)
674+
})
675+
645676
it('transforms QuickBooks batch item responses without hiding item faults', async () => {
646677
const response = new Response(
647678
JSON.stringify({

apps/sim/tools/quickbooks/send_document.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { truncate } from '@sim/utils/string'
22
import { readResponseToBufferWithLimit } from '@/lib/core/utils/stream-limits'
33
import type { QuickBooksFileResponse, QuickBooksSendDocumentParams } from '@/tools/quickbooks/types'
4-
import { buildQuickBooksHeaders, buildQuickBooksSendDocumentUrl } from '@/tools/quickbooks/utils'
4+
import {
5+
assertQuickBooksPdfResponse,
6+
buildQuickBooksHeaders,
7+
buildQuickBooksSendDocumentUrl,
8+
} from '@/tools/quickbooks/utils'
59
import type { ToolConfig } from '@/tools/types'
610

711
const QUICKBOOKS_MAX_SENT_DOCUMENT_BYTES = 25 * 1024 * 1024
@@ -79,6 +83,7 @@ export const quickBooksSendDocumentTool: ToolConfig<
7983
`QuickBooks API error (${response.status}): ${truncate(buffer.toString('utf8'), 500) || response.statusText || 'Request failed'}`
8084
)
8185
}
86+
assertQuickBooksPdfResponse(response, buffer, 'sent document')
8287
return {
8388
success: true,
8489
output: {

apps/sim/tools/quickbooks/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,18 @@ export const QUICKBOOKS_SIMPLIFIED_DELETE_ENTITIES = [
119119
'VendorCredit',
120120
] as const
121121

122-
export const QUICKBOOKS_DELETABLE_ENTITIES = [
123-
...QUICKBOOKS_SIMPLIFIED_DELETE_ENTITIES,
122+
export const QUICKBOOKS_FULL_DELETE_ENTITIES = [
124123
'Attachable',
125124
'Deposit',
126125
'InventoryAdjustment',
127126
'Transfer',
128127
] as const
129128

129+
export const QUICKBOOKS_DELETABLE_ENTITIES = [
130+
...QUICKBOOKS_SIMPLIFIED_DELETE_ENTITIES,
131+
...QUICKBOOKS_FULL_DELETE_ENTITIES,
132+
] as const
133+
130134
export const QUICKBOOKS_CDC_ENTITIES = QUICKBOOKS_READABLE_ENTITIES.filter(
131135
(entity) => !['TaxAgency', 'TaxCode', 'TaxRate', 'TimeActivity'].includes(entity)
132136
) as Exclude<

apps/sim/tools/quickbooks/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ export function buildQuickBooksHeaders(accessToken: string): Record<string, stri
8181
}
8282
}
8383

84+
export function assertQuickBooksPdfResponse(
85+
response: Response,
86+
buffer: Buffer,
87+
label: string
88+
): void {
89+
const header = buffer.subarray(0, Math.min(buffer.length, 1024))
90+
if (header.indexOf(Buffer.from('%PDF-')) >= 0) return
91+
92+
const contentType = response.headers.get('content-type')?.split(';', 1)[0]?.trim()
93+
const detail = truncate(buffer.toString('utf8').trim(), 500)
94+
const contentTypeDetail = contentType ? ` (${contentType})` : ''
95+
throw new Error(
96+
`QuickBooks ${label} returned a non-PDF response${contentTypeDetail}: ${detail || 'Empty response'}`
97+
)
98+
}
99+
84100
export function buildQuickBooksQueryEndpoint(params: {
85101
realmId: string
86102
apiEnvironment?: QuickBooksEnvironment | string

0 commit comments

Comments
 (0)