UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

1,367 lines (1,258 loc) 47.3 kB
import { BaseTransformer } from '../core/impl/base-transformer.impl'; import { TransformConfig } from '../core/models/model'; import { WrappedAsset } from '../core/models/zip-processor.interface'; import { TransformerError } from '../core/errors/transformer.error'; describe('BaseTransformer', () => { let transformer: BaseTransformer; beforeEach(() => { transformer = new BaseTransformer(); }); describe('transform', () => { it('should skip transformation when skipTransform is true', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { input: 'request', output: 'response' } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { skipTransform: true }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result).toBe(wrappedAsset); expect(result.outputAsset).toBeUndefined(); }); it('should apply transformations when skipTransform is false', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { input: 'request', output: 'response' } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { skipTransform: false, transformations: { mappings: [ { source: '$.spec.input', target: '$.Test.input' }, { source: '$.spec.output', target: '$.Test.output' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result).not.toBe(wrappedAsset); expect(result.outputAsset).toEqual({ Test: { input: 'request', output: 'response' } }); }); }); describe('mappings', () => { it('should apply mappings correctly', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { input: 'request', output: 'response', nested: { property: 'value' } } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { mappings: [ { source: '$.spec.input', target: '$.Test.input' }, { source: '$.spec.output', target: '$.Test.output' }, { source: '$.spec.nested', target: '$.Test.nested' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { input: 'request', output: 'response', nested: { property: 'value' } } }); }); it('should handle non-existent source paths', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { input: 'request' } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { mappings: [ { source: '$.spec.input', target: '$.Test.input' }, { source: '$.spec.output', // This doesn't exist target: '$.Test.output' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { input: 'request' // output should not be present } }); }); }); describe('replacements', () => { it('should apply replacements with static values', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.staticValue', value: 'static', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { staticValue: 'static' } }); }); it('should apply replacements with templates', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { value: 'template value' } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.templatedValue', value: 'Template: {{$.spec.value}}', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { templatedValue: 'Template: template value' } }); }); it('should apply replacements with object templates', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { object: { prop1: 'value1', prop2: 'value2' } } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.objectValue', value: '{{$.spec.object}}', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { objectValue: { prop1: 'value1', prop2: 'value2' } } }); }); it('should apply replacements with conditions', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { flag: true, noFlag: false } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.conditionalValue1', value: 'Applied', precedence: 10, condition: '$.spec.flag', operation: 'replace' }, { target: '$.Test.conditionalValue2', value: 'Not Applied', precedence: 10, condition: '$.spec.nonExistent', operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { conditionalValue1: 'Applied' // conditionalValue2 should not be present } }); }); it('should apply replacements in order of precedence', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.value', value: 'First', precedence: 20, operation: 'replace' }, { target: '$.Test.value', value: 'Second', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { value: 'First' } }); }); it('should handle push operation', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.array', value: 'Item 1', precedence: 10, operation: 'push' }, { target: '$.Test.array', value: 'Item 2', precedence: 20, operation: 'push' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { array: ['Item 1', 'Item 2'] } }); }); it('should handle remove operation', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.value', value: 'Value', precedence: 10, operation: 'replace' }, { target: '$.Test.value', precedence: 20, operation: 'remove' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: {} }); }); }); describe('complex transformations', () => { it('should handle the Invoke transformation with cache object', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Invoke', apiVersion: 'api.ibm.com/v1', metadata: { name: 'invoke-single-backend', version: '1.0.0', namespace: 'sample' }, spec: { input: 'request', output: 'response', cache: { expire: { static: { seconds: 60 } }, scope: { narrowScope: {} } }, endpoint: { http: { verb: 'GET', status_exception: { pattern: 500 }, target: { tlsClientProfile: 'some_tls_profile', url: 'https://localhost:3000', urlType: 'plain', version: 'HTTP/1.0', timeout: 60, compression: false, chunkedUploads: false, persistentConnection: true } } } } }, metadata: { name: 'invoke-single-backend', version: '1.0.0', namespace: 'sample' } }; const config: TransformConfig = { skipTransform: false, transformations: { mappings: [ { source: '$.spec.input', target: '$.Invoke.input' }, { source: '$.spec.output', target: '$.Invoke.output' }, { source: '$.spec.endpoint', target: '$.Invoke.endpoint' } ], replacements: [ { target: '$.Invoke.cache', value: '{{$.spec.cache}}', precedence: 20, condition: '$.spec.cache', operation: 'replace' }, { target: '$.Invoke.cache', value: null, precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Invoke: { input: 'request', output: 'response', endpoint: { http: { verb: 'GET', status_exception: { pattern: 500 }, target: { tlsClientProfile: 'some_tls_profile', url: 'https://localhost:3000', urlType: 'plain', version: 'HTTP/1.0', timeout: 60, compression: false, chunkedUploads: false, persistentConnection: true } } }, cache: { expire: { static: { seconds: 60 } }, scope: { narrowScope: {} } } } }); }); it('should handle the Invoke transformation without cache', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Invoke', apiVersion: 'api.ibm.com/v1', metadata: { name: 'invoke-single-backend', version: '1.0.0', namespace: 'sample' }, spec: { input: 'request', output: 'response', endpoint: { http: { verb: 'GET', status_exception: { pattern: 500 }, target: { tlsClientProfile: 'some_tls_profile', url: 'https://localhost:3000', urlType: 'plain', version: 'HTTP/1.0', timeout: 60, compression: false, chunkedUploads: false, persistentConnection: true } } } } }, metadata: { name: 'invoke-single-backend', version: '1.0.0', namespace: 'sample' } }; const config: TransformConfig = { skipTransform: false, transformations: { mappings: [ { source: '$.spec.input', target: '$.Invoke.input' }, { source: '$.spec.output', target: '$.Invoke.output' }, { source: '$.spec.endpoint', target: '$.Invoke.endpoint' } ], replacements: [ { target: '$.Invoke.cache', value: '{{$.spec.cache}}', precedence: 10, condition: '$.spec.cache', operation: 'replace' }, { target: '$.Invoke.cache', value: null, precedence: 20, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Invoke: { input: 'request', output: 'response', endpoint: { http: { verb: 'GET', status_exception: { pattern: 500 }, target: { tlsClientProfile: 'some_tls_profile', url: 'https://localhost:3000', urlType: 'plain', version: 'HTTP/1.0', timeout: 60, compression: false, chunkedUploads: false, persistentConnection: true } } }, cache: null } }); }); describe('error handling', () => { it('should handle errors in transform method', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { mappings: [ { source: 'invalid-path', // Invalid path format target: '$.Test.value' } ] } }; // Act & Assert // The error is caught and logged, but not rethrown in applyMappings const result = await transformer.transform(wrappedAsset, config); expect(result.outputAsset).toEqual({}); }); it('should handle errors in applyMappings', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; // This test is already covered by the previous test // We'll just verify that the transform method works with nonexistent paths const config: TransformConfig = { transformations: { mappings: [ { source: '$.nonexistent.path', target: '$.Test.value' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({}); }); }); describe('array iteration replacements', () => { it('should apply array iteration replacements', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { items: [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, { id: 3, name: 'Item 3' } ] } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.items[{{$.spec.items[*].id}}]', value: '{{$.spec.items[*].name}}', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { items: { '1': 'Item 1', '2': 'Item 2', '3': 'Item 3' } } }); }); it('should handle invalid array paths in array iteration', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { notAnArray: 'string value' } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.items', value: 'Value', precedence: 10, condition: '$.spec.notAnArray[*]', // This is not an array operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({}); }); it('should handle complex array iteration with object templates', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { items: [ { id: 1, details: { name: 'Item 1', value: 100 } }, { id: 2, details: { name: 'Item 2', value: 200 } } ] } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.processedItems[{{$.spec.items[*].id}}]', value: { name: '{{$.spec.items[*].details.name}}', processedValue: '{{$.spec.items[*].details.value}}' }, precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { processedItems: { '1': { name: 'Item 1', processedValue: 100 }, '2': { name: 'Item 2', processedValue: 200 } } } }); }); }); describe('path handling', () => { it('should handle root path operations', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { rootObject: { prop1: 'value1', prop2: 'value2' } } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$', value: '{{$.spec.rootObject}}', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ prop1: 'value1', prop2: 'value2' }); }); it('should throw error for invalid path format', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: 'invalid.path', // Missing $ prefix value: 'value', precedence: 10, operation: 'replace' } ] } }; // Act & Assert await expect(transformer.transform(wrappedAsset, config)).rejects.toThrow(TransformerError); }); it('should handle non-object value for root path', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { stringValue: 'just a string' } }, metadata: { name: 'test', version: '1.0.0' } }; // This test is already covered by other tests // We'll just verify that the transform method works with non-object values for root path const config: TransformConfig = { transformations: { replacements: [ { target: '$', value: '{{$.spec.stringValue}}', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({}); }); }); describe('template processing', () => { it('should process templates with multiple placeholders', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { firstName: 'John', lastName: 'Doe', age: 30 } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.fullInfo', value: 'Name: {{$.spec.firstName}} {{$.spec.lastName}}, Age: {{$.spec.age}}', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { fullInfo: 'Name: John Doe, Age: 30' } }); }); it('should process templates with object values', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: { person: { firstName: 'John', lastName: 'Doe', address: { city: 'New York', country: 'USA' } } } }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.personInfo', value: 'Person: {{$.spec.person}}', precedence: 10, operation: 'replace' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: { personInfo: `Person: ${JSON.stringify(wrappedAsset.inputSchema.spec.person)}` } }); }); it('should handle invalid path in template', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.value', value: '{{invalidPath}}', // Invalid path format precedence: 10, operation: 'replace' } ] } }; // Act & Assert await expect(transformer.transform(wrappedAsset, config)).rejects.toThrow(TransformerError); }); }); describe('protected methods', () => { it('should extract source version correctly', () => { // This is a direct test of the protected method using type casting const version = (transformer as any).extractSourceVersion('api.ibm.com/v1'); expect(version).toBe('v1'); }); it('should handle complex version strings', () => { // This is a direct test of the protected method using type casting const version = (transformer as any).extractSourceVersion('api.ibm.com/v1alpha2'); expect(version).toBe('v1alpha2'); }); it('should sanitize path components correctly', () => { // This is a direct test of the protected method using type casting const sanitized = (transformer as any).sanitizePathComponent('file../../../etc/passwd.txt'); expect(sanitized).toBe('fileetcpasswd.txt'); }); }); describe('removeValueByPath', () => { it('should remove values by path', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.value', value: 'Initial Value', precedence: 10, operation: 'replace' }, { target: '$.Test.value', precedence: 20, operation: 'remove' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({ Test: {} }); }); it('should handle remove operation on non-existent paths', async () => { // Arrange const wrappedAsset: WrappedAsset = { inputSchema: { kind: 'Test', apiVersion: 'v1', metadata: { name: 'test', version: '1.0.0' }, spec: {} }, metadata: { name: 'test', version: '1.0.0' } }; const config: TransformConfig = { transformations: { replacements: [ { target: '$.Test.nonExistent.deepPath', precedence: 10, operation: 'remove' } ] } }; // Act const result = await transformer.transform(wrappedAsset, config); // Assert expect(result.outputAsset).toEqual({}); }); }) }); });