ToolJet/server/test/modules/data-queries/util.service.spec.ts
Rudhra Deep Biswas ae946f2713
Prevent Stringify Query Resolved Value (#12788)
* lts to pre prelease query

* Add test cases for query options resolution

* string and undefined

* comments

* fixes brackets

---------

Co-authored-by: Akshay Sasidharan <akshaysasidharan93@gmail.com>
Co-authored-by: Midhun G S <gsmithun4@gmail.com>
2025-06-25 18:40:13 +05:30

484 lines
14 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { DataQueriesUtilService } from '../../../src/modules/data-queries/util.service';
import { ConfigService } from '@nestjs/config';
import { VersionRepository } from '../../../src/modules/versions/repository';
import { AppEnvironmentUtilService } from '../../../src/modules/app-environments/util.service';
import { DataSourcesUtilService } from '../../../src/modules/data-sources/util.service';
import { PluginsServiceSelector } from '../../../src/modules/data-sources/services/plugin-selector.service';
describe('DataQueriesUtilService', () => {
let service: DataQueriesUtilService;
let dataSourceUtilService: DataSourcesUtilService;
beforeEach(async () => {
const mockDataSourceUtilService = {
resolveConstants: jest.fn(),
parseSourceOptions: jest.fn(),
getAuthUrl: jest.fn(),
updateOAuthAccessToken: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
DataQueriesUtilService,
{
provide: ConfigService,
useValue: {
get: jest.fn(),
},
},
{
provide: VersionRepository,
useValue: {
findOne: jest.fn(),
},
},
{
provide: AppEnvironmentUtilService,
useValue: {
getOptions: jest.fn(),
},
},
{
provide: DataSourcesUtilService,
useValue: mockDataSourceUtilService,
},
{
provide: PluginsServiceSelector,
useValue: {
getService: jest.fn(),
},
},
],
}).compile();
service = module.get<DataQueriesUtilService>(DataQueriesUtilService);
dataSourceUtilService = module.get<DataSourcesUtilService>(DataSourcesUtilService);
});
describe('parseQueryOptions', () => {
it('should traverse objects and arrays properly', async () => {
const object = {
nestedObject: {
key: 'value',
},
array: [1, 2, 3],
nestedArray: [{ key: 'value' }],
};
const options = {};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual(object);
});
it('should replace a simple variable', async () => {
const object = {
key: '{{variable}}',
};
const options = {
'{{variable}}': 'replaced value',
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
key: 'replaced value',
});
});
it('should replace multiple variables in a string', async () => {
const object = {
key: 'Hello {{name}}, welcome to {{place}}!',
};
const options = {
'{{name}}': 'John',
'{{place}}': 'ToolJet',
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
key: 'Hello John, welcome to ToolJet!',
});
});
it('should replace newlines with spaces', async () => {
const object = {
key: 'Hello\nWorld',
};
const options = {};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
key: 'Hello\nWorld',
});
});
it('should resolve constants', async () => {
const object = {
key: '{{constants.API_KEY}}',
};
const options = {};
jest.spyOn(dataSourceUtilService, 'resolveConstants').mockResolvedValue('resolved-api-key');
const result = await service.parseQueryOptions(object, options, 'org-id', 'env-id', { id: 'user-id' } as any);
expect(dataSourceUtilService.resolveConstants).toHaveBeenCalledWith('{{constants.API_KEY}}', 'org-id', 'env-id', {
id: 'user-id',
});
expect(result).toEqual({
key: 'resolved-api-key',
});
});
it('should resolve secrets', async () => {
const object = {
key: '{{secrets.DB_PASSWORD}}',
};
const options = {};
jest.spyOn(dataSourceUtilService, 'resolveConstants').mockResolvedValue('secret-password');
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(dataSourceUtilService.resolveConstants).toHaveBeenCalledWith(
'{{secrets.DB_PASSWORD}}',
'org-id',
undefined,
undefined
);
expect(result).toEqual({
key: 'secret-password',
});
});
it('should resolve globals.server variables', async () => {
const object = {
key: '{{globals.server.BASE_URL}}',
};
const options = {};
jest.spyOn(dataSourceUtilService, 'resolveConstants').mockResolvedValue('https://api.example.com');
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(dataSourceUtilService.resolveConstants).toHaveBeenCalledWith(
'{{globals.server.BASE_URL}}',
'org-id',
undefined,
undefined
);
expect(result).toEqual({
key: 'https://api.example.com',
});
});
it('should replace variables in nested objects', async () => {
const object = {
level1: {
level2: {
key: '{{variable}}',
},
},
};
const options = {
'{{variable}}': 'replaced value',
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
level1: {
level2: {
key: 'replaced value',
},
},
});
});
it('should replace variables in arrays', async () => {
const object = {
array: ['{{var1}}', '{{var2}}', 'static'],
};
const options = {
'{{var1}}': 'value1',
'{{var2}}': 'value2',
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
array: ['value1', 'value2', 'static'],
});
});
it('should handle object value replacements', async () => {
const object = {
key: '{{objectVar}}',
};
const options = {
'{{objectVar}}': { foo: 'bar' },
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
key: { foo: 'bar' },
});
});
it('should handle null and undefined replacements', async () => {
const object = {
nullKey: '{{nullVar}}',
undefinedKey: '{{undefinedVar}}',
};
const options = {
'{{nullVar}}': null,
'{{undefinedVar}}': undefined,
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
nullKey: null,
undefinedKey: undefined,
});
});
it('should handle numeric replacements', async () => {
const object = {
key: '{{numVar}}',
};
const options = {
'{{numVar}}': 42,
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
key: 42,
});
});
it('should handle a complex case with multiple replacement types', async () => {
const object = {
simpleVar: '{{var}}',
multipleVars: 'Hello {{name}} at {{company}}',
constant: '{{constants.API_URL}}',
nested: {
array: [
'{{var}}',
{
deepNested: '{{secrets.API_KEY}}',
},
],
},
};
const options = {
'{{var}}': 'replaced',
'{{name}}': 'John',
'{{company}}': 'ToolJet',
};
jest.spyOn(dataSourceUtilService, 'resolveConstants').mockImplementation(async (value) => {
if (value === '{{constants.API_URL}}') return 'https://api.example.com';
if (value === '{{secrets.API_KEY}}') return 'secret-key';
return value;
});
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
simpleVar: 'replaced',
multipleVars: 'Hello John at ToolJet',
constant: 'https://api.example.com',
nested: {
array: [
'replaced',
{
deepNested: 'secret-key',
},
],
},
});
});
it('should fail to resolve constants with spaces in variable names', async () => {
const object = {
key: '{{ constants.API_KEY}}',
anotherKey: '{{ secrets.DB_PASSWORD }}',
thirdKey: '{{ globals.server.BASE_URL }}',
};
const options = {};
jest.spyOn(dataSourceUtilService, 'resolveConstants').mockResolvedValue('should-not-be-used');
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(dataSourceUtilService.resolveConstants).not.toHaveBeenCalledWith(
'{{ constants.API_KEY}}',
'org-id',
undefined,
undefined
);
expect(result).toEqual(object);
});
it('should not replace malformed variables', async () => {
const object = {
incomplete1: '{{ incomplete',
incomplete2: 'incomplete }}',
malformed: '{ variable }',
};
const options = {
'{{ incomplete': 'should not be used',
'incomplete }}': 'should not be used',
'{ variable }': 'should not be used',
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual(object);
});
it('should handle variables without replacement values', async () => {
const object = {
key: '{{nonExistentVar}}',
};
const options = {};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
key: undefined,
});
});
it('should handle a mix of valid and invalid variable formats', async () => {
const object = {
valid: '{{validVar}}',
invalid: '{{ invalidVar }}',
mixed: 'Hello {{validVar}} and {{ invalidVar }}!',
};
const options = {
'{{validVar}}': 'replaced',
'{{ invalidVar }}': undefined,
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
valid: 'replaced',
invalid: undefined,
mixed: 'Hello replaced and undefined!',
});
});
it('should handle spaces in constants/secrets/globals references', async () => {
const object = {
secrets: '{{secrets.API_KEY}}',
secretsWithSpaces: '{{ secrets.API_KEY }}',
constants: '{{constants.DB_URL}}',
constantsWithSpaces: '{{ constants.DB_URL }}',
globals: '{{globals.server.URL}}',
globalsWithSpaces: '{{ globals.server.URL }}',
};
const options = {};
jest.spyOn(dataSourceUtilService, 'resolveConstants').mockImplementation(async (input) => {
if (input === '{{secrets.API_KEY}}') return 'correct-secret';
if (input === '{{constants.DB_URL}}') return 'correct-constant';
if (input === '{{globals.server.URL}}') return 'correct-global';
// These conditions should be included for proper handling of spaces
if (input === '{{ secrets.API_KEY }}') return 'correct-secret-with-spaces';
if (input === '{{ constants.DB_URL }}') return 'correct-constant-with-spaces';
if (input === '{{ globals.server.URL }}') return 'correct-global-with-spaces';
return input;
});
const result = await service.parseQueryOptions(object, options, 'org-id');
// This expectation will fail because the current implementation doesn't handle spaces correctly
expect(result).toEqual({
secrets: 'correct-secret',
secretsWithSpaces: 'correct-secret-with-spaces', // Should be resolved but currently isn't
constants: 'correct-constant',
constantsWithSpaces: 'correct-constant-with-spaces', // Should be resolved but currently isn't
globals: 'correct-global',
globalsWithSpaces: 'correct-global-with-spaces', // Should be resolved but currently isn't
});
});
it('should handle all JavaScript data types and special values', async () => {
const date = new Date('2023-05-15T12:00:00Z');
const regex = /test/gi;
const object = {
string: '{{stringVar}}',
number: '{{numberVar}}',
boolean: '{{boolVar}}',
nullVal: '{{nullVar}}',
undefinedVal: '{{undefinedVar}}',
nanVal: '{{nanVar}}',
infinityVal: '{{infinityVar}}',
negInfinityVal: '{{negInfinityVar}}',
emptyString: '{{emptyStringVar}}',
emptyArray: '{{emptyArrayVar}}',
emptyObject: '{{emptyObjectVar}}',
dateObj: '{{dateVar}}',
regexObj: '{{regexVar}}',
bigInt: '{{bigIntVar}}',
nestedObject: '{{nestedObjVar}}',
arrayMixed: '{{arrayMixedVar}}',
// Using variables within strings
mixedString: 'String with {{numberVar}} and {{boolVar}} values',
};
const options = {
'{{stringVar}}': 'test string',
'{{numberVar}}': 42,
'{{boolVar}}': true,
'{{nullVar}}': null,
'{{undefinedVar}}': undefined,
'{{nanVar}}': NaN,
'{{infinityVar}}': Infinity,
'{{negInfinityVar}}': -Infinity,
'{{emptyStringVar}}': '',
'{{emptyArrayVar}}': [],
'{{emptyObjectVar}}': {},
'{{dateVar}}': date,
'{{regexVar}}': regex,
'{{bigIntVar}}': BigInt(9007199254740991),
'{{nestedObjVar}}': {
key1: 'value1',
key2: 123,
key3: {
nestedKey: 'nested value',
},
},
'{{arrayMixedVar}}': [1, 'string', true, null, { key: 'value' }],
};
const result = await service.parseQueryOptions(object, options, 'org-id');
expect(result).toEqual({
string: 'test string',
number: 42,
boolean: true,
nullVal: null,
undefinedVal: undefined,
nanVal: NaN,
infinityVal: Infinity,
negInfinityVal: -Infinity,
emptyString: '',
emptyArray: [],
emptyObject: {},
dateObj: date,
regexObj: regex,
bigInt: BigInt(9007199254740991),
nestedObject: {
key1: 'value1',
key2: 123,
key3: {
nestedKey: 'nested value',
},
},
arrayMixed: [1, 'string', true, null, { key: 'value' }],
// Mixed string should have the values converted to strings when interpolated
mixedString: 'String with 42 and true values',
});
});
});
});