Skip to content

Commit dc94fee

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(oracle-epm-data): address independent validation findings
1 parent ef7affb commit dc94fee

12 files changed

Lines changed: 248 additions & 19 deletions

File tree

apps/sim/blocks/blocks/oracle_epm_data.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1+
import { utf8ByteLength } from '@sim/utils/paste'
12
import { NetSuiteIcon } from '@/components/icons'
3+
import { MAX_INLINE_MATERIALIZATION_BYTES } from '@/lib/execution/payloads/limits'
24
import { getScopesForService } from '@/lib/oauth/utils'
35
import type { BlockConfig, BlockMeta } from '@/blocks/types'
46
import { AuthMode, IntegrationType } from '@/blocks/types'
57
import { normalizeFileInput } from '@/blocks/utils'
68

9+
function assertJsonInputSize(value: unknown, label: string): void {
10+
if (
11+
typeof value === 'string' &&
12+
utf8ByteLength(value, MAX_INLINE_MATERIALIZATION_BYTES) > MAX_INLINE_MATERIALIZATION_BYTES
13+
) {
14+
throw new Error(`${label} exceeds the 16 MiB inline JSON limit`)
15+
}
16+
}
17+
718
function parseJson(value: unknown, label: string): unknown {
819
if (value === undefined || value === null || value === '') return undefined
920
if (typeof value !== 'string') return value
21+
assertJsonInputSize(value, label)
1022
try {
1123
return JSON.parse(value)
1224
} catch {
@@ -1137,6 +1149,7 @@ export const OracleEpmDataBlock: BlockConfig = {
11371149
const operation = params.operation
11381150
// Agent calls already use canonical typed tool inputs, without an editor operation.
11391151
if (!operation) return params
1152+
if (operation === 'upload_file') assertJsonInputSize(params.file, 'File Reference')
11401153
const fileName = [
11411154
'run_integration',
11421155
'run_data_rule',

apps/sim/lib/internal/oracle-epm-data/contracts.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import {
1919
oracleEpmDataStatusSchema,
2020
projectOracleEpmDataResult,
2121
} from '@/lib/internal/oracle-epm-data/contracts'
22+
import { executeOracleEpmDataExportDataIntegrationOperation } from '@/lib/internal/oracle-epm-data/operations/export-data-integration'
23+
import { executeOracleEpmDataImportMappingsOperation } from '@/lib/internal/oracle-epm-data/operations/import-mappings'
24+
import { executeOracleEpmDataRunDataRuleOperation } from '@/lib/internal/oracle-epm-data/operations/run-data-rule'
25+
import { executeOracleEpmDataRunIntegrationOperation } from '@/lib/internal/oracle-epm-data/operations/run-integration'
26+
import { executeOracleEpmDataRunPipelineOperation } from '@/lib/internal/oracle-epm-data/operations/run-pipeline'
27+
import { executeOracleEpmDataSetPovLockOperation } from '@/lib/internal/oracle-epm-data/operations/set-pov-lock'
2228

2329
const auth = {
2430
oauthCredential: 'credential',
@@ -83,6 +89,135 @@ describe('Data Integration foundation contracts', () => {
8389
expect(mocks.fetch).not.toHaveBeenCalled()
8490
})
8591

92+
it.each([
93+
{
94+
name: 'integration',
95+
run: () =>
96+
executeOracleEpmDataRunIntegrationOperation({
97+
...auth,
98+
jobName: 'Load',
99+
periodName: '{Jan-26}',
100+
importMode: 'Replace',
101+
exportMode: 'Merge',
102+
}),
103+
body: {
104+
jobType: 'INTEGRATION',
105+
jobName: 'Load',
106+
periodName: '{Jan-26}',
107+
importMode: 'Replace',
108+
exportMode: 'Merge',
109+
},
110+
response: { synthetic: 'opaque response' },
111+
},
112+
{
113+
name: 'pipeline',
114+
run: () => executeOracleEpmDataRunPipelineOperation({ ...auth, pipelineCode: 'Load26' }),
115+
body: { jobType: 'PIPELINE', jobName: 'Load26' },
116+
response: { synthetic: 'opaque response' },
117+
},
118+
{
119+
name: 'data rule',
120+
run: () =>
121+
executeOracleEpmDataRunDataRuleOperation({
122+
...auth,
123+
jobName: 'Rule',
124+
startPeriod: 'Jan-26',
125+
endPeriod: 'Jan-26',
126+
importMode: 'APPEND',
127+
exportMode: 'NONE',
128+
}),
129+
body: {
130+
jobType: 'DATARULE',
131+
jobName: 'Rule',
132+
startPeriod: 'Jan-26',
133+
endPeriod: 'Jan-26',
134+
importMode: 'APPEND',
135+
exportMode: 'NONE',
136+
},
137+
response: { status: 0, jobId: 42 },
138+
},
139+
{
140+
name: 'mapping import',
141+
run: () =>
142+
executeOracleEpmDataImportMappingsOperation({
143+
...auth,
144+
dimension: 'ALL',
145+
fileName: 'map.csv',
146+
}),
147+
body: { jobType: 'MAPPINGIMPORT', jobName: 'ALL', fileName: 'map.csv' },
148+
response: { status: 0, jobId: 42 },
149+
},
150+
{
151+
name: 'snapshot export',
152+
run: () =>
153+
executeOracleEpmDataExportDataIntegrationOperation({
154+
...auth,
155+
snapshotType: 'ALL',
156+
fileName: 'snapshot.zip',
157+
}),
158+
body: { action: 'EXPORT', snapshotType: 'ALL', fileName: 'snapshot.zip' },
159+
response: { status: 0, jobId: 42 },
160+
},
161+
{
162+
name: 'application POV lock',
163+
run: () =>
164+
executeOracleEpmDataSetPovLockOperation({
165+
...auth,
166+
period: 'Jan-26',
167+
category: 'Actual',
168+
application: 'Plan',
169+
lockType: 'application',
170+
lockOperation: 'lock',
171+
}),
172+
body: {
173+
period: 'Jan-26',
174+
category: 'Actual',
175+
application: 'Plan',
176+
locktype: 'application',
177+
operation: 'lock',
178+
},
179+
response: { status: 0, response: 'Synthetic result' },
180+
},
181+
])(
182+
'serializes $name without absent optional fields through the real foundation',
183+
async ({ run, body, response }) => {
184+
mocks.fetch.mockResolvedValue(Response.json(response))
185+
expect(await run()).toMatchObject({ success: true })
186+
expect(mocks.fetch).toHaveBeenCalledTimes(1)
187+
expect(JSON.parse(mocks.fetch.mock.calls[0][2].body)).toEqual(body)
188+
}
189+
)
190+
191+
it('preserves explicit false and empty option values at the transport boundary', async () => {
192+
mocks.fetch.mockImplementation(async () => Response.json({ status: 0, jobId: 42 }))
193+
expect(
194+
await executeOracleEpmDataImportMappingsOperation({
195+
...auth,
196+
dimension: 'ALL',
197+
fileName: 'map.csv',
198+
validationMode: false,
199+
})
200+
).toMatchObject({ success: true })
201+
expect(JSON.parse(mocks.fetch.mock.calls[0][2].body)).toEqual({
202+
jobType: 'MAPPINGIMPORT',
203+
jobName: 'ALL',
204+
fileName: 'map.csv',
205+
validationMode: false,
206+
})
207+
expect(
208+
await executeOracleEpmDataRunPipelineOperation({
209+
...auth,
210+
pipelineCode: 'Load26',
211+
variables: { OPTIONAL: '' },
212+
})
213+
).toMatchObject({ success: true })
214+
expect(JSON.parse(mocks.fetch.mock.calls[1][2].body)).toEqual({
215+
jobType: 'PIPELINE',
216+
jobName: 'Load26',
217+
variables: { OPTIONAL: '' },
218+
})
219+
})
220+
86221
it('normalizes documented numeric strings without inventing missing job IDs', () => {
87222
expect(oracleEpmDataJobSchema.parse({ status: '-1', jobId: 42, outputFileName: null })).toEqual(
88223
{ status: -1, jobId: '42', details: null, outputFileName: null }

apps/sim/lib/internal/oracle-epm-data/operations.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ describe('Data Integration provider operations', () => {
184184
).toEqual({ success: true, output: { httpStatus: 202, data: synthetic } })
185185
expect(mocks.request).toHaveBeenCalledOnce()
186186
expect(mocks.request).toHaveBeenCalledWith(endpoints.submitJob, {
187-
json: { ...input, jobType: 'INTEGRATION', fileName: undefined },
187+
json: { ...input, jobType: 'INTEGRATION' },
188188
signal: undefined,
189189
})
190190
})
@@ -240,9 +240,7 @@ describe('Data Integration provider operations', () => {
240240
expect(
241241
mocks.request.mock.calls.map(([endpoint, input]) => {
242242
expect(endpoint).toBe(endpoints.submitJob)
243-
return Object.fromEntries(
244-
Object.entries(input.json).filter(([, value]) => value !== undefined)
245-
)
243+
return input.json
246244
})
247245
).toEqual([
248246
{

apps/sim/lib/internal/oracle-epm-data/operations/export-data-integration.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { filterUndefined } from '@sim/utils/object'
12
import {
23
executeOracleEpmDataOperation,
34
oracleEpmDataEndpoints,
@@ -12,12 +13,12 @@ export const executeOracleEpmDataExportDataIntegrationOperation: InternalToolOpe
1213
> = (params, signal) =>
1314
executeOracleEpmDataOperation('export_data_integration', params, signal, async (input) => {
1415
const response = await requestOracleEpmDataJson(input, oracleEpmDataEndpoints.snapshot, {
15-
json: {
16+
json: filterUndefined({
1617
action: 'EXPORT',
1718
snapshotType: input.snapshotType,
1819
fileName: input.fileName,
1920
overwriteFile: input.overwriteFile,
20-
},
21+
}),
2122
signal,
2223
})
2324
return finishOracleEpmDataJob(input, response, input.waitForCompletion, signal)

apps/sim/lib/internal/oracle-epm-data/operations/import-mappings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { filterUndefined } from '@sim/utils/object'
12
import {
23
executeOracleEpmDataOperation,
34
oracleEpmDataEndpoints,
@@ -12,14 +13,14 @@ export const executeOracleEpmDataImportMappingsOperation: InternalToolOperationI
1213
> = (params, signal) =>
1314
executeOracleEpmDataOperation('import_mappings', params, signal, async (input) => {
1415
const response = await requestOracleEpmDataJson(input, oracleEpmDataEndpoints.submitJob, {
15-
json: {
16+
json: filterUndefined({
1617
jobType: 'MAPPINGIMPORT',
1718
jobName: input.dimension,
1819
fileName: input.fileName,
1920
importMode: input.importMode,
2021
validationMode: input.validationMode,
2122
locationName: input.locationName,
22-
},
23+
}),
2324
signal,
2425
})
2526
return finishOracleEpmDataJob(input, response, input.waitForCompletion, signal)

apps/sim/lib/internal/oracle-epm-data/operations/run-data-rule.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { filterUndefined } from '@sim/utils/object'
12
import {
23
executeOracleEpmDataOperation,
34
oracleEpmDataEndpoints,
@@ -12,15 +13,15 @@ export const executeOracleEpmDataRunDataRuleOperation: InternalToolOperationImpl
1213
> = (params, signal) =>
1314
executeOracleEpmDataOperation('run_data_rule', params, signal, async (input) => {
1415
const response = await requestOracleEpmDataJson(input, oracleEpmDataEndpoints.submitJob, {
15-
json: {
16+
json: filterUndefined({
1617
jobType: 'DATARULE',
1718
jobName: input.jobName,
1819
startPeriod: input.startPeriod,
1920
endPeriod: input.endPeriod,
2021
importMode: input.importMode,
2122
exportMode: input.exportMode,
2223
fileName: input.fileName,
23-
},
24+
}),
2425
signal,
2526
})
2627
return finishOracleEpmDataJob(input, response, input.waitForCompletion, signal)

apps/sim/lib/internal/oracle-epm-data/operations/run-integration.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { filterUndefined } from '@sim/utils/object'
12
import {
23
executeOracleEpmDataOperation,
34
oracleEpmDataEndpoints,
@@ -12,7 +13,7 @@ export const executeOracleEpmDataRunIntegrationOperation: InternalToolOperationI
1213
> = (params, signal) =>
1314
executeOracleEpmDataOperation('run_integration', params, signal, async (input) => {
1415
const response = await requestOracleEpmDataJson(input, oracleEpmDataEndpoints.submitJob, {
15-
json: {
16+
json: filterUndefined({
1617
jobType: 'INTEGRATION',
1718
jobName: input.jobName,
1819
periodName: input.periodName,
@@ -22,7 +23,7 @@ export const executeOracleEpmDataRunIntegrationOperation: InternalToolOperationI
2223
executionMode: input.executionMode,
2324
sourceFilters: input.sourceFilters,
2425
targetOptions: input.targetOptions,
25-
},
26+
}),
2627
signal,
2728
})
2829
return { success: true, output: { httpStatus: response.status, data: response.data } }

apps/sim/lib/internal/oracle-epm-data/operations/run-pipeline.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { filterUndefined } from '@sim/utils/object'
12
import {
23
executeOracleEpmDataOperation,
34
oracleEpmDataEndpoints,
@@ -12,7 +13,11 @@ export const executeOracleEpmDataRunPipelineOperation: InternalToolOperationImpl
1213
> = (params, signal) =>
1314
executeOracleEpmDataOperation('run_pipeline', params, signal, async (input) => {
1415
const response = await requestOracleEpmDataJson(input, oracleEpmDataEndpoints.submitJob, {
15-
json: { jobType: 'PIPELINE', jobName: input.pipelineCode, variables: input.variables },
16+
json: filterUndefined({
17+
jobType: 'PIPELINE',
18+
jobName: input.pipelineCode,
19+
variables: input.variables,
20+
}),
1621
signal,
1722
})
1823
return { success: true, output: { httpStatus: response.status, data: response.data } }

apps/sim/lib/internal/oracle-epm-data/operations/set-pov-lock.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { filterUndefined } from '@sim/utils/object'
12
import {
23
executeOracleEpmDataOperation,
34
oracleEpmDataEndpoints,
@@ -13,15 +14,15 @@ export const executeOracleEpmDataSetPovLockOperation: InternalToolOperationImple
1314
> = (params, signal) =>
1415
executeOracleEpmDataOperation('set_pov_lock', params, signal, async (input) => {
1516
const response = await requestOracleEpmDataJson(input, oracleEpmDataEndpoints.setPov, {
16-
json: {
17+
json: filterUndefined({
1718
period: input.period,
1819
category: input.category,
1920
locktype: input.lockType,
2021
operation: input.lockOperation,
2122
...(input.lockType === 'application'
2223
? { application: input.application, unlockbylocation: input.unlockByLocation }
2324
: { location: input.locationName }),
24-
},
25+
}),
2526
signal,
2627
})
2728
return projectOracleEpmDataResult(response, oracleEpmDataMessageSchema, (data) => data)

apps/sim/lib/selectors/server/providers/oracle-epm-data.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,41 @@ describe('Data Integration server selectors', () => {
188188
)
189189
})
190190

191+
it.each([
192+
[401, new SelectorConnectionUnavailableError(401)],
193+
[403, new SelectorConnectionUnavailableError(403)],
194+
[429, new SelectorOptionsUnavailableError(429)],
195+
[500, new SelectorOptionsUnavailableError(502)],
196+
[200, new SelectorOptionsUnavailableError(502)],
197+
])(
198+
'preserves safe HTTP %s categories without exposing provider bodies',
199+
async (httpStatus, expected) => {
200+
const result = {
201+
success: false,
202+
output: { httpStatus, status: 401, details: 'synthetic-private-canary' },
203+
error: 'synthetic-private-canary',
204+
}
205+
mocks.connections.mockResolvedValue(result)
206+
mocks.files.mockResolvedValue(result)
207+
mocks.pov.mockResolvedValue(result)
208+
for (const key of [
209+
'oracle_epm_data.connections',
210+
'oracle_epm_data.files',
211+
'oracle_epm_data.locations',
212+
] as const) {
213+
await expect(
214+
attachments[key].execute(
215+
{
216+
...args(key),
217+
context: { application: 'Plan', period: 'Jan-26', category: 'Actual' },
218+
},
219+
auth
220+
)
221+
).rejects.toEqual(expected)
222+
}
223+
}
224+
)
225+
191226
it('honors cancellation before calling an operation', async () => {
192227
await expect(
193228
attachments['oracle_epm_data.connections'].execute(

0 commit comments

Comments
 (0)