import { IparamDefnsValidator } from '../../src/validator/iparam-defns-validator'; import { IPARAMS_MAX_COUNT } from '../../src/validator/iparam-constants'; import { CBFileSystem } from '../../src/node/file-system'; import { IparamDefn, IparamDefns } from '../../src/types/common'; 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; function makeOptions(count: number): string[] { return Array.from({ length: count }, (_, i) => `opt${i}`); } const minimalParam: IparamDefn = { name: 'p', display_name: 'P', description: 'd', type: 'TEXT', required: false }; function makeIparamSchema(parameters: IparamDefn[], sectionName = 'default_section'): IparamDefns { return { installation_parameters: { sections: [ { name: sectionName, display_name: 'Default Section', description: 'Test section', parameters } ] } }; } function makeIparamSchemaWithParameterCount(count: number): IparamDefns { return makeIparamSchema( Array.from({ length: count }, (_, i) => ({ name: `param_${i}`, display_name: `Param ${i}`, description: 'd', type: 'TEXT', required: false })) ); } describe('IparamDefnsValidator', () => { let validator: IparamDefnsValidator; beforeEach(() => { jest.clearAllMocks(); validator = new IparamDefnsValidator(mockFileSystem); }); describe('validateAndGetIparamDefnsFile', () => { it('should throw when file does not exist', () => { mockFileSystem.existsSync.mockReturnValue(false); expect(() => validator.validateAndGetIparamDefnsFile('/app/iparams.json')) .toThrow('iparams.json not found'); expect(mockFileSystem.existsSync).toHaveBeenCalledWith('/app/iparams.json'); }); it('should return validated iparamDefns when valid', () => { const validDefns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'Secret key', type: 'SECRET', required: true } ]); mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(JSON.stringify(validDefns)); const result = validator.validateAndGetIparamDefnsFile('/app/iparams.json'); expect(result).toEqual(validDefns); expect(mockFileSystem.readFileSync).toHaveBeenCalledWith('/app/iparams.json', 'utf8'); }); it('should throw on invalid JSON', () => { mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue('not json'); expect(() => validator.validateAndGetIparamDefnsFile('/app/iparams.json')) .toThrow(/Failed to parse iparams.json/); }); it('should throw with validation message when JSON is valid but validate throws', () => { mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue('[{"name":"x"}]'); expect(() => validator.validateAndGetIparamDefnsFile('/app/iparams.json')) .toThrow(/iparams.json must be an object/); }); it('should attach rawContent to thrown error when validation fails', () => { const invalidDefns = [{ name: 'x' }]; mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(JSON.stringify(invalidDefns)); let thrown: unknown; try { validator.validateAndGetIparamDefnsFile('/app/iparams.json'); } catch (error) { thrown = error; } expect(thrown).toBeDefined(); expect((thrown as Error & { rawContent?: IparamDefns }).rawContent).toEqual(invalidDefns); }); }); describe('validate', () => { it('should accept empty iparamDefns object', () => { expect(() => validator.validate({} as IparamDefns)).not.toThrow(); }); it('should reject partial iparamDefns with empty sections when installation_parameters is present', () => { expect(() => validator.validate({ installation_parameters: { sections: [] } } as IparamDefns)).toThrow( 'installation_parameters.sections must contain at least one section' ); }); it('should throw when installation_parameters is present but invalid', () => { expect(() => validator.validate({ installation_parameters: null } as any)).toThrow( 'iparams.json must include installation_parameters as an object containing a sections array' ); }); it('should throw when iparamDefns contains unknown root keys', () => { expect(() => validator.validate({ installation_parameters: { sections: [] }, extra: true } as any)).toThrow('iparams.json must only contain "installation_parameters"'); }); it('should throw when installation_parameters contains unknown keys', () => { const defns = { installation_parameters: { dafsdfa: [{ name: 's', display_name: 'S', description: 'd', parameters: [] }] } } as any; expect(() => validator.validate(defns)).toThrow( 'installation_parameters must only contain "sections"' ); }); it('should throw when installation_parameters is missing sections property', () => { expect(() => validator.validate({ installation_parameters: {} } as any)).toThrow( 'installation_parameters must include sections in iparams.json' ); }); it('should throw when installation_parameters.sections is not an array', () => { const defns = { installation_parameters: { sections: { name: 's' } } } as any; expect(() => validator.validate(defns)).toThrow( 'installation_parameters.sections must be an array of section objects' ); }); it('should throw when installation_parameters.sections is empty', () => { const defns = { installation_parameters: { sections: [] } } as IparamDefns; expect(() => validator.validate(defns)).toThrow( 'installation_parameters.sections must contain at least one section' ); }); it('should throw when duplicate section names exist', () => { const defns: IparamDefns = { installation_parameters: { sections: [ { name: 'billing', display_name: 'Billing', description: 'd', parameters: [minimalParam] }, { name: 'billing', display_name: 'Billing 2', description: 'd2', parameters: [minimalParam] } ] } }; expect(() => validator.validate(defns)).toThrow('Duplicate section names found: billing'); }); it('should throw when duplicate section names differ only by trim', () => { const defns: IparamDefns = { installation_parameters: { sections: [ { name: 'billing', display_name: 'Billing', description: 'd', parameters: [minimalParam] }, { name: 'billing ', display_name: 'Billing 2', description: 'd2', parameters: [minimalParam] } ] } }; expect(() => validator.validate(defns)).toThrow(/Duplicate section names found: billing/); }); it('should throw when section at index is not an object', () => { const defns = { installation_parameters: { sections: [null] } } as any; expect(() => validator.validate(defns)).toThrow('Section at index 0 must be an object'); }); it('should throw when section name is missing', () => { const defns: IparamDefns = { installation_parameters: { sections: [ { name: ' ', display_name: 'Billing', description: 'd', parameters: [] } as any ] } }; expect(() => validator.validate(defns)).toThrow(/Section at index 0: "name" must be a non-empty string/); }); it('should throw when section name contains invalid characters', () => { const defns = makeIparamSchema([], 'billing section'); expect(() => validator.validate(defns)).toThrow( /Section "billing section": "name" can only contain letters, numbers, underscores, and hyphens/ ); }); it('should throw when section name exceeds max length', () => { const defns = makeIparamSchema([], 'a'.repeat(51)); expect(() => validator.validate(defns)).toThrow( /"name" must be between 1 and 50 characters/ ); }); it('should throw when section display_name is empty', () => { const defns: IparamDefns = { installation_parameters: { sections: [ { name: 'billing', display_name: ' ', description: 'd', parameters: [] } ] } }; expect(() => validator.validate(defns)).toThrow( /Section "billing": "display_name" must be a non-empty string/ ); }); it('should throw when section parameters is not an array', () => { const defns = { installation_parameters: { sections: [ { name: 'billing', display_name: 'Billing', description: 'd', parameters: {} } ] } } as any; expect(() => validator.validate(defns)).toThrow('Section "billing": "parameters" must be an array'); }); it('should throw when section parameters is empty', () => { const defns: IparamDefns = { installation_parameters: { sections: [ { name: 'billing', display_name: 'Billing', description: 'd', parameters: [] } ] } }; expect(() => validator.validate(defns)).toThrow( 'Section "billing": "parameters" must contain at least one parameter' ); }); it('should throw when iparamDefns exceeds max parameter count', () => { const defns = makeIparamSchemaWithParameterCount(IPARAMS_MAX_COUNT + 1); expect(() => validator.validate(defns)).toThrow( new RegExp( `iparams\\.json cannot define more than ${IPARAMS_MAX_COUNT} parameters \\(found ${IPARAMS_MAX_COUNT + 1}\\)` ) ); }); it('should accept iparamDefns at exactly max parameter count', () => { const defns = makeIparamSchemaWithParameterCount(IPARAMS_MAX_COUNT); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when parameter at index is not an object', () => { const defns = { installation_parameters: { sections: [ { name: 'default_section', display_name: 'Default Section', description: 'd', parameters: [null, { name: 'p', display_name: 'P', description: 'd', type: 'TEXT' }] } ] } } as any; expect(() => validator.validate(defns)).toThrow( 'Section "default_section": parameter at index 0 must be an object' ); }); it('should throw when parameter name is missing', () => { const defns = makeIparamSchema([{ display_name: 'X', description: 'd', type: 'TEXT' } as any]); expect(() => validator.validate(defns)).toThrow(/"name" must be a non-empty string/); }); it('should throw when parameter name exceeds 50 characters', () => { const defns = makeIparamSchema([{ name: 'a'.repeat(51), display_name: 'P', description: 'd', type: 'TEXT' }]); expect(() => validator.validate(defns)).toThrow(/"name" must be between 1 and 50 characters/); }); it('should throw when description is empty', () => { const defns = makeIparamSchema([{ name: 'p', display_name: 'P', description: ' ', type: 'TEXT' }]); expect(() => validator.validate(defns)).toThrow(/"description" must be a non-empty string/); }); it('should throw when type is invalid', () => { const defns = makeIparamSchema([ { name: 'p', display_name: 'P', description: 'd', type: 'INVALID' as any } ]); expect(() => validator.validate(defns)).toThrow(/"type" must be one of/); }); it('should throw when duplicate parameter names exist within a section', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: false }, { name: 'api_key', display_name: 'API Key 2', description: 'd2', type: 'TEXT', required: false } ]); expect(() => validator.validate(defns)).toThrow( 'Duplicate parameter names found in section "default_section": api_key' ); }); it('should throw when parameter names are duplicates after trim within a section', () => { const defns = makeIparamSchema([ { name: 'key', display_name: 'Key', description: 'd', type: 'TEXT', required: false }, { name: 'key ', display_name: 'Key 2', description: 'd2', type: 'TEXT', required: false } ]); expect(() => validator.validate(defns)).toThrow(/Duplicate parameter names found in section "default_section": key/); }); it('should allow duplicate parameter names across sections', () => { const defns: IparamDefns = { installation_parameters: { sections: [ { name: 'section_a', display_name: 'Section A', description: 'd', parameters: [ { name: 'fee_percentage', display_name: 'Fee', description: 'd', type: 'NUMBER', required: false } ] }, { name: 'section_b', display_name: 'Section B', description: 'd', parameters: [ { name: 'fee_percentage', display_name: 'Fee', description: 'd', type: 'NUMBER', required: false } ] } ] } }; expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when display_name is empty', () => { const defns = makeIparamSchema([{ name: 'p', display_name: ' ', description: 'd', type: 'TEXT' }]); expect(() => validator.validate(defns)).toThrow(/"display_name" must be a non-empty string/); }); it('should throw when description exceeds 250 characters', () => { const defns = makeIparamSchema([{ name: 'p', display_name: 'P', description: 'x'.repeat(251), type: 'TEXT' }]); expect(() => validator.validate(defns)).toThrow(/"description" cannot exceed 250 characters/); }); it('should throw when name contains invalid characters', () => { const defns = makeIparamSchema([{ name: 'api key', display_name: 'API Key', description: 'd', type: 'TEXT' }]); expect(() => validator.validate(defns)).toThrow(/"name" can only contain letters, numbers, underscores, and hyphens/); }); it('should throw when required is not boolean', () => { const defns = makeIparamSchema([{ name: 'p', display_name: 'P', description: 'd', type: 'TEXT', required: 'yes' as any }]); expect(() => validator.validate(defns)).toThrow(/"required" must be a boolean/); }); it('should throw when DROPDOWN type has no options', () => { const defns = makeIparamSchema([ { name: 'region', display_name: 'Region', description: 'd', type: 'DROPDOWN', required: false } ]); expect(() => validator.validate(defns)).toThrow(/"options" is required and must be a non-empty array of strings for DROPDOWN type/); }); it('should throw when MULTISELECT_DROPDOWN type has no options', () => { const defns = makeIparamSchema([ { name: 'tags', display_name: 'Tags', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false } ]); expect(() => validator.validate(defns)).toThrow(/"options" is required and must be a non-empty array of strings for MULTISELECT_DROPDOWN type/); }); it('should throw when TEXT type has options', () => { const defns = makeIparamSchema([ { name: 't', display_name: 'T', description: 'd', type: 'TEXT', required: false, options: ['a'] } ]); expect(() => validator.validate(defns)).toThrow( /"options" is only allowed for DROPDOWN and MULTISELECT_DROPDOWN types/ ); }); it('should throw when NUMBER type has options (including empty array)', () => { const defns = makeIparamSchema([{ name: 'n', display_name: 'N', description: 'd', type: 'NUMBER', required: false, options: [] }]); expect(() => validator.validate(defns)).toThrow( /"options" is only allowed for DROPDOWN and MULTISELECT_DROPDOWN types/ ); }); it('should throw when SECRET type has options set to null', () => { const defns = makeIparamSchema([{ name: 's', display_name: 'S', description: 'd', type: 'SECRET', required: true, options: null as any }]); expect(() => validator.validate(defns)).toThrow( /"options" is only allowed for DROPDOWN and MULTISELECT_DROPDOWN types/ ); }); it('should throw when SECRET type has default value', () => { const defns = makeIparamSchema([ { name: 'secret', display_name: 'Secret', description: 'd', type: 'SECRET', required: false, default: 'x' } ]); expect(() => validator.validate(defns)).toThrow(/SECRET type cannot have a default value/); }); it('should not throw when valid defns with all required fields', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'Secret key', type: 'TEXT', required: true }, { name: 'count', display_name: 'Count', description: 'A number', type: 'NUMBER', required: false, default: 0 } ]); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when default is empty string', () => { const defns = makeIparamSchema([{ name: 'p', display_name: 'P', description: 'd', type: 'TEXT', default: '' }]); expect(() => validator.validate(defns)).toThrow(/default value cannot be an empty string/); }); it('should throw when DROPDOWN option at index is not string', () => { const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', options: ['a', 1 as any] } ]); expect(() => validator.validate(defns)).toThrow(/option at index 1 must be a string/); }); it('should throw when DROPDOWN option is empty string', () => { const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', options: ['a', ''] } ]); expect(() => validator.validate(defns)).toThrow(/option at index 1 cannot be an empty string/); }); it('should throw when DROPDOWN options contain duplicate entries (after trim)', () => { const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', options: ['a', 'a'] } ]); expect(() => validator.validate(defns)).toThrow( /Parameter "default_section.r": "options" must not contain duplicate entries/ ); }); it('should throw when DROPDOWN options duplicate differs only by surrounding whitespace', () => { const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', options: ['a', ' a '] } ]); expect(() => validator.validate(defns)).toThrow( /"options" must not contain duplicate entries/ ); }); it('should throw when DROPDOWN option exceeds max string length', () => { const longOption = 'x'.repeat(251); const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', options: ['a', longOption] } ]); expect(() => validator.validate(defns)).toThrow(/option at index 1 cannot exceed 250 characters/); }); it('should throw when DROPDOWN options exceed max count', () => { const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', options: makeOptions(21) } ]); expect(() => validator.validate(defns)).toThrow(/"options" cannot exceed 20 items for DROPDOWN type/); }); it('should throw when MULTISELECT_DROPDOWN options exceed max count', () => { const defns = makeIparamSchema([ { name: 'm', display_name: 'M', description: 'd', type: 'MULTISELECT_DROPDOWN', options: makeOptions(21) } ]); expect(() => validator.validate(defns)).toThrow(/"options" cannot exceed 20 items for MULTISELECT_DROPDOWN type/); }); it('should accept valid NUMBER default', () => { const defns = makeIparamSchema([ { name: 'n', display_name: 'N', description: 'd', type: 'NUMBER', required: false, default: 42 } ]); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when NUMBER default is not finite', () => { const defns = makeIparamSchema([{ name: 'n', display_name: 'N', description: 'd', type: 'NUMBER' as const, default: NaN }]); expect(() => validator.validate(defns)).toThrow(/default value must be a finite number/); }); it('should accept valid TEXT default', () => { const defns = makeIparamSchema([ { name: 't', display_name: 'T', description: 'd', type: 'TEXT', required: false, default: 'hello' } ]); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when TEXT default exceeds 250 characters', () => { const defns = makeIparamSchema([{ name: 't', display_name: 'T', description: 'd', type: 'TEXT' as const, default: 'x'.repeat(251) }]); expect(() => validator.validate(defns)).toThrow(/default value cannot exceed 250 characters/); }); it('should throw when MULTISELECT_DROPDOWN default array item is not in options', () => { const defns = makeIparamSchema([ { name: 'm', display_name: 'M', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'], default: ['a', 'invalid'] } ]); expect(() => validator.validate(defns)).toThrow(/default value array item at index 1 must be one of: a, b/); }); it('should accept valid BOOLEAN default', () => { const defns = makeIparamSchema([ { name: 'b', display_name: 'B', description: 'd', type: 'BOOLEAN', required: false, default: true } ]); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when BOOLEAN default is not boolean', () => { const defns = makeIparamSchema([{ name: 'b', display_name: 'B', description: 'd', type: 'BOOLEAN' as const, default: 'true' as any }]); expect(() => validator.validate(defns)).toThrow(/default value must be a boolean/); }); it('should accept valid URL default', () => { const defns = makeIparamSchema([ { name: 'u', display_name: 'U', description: 'd', type: 'URL', required: false, default: 'https://example.com' } ]); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when URL default is invalid', () => { const defns = makeIparamSchema([{ name: 'u', display_name: 'U', description: 'd', type: 'URL' as const, default: 'not-a-url' }]); expect(() => validator.validate(defns)).toThrow(/default value must be a valid URL/); }); it('should throw when URL default uses a disallowed protocol', () => { const defns = makeIparamSchema([ { name: 'u', display_name: 'U', description: 'd', type: 'URL', default: 'ftp://example.com' } ]); expect(() => validator.validate(defns)).toThrow(/default value must be a valid URL with protocol http: or https:/); }); it('should accept valid DATE default', () => { const defns = makeIparamSchema([ { name: 'd', display_name: 'D', description: 'd', type: 'DATE', required: false, default: '2025-12-31' } ]); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when DATE default has wrong format', () => { const defns = makeIparamSchema([{ name: 'd', display_name: 'D', description: 'd', type: 'DATE' as const, default: '31-12-2025' }]); expect(() => validator.validate(defns)).toThrow(/default value must be a valid date in YYYY-MM-DD format/); }); it('should accept valid DROPDOWN default in options', () => { const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', required: false, options: ['us', 'eu'], default: 'us' } ]); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when DROPDOWN default not in options', () => { const defns = makeIparamSchema([ { name: 'r', display_name: 'R', description: 'd', type: 'DROPDOWN', required: false, options: ['us', 'eu'], default: 'asia' } ]); expect(() => validator.validate(defns)).toThrow(/default value must be one of/); }); it('should accept valid MULTISELECT_DROPDOWN default', () => { const defns = makeIparamSchema([ { name: 'm', display_name: 'M', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'], default: ['a'] } ]); expect(() => validator.validate(defns)).not.toThrow(); }); it('should throw when MULTISELECT_DROPDOWN default is not array', () => { const defns = makeIparamSchema([{ name: 'm', display_name: 'M', description: 'd', type: 'MULTISELECT_DROPDOWN' as const, options: ['a'], default: 'a' as any }]); expect(() => validator.validate(defns)).toThrow(/default value must be an array of strings/); }); it('should throw when MULTISELECT_DROPDOWN default item not in options', () => { const defns = makeIparamSchema([ { name: 'm', display_name: 'M', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'], default: ['a', 'c'] } ]); expect(() => validator.validate(defns)).toThrow(/default value array item at index 1 must be one of/); }); it('should throw when MULTISELECT_DROPDOWN default contains duplicate entries', () => { const defns = makeIparamSchema([ { name: 'm', display_name: 'M', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['a', 'b'], default: ['a', 'a'] } ]); expect(() => validator.validate(defns)).toThrow( /multiselect default value must not contain duplicate entries/ ); }); }); });