Skip to content

Commit 117f122

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(quickbooks): validate attachment uploads
1 parent a0486f8 commit 117f122

5 files changed

Lines changed: 85 additions & 4 deletions

File tree

apps/sim/app/api/tools/quickbooks/upload-attachment/route.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,64 @@ describe('POST /api/tools/quickbooks/upload-attachment', () => {
135135
)
136136
})
137137

138+
it('normalizes linked entity names before building attachment metadata', async () => {
139+
const response = await POST(
140+
createMockRequest('POST', {
141+
...baseBody,
142+
entity: 'purchaseorder',
143+
})
144+
)
145+
expect(response.status).toBe(200)
146+
147+
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]
148+
const metadata = (init.body as FormData).get('file_metadata_01')
149+
expect(metadata).toBeInstanceOf(Blob)
150+
await expect((metadata as Blob).text()).resolves.toContain(
151+
'"EntityRef":{"type":"PurchaseOrder","value":"17"}'
152+
)
153+
})
154+
155+
it('rejects nested QuickBooks upload faults returned with HTTP 200', async () => {
156+
mockFetch.mockResolvedValueOnce(
157+
new Response(
158+
JSON.stringify({
159+
AttachableResponse: [
160+
{
161+
Fault: {
162+
Error: [{ Message: 'ValidationFault', Detail: 'Unsupported entity reference' }],
163+
},
164+
},
165+
],
166+
time: '2026-07-29T23:00:00Z',
167+
}),
168+
{ headers: { 'content-type': 'application/json' }, status: 200 }
169+
)
170+
)
171+
172+
const response = await POST(createMockRequest('POST', baseBody))
173+
expect(response.status).toBe(500)
174+
await expect(response.json()).resolves.toEqual({
175+
success: false,
176+
error: 'QuickBooks API error (200): Unsupported entity reference',
177+
})
178+
})
179+
180+
it('rejects timestamp-only QuickBooks upload responses', async () => {
181+
mockFetch.mockResolvedValueOnce(
182+
new Response(JSON.stringify({ time: '2026-07-29T23:00:00Z' }), {
183+
headers: { 'content-type': 'application/json' },
184+
status: 200,
185+
})
186+
)
187+
188+
const response = await POST(createMockRequest('POST', baseBody))
189+
expect(response.status).toBe(500)
190+
await expect(response.json()).resolves.toEqual({
191+
success: false,
192+
error: 'QuickBooks attachment upload returned no attachment',
193+
})
194+
})
195+
138196
it('rejects files over the QuickBooks 100 MB attachment limit', async () => {
139197
mockProcessFilesToUserFiles.mockReturnValueOnce([
140198
{ ...baseBody.file, size: 100 * 1024 * 1024 + 1 },

apps/sim/app/api/tools/quickbooks/upload-attachment/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.
1212
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
1313
import { assertToolFileAccess } from '@/app/api/files/authorization'
1414
import {
15+
assertQuickBooksAttachmentUploadResponse,
1516
buildQuickBooksHeaders,
1617
buildQuickBooksUploadUrl,
18+
normalizeQuickBooksAttachmentEntity,
1719
parseQuickBooksJson,
1820
} from '@/tools/quickbooks/utils'
1921

@@ -65,11 +67,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
6567
}
6668

6769
const contentType = downloaded.contentType || userFile.type || 'application/octet-stream'
70+
const entity = normalizeQuickBooksAttachmentEntity(params.entity)
6871
const metadata = {
6972
AttachableRef: [
7073
{
7174
EntityRef: {
72-
type: params.entity,
75+
type: entity,
7376
value: params.entityId,
7477
},
7578
IncludeOnSend: params.includeOnSend ?? false,
@@ -105,7 +108,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
105108
body: formData,
106109
}
107110
)
108-
const data = await parseQuickBooksJson(response)
111+
const data = assertQuickBooksAttachmentUploadResponse(await parseQuickBooksJson(response))
109112
return NextResponse.json({ success: true, output: { result: data } })
110113
} catch (error) {
111114
const notReady = docNotReadyResponse(error)

apps/sim/blocks/blocks/quickbooks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,9 @@ export const QuickBooksBlock: BlockConfig<QuickBooksResponse> = {
264264
{
265265
id: 'attachmentEntity',
266266
title: 'Linked Entity Type',
267-
type: 'short-input',
268-
placeholder: 'PurchaseOrder',
267+
type: 'dropdown',
268+
options: buildEntityOptions(QUICKBOOKS_READABLE_ENTITIES),
269+
value: () => 'PurchaseOrder',
269270
condition: { field: 'operation', value: 'upload_attachment' },
270271
required: { field: 'operation', value: 'upload_attachment' },
271272
},

apps/sim/tools/quickbooks/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ export interface QuickBooksFault {
290290
}
291291

292292
export interface QuickBooksApiEnvelope extends QuickBooksRecord {
293+
AttachableResponse?: Array<{
294+
Attachable?: QuickBooksRecord
295+
Fault?: QuickBooksFault
296+
}>
293297
QueryResponse?: Record<string, unknown> & {
294298
Fault?: QuickBooksFault
295299
}

apps/sim/tools/quickbooks/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,20 @@ export async function parseQuickBooksJson(response: Response): Promise<QuickBook
477477
return data
478478
}
479479

480+
export function assertQuickBooksAttachmentUploadResponse(
481+
data: QuickBooksApiEnvelope
482+
): QuickBooksApiEnvelope {
483+
const uploaded = data.AttachableResponse?.some((item) => isQuickBooksRecord(item.Attachable))
484+
if (!uploaded) {
485+
throw new Error('QuickBooks attachment upload returned no attachment')
486+
}
487+
return data
488+
}
489+
490+
export function normalizeQuickBooksAttachmentEntity(value: string): QuickBooksEntityName {
491+
return normalizeQuickBooksEntity(value, QUICKBOOKS_READABLE_ENTITIES, 'linked to an attachment')
492+
}
493+
480494
export function extractQuickBooksRecords(
481495
data: QuickBooksApiEnvelope,
482496
preferredEntity?: QuickBooksEntityName
@@ -730,6 +744,7 @@ function extractQuickBooksError(data: QuickBooksApiEnvelope): string | null {
730744
const errors = [
731745
...extractFaultErrors(data.Fault),
732746
...extractFaultErrors(data.QueryResponse?.Fault),
747+
...(data.AttachableResponse ?? []).flatMap((item) => extractFaultErrors(item.Fault)),
733748
]
734749
const message = errors
735750
.map((error) => error.Detail || error.Message || error.code)

0 commit comments

Comments
 (0)