@apistudio/apim-cli
Version:
CLI for API Management Products
721 lines (586 loc) • 23 kB
text/typescript
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-explicit-any */
import path from 'path';
import AdmZip from 'adm-zip';
import { showError, showInfo, showWarning } from '../common/message-helper.js';
import { readMultiYaml } from '../common/yaml-helper.js';
import { readFile } from '../common/fs-helper.js';
import {
getAPIRefToBuild,
processEndpointFromResponse,
addEndpointToZip,
testAssets,
combineTestAsset,
testProjects,
createJSONBuffer,
APIEndpoints,
updateEndpointZip,
findProjectForApi,
constructKey,
buildAndDeployAssets,
formattedEndpoints
} from './test-helper.js';
import {
INVALID_OR_EMPTY_DEPLOYMENT_RESPONSES,
NO_VALID_API_ASSET_FOUND,
ADDING_ENDPOINT_FILE_FAILED,
ERROR_IN_COMBINING_TEST_ASSET,
JSON_FILE_MISSING,
UPDATE_ENDPOINT_FAILED,
APIENDPOINTS
} from '../../constants/message-constants.js';
import { BuildTestAssetsResult, executeBuildTestAssets } from '../../testers/project/projects-asset-testers.js';
import { GatewayResponseAPI } from '../../model/studio/deploy-response-model.js';
import { ENDPOINT_FILE } from '../../constants/app-constants.js';
import { KindEnums } from '@apic/api-model/common/StudioEnums.js';
import { getAllProjectNames } from './root-dir-helper.js';
import { searchAssetByKind } from './asset-searchByKind-helper.js';
import { executeDeployment } from '../../deployers/project/projects-deployer.js';
import { buildAssets } from '../../actions/helpers/build-action-helper.js';
import { GatewaysJson } from '@apic/studio-shared';
import Table from 'cli-table3';
jest.mock('path');
jest.mock('cli-table3', () => {
const mockTableInstance = {
push: jest.fn(),
toString: jest.fn().mockReturnValue('mocked table output'),
};
return jest.fn(() => mockTableInstance);
});
jest.mock('../../actions/helpers/build-action-helper',() => ({
buildAssets: jest.fn()
}));
jest.mock('../../deployers/project/projects-deployer',() => ({
executeDeployment: jest.fn()
}))
jest.mock('adm-zip', () => {
return jest.fn().mockImplementation(() => ({
addFile: jest.fn(),
toBuffer: jest.fn(),
getEntries: jest.fn(),
getEntry: jest.fn(),
updateFile: jest.fn()
}));
});
jest.mock('../common/message-helper', () => ({
showError: jest.fn(),
showInfo: jest.fn(),
showWarning: jest.fn()
}));
jest.mock('./root-dir-helper', ()=>({
getAllProjectNames: jest.fn()
}));
jest.mock('./asset-searchByKind-helper',() =>({
searchAssetByKind: jest.fn()
}));
jest.mock('@apic/studio-build', () => ({
processProjectBuild: jest.fn(),
}));
jest.mock('@apic/studio-deploy', () => ({
processDeployment: jest.fn(),
}));
jest.mock('../../testers/project/projects-asset-testers');
jest.mock('../common/yaml-helper.js');
jest.mock('../common/fs-helper.js');
jest.mock('env-paths', () => {
return jest.fn((name: string) => ({
data: `/mock/path/${name}-data`,
config: `/mock/path/${name}-config`,
cache: `/mock/path/${name}-cache`,
log: `/mock/path/${name}-log`,
temp: `/mock/path/${name}-temp`,
}));
});
jest.mock('../../debug/debug-manager.js', () => ({
DebugManager: {
getInstance: jest.fn(() => ({
setDebugEnabled: jest.fn(),
isDebugEnabled: jest.fn()
})),
},
}));
describe ('Test-helper test Suite', () => {
describe('getAPIRefToBuild', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return the API reference if present in the YAML file', () => {
const testFilePath = 'some/test/file.yaml';
const parentDirPath = 'some/test';
const testFileName = 'file.yaml';
const yamlContent = 'yaml content';
const parsedYaml = [{
kind: 'test',
spec: {
api: { $ref: 'api-ref' },
},
}];
(path.dirname as jest.Mock).mockReturnValue(parentDirPath);
(path.basename as jest.Mock).mockReturnValue(testFileName);
(readFile as jest.Mock).mockReturnValue(yamlContent);
(readMultiYaml as jest.Mock).mockReturnValue(parsedYaml);
const result = getAPIRefToBuild(testFilePath);
expect(path.dirname).toHaveBeenCalledWith(testFilePath);
expect(path.basename).toHaveBeenCalledWith(testFilePath);
expect(readFile).toHaveBeenCalledWith(parentDirPath, testFileName);
expect(readMultiYaml).toHaveBeenCalledWith(testFileName, yamlContent);
expect(result).toEqual(['api-ref']);
});
it('should show an error and return null if API reference is not found', () => {
const testFilePath = 'some/test/file.yaml';
const parentDirPath = 'some/test';
const testFileName = 'file.yaml';
const yamlContent = 'yaml content';
const parsedYaml = [{
spec: {
api: {},
},
}];
(path.dirname as jest.Mock).mockReturnValue(parentDirPath);
(path.basename as jest.Mock).mockReturnValue(testFileName);
(readFile as jest.Mock).mockReturnValue(yamlContent);
(readMultiYaml as jest.Mock).mockReturnValue(parsedYaml);
const result = getAPIRefToBuild(testFilePath);
// expect(showError).toHaveBeenCalledWith(NO_VALID_API_ASSET_FOUND);
expect(result).toHaveLength(0);
});
});
describe('processEndpointFromResponse', () => {
it('should process and return endpoint JSON from response', () => {
const responses: GatewayResponseAPI[] = [
{
namespace: 'namespace1',
assetName: 'asset1',
version: '1.0',
gatewayEndpoints: ['endpoint1', 'endpoint2'],
name: 'api1',
kind: 'someKind',
},
{
namespace: 'namespace1',
assetName: 'asset1',
version: '1.0',
gatewayEndpoints: ['endpoint3'],
name: 'api1',
kind: 'someKind',
},
];
const result = processEndpointFromResponse(responses);
const expectedJSON = JSON.stringify({
'namespace1:asset1:1.0': ['endpoint1', 'endpoint2', 'endpoint3'],
}, null, 2);
expect(result).toEqual(Buffer.from(expectedJSON, 'utf-8'));
});
it('should throw an error if responses are invalid or empty', () => {
expect(() => processEndpointFromResponse([] as GatewayResponseAPI[])).toThrowError(INVALID_OR_EMPTY_DEPLOYMENT_RESPONSES);
expect(() => processEndpointFromResponse(null as unknown as GatewayResponseAPI[])).toThrowError(INVALID_OR_EMPTY_DEPLOYMENT_RESPONSES);
expect(() => processEndpointFromResponse(undefined as unknown as GatewayResponseAPI[])).toThrowError(INVALID_OR_EMPTY_DEPLOYMENT_RESPONSES);
});
});
describe('addEndpointToZip', () => {
let mockAddFile: jest.Mock;
let mockToBuffer: jest.Mock;
let admZipInstance: AdmZip;
beforeEach(() => {
admZipInstance = new AdmZip() as jest.Mocked<AdmZip>;
mockAddFile = admZipInstance.addFile as jest.Mock;
mockToBuffer = admZipInstance.toBuffer as jest.Mock;
(AdmZip as jest.Mock).mockImplementation(() => admZipInstance);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should add the endpoint file to the zip and return the updated buffer', () => {
const testZipBuffer = Buffer.from('test zip content');
const endpointFile = Buffer.from('endpoint content');
const updatedBuffer = Buffer.from('updated zip content');
mockToBuffer.mockReturnValue(updatedBuffer);
const result = addEndpointToZip(testZipBuffer, endpointFile);
expect(AdmZip).toHaveBeenCalledWith(testZipBuffer);
expect(mockAddFile).toHaveBeenCalledWith(ENDPOINT_FILE, endpointFile);
expect(result).toBe(updatedBuffer);
});
it('should throw an error if adding the endpoint file fails', () => {
// Arrange
const testZipBuffer = Buffer.from('test zip content');
const endpointFile = Buffer.from('endpoint content');
const errorMessage = 'mock error';
mockAddFile.mockImplementation(() => { throw new Error(errorMessage); });
expect(() => addEndpointToZip(testZipBuffer, endpointFile))
.toThrow(`${ADDING_ENDPOINT_FILE_FAILED} ${errorMessage}`);
});
});
describe('testAssets', () => {
const rootDir = 'C:/Users/Downloads/CLI_DEMO_ASSETS';
const projects = 'projectA';
const assets = 'dev:test:1.0';
beforeEach(() => {
jest.clearAllMocks();
});
it('should call showInfo with correct messages and executeBuildTestAssets with correct arguments', async () => {
const mockResult: BuildTestAssetsResult = {
zipBuffer: Buffer.from('test buffer'),
apiReference: 'testApiReference'
};
(executeBuildTestAssets as jest.Mock).mockResolvedValue(mockResult);
const result = await testAssets(rootDir, projects, assets);
expect(showInfo).toHaveBeenCalledWith('--------------------------');
expect(showInfo).toHaveBeenCalledWith('Test Started');
expect(showInfo).toHaveBeenCalledTimes(3);
expect(executeBuildTestAssets).toHaveBeenCalledWith(rootDir, projects, assets);
expect(result).toBe(mockResult);
});
it('should handle errors thrown by executeBuildTestAssets', async () => {
const errorMessage = 'Failed to build test assets';
(executeBuildTestAssets as jest.Mock).mockRejectedValue(new Error(errorMessage));
await expect(testAssets(rootDir, projects, assets)).rejects.toThrow(errorMessage);
expect(showInfo).toHaveBeenCalledWith('--------------------------');
expect(showInfo).toHaveBeenCalledWith('Test Started');
expect(showInfo).toHaveBeenCalledTimes(3); // Two lines and one "Test Started"
expect(executeBuildTestAssets).toHaveBeenCalledWith(rootDir, projects, assets);
});
});
describe('combineTestAsset', () => {
const rootDir = 'C:/Users/Downloads/CLI_DEMO_ASSETS';
const assetsToTest = {
projectA: 'metadataA',
projectB: 'metadataB',
};
let mockResult: BuildTestAssetsResult;
let mockCombinedZip: AdmZip;
beforeEach(() => {
mockResult = {
zipBuffer: Buffer.from('project zip buffer'),
apiReference: 'api-ref',
};
mockCombinedZip = new AdmZip() as jest.Mocked<AdmZip>;
(AdmZip as jest.Mock).mockImplementation(() => mockCombinedZip);
mockCombinedZip.addFile = jest.fn();
mockCombinedZip.toBuffer = jest.fn().mockReturnValue(Buffer.from('final zip content'));
mockCombinedZip.getEntries = jest.fn();
mockCombinedZip.getEntry = jest.fn();
jest.clearAllMocks();
});
it('should successfully combine test assets into one zip', async () => {
const testAssetsSpy = jest.spyOn(require('./test-helper'), 'testAssets');
testAssetsSpy.mockResolvedValue(mockResult);
const mockProjectZip = new AdmZip() as jest.Mocked<AdmZip>;
(AdmZip as jest.Mock).mockReturnValue(mockProjectZip);
const mockEntry = {
entryName: 'testEntry',
getData: jest.fn().mockReturnValue(Buffer.from('data')),
} as unknown as AdmZip.IZipEntry;
mockProjectZip.getEntries = jest.fn().mockReturnValue([mockEntry]);
const result = await combineTestAsset(rootDir, assetsToTest);
expect(mockCombinedZip.addFile).toHaveBeenCalledWith(mockEntry.entryName, Buffer.from('data'));
expect(result).toEqual({
zipBuffer: mockCombinedZip.toBuffer(),
apiReference: {
projectA: 'api-ref',
projectB: 'api-ref',
}
});
testAssetsSpy.mockRestore();
});
it('should throw an error when combining fails', async () => {
const testAssetsSpy = jest.spyOn(require('./test-helper'), 'testAssets');
const errorMessage = 'Failed to build test assets';
testAssetsSpy.mockRejectedValue(new Error(errorMessage));
await expect(combineTestAsset(rootDir, assetsToTest)).rejects.toThrow(errorMessage);
expect(showError).toHaveBeenCalledWith(`${ERROR_IN_COMBINING_TEST_ASSET} ${errorMessage}`);
testAssetsSpy.mockRestore();
});
});
describe('testProjects', () => {
const rootDir = 'C:/Users/Downloads/CLI_DEMO_ASSETS';
const projects = 'projectA';
beforeEach(() => {
jest.clearAllMocks();
});
it('should test specific projects when all is false', async () => {
const mockAssetResults = { projectA: 'metadataA' };
(searchAssetByKind as jest.Mock).mockResolvedValue(mockAssetResults);
const result = await testProjects(false, rootDir, projects);
expect(searchAssetByKind).toHaveBeenCalledWith(KindEnums.Test, rootDir, projects);
expect(result).toEqual(mockAssetResults);
});
it('should test all projects when all is true', async () => {
const mockAllProjects = 'projectA,projectB';
const mockAssetResults = { projectA: 'metadataA', projectB: 'metadataB' };
(getAllProjectNames as jest.Mock).mockReturnValue(mockAllProjects);
(searchAssetByKind as jest.Mock).mockResolvedValue(mockAssetResults);
const result = await testProjects(true, rootDir, '');
expect(getAllProjectNames).toHaveBeenCalledWith(rootDir);
expect(searchAssetByKind).toHaveBeenCalledWith(KindEnums.Test, rootDir, mockAllProjects);
expect(result).toEqual(mockAssetResults);
});
});
describe('createJSONBuffer', () => {
it('should create a JSON buffer from API endpoints', () => {
const jsonObject: APIEndpoints = {
'namespace1:asset1:1.0': ['endpoint1', 'endpoint2']
};
const result = createJSONBuffer(jsonObject);
const expectedJSON = JSON.stringify(jsonObject, null, 2);
expect(result).toEqual(Buffer.from(expectedJSON, 'utf-8'));
});
});
describe('updateEndpointZip', () => {
let mockGetEntry: jest.Mock;
let admZipInstance: AdmZip;
beforeEach(() => {
admZipInstance = new AdmZip() as jest.Mocked<AdmZip>;
mockGetEntry =admZipInstance.getEntry as jest.Mock;
(AdmZip as jest.Mock).mockImplementation(() => admZipInstance);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should throw an error if JSON parsing fails', async () => {
const testZipBuffer = Buffer.from('test zip content');
const newEndpoints = Buffer.from('invalid json');
const jsonEntry = {
getData: jest.fn().mockReturnValue(Buffer.from('{')),
} as unknown as AdmZip.IZipEntry;
mockGetEntry.mockReturnValue(jsonEntry);
await expect(updateEndpointZip(testZipBuffer, newEndpoints))
.rejects.toThrowError(new RegExp(`${UPDATE_ENDPOINT_FAILED} .*`));
});
it('should throw an error if the entry is not found', async () => {
const testZipBuffer = Buffer.from('test zip content');
const newEndpoints = Buffer.from(JSON.stringify({ key: ['new-endpoint'] }));
mockGetEntry.mockReturnValue(null);
await expect(updateEndpointZip(testZipBuffer, newEndpoints))
.rejects.toThrowError(`${UPDATE_ENDPOINT_FAILED} ${JSON_FILE_MISSING}`);
});
});
describe('findProjectForApi', () => {
it('should correctly map APIs found in the apiReference', () => {
const apiReference: Record<string, string> = {
'Project1': 'api1, api2',
'Project2': 'api3, api4',
};
const notFoundApisList: string[] = ['api1', 'api3'];
const expected: Record<string, string> = {
'Project1': 'api1',
'Project2': 'api3',
};
const result = findProjectForApi(apiReference, notFoundApisList);
expect(result).toEqual(expected);
});
it('should return an empty object if apiReference is empty', () => {
const apiReference: Record<string, string> = {};
const notFoundApisList: string[] = ['api1', 'api2'];
const expected: Record<string, string> = {};
const result = findProjectForApi(apiReference, notFoundApisList);
expect(result).toEqual(expected);
});
it('should return an empty object if notFoundApisList is empty', () => {
const apiReference: Record<string, string> = {
'Project1': 'api1, api2',
'Project2': 'api3, api4',
};
const notFoundApisList: string[] = [];
const expected: Record<string, string> = {};
const result = findProjectForApi(apiReference, notFoundApisList);
expect(result).toEqual(expected);
});
it('should return an empty object if no APIs from notFoundApisList are found in apiReference', () => {
const apiReference: Record<string, string> = {
'Project1': 'api1, api2',
'Project2': 'api3, api4',
};
const notFoundApisList: string[] = ['api5', 'api6'];
const expected: Record<string, string> = {};
const result = findProjectForApi(apiReference, notFoundApisList);
expect(result).toEqual(expected);
});
it('should aggregate multiple matching APIs in the result for a single project', () => {
const apiReference: Record<string, string> = {
'Project1': 'api1, api2, api3',
'Project2': 'api4, api5',
};
const notFoundApisList: string[] = ['api1', 'api2', 'api3'];
const expected: Record<string, string> = {
'Project1': 'api1,api2,api3',
};
const result = findProjectForApi(apiReference, notFoundApisList);
expect(result).toEqual(expected);
});
});
describe('constructKey', () => {
it('should return the correct key for a valid response', () => {
const response = {
namespace: 'testNamespace',
assetName: 'testAsset',
version: '1.0.0',
gatewayEndpoints: [],
} as unknown as GatewayResponseAPI;
const result = constructKey(response);
expect(result).toBe('testNamespace:testAsset:1.0.0');
});
it('should handle empty strings in the response', () => {
const response = {
namespace: '',
assetName: 'testAsset',
version: '1.0.0',
gatewayEndpoints: [],
} as unknown as GatewayResponseAPI;
const result = constructKey(response);
expect(result).toBe(':testAsset:1.0.0');
});
it('should handle missing asset name', () => {
const response = {
namespace: 'testNamespace',
assetName: '',
version: '1.0.0',
gatewayEndpoints: [],
} as unknown as GatewayResponseAPI;
const result = constructKey(response);
expect(result).toBe('testNamespace::1.0.0');
});
it('should handle missing version', () => {
const response = {
namespace: 'testNamespace',
assetName: 'testAsset',
version: '',
gatewayEndpoints: [],
} as unknown as GatewayResponseAPI;
const result = constructKey(response);
expect(result).toBe('testNamespace:testAsset:');
});
it('should handle all fields as empty strings', () => {
const response = {
namespace: '',
assetName: '',
version: '',
gatewayEndpoints: [],
} as unknown as GatewayResponseAPI;
const result = constructKey(response);
expect(result).toBe('::');
});
it('should handle numeric version values', () => {
const response = {
namespace: 'testNamespace',
assetName: 'testAsset',
version: '2',
gatewayEndpoints: [],
} as unknown as GatewayResponseAPI;
const result = constructKey(response);
expect(result).toBe('testNamespace:testAsset:2');
});
});
describe('buildAndDeployAssets', () => {
const mockBuildAssets = buildAssets as jest.MockedFunction<typeof buildAssets>;
const mockExecuteDeployment = executeDeployment as jest.MockedFunction<typeof executeDeployment>;
const mockShowError = showError as jest.MockedFunction<typeof showError>;
beforeEach(() => {
jest.clearAllMocks();
});
it('should build and combine assets into a single zip and deploy', async () => {
const mockZipBuffer = Buffer.from('mock-zip-data');
const mockCombinedZip = {
addFile: jest.fn(),
toBuffer: jest.fn().mockReturnValue(mockZipBuffer)
};
const mockProjectZip = {
getEntries: jest.fn().mockReturnValue([
{
entryName: 'file1.txt',
getData: jest.fn().mockReturnValue(Buffer.from('file-data'))
}
] as any)
};
(AdmZip as jest.Mock).mockImplementation((buffer?: Buffer) =>
buffer ? (mockProjectZip as any) : (mockCombinedZip as any)
);
mockBuildAssets.mockResolvedValue(Buffer.from('mock-project-zip-data'));
const mockDeploymentResult: GatewayResponseAPI[] = [];
mockExecuteDeployment.mockResolvedValue(mockDeploymentResult);
const localDir = 'mock-local-dir';
const assetsToBuildAndDeploy = {
project1: 'metadata1',
project2: 'metadata2'
};
const gatewayJson: GatewaysJson = {
gateways: [],
overwrite: 'ALL',
skip: 'NONE'
};
const result = await buildAndDeployAssets(localDir, assetsToBuildAndDeploy, gatewayJson);
expect(AdmZip).toHaveBeenCalledTimes(3);
expect(mockCombinedZip.addFile).toHaveBeenCalledTimes(2);
expect(mockCombinedZip.toBuffer).toHaveBeenCalled();
expect(mockExecuteDeployment).toHaveBeenCalledWith(gatewayJson, mockZipBuffer);
expect(result).toEqual({
buildBuffer: mockZipBuffer,
deploymentResult: mockDeploymentResult
});
expect(mockShowError).not.toHaveBeenCalled();
});
it('should handle errors during asset building', async () => {
mockBuildAssets.mockRejectedValue(new Error('Build failed'));
const localDir = 'mock-local-dir';
const assetsToBuildAndDeploy = {
'project1': 'metadata1'
};
const gatewayJson = { gateways: [], overwrite: 'ALL', skip: 'NONE' } as GatewaysJson;
await buildAndDeployAssets(localDir, assetsToBuildAndDeploy, gatewayJson);
expect(mockShowError).toHaveBeenCalledWith('Failed to build assets for project: project1: Build failed');
});
it('should handle errors during deployment', async () => {
const mockZipBuffer = Buffer.from('mock-zip-data');
const mockCombinedZip = {
addFile: jest.fn(),
toBuffer: jest.fn().mockReturnValue(mockZipBuffer)
};
(AdmZip as jest.Mock).mockImplementation(() => mockCombinedZip as any);
mockBuildAssets.mockResolvedValue(Buffer.from('mock-project-zip-data'));
mockExecuteDeployment.mockRejectedValue(new Error('Deployment failed'));
const localDir = 'mock-local-dir';
const assetsToBuildAndDeploy = {
'project1': 'metadata1'
};
const gatewayJson = { gateways: [], overwrite: 'ALL', skip: 'NONE' } as GatewaysJson;
await expect(buildAndDeployAssets(localDir, assetsToBuildAndDeploy, gatewayJson))
.rejects
.toThrow('Deployment failed');
});
});
describe('formattedEndpoints', () => {
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
jest.clearAllMocks();
consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => {
consoleSpy.mockRestore();
});
it('should format endpoints into a table and log it', () => {
const endpoints = {
'User API': ['/users', '/users/:id'],
'Product API': ['/products', '/products/:id'],
};
formattedEndpoints(endpoints);
expect(showInfo).toHaveBeenCalledWith(APIENDPOINTS);
const mockTableInstance = (Table as jest.Mock).mock.results[0].value;
expect(mockTableInstance.push).toHaveBeenCalledTimes(2);
expect(mockTableInstance.push).toHaveBeenCalledWith(['User API', '/users\n/users/:id']);
expect(mockTableInstance.push).toHaveBeenCalledWith(['Product API', '/products\n/products/:id']);
expect(consoleSpy).toHaveBeenCalledWith('mocked table output');
});
it('should handle empty endpoints without error', () => {
const endpoints = {};
formattedEndpoints(endpoints);
expect(showInfo).toHaveBeenCalledWith(APIENDPOINTS);
expect(consoleSpy).toHaveBeenCalledWith('mocked table output');
expect(jest.mocked(Table)).toHaveBeenCalledWith({
head: ['APIs', 'Gateway Endpoints'],
style: {
head: ['blue'],
border: ['yellow'],
},
});
});
});
});