import { JsonValue } from '@bufbuild/protobuf'; import { cloneDeep } from 'lodash'; import { describe, it, expect } from 'vitest'; import { Api } from '@superblocksteam/types'; import { sanitizeV2RequestBody } from './apiv2.js'; describe('sanitizeV2RequestBody', () => { it('properly sets the transcribeAudioToTextTranslateToEnglish value in a v2 request body', () => { const apiBody = { blocks: [ { name: 'Step1', step: { integration: 'some-openai-integration', openai: { transcribeAudioToTextTranslateToEnglish: true } } }, { name: 'Conditional1', conditional: { if: { condition: '{{ true }}', blocks: [ { name: 'Something1', step: { integration: 'some-openai-integration', openai: { transcribeAudioToTextTranslateToEnglish: 'checked' } } } ] } } } ] }; const sanitizedApiBody = cloneDeep(apiBody); // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[1].conditional.if.blocks[0].step.openai.transcribeAudioToTextTranslateToEnglish = true; expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('leaves the transcribeAudioToTextTranslateToEnglish value alone if already a boolean', () => { const apiBody = { blocks: [ { name: 'Step1', step: { integration: 'some-openai-integration', openai: { transcribeAudioToTextTranslateToEnglish: true } } }, { name: 'Conditional1', conditional: { if: { condition: '{{ true }}', blocks: [ { name: 'Something1', step: { integration: 'some-openai-integration', openai: { transcribeAudioToTextTranslateToEnglish: true } } } ] } } } ] }; const sanitizedApiBody = cloneDeep(apiBody); expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('is able to sanitize null values in DSL', async () => { // api that has some null values. // protobuf does not support null values, so we need to convert them to ValuePb.fromJson(null) const apiBody = { blocks: [ { name: 'say hello', step: { python: { body: "print 'hello world'" }, integration: 'python' } } ], trigger: { workflow: { options: { profiles: { modes: { editor: { default: '12345678-0000-aaaa-bbbb-01234567890a', available: ['12345678-0000-aaaa-bbbb-01234567890a', '11111111-2222-3333-4444-aaaaaaaaaaaa'] }, deployed: { default: '11111111-2222-3333-4444-aaaaaaaaaaaa', available: ['11111111-2222-3333-4444-aaaaaaaaaaaa', '12345678-0000-aaaa-bbbb-01234567890a'] } } } }, parameters: { body: { top_level_scalar_param1: 'value1', top_level_object_param1: [ { nested_scalar_param1: 'value2', nested_scalar_param2: 'value3', nested_null_param1: null } ], some_null_param1: null, some_null_param2: null, some_name: 'Test name', some_url: 'https://example.com' } } } }, metadata: { id: '12345678-1111-2222-3333-000000000000', name: 'say hello', tags: { type: 'workflow', subtype: 'python' }, folder: 'folder1', version: 'v1', timestamps: { updated: '2024-04-04T18:05:10.427Z' }, description: 'Some description', organization: '55555555-0000-1111-7777-aaaaaaaaaaaa' } }; async function toApiPbIgnoreUnknownFields() { return Api.fromJson(apiBody, { ignoreUnknownFields: true }); } // the initial conversion should fail because of the null values await expect(toApiPbIgnoreUnknownFields()).rejects.toThrow( 'cannot decode field api.v1.Trigger.Workflow.Parameters.body from JSON: map value null' ); const sanitizedApiBody = sanitizeV2RequestBody(apiBody) as JsonValue; // the sanitized body should be able to be converted to the protobuf object const apiPb = Api.fromJson(sanitizedApiBody, { ignoreUnknownFields: true }); expect(apiPb).toBeDefined(); expect(JSON.parse(apiPb.toJsonString())).toEqual(apiBody); }); it('sanitizes field api.v1.Variables.items', () => { const apiBody = { blocks: [ { name: 'Variables1', variables: { items: { v1: { mode: 'MODE_UNSPECIFIED', type: 'TYPE_SIMPLE', value: '{{10}}' }, v2: { mode: 'MODE_UNSPECIFIED', type: 'TYPE_SIMPLE', value: '{{100}}' } } } } ] }; const expectedSanitizedApiBody = { blocks: [ { name: 'Variables1', variables: { items: [ { key: 'v1', mode: 'MODE_UNSPECIFIED', type: 'TYPE_SIMPLE', value: '{{10}}' }, { key: 'v2', mode: 'MODE_UNSPECIFIED', type: 'TYPE_SIMPLE', value: '{{100}}' } ] } } ] }; expect(sanitizeV2RequestBody(apiBody)).toEqual(expectedSanitizedApiBody); }); it('does not impact loop variables', () => { const apiBody = { blocks: [ { loop: { type: 'TYPE_FOR', range: '{{10}}', blocks: [ { name: 'Step1', step: { postgres: { body: 'SELECT * FROM orders where price > {{200 + (index.value*10)}}', filterBy: [], operation: 'run_sql', mappedColumns: [], usePreparedSql: true }, integration: 'some-postgres-integration' } } ], variables: { item: 'item', index: 'index' } }, name: 'Loop1' } ], trigger: { application: { id: 'some-application-id' } }, metadata: { id: 'some-api-id', name: 'api name', tags: {}, folder: '', version: '', description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('does not impact GraphQL variables', () => { const apiBody = { blocks: [ { name: 'Step1', step: { graphql: { body: 'query {\n allFilms {\n films {\n title\n }\n }\n}', path: 'https://swapi-graphql.netlify.app/.netlify/functions/index', custom: { variables: { value: '{"test": 123}' } }, headers: [ { key: 'Content-Type', value: 'application/json' } ], superblocksMetadata: { pluginVersion: '0.0.8' } }, integration: 'graphql' } } ], trigger: { application: { id: 'some-application-id', options: {} } }, metadata: { id: 'some-api-id', name: 'api name', timestamps: {}, organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('does not impact try-catch blocks', () => { const apiBody = { blocks: [ { name: 'TryCatch1', tryCatch: { try: { blocks: [ { name: 'Step1', step: { javascript: { body: 'const obj = {str: "hello world"};\nconsole.log(obj.str);\nreturn obj;\n' }, integration: 'javascript' } } ] }, catch: { blocks: [ { name: 'Step1', step: { javascript: { body: 'const obj = {str: "hello world"};\nconsole.log(obj.str);\nreturn obj;\n' }, integration: 'javascript' } } ] }, variables: { error: 'error' } } } ], trigger: { application: { id: 'some-application-id', pageId: 'some-page-id' } }, metadata: { id: 'some-api-id', name: 'api name', tags: {}, folder: '', version: '', timestamps: { updated: new Date().toISOString() }, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('sets newValues to an empty string for mariadb if newValues is an empty list', () => { const apiBody = { blocks: [ { name: 'Step1', step: { mariadb: { body: '-- You can use SQL to query data (ex. SELECT * FROM orders;)\\n\\nSELECT * FROM survey WHERE ID = {{Table1.selectedRow.id}} LIMIT 1', table: '', filterBy: [], newValues: [], oldValues: [], operation: 'run_sql', mappingMode: '', mappedColumns: [], usePreparedSql: true, superblocksMetadata: { pluginVersion: '0.0.10' }, useAdvancedMatching: 'auto' }, integration: 'some-mariadb-integration' } } ], trigger: { application: { id: 'some-application-id', options: { executeOnPageLoad: false } } }, metadata: { id: 'some-api-id', name: 'some-api-name', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[0].step.mariadb.newValues = ''; expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('keeps newValues for mariadb if newValues is a string', () => { const apiBody = { blocks: [ { name: 'Step1', step: { mariadb: { body: '-- You can use SQL to query data (ex. SELECT * FROM orders;)\\n\\nSELECT * FROM survey WHERE ID = {{Table1.selectedRow.id}} LIMIT 1', table: '', filterBy: [], newValues: 'some-new-values', oldValues: [], operation: 'run_sql', mappingMode: '', mappedColumns: [], usePreparedSql: true, superblocksMetadata: { pluginVersion: '0.0.10' }, useAdvancedMatching: 'auto' }, integration: 'some-mariadb-integration' } } ], trigger: { application: { id: 'some-application-id', options: { executeOnPageLoad: false } } }, metadata: { id: 'some-api-id', name: 'some-api-name', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('sets newValues to an empty string for mysql if newValues is an empty list', () => { const apiBody = { blocks: [ { name: 'Step1', step: { mysql: { body: '-- You can use SQL to query data (ex. SELECT * FROM orders;)\\n\\nSELECT * FROM survey WHERE ID = {{Table1.selectedRow.id}} LIMIT 1', table: '', filterBy: [], newValues: [], oldValues: [], operation: 'run_sql', mappingMode: '', mappedColumns: [], usePreparedSql: true, superblocksMetadata: { pluginVersion: '0.0.10' }, useAdvancedMatching: 'auto' }, integration: 'some-mysql-integration' } } ], trigger: { application: { id: 'some-application-id', options: { executeOnPageLoad: false } } }, metadata: { id: 'some-api-id', name: 'some-api-name', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[0].step.mysql.newValues = ''; expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('keeps newValues for mysql if newValues is a string', () => { const apiBody = { blocks: [ { name: 'Step1', step: { mysql: { body: '-- You can use SQL to query data (ex. SELECT * FROM orders;)\\n\\nSELECT * FROM survey WHERE ID = {{Table1.selectedRow.id}} LIMIT 1', table: '', filterBy: [], newValues: 'some-new-values', oldValues: [], operation: 'run_sql', mappingMode: '', mappedColumns: [], usePreparedSql: true, superblocksMetadata: { pluginVersion: '0.0.10' }, useAdvancedMatching: 'auto' }, integration: 'some-mysql-integration' } } ], trigger: { application: { id: 'some-application-id', options: { executeOnPageLoad: false } } }, metadata: { id: 'some-api-id', name: 'some-api-name', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('casts generateTextNewTextMaxTokens from integer to string', () => { const apiBody = { blocks: [ { name: 'Step1', step: { openai: { action: 'Generate Image', aiModel: 'text-davinci-003', embeddingText: '', generateCodeType: '', generateTextType: '', checkModerationText: '', generateImageMethod: 'Generate from Prompt', superblocksMetadata: { pluginVersion: '0.0.2' }, checkModerationAiModel: '', generateCodeNewCodePrompt: '', generateTextNewTextPrompt: '', generateCodeEditCodePrompt: '', generateCodeNewCodeAiModel: '', generateTextEditTextPrompt: '', generateTextNewTextAiModel: '', generateCodeEditCodeAiModel: '', generateTextEditTextAiModel: '', generateImageEditImagePrompt: '', generateTextEmbeddingAiModel: '', generateTextNewTextMaxTokens: 1, //this is a bad value that should be sanitized to string transcribeAudioToTextAiModel: '', generateChatGptResponsePrompt: '', generateChatGPTResponseAiModel: '', generateCodeEditCodeCodeToEdit: '', generateTextEditTextTextToEdit: '', transcribeAudioToTextAudioFile: '', generateImageEditImageImageMask: '', generateImageVaryImageImageFile: '', generateImageVaryImageImageSize: '', generateChatGptResponseMaxTokens: 1, generateImageEditImageImageSizes: '', transcribeAudioToTextInputLanguage: '', generateImageEditImageImageFileToEdit: '', generateImageGenerateFromPromptPrompt: '{{Prompt.value}}', transcribeAudioToTextTranslateToEnglish: 'false', generateChatGptResponseSystemInstruction: '', generateImageGenerateFromPromptImageImageSize: '512x512' }, integration: 'some-openai-integration' } } ], trigger: { application: { id: 'some-app-id', options: { executeOnPageLoad: false } } }, metadata: { id: 'some-api-id', name: 'Generate_Image2', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[0].step.openai.generateTextNewTextMaxTokens = '1'; // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[0].step.openai.transcribeAudioToTextTranslateToEnglish = false; expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('casts gsheets.v1.Plugin.rowNumber from integer to string when rowNumber=0', () => { const apiBody = { blocks: [ { name: 'Step1', step: { gsheets: { body: '', data: '', range: '', action: 'READ_SPREADSHEET', format: '', rowNumber: 0, // this is a bad value that should be sanitized to string sheetTitle: 'Sync data', spreadsheetId: 'some-spreadsheet-id', headerRowNumber: 1, includeHeaderRow: false, preserveHeaderRow: false, superblocksMetadata: { pluginVersion: '0.0.17' }, extractFirstRowHeader: true, writeToDestinationType: '' }, integration: 'some-gsheets-integration' } } ], trigger: { application: { id: 'some-application-id', options: {} } }, metadata: { id: 'some-api-id', name: 'some-api-name', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[0].step.gsheets.rowNumber = '0'; expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('casts gsheets.v1.Plugin.rowNumber from integer to string when rowNumber=100', () => { const apiBody = { blocks: [ { name: 'Step1', step: { gsheets: { body: '', data: '', range: '', action: 'READ_SPREADSHEET', format: '', rowNumber: 100, sheetTitle: 'Sync data', spreadsheetId: 'some-spreadsheet-id', headerRowNumber: 1, includeHeaderRow: false, preserveHeaderRow: false, superblocksMetadata: { pluginVersion: '0.0.17' }, extractFirstRowHeader: true, writeToDestinationType: '' }, integration: 'some-gsheets-integration' } } ], trigger: { application: { id: 'some-application-id', options: {} } }, metadata: { id: 'some-api-id', name: 'some-api-name', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[0].step.gsheets.rowNumber = '100'; expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it("doesn't crash if rowNumber is not in gsheets.v1.Plugin", () => { const apiBody = { blocks: [ { name: 'Step1', step: { gsheets: { body: '', data: '', range: '', action: 'READ_SPREADSHEET', format: '', sheetTitle: 'Sync data', spreadsheetId: 'some-spreadsheet-id', headerRowNumber: 1, includeHeaderRow: false, preserveHeaderRow: false, superblocksMetadata: { pluginVersion: '0.0.17' }, extractFirstRowHeader: true, writeToDestinationType: '' }, integration: 'some-gsheets-integration' } } ], trigger: { application: { id: 'some-application-id', options: {} } }, metadata: { id: 'some-api-id', name: 'some-api-name', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; // please don't crash sanitizeV2RequestBody(apiBody); }); it('handles api.v1.Trigger.Workflow.Parameters.QueryParam', () => { const apiBody = { blocks: [ { name: 'Step1', step: { postgres: { body: '-- You can use SQL to query data (ex. SELECT * FROM orders;)\\n\\n', table: '', schema: '', filterBy: [], newValues: '', oldValues: '', operation: 'run_sql', httpMethod: '', deletedRows: '', mappingMode: '', insertedRows: '', mappedColumns: [], usePreparedSql: true, superblocksMetadata: { pluginVersion: '0.0.10' }, useAdvancedMatching: 'auto' }, integration: 'some-postgres-integration' } } ], trigger: { workflow: { options: { profiles: { modes: { editor: { default: 'stag-profile-id', available: ['stag-profile-id', 'prod-profile-id'] }, preview: { default: '', available: [] }, deployed: { default: 'prod-profile-id', available: ['stag-profile-id', 'prod-profile-id'] } } } }, parameters: { body: {}, query: { '': '', // bad value that should be sanitized param1: { values: ['value1'] }, // good value that should be left alone param2: { values: ['value1', 'value2', 'value3'] } // good value that should be left alone } } } }, metadata: { id: 'some-workflow-id', name: 'some-workflow-name', tags: {}, folder: '', version: '', timestamps: {}, description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).trigger.workflow.parameters.query[''] = { values: [''] }; expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); it('handles api.v1.Variables.Config.value', () => { const apiBody = { blocks: [ { name: 'Variables', variables: { items: [ { key: 'months', mode: 'MODE_READWRITE', type: 'TYPE_SIMPLE', value: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] //bad value that should be sanitized }, { key: 'years', mode: 'MODE_READWRITE', type: 'TYPE_SIMPLE', value: ['2020', '2021', '2022', '2023'] //bad value that should be sanitized }, { key: 'days', mode: 'MODE_READWRITE', type: 'TYPE_SIMPLE', value: '{{s,m,t,w,t,f,s}}' //legal value that should be left alone } ] } } ], trigger: { application: { id: 'some-application-id', options: { executeOnPageLoad: true } } }, metadata: { id: 'some-api-id', name: 'some-api-name', tags: {}, folder: '', version: '', description: '', organization: 'some-organization-id' } }; const sanitizedApiBody = cloneDeep(apiBody); // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[0].variables.items[0].value = '{{Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec}}'; // eslint-disable-next-line @typescript-eslint/no-explicit-any (sanitizedApiBody as any).blocks[0].variables.items[1].value = '{{2020,2021,2022,2023}}'; expect(sanitizeV2RequestBody(apiBody)).toEqual(sanitizedApiBody); }); });