UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

721 lines (586 loc) 23 kB
/* 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'], }, }); }); }); });