UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

705 lines (589 loc) 21.1 kB
/** * Copyright IBM Corp. 2024, 2025 */ import { AssetSchemaValidator } from '../../src/validator/schema-validator.impl.js'; import { GatewayLabels } from '@apic/studio-client-model'; import { SchemaHandler, YamlContent } from '@apic/studio-shared'; import { AssetValidator } from '../../src/service/validation-service.js'; jest.mock('@apic/studio-shared', () => ({ SchemaHandler: jest.fn().mockImplementation(() => ({ getSchema: jest.fn().mockReturnValue(JSON.stringify({ type: 'object' })), })), isValidAsset: jest.fn().mockImplementation((asset: any) => { if (asset && asset.kind) { return true; } return false; }), YamlContent: jest.fn(), Logger: { createChildLogger: jest.fn().mockReturnValue({ debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), log: jest.fn(), }), }, Component: { Core: 'Core', }, })); jest.mock('@apic/studio-shared', () => ({ Component: { Build: 'Build', }, LogComponent: () => { return () => { /* noop */ }; }, Logger: { error: jest.fn(), warn: jest.fn(), info: jest.fn(), }, toError: jest.fn((e) => (e instanceof Error ? e : new Error(String(e)))), ErrorResponse: jest.fn(), Metadata_Ref: jest.fn(), SpecObject: jest.fn(), UpperCaseKinds: jest.fn(), loadYaml: jest.fn((content) => require('js-yaml').load(content)), SchemaHandler: jest.fn().mockImplementation(() => ({ getSchema: jest.fn().mockReturnValue(JSON.stringify({ type: 'object' })), })), isValidAsset: jest.fn().mockImplementation((asset: any) => { if (asset && asset.kind) { return true; } return false; }), YamlContent: jest.fn(), })); jest.mock('../../src/service/validation-service.js', () => ({ AssetValidator: jest.fn().mockImplementation(() => ({ validateAssets: jest.fn().mockReturnValue({ valid: true, errors: [] }), })), })); jest.mock('@apic/studio-logger', () => { const mockLogger = { logDebug: jest.fn(), logInfo: jest.fn(), logWarn: jest.fn(), logError: jest.fn(), }; return { Components: { ApimIntegratorComponent: 'ApimIntegratorComponent', }, Logger: jest.fn().mockImplementation(() => mockLogger), LoggerBase: jest.fn().mockImplementation(function (this: any, logger: any) { this.logger = logger; this.logDebug = jest.fn(); this.logInfo = jest.fn(); this.logWarn = jest.fn(); this.logError = jest.fn(); }), LoggerConfig: { isLoggerEnabled: jest.fn(), }, }; }); jest.mock('@apic/studio-client-model', () => ({ GatewayLabels: { WMGW: 'webMethods', LWGW: 'nano', DPGW: 'datapower', }, })); const originalConsoleError = console.error; const originalConsoleWarn = console.warn; const originalConsoleLog = console.log; const mockZipFile = { async: jest.fn(), dir: false, name: 'test.yaml', }; const mockZipContent = { loadAsync: jest.fn().mockResolvedValue({ files: { 'test.yaml': { ...mockZipFile }, 'api.yaml': { ...mockZipFile, name: 'api.yaml' }, 'policy.yaml': { ...mockZipFile, name: 'policy.yaml' }, 'directory/': { dir: true, name: 'directory/' }, }, }), }; jest.mock('jszip', () => { return jest.fn().mockImplementation(() => mockZipContent); }); jest.mock('js-yaml', () => ({ loadAll: jest.fn().mockImplementation((content: string) => { if (content.includes('invalid')) { throw new Error('Invalid YAML'); } if (content.includes('api')) { return [ { kind: 'API', metadata: { namespace: 'test', name: 'api', version: '1.0', }, spec: { 'policy-sequence': [{ $ref: 'test:policy:1.0' }], }, }, ]; } if (content.includes('stagedpolicy')) { return [ { kind: 'StagedPolicySequence', metadata: { namespace: 'test', name: 'policy', version: '1.0', }, spec: { policies: [{ $ref: 'test:nested:1.0' }], }, }, ]; } if (content.includes('freeflowpolicy')) { return [ { kind: 'FreeFlowPolicySequence', metadata: { namespace: 'test', name: 'policy', version: '1.0', }, spec: { policies: [{ $ref: 'test:nested:1.0' }], }, }, ]; } if (content.includes('datapowerassembly')) { return [ { kind: 'DataPowerAssembly', metadata: { namespace: 'test', name: 'policy', version: '1.0', }, spec: { policies: [{ $ref: 'test:nested:1.0' }], }, }, ]; } if (content.includes('nested')) { return [ { kind: 'NestedPolicy', metadata: { namespace: 'test', name: 'nested', version: '1.0', }, spec: {}, }, ]; } return [ { kind: 'TestKind', metadata: { namespace: 'test', name: 'test', version: '1.0', }, spec: {}, }, ]; }), })); describe('AssetSchemaValidator', () => { let validator: AssetSchemaValidator; beforeEach(() => { jest.clearAllMocks(); console.error = jest.fn(); console.warn = jest.fn(); console.log = jest.fn(); validator = new AssetSchemaValidator(); mockZipFile.async.mockResolvedValue('test content'); }); afterEach(() => { console.error = originalConsoleError; console.warn = originalConsoleWarn; console.log = originalConsoleLog; }); describe('validateApiFile', () => { it('should validate API file successfully with WebMethods gateway', async () => { mockZipFile.async .mockResolvedValueOnce('api content') .mockResolvedValueOnce('stagedpolicy content'); const result = await validator.validateApiFile(Buffer.from('test'), [GatewayLabels.WMGW]); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should validate API file successfully with DataPower Nano Gateway', async () => { mockZipFile.async .mockResolvedValueOnce('api content') .mockResolvedValueOnce('freeflowpolicy content'); const result = await validator.validateApiFile(Buffer.from('test'), [GatewayLabels.LWGW]); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should return error when WebMethods gateway is selected but no staged policy sequences are referenced', async () => { // Setup mocks for no policy sequences mockZipFile.async.mockResolvedValue('test content'); const result = await validator.validateApiFile(Buffer.from('test'), [GatewayLabels.WMGW]); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain( 'WebMethods gateway is selected but no staged policy sequences are referenced' ); }); it('should return error when DataPower Nano Gateway is selected but no free flow policy sequences are referenced', async () => { // Setup mocks for no policy sequences mockZipFile.async.mockResolvedValue('test content'); const result = await validator.validateApiFile(Buffer.from('test'), [GatewayLabels.LWGW]); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain( 'DataPower Nano gateway is selected but no free flow policy sequences are referenced' ); }); it('should handle errors during validation', async () => { // Force an error mockZipContent.loadAsync.mockRejectedValueOnce(new Error('Test error')); const result = await validator.validateApiFile(Buffer.from('test'), [GatewayLabels.WMGW]); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain( 'WebMethods gateway is selected but no staged policy sequences are referenced' ); }); }); describe('readAndConsolidateYamlFiles', () => { it('should read and consolidate YAML files', async () => { const result = await (validator as any).readAndConsolidateYamlFiles(Buffer.from('test')); expect(result).toBeInstanceOf(Array); expect(result.length).toBeGreaterThan(0); }); it('should handle invalid YAML files', async () => { mockZipFile.async.mockResolvedValue('invalid yaml content'); const result = await (validator as any).readAndConsolidateYamlFiles(Buffer.from('test')); expect(result).toBeInstanceOf(Array); }); it('should handle errors during processing', async () => { mockZipContent.loadAsync.mockRejectedValueOnce(new Error('Test error')); const result = await (validator as any).readAndConsolidateYamlFiles(Buffer.from('test')); expect(result).toEqual([]); }); }); describe('categorizePolicySequences', () => { it('should categorize staged policy sequences', async () => { // Setup mocks for staged policy sequence mockZipFile.async .mockResolvedValueOnce('api content') .mockResolvedValueOnce('stagedpolicy content') .mockResolvedValueOnce('nested content'); const result = await validator.categorizePolicySequences(Buffer.from('test')); expect(result.wmgwReferences.size).toBe(2); expect(result.wmgwReferences.has('test:policy:1.0')).toBe(true); expect(result.lwgwReferences.size).toBe(0); }); it('should categorize free flow policy sequences', async () => { // Setup mocks for free flow policy sequence mockZipFile.async .mockResolvedValueOnce('api content') .mockResolvedValueOnce('freeflowpolicy content') .mockResolvedValueOnce('nested content'); const result = await validator.categorizePolicySequences(Buffer.from('test')); expect(result.lwgwReferences.size).toBe(2); expect(result.lwgwReferences.has('test:policy:1.0')).toBe(true); expect(result.wmgwReferences.size).toBe(0); }); it('should handle missing policy references', async () => { // Setup mocks for API with no matching policy mockZipFile.async.mockResolvedValue('api content'); const result = await validator.categorizePolicySequences(Buffer.from('test')); expect(result.wmgwReferences.size).toBe(0); expect(result.lwgwReferences.size).toBe(0); }); }); describe('findAllNestedReferences', () => { it('should find all nested references', () => { const asset = { spec: { policies: [{ $ref: 'test:ref1:1.0' }, { $ref: 'test:ref2:1.0' }], }, } as YamlContent; const allAssets = [ { metadata: { namespace: 'test', name: 'ref1', version: '1.0', }, spec: { nestedRef: { $ref: 'test:ref3:1.0' }, }, } as YamlContent, ]; const referenceSet = new Set<string>(); (validator as any).findAllNestedReferences(asset, allAssets, referenceSet); expect(referenceSet.size).toBeGreaterThan(0); }); it('should handle null or undefined assets', () => { const referenceSet = new Set<string>(); (validator as any).findAllNestedReferences(null, [], referenceSet); (validator as any).findAllNestedReferences({ metadata: {} }, [], referenceSet); expect(referenceSet.size).toBe(0); }); }); describe('findAllRefsRecursive', () => { it('should find all references recursively', () => { const data = { prop1: { $ref: 'test:ref1:1.0' }, prop2: [{ $ref: 'test:ref2:1.0' }, { nested: { $ref: 'test:ref3:1.0' } }], }; const referenceSet = new Set<string>(); (validator as any).findAllRefsRecursive(data, referenceSet); expect(referenceSet.size).toBe(3); expect(referenceSet.has('test:ref1:1.0')).toBe(true); expect(referenceSet.has('test:ref2:1.0')).toBe(true); expect(referenceSet.has('test:ref3:1.0')).toBe(true); }); it('should handle null or undefined data', () => { const referenceSet = new Set<string>(); (validator as any).findAllRefsRecursive(null, referenceSet); (validator as any).findAllRefsRecursive(undefined, referenceSet); expect(referenceSet.size).toBe(0); }); }); describe('validateSchema', () => { it('should validate schema successfully', async () => { jest.spyOn(validator, 'validateApiFile').mockResolvedValueOnce({ valid: true, errors: [], }); (AssetValidator as jest.Mock).mockImplementation(() => ({ validateAssets: jest.fn().mockReturnValue({ valid: true, errors: [] }), })); const result = await validator.validateSchema(Buffer.from('test'), [GatewayLabels.WMGW]); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should return errors from API validation', async () => { jest.spyOn(validator, 'validateApiFile').mockResolvedValueOnce({ valid: false, errors: ['API validation error'], }); const result = await validator.validateSchema(Buffer.from('test'), [GatewayLabels.WMGW]); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toBe('API validation error'); }); it('should validate common assets', async () => { jest.spyOn(validator, 'validateApiFile').mockResolvedValueOnce({ valid: true, errors: [], }); jest.spyOn(validator as any, 'readAndConsolidateYamlFiles').mockResolvedValueOnce([ { kind: 'API', metadata: { namespace: 'test', name: 'api', version: '1.0' }, spec: {}, }, { kind: 'Properties', metadata: { namespace: 'test', name: 'props', version: '1.0' }, spec: {}, }, ]); const result = await validator.validateSchema(Buffer.from('test'), [GatewayLabels.WMGW]); expect(result.valid).toBe(true); expect(SchemaHandler).toHaveBeenCalled(); expect(AssetValidator).toHaveBeenCalled(); }); it('should validate WebMethods gateway specific assets', async () => { jest.spyOn(validator, 'validateApiFile').mockResolvedValueOnce({ valid: true, errors: [], }); jest.spyOn(validator, 'categorizePolicySequences').mockResolvedValue({ wmgwReferences: new Set(['test:policy:1.0']), lwgwReferences: new Set(), dpgwReferences: new Set(), }); jest.spyOn(validator as any, 'readAndConsolidateYamlFiles').mockResolvedValueOnce([ { kind: 'StagedPolicySequence', metadata: { namespace: 'test', name: 'policy', version: '1.0' }, spec: {}, }, ]); const result = await validator.validateSchema(Buffer.from('test'), [GatewayLabels.WMGW]); expect(result.valid).toBe(true); expect(SchemaHandler).toHaveBeenCalledWith('webMethods'); }); it('should validate DataPower Nano Gateway specific assets', async () => { jest.spyOn(validator, 'validateApiFile').mockResolvedValueOnce({ valid: true, errors: [], }); jest.spyOn(validator, 'categorizePolicySequences').mockResolvedValue({ wmgwReferences: new Set(), lwgwReferences: new Set(['test:policy:1.0']), dpgwReferences: new Set(), }); jest.spyOn(validator as any, 'readAndConsolidateYamlFiles').mockResolvedValueOnce([ { kind: 'FreeFlowPolicySequence', metadata: { namespace: 'test', name: 'policy', version: '1.0' }, spec: {}, }, ]); const result = await validator.validateSchema(Buffer.from('test'), [GatewayLabels.LWGW]); expect(result.valid).toBe(true); expect(SchemaHandler).toHaveBeenCalledWith('nano'); }); it('should validate DataPower API Gateway specific assets', async () => { jest.spyOn(validator, 'validateApiFile').mockResolvedValueOnce({ valid: true, errors: [], }); jest.spyOn(validator, 'categorizePolicySequences').mockResolvedValue({ wmgwReferences: new Set(), lwgwReferences: new Set(), dpgwReferences: new Set(['test:policy:1.0']), }); jest.spyOn(validator as any, 'readAndConsolidateYamlFiles').mockResolvedValueOnce([ { kind: 'DataPowerAssembly', metadata: { namespace: 'test', name: 'policy', version: '1.0' }, spec: {}, }, ]); const result = await validator.validateSchema(Buffer.from('test'), [GatewayLabels.DPGW]); expect(result.valid).toBe(true); expect(SchemaHandler).toHaveBeenCalledWith('datapower'); }); it('should validate all WebMethods, DataPower Nano Gateway and DataPower API Gateway assets', async () => { jest.spyOn(validator, 'validateApiFile').mockResolvedValueOnce({ valid: true, errors: [], }); jest.spyOn(validator as any, 'validateGatewaySpecificAssets').mockResolvedValue(undefined); const result = await validator.validateSchema(Buffer.from('test'), [ GatewayLabels.WMGW, GatewayLabels.LWGW, GatewayLabels.DPGW, ]); expect(result.valid).toBe(true); expect((validator as any).validateGatewaySpecificAssets).toHaveBeenCalledTimes(3); }); it('should handle errors during validation', async () => { jest.spyOn(validator, 'validateApiFile').mockResolvedValueOnce({ valid: true, errors: [], }); jest .spyOn(validator as any, 'readAndConsolidateYamlFiles') .mockRejectedValueOnce(new Error('Test error')); const result = await validator.validateSchema(Buffer.from('test'), [GatewayLabels.WMGW]); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain('Error validating schema'); }); }); describe('validateGatewaySpecificAssets', () => { it('should validate gateway specific assets', async () => { jest.spyOn(validator, 'categorizePolicySequences').mockResolvedValueOnce({ wmgwReferences: new Set(['test:policy:1.0']), lwgwReferences: new Set(), dpgwReferences: new Set(), }); const assets = [ { kind: 'StagedPolicySequence', metadata: { namespace: 'test', name: 'policy', version: '1.0' }, spec: {}, }, ] as YamlContent[]; const errors: string[] = []; await (validator as any).validateGatewaySpecificAssets( assets, GatewayLabels.WMGW, errors, Buffer.from('test') ); expect(errors).toHaveLength(0); expect(SchemaHandler).toHaveBeenCalledWith('webMethods'); }); it('should add errors for invalid assets', async () => { jest.spyOn(validator, 'categorizePolicySequences').mockResolvedValueOnce({ wmgwReferences: new Set(['test:policy:1.0']), lwgwReferences: new Set(), dpgwReferences: new Set(), }); (AssetValidator as jest.Mock).mockImplementation(() => ({ validateAssets: jest.fn().mockReturnValue({ valid: false, errors: ['Validation error'], }), })); const assets = [ { kind: 'StagedPolicySequence', metadata: { namespace: 'test', name: 'policy', version: '1.0' }, spec: {}, }, ] as YamlContent[]; const errors: string[] = []; await (validator as any).validateGatewaySpecificAssets( assets, GatewayLabels.WMGW, errors, Buffer.from('test') ); expect(errors).toHaveLength(1); expect(errors[0]).toContain('Validation error'); }); it('should skip assets without kind or metadata', async () => { jest.spyOn(validator, 'categorizePolicySequences').mockResolvedValueOnce({ wmgwReferences: new Set(['test:policy:1.0']), lwgwReferences: new Set(), dpgwReferences: new Set(), }); const assets = [{ spec: {} }, { kind: 'Test', spec: {} }] as YamlContent[]; const errors: string[] = []; await (validator as any).validateGatewaySpecificAssets( assets, GatewayLabels.WMGW, errors, Buffer.from('test') ); expect(errors).toHaveLength(0); }); it('should handle unsupported gateway types', async () => { const assets = [] as YamlContent[]; const errors: string[] = []; await (validator as any).validateGatewaySpecificAssets( assets, 'unsupported' as GatewayLabels, errors, Buffer.from('test') ); expect(errors).toHaveLength(0); }); }); });