import { MissingRequiredParamsError } from '../../src/errors'; import { IparamInputsValidator } from '../../src/validator/iparams-inputs-validator'; import { CBFileSystem } from '../../src/node/file-system'; import { IparamDefn, IparamDefns } from '../../src/types/common'; function makeIparamSchema(parameters: IparamDefn[], sectionName = 'default_section'): IparamDefns { return { installation_parameters: { sections: [ { name: sectionName, display_name: 'Default Section', description: 'Test section', parameters } ] } }; } jest.mock('../../src/logger/internal-logger', () => ({ __logger: { warn: jest.fn(), info: jest.fn(), error: jest.fn(), debug: jest.fn() } })); const mockFileSystem = { existsSync: jest.fn(), readFileSync: jest.fn(), writeFileSync: jest.fn(), readdirSync: jest.fn(), mkdirSync: jest.fn(), unlinkSync: jest.fn(), rmdirSync: jest.fn(), statSync: jest.fn(), createWriteStream: jest.fn(), copyFileSync: jest.fn(), lstatSync: jest.fn(), renameSync: jest.fn(), cpSync: jest.fn() } as jest.Mocked; describe('IparamInputsValidator', () => { let validator: IparamInputsValidator; beforeEach(() => { jest.clearAllMocks(); validator = new IparamInputsValidator(mockFileSystem); }); describe('validateAndGetIparamInputsFile', () => { it('should throw when file does not exist', () => { mockFileSystem.existsSync.mockReturnValue(false); expect(() => validator.validateAndGetIparamInputsFile('/app/iparams.local.json')).toThrow( 'iparams.local.json not found' ); expect(mockFileSystem.readFileSync).not.toHaveBeenCalled(); }); it('should return parsed inputs when file exists and is valid object', () => { const inputs = { api_key: 'secret', count: 42 }; mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(JSON.stringify(inputs)); const result = validator.validateAndGetIparamInputsFile('/app/iparams.local.json'); expect(result).toEqual(inputs); expect(mockFileSystem.readFileSync).toHaveBeenCalledWith('/app/iparams.local.json', 'utf8'); }); it('should throw when file content is invalid JSON', () => { mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue('not json {'); expect(() => validator.validateAndGetIparamInputsFile('/app/iparams.local.json')) .toThrow(/Failed to parse iparams.local.json/); }); it('should throw when root value is not an object', () => { mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue('[]'); expect(() => validator.validateAndGetIparamInputsFile('/app/iparams.local.json')) .toThrow('iparams.local.json must be an object'); }); }); describe('ensureInputsIsObject', () => { it('should throw when inputs is null, undefined, or array', () => { expect(() => validator.ensureInputsIsObject(null)).toThrow('Inputs must be an object'); expect(() => validator.ensureInputsIsObject(undefined)).toThrow('Inputs must be an object'); expect(() => validator.ensureInputsIsObject([])).toThrow('Inputs must be an object'); }); it('should not throw when inputs is a plain object', () => { expect(() => validator.ensureInputsIsObject({})).not.toThrow(); expect(() => validator.ensureInputsIsObject({ k: 'v' })).not.toThrow(); }); }); describe('validateInputs', () => { it('should throw when inputs is not an object', () => { const defns = makeIparamSchema([ { name: 'k', display_name: 'K', description: 'd', type: 'TEXT', required: false } ]); expect(() => validator.validateInputs(null as any, defns)).toThrow('Inputs must be an object'); expect(() => validator.validateInputs(undefined as any, defns)).toThrow('Inputs must be an object'); expect(() => validator.validateInputs([] as any, defns)).toThrow('Inputs must be an object'); }); const validDefns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true }, { name: 'count', display_name: 'Count', description: 'd', type: 'NUMBER', required: false } ]); it('should return validated inputs when values are valid', () => { const inputs = { default_section: { api_key: 'key123', count: 10 } }; const result = validator.validateInputs(inputs, validDefns); expect(result).toEqual({ default_section: { api_key: 'key123', count: 10 } }); }); it('should throw MissingRequiredParamsError when required param is missing', () => { const inputs = { default_section: { count: 10 } }; let thrown: unknown; try { validator.validateInputs(inputs, validDefns); } catch (e) { thrown = e; } expect(thrown).toBeInstanceOf(MissingRequiredParamsError); expect((thrown as MissingRequiredParamsError).missing).toEqual(['default_section.api_key']); }); it('should throw when TEXT param has non-string value', () => { const inputs = { default_section: { api_key: 123 as any, count: 10 } }; expect(() => validator.validateInputs(inputs, validDefns)) .toThrow(/Parameter "default_section.api_key": value must be a non-empty string/); }); it('should throw when NUMBER param has non-number value', () => { const inputs = { default_section: { api_key: 'ok', count: 'ten' as any } }; expect(() => validator.validateInputs(inputs, validDefns)) .toThrow(/Parameter "default_section.count": value must be a finite number/); }); it('should throw when NUMBER param exceeds MAX_SAFE_INTEGER', () => { const defns = makeIparamSchema([ { name: 'n', display_name: 'N', description: 'd', type: 'NUMBER', required: false } ]); const inputs = { default_section: { n: Number.MAX_SAFE_INTEGER + 1 } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/value must be within the range of/); }); it('should throw when NUMBER param is below MIN_SAFE_INTEGER', () => { const defns = makeIparamSchema([ { name: 'n', display_name: 'N', description: 'd', type: 'NUMBER', required: false } ]); const inputs = { default_section: { n: Number.MIN_SAFE_INTEGER - 1 } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/value must be within the range of/); }); it('should throw when BOOLEAN param has non-boolean value', () => { const defns = makeIparamSchema([ { name: 'flag', display_name: 'Flag', description: 'd', type: 'BOOLEAN', required: false } ]); const inputs = { default_section: { flag: 'true' as any } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/Parameter "default_section.flag": value must be a boolean/); }); it('should throw when URL param has invalid URL', () => { const defns = makeIparamSchema([ { name: 'endpoint', display_name: 'Endpoint', description: 'd', type: 'URL', required: false } ]); const inputs = { default_section: { endpoint: 'not-a-url' } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/Parameter "default_section.endpoint": value must be a valid URL/); }); it('should throw when URL param uses a disallowed protocol', () => { const defns = makeIparamSchema([ { name: 'endpoint', display_name: 'Endpoint', description: 'd', type: 'URL', required: false } ]); const inputs = { default_section: { endpoint: 'ftp://files.example.com' } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/Parameter "default_section.endpoint": value must be a valid URL with protocol http: or https:/); }); it('should throw when URL param uses javascript: scheme', () => { const defns = makeIparamSchema([ { name: 'endpoint', display_name: 'Endpoint', description: 'd', type: 'URL', required: false } ]); const inputs = { default_section: { endpoint: 'javascript:alert(1)' } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/protocol http: or https:/); }); it('should throw when DATE param has invalid format', () => { const defns = makeIparamSchema([ { name: 'start_date', display_name: 'Start', description: 'd', type: 'DATE', required: false } ]); const inputs = { default_section: { start_date: '31-12-2025' } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/Parameter "default_section.start_date": value must be a valid date in YYYY-MM-DD format/); }); it('should accept valid DATE value', () => { const defns = makeIparamSchema([ { name: 'start_date', display_name: 'Start', description: 'd', type: 'DATE', required: false } ]); const inputs = { default_section: { start_date: '2025-12-31' } }; const result = validator.validateInputs(inputs, defns); expect(result).toEqual({ default_section: { start_date: '2025-12-31' } }); }); it('should throw when DATE value is invalid date', () => { const defns = makeIparamSchema([ { name: 'd', display_name: 'D', description: 'd', type: 'DATE', required: false } ]); const inputs = { default_section: { d: '2025-02-30' } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/value must be a valid date in YYYY-MM-DD format/); }); it('should throw when DROPDOWN value is not in options', () => { const defns = makeIparamSchema([ { name: 'region', display_name: 'Region', description: 'd', type: 'DROPDOWN', required: false, options: ['us', 'eu'] } ]); const inputs = { default_section: { region: 'asia' } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/Parameter "default_section.region": value must be one of: us, eu/); }); it('should accept valid DROPDOWN value', () => { const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', required: false, options: ['any'] } ]); const inputs = { default_section: { r: 'any' } }; const result = validator.validateInputs(inputs, defns); expect(result).toEqual({ default_section: { r: 'any' } }); }); it('should throw when MULTISELECT_DROPDOWN value is not array', () => { const defns = makeIparamSchema([ { name: 'tags', display_name: 'Tags', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'] } ]); const inputs = { default_section: { tags: 'a' as any } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/Parameter "default_section.tags": value must be an array of strings for MULTISELECT_DROPDOWN type/); }); it('should accept empty array for MULTISELECT_DROPDOWN and return no value', () => { const defns = makeIparamSchema([ { name: 'tags', display_name: 'Tags', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'] } ]); const inputs = { default_section: { tags: [] } }; const result = validator.validateInputs(inputs, defns); expect(result).toEqual({}); }); it('should throw when MULTISELECT_DROPDOWN value item is not string', () => { const defns = makeIparamSchema([ { name: 'tags', display_name: 'Tags', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'] } ]); const inputs = { default_section: { tags: ['a', 1 as any] } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/value array item at index 1 must be a non-empty string/); }); it('should throw when MULTISELECT_DROPDOWN value item not in options', () => { const defns = makeIparamSchema([ { name: 'tags', display_name: 'Tags', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'] } ]); const inputs = { default_section: { tags: ['a', 'c'] } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/value array item at index 1 must be one of: a, b/); }); it('should throw when MULTISELECT_DROPDOWN value contains duplicate entries (after trim)', () => { const defns = makeIparamSchema([ { name: 'tags', display_name: 'Tags', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'] } ]); const inputs = { default_section: { tags: ['a', 'a'] } }; expect(() => validator.validateInputs(inputs, defns)) .toThrow(/Parameter "default_section.tags": multiselect value must not contain duplicate entries/); }); it('should accept valid URL value', () => { const defns = makeIparamSchema([ { name: 'endpoint', display_name: 'Endpoint', description: 'd', type: 'URL', required: false } ]); const inputs = { default_section: { endpoint: 'https://api.example.com' } }; const result = validator.validateInputs(inputs, defns); expect(result).toEqual({ default_section: { endpoint: 'https://api.example.com' } }); }); it('should accept http URL value', () => { const defns = makeIparamSchema([ { name: 'endpoint', display_name: 'Endpoint', description: 'd', type: 'URL', required: false } ]); const inputs = { default_section: { endpoint: 'http://localhost:3000/path' } }; const result = validator.validateInputs(inputs, defns); expect(result).toEqual({ default_section: { endpoint: 'http://localhost:3000/path' } }); }); it('should return only defined params and warn on unknown section and param keys', () => { const { __logger } = require('../../src/logger/internal-logger'); const inputs = { default_section: { api_key: 'key', count: 1, unknown_key: 'x' }, unknown_section: { a: 1 } }; const result = validator.validateInputs(inputs, validDefns); expect(result).toEqual({ default_section: { api_key: 'key', count: 1 } }); expect(__logger.warn).toHaveBeenCalledWith('Unknown parameter "default_section.unknown_key" will be ignored'); expect(__logger.warn).toHaveBeenCalledWith('Unknown section "unknown_section" will be ignored'); }); it('should validate inputs for multiple sections', () => { const defns: IparamDefns = { installation_parameters: { sections: [ { name: 'auth', display_name: 'Auth', description: 'd', parameters: [ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true } ] }, { name: 'config', display_name: 'Config', description: 'd', parameters: [ { name: 'region', display_name: 'Region', description: 'd', type: 'TEXT', required: false } ] } ] } }; const inputs = { auth: { api_key: 'secret' }, config: { region: 'us' } }; const result = validator.validateInputs(inputs, defns); expect(result).toEqual(inputs); }); it('should throw when section inputs is not an object', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'Key', description: 'd', type: 'TEXT', required: false } ]); const inputs = { default_section: [] as any }; expect(() => validator.validateInputs(inputs, defns)).toThrow( 'Section "default_section": inputs must be an object' ); }); it('should report missing required params with section prefix across sections', () => { const defns: IparamDefns = { installation_parameters: { sections: [ { name: 'auth', display_name: 'Auth', description: 'd', parameters: [ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true } ] }, { name: 'config', display_name: 'Config', description: 'd', parameters: [ { name: 'site_id', display_name: 'Site ID', description: 'd', type: 'TEXT', required: true } ] } ] } }; const inputs = { auth: { api_key: 'secret' } }; expect(() => validator.validateInputs(inputs, defns)).toThrow(MissingRequiredParamsError); try { validator.validateInputs(inputs, defns); } catch (e) { expect((e as MissingRequiredParamsError).missing).toEqual(['config.site_id']); } }); }); });