Skip to content

Commit a380c44

Browse files
Bill Leoutsakoswaleedlatif1
authored andcommitted
fix(netsuite): address final audit findings
1 parent 668c673 commit a380c44

4 files changed

Lines changed: 38 additions & 11 deletions

File tree

apps/sim/app/api/tools/netsuite/objects/route.test.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,11 @@ describe('POST /api/tools/netsuite/objects', () => {
234234
})
235235
})
236236

237-
it('normalizes, deduplicates, sorts, and caps record types', async () => {
238-
const items = Array.from({ length: 1_001 }, (_, index) => ({
239-
name: `record_${String(1_000 - index).padStart(4, '0')}`,
237+
it('normalizes, deduplicates, and sorts up to 1,000 unique record types', async () => {
238+
const items = Array.from({ length: 1_000 }, (_, index) => ({
239+
name: `record_${String(999 - index).padStart(4, '0')}`,
240240
}))
241-
items.push({ name: 'record_0000' })
241+
items.push({ name: 'record_0000' }, { name: 'record_0999' })
242242
mockListRecordTypes.mockResolvedValueOnce(success({ items }))
243243

244244
const response = await POST(request(RECORD_TYPES_BODY), {})
@@ -250,6 +250,21 @@ describe('POST /api/tools/netsuite/objects', () => {
250250
expect((body.objects as { id: string }[]).at(-1)?.id).toBe('record_0999')
251251
})
252252

253+
it('fails closed instead of returning a partial record-type catalog', async () => {
254+
mockListRecordTypes.mockResolvedValueOnce(
255+
success({
256+
items: Array.from({ length: 1_001 }, (_, index) => ({ name: `record_${index}` })),
257+
})
258+
)
259+
260+
const response = await POST(request(RECORD_TYPES_BODY), {})
261+
262+
expect(response.status).toBe(502)
263+
expect(await json(response)).toEqual({
264+
error: 'NetSuite returned an invalid object-discovery response.',
265+
})
266+
})
267+
253268
it('fails closed on a malformed provider envelope', async () => {
254269
mockListRecordTypes.mockResolvedValueOnce(success({ items: [{ name: 42 }] }))
255270

apps/sim/app/api/tools/netsuite/objects/route.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,19 @@ function dedupeAndSort(objects: NetSuiteSelectorObject[]): NetSuiteSelectorObjec
7878
}
7979

8080
function normalizeRecordTypes(data: unknown): NetSuiteSelectorObject[] {
81-
const objects = requireItems(data, 'record-type catalog').map((item) => {
81+
const objects: NetSuiteSelectorObject[] = []
82+
const names = new Set<string>()
83+
for (const item of requireItems(data, 'record-type catalog')) {
8284
const name = requireString(item.name, 'record type name', MAX_ID_LENGTH)
83-
return { id: name, label: name, detail: null }
84-
})
85-
return dedupeAndSort(objects).slice(0, MAX_RECORD_TYPES)
85+
if (!names.has(name)) {
86+
if (names.size >= MAX_RECORD_TYPES) {
87+
throw new Error('NetSuite returned too many record types')
88+
}
89+
names.add(name)
90+
objects.push({ id: name, label: name, detail: null })
91+
}
92+
}
93+
return dedupeAndSort(objects)
8694
}
8795

8896
function taskIdFromHref(href: unknown, origin: string, jobId: string): string {

apps/sim/tools/netsuite/netsuite.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,15 @@ const SOURCE_MATRIX: SourceMatrixEntry[] = [
238238
{
239239
id: 'netsuite_get_record',
240240
source: GET_RECORD_SOURCE,
241+
additionalSources: [FORM_SOURCE],
241242
method: 'GET',
242243
path: '/services/rest/record/v1/customer/A%2FB',
243-
query: { expandSubResources: 'true' },
244+
query: { expand: 'salesRep', expandSubResources: 'true' },
244245
successStatus: 200,
245246
execute: invoke(netsuiteGetRecordTool, {
246247
recordType: 'customer',
247248
recordId: 'A/B',
249+
expand: 'salesRep',
248250
expandSubResources: true,
249251
}),
250252
},
@@ -662,7 +664,7 @@ describe('NetSuite operation contracts', () => {
662664
if (entry.body !== undefined && !expectedHeaders['Content-Type']) {
663665
expectedHeaders['Content-Type'] = 'application/json'
664666
}
665-
if (entry.body !== undefined) {
667+
if (entry.body !== undefined && entry.path.startsWith('/services/rest/record/')) {
666668
expectedHeaders['X-NetSuite-PropertyNameValidation'] = 'error'
667669
}
668670
expect(

apps/sim/tools/netsuite/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,9 @@ async function sendSuiteTalkRequest(
412412
const hasBody = serializedBody !== undefined
413413
if (hasBody) {
414414
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json'
415-
headers['X-NetSuite-PropertyNameValidation'] = 'error'
415+
if (request.path.startsWith('/services/rest/record/')) {
416+
headers['X-NetSuite-PropertyNameValidation'] = 'error'
417+
}
416418
}
417419
return fetch(url, {
418420
method: request.method,

0 commit comments

Comments
 (0)