import { MissingRequiredParamsError } from '../../src/errors'; import { IparamsService } from '../../src/libs/iparams-service'; 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 } ] } }; } 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('IparamsService', () => { let service: IparamsService; beforeEach(() => { jest.clearAllMocks(); service = new IparamsService(mockFileSystem); }); describe('iparamsFileExists', () => { it('should return true when iparams.json exists', () => { mockFileSystem.existsSync.mockReturnValue(true); const result = service.iparamsFileExists('/app'); expect(result).toBe(true); expect(mockFileSystem.existsSync).toHaveBeenCalledWith('/app/iparams.json'); }); it('should return false when iparams.json does not exist', () => { mockFileSystem.existsSync.mockReturnValue(false); const result = service.iparamsFileExists('/app'); expect(result).toBe(false); }); }); describe('validateIparamDefns', () => { it('should throw when iparams.json does not exist', () => { mockFileSystem.existsSync.mockReturnValue(false); expect(() => service.validateIparamDefns('/app')).toThrow('iparams.json not found'); }); it('should not throw when iparams.json is valid', () => { const validDefns = makeIparamSchema([ { name: 'key', display_name: 'Key', description: 'd', type: 'TEXT', required: false } ]); mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(JSON.stringify(validDefns)); expect(() => service.validateIparamDefns('/app')).not.toThrow(); }); it('should not throw when iparams.json is an empty object', () => { mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue('{}'); expect(() => service.validateIparamDefns('/app')).not.toThrow(); }); it('should throw when iparams.json has invalid JSON', () => { mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue('invalid json {'); expect(() => service.validateIparamDefns('/app')).toThrow(/Failed to parse iparams.json/); }); }); describe('loadIparamDefns', () => { it('should return iparam defns when iparams.json exists and is valid', () => { const validDefns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true } ]); mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(JSON.stringify(validDefns)); const result = service.loadIparamDefns('/app'); expect(result).toEqual(validDefns); expect(mockFileSystem.readFileSync).toHaveBeenCalledWith('/app/iparams.json', 'utf8'); }); it('should throw when iparams.json does not exist', () => { mockFileSystem.existsSync.mockReturnValue(false); expect(() => service.loadIparamDefns('/app')).toThrow('iparams.json not found'); }); }); describe('loadInputs', () => { it('should throw when iparams.local.json does not exist', () => { mockFileSystem.existsSync.mockReturnValue(false); expect(() => service.loadInputs('/app')).toThrow('iparams.local.json not found'); expect(mockFileSystem.readFileSync).not.toHaveBeenCalled(); }); it('should return parsed inputs when iparams.local.json exists and is valid object', () => { const inputs = { api_key: 'secret123' }; mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(JSON.stringify(inputs)); const result = service.loadInputs('/app'); expect(result).toEqual(inputs); expect(mockFileSystem.readFileSync).toHaveBeenCalledWith( expect.stringContaining('iparams.local.json'), 'utf8' ); }); it('should throw when file content is invalid JSON', () => { mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue('not json {'); expect(() => service.loadInputs('/app')).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(() => service.loadInputs('/app')).toThrow('iparams.local.json must be an object'); }); }); describe('mergeInputsWithDefaults (for execution)', () => { it('should merge inputs with defaults when required param is provided', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true }, { name: 'optional', display_name: 'Optional', description: 'd', type: 'TEXT', required: false, default: 'default_val' } ]); const inputs = { default_section: { api_key: 'secret' } }; const result = service.mergeInputsWithDefaults(inputs, defns); expect(result).toEqual({ default_section: { api_key: 'secret', optional: 'default_val' } }); }); it('should use default when value is explicitly null', () => { const defns = makeIparamSchema([ { name: 'a', display_name: 'A', description: 'd', type: 'TEXT', required: false, default: 'default_a' } ]); const inputs = { default_section: { a: null as any } }; const result = service.mergeInputsWithDefaults(inputs, defns); expect(result).toEqual({ default_section: { a: 'default_a' } }); }); it('should throw when required param is missing and has no default', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true } ]); const inputs = {}; expect(() => service.mergeInputsWithDefaults(inputs, defns)) .toThrow(MissingRequiredParamsError); try { service.mergeInputsWithDefaults(inputs, defns); } catch (e) { expect((e as MissingRequiredParamsError).missing).toEqual(['default_section.api_key']); } }); it('should throw when multiple required params are missing', () => { const defns = makeIparamSchema([ { name: 'a', display_name: 'A', description: 'd', type: 'TEXT', required: true }, { name: 'b', display_name: 'B', description: 'd', type: 'TEXT', required: true } ]); const inputs = {}; expect(() => service.mergeInputsWithDefaults(inputs, defns)) .toThrow(MissingRequiredParamsError); try { service.mergeInputsWithDefaults(inputs, defns); } catch (e) { expect((e as MissingRequiredParamsError).missing).toEqual(['default_section.a', 'default_section.b']); } }); it('should merge inputs with defaults across 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, default: 'us' } ] } ] } }; const inputs = { auth: { api_key: 'secret' } }; const result = service.mergeInputsWithDefaults(inputs, defns); expect(result).toEqual({ auth: { api_key: 'secret' }, config: { region: 'us' } }); }); it('should throw when required params are missing across 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: 'site_id', display_name: 'Site ID', description: 'd', type: 'TEXT', required: true } ] } ] } }; const inputs = { auth: { api_key: 'secret' } }; expect(() => service.mergeInputsWithDefaults(inputs, defns)) .toThrow(MissingRequiredParamsError); try { service.mergeInputsWithDefaults(inputs, defns); } catch (e) { expect((e as MissingRequiredParamsError).missing).toEqual(['config.site_id']); } }); }); describe('validateAndGetInputsForExecution', () => { it('should return merged inputs with defaults when valid', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true }, { name: 'optional', display_name: 'Optional', description: 'd', type: 'TEXT', required: false, default: 'default_val' } ]); const inputs = { default_section: { api_key: 'secret' } }; mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockImplementation((p: string) => { if (p.endsWith('iparams.json')) return JSON.stringify(defns); if (p.endsWith('iparams.local.json')) return JSON.stringify(inputs); return '{}'; }); const result = service.validateAndGetInputsForExecution('/app'); expect(result).toEqual({ default_section: { api_key: 'secret', optional: 'default_val' } }); }); it('should throw when required param is missing and has no default', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true } ]); const inputs = {}; mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockImplementation((p: string) => { if (p.endsWith('iparams.json')) return JSON.stringify(defns); if (p.endsWith('iparams.local.json')) return JSON.stringify(inputs); return '{}'; }); expect(() => service.validateAndGetInputsForExecution('/app')).toThrow(/Missing required/); }); }); describe('saveIparamDefns', () => { it('should validate and write iparam defns to file', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true } ]); mockFileSystem.existsSync.mockReturnValue(true); service.saveIparamDefns('/app', defns); expect(mockFileSystem.writeFileSync).toHaveBeenCalledWith( expect.stringContaining('iparams.json'), JSON.stringify(defns, null, '\t') ); }); it('should throw when defns are invalid', () => { mockFileSystem.existsSync.mockReturnValue(true); expect(() => service.saveIparamDefns('/app', { installation_parameters: {} } as any)).toThrow( 'installation_parameters must include sections in iparams.json' ); expect(mockFileSystem.writeFileSync).not.toHaveBeenCalled(); }); it('should throw when iparams.json does not exist', () => { mockFileSystem.existsSync.mockReturnValue(false); const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: true } ]); expect(() => service.saveIparamDefns('/app', defns)).toThrow('iparams.json not found'); expect(mockFileSystem.writeFileSync).not.toHaveBeenCalled(); }); }); describe('mergeInputsWithDefaults (empty array → default)', () => { it('should merge inputs with defaults and skip empty arrays', () => { const defns = makeIparamSchema([ { name: 'a', display_name: 'A', description: 'd', type: 'TEXT', required: false, default: 'default_a' }, { name: 'b', display_name: 'B', description: 'd', type: 'TEXT', required: false } ]); const inputs = { default_section: { b: 'provided' } }; const result = service.mergeInputsWithDefaults(inputs, defns); expect(result).toEqual({ default_section: { a: 'default_a', b: 'provided' } }); }); it('should not include param when value is empty string and no default', () => { const defns = makeIparamSchema([ { name: 'a', display_name: 'A', description: 'd', type: 'TEXT', required: false } ]); const inputs = { default_section: { a: '' } }; const result = service.mergeInputsWithDefaults(inputs, defns); expect(result).toEqual({}); }); it('should treat whitespace-only string as absent and use default', () => { const defns = makeIparamSchema([ { name: 'a', display_name: 'A', description: 'd', type: 'TEXT', required: false, default: 'fallback' } ]); const inputs = { default_section: { a: ' \t ' } }; const result = service.mergeInputsWithDefaults(inputs, defns); expect(result).toEqual({ default_section: { a: 'fallback' } }); }); it('should include default when value is empty array for multiselect', () => { const defns = makeIparamSchema([ { name: 'tags', display_name: 'Tags', description: 'd', type: 'MULTISELECT_DROPDOWN', required: false, options: ['x'], default: ['x'] } ]); const inputs = { default_section: { tags: [] } }; const result = service.mergeInputsWithDefaults(inputs, defns); expect(result).toEqual({ default_section: { tags: ['x'] } }); }); }); describe('saveInputs', () => { it('should load defns, merge and validate inputs, then write to iparams.local.json', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: false } ]); const inputs = { default_section: { api_key: 'secret' } }; mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockImplementation((p: string) => p.endsWith('iparams.json') && !p.includes('local') ? JSON.stringify(defns) : '{}' ); service.saveInputs('/app', inputs); expect(mockFileSystem.writeFileSync).toHaveBeenCalledWith( expect.stringContaining('iparams.local.json'), JSON.stringify(inputs, null, '\t') ); }); it('should throw when iparams.local.json does not exist', () => { const defns = makeIparamSchema([ { name: 'api_key', display_name: 'API Key', description: 'd', type: 'TEXT', required: false } ]); mockFileSystem.existsSync.mockImplementation((p: string) => p.endsWith('iparams.json') && !p.includes('local')); mockFileSystem.readFileSync.mockImplementation((p: string) => p.endsWith('iparams.json') && !p.includes('local') ? JSON.stringify(defns) : '{}' ); expect(() => service.saveInputs('/app', { default_section: { api_key: 'secret' } })).toThrow('iparams.local.json not found'); expect(mockFileSystem.writeFileSync).not.toHaveBeenCalled(); }); }); });