@apistudio/apim-cli
Version:
CLI for API Management Products
705 lines (589 loc) • 21.1 kB
text/typescript
/**
* 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);
});
});
});