@apistudio/apim-cli
Version:
CLI for API Management Products
309 lines (251 loc) • 11.9 kB
text/typescript
/**
* Copyright Super iPaaS Integration LLC, an IBM Company 2024
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import AdmZip from 'adm-zip';
import { loadCacheWithProject } from './asset-cache-helper.js';
import { isYamlFile } from '../common/fs-helper.js';
import { readMultiYaml } from '../common/yaml-helper.js';
import { isValidAsset } from './asset-helper.js';
import { AssetCache } from '../../cache/asset-cache.js';
import { getRefsFromAsset } from '../../handlers/asset-handler.js';
import { BaseAsset } from '../../model/assets-model.js';
jest.mock('../common/fs-helper.js', () => ({
isYamlFile: jest.fn(),
}));
jest.mock('../common/yaml-helper.js', () => ({
readMultiYaml: jest.fn(),
}));
jest.mock('./asset-helper.js', () => ({
isValidAsset: jest.fn(),
}));
jest.mock('../common/message-helper.js', () => ({
showWarning: jest.fn(),
showInfo: jest.fn()
}));
jest.mock('../../cache/asset-cache.js', () => {
return {
AssetCache: {
getInstance: jest.fn().mockReturnValue({
markAsProcessed: jest.fn(),
checkAndMarkAsUnProcessed: jest.fn(),
}),
},
};
});
jest.mock('../../handlers/asset-handler.js', () => ({
getRefsFromAsset: jest.fn(),
}));
describe('Asset cache helper function test suite', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should handle empty zip', () => {
const zip = new AdmZip();
jest.spyOn(zip, 'getEntries').mockReturnValue([]);
loadCacheWithProject(zip);
expect(readMultiYaml).not.toHaveBeenCalled();
expect(isValidAsset).not.toHaveBeenCalled();
expect(AssetCache.getInstance().markAsProcessed).not.toHaveBeenCalled();
expect(getRefsFromAsset).not.toHaveBeenCalled();
});
it('should handle zip with invalid asset', () => {
const zip = new AdmZip();
const mockAsset = { kind: 'invalidKind', metadata: { name: 'invalidName' } } as BaseAsset;
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'asset.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('invalid yaml')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock).mockReturnValue([mockAsset]);
(isValidAsset as jest.Mock).mockReturnValue(false);
loadCacheWithProject(zip);
expect(readMultiYaml).toHaveBeenCalledWith('asset.yml', 'invalid yaml');
expect(isValidAsset).toHaveBeenCalledWith(mockAsset);
expect(AssetCache.getInstance().markAsProcessed).not.toHaveBeenCalled();
expect(getRefsFromAsset).not.toHaveBeenCalled();
});
it('should handle zip with non-yaml files', () => {
const zip = new AdmZip();
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'file.txt', isDirectory: false, getData: jest.fn() } as any,
{ entryName: 'image.png', isDirectory: false, getData: jest.fn() } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(false);
loadCacheWithProject(zip);
expect(readMultiYaml).not.toHaveBeenCalled();
expect(isValidAsset).not.toHaveBeenCalled();
expect(AssetCache.getInstance().markAsProcessed).not.toHaveBeenCalled();
expect(getRefsFromAsset).not.toHaveBeenCalled();
});
it('should handle zip with one valid asset', () => {
const zip = new AdmZip();
const mockAsset = { kind: 'validKind', metadata: { name: 'validName' } } as BaseAsset;
const mockRefs = [{ kind: 'refKind', metadata: { name: 'refName' } }] as BaseAsset[];
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'asset.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('valid yaml')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock).mockReturnValue([mockAsset]);
(isValidAsset as jest.Mock).mockReturnValue(true);
(getRefsFromAsset as jest.Mock).mockReturnValue(mockRefs);
loadCacheWithProject(zip);
expect(readMultiYaml).toHaveBeenCalledWith('asset.yml', 'valid yaml');
expect(isValidAsset).toHaveBeenCalledWith(mockAsset);
expect(AssetCache.getInstance().markAsProcessed).toHaveBeenCalledWith(mockAsset);
expect(getRefsFromAsset).toHaveBeenCalledWith(mockAsset);
expect(AssetCache.getInstance().checkAndMarkAsUnProcessed).toHaveBeenCalledWith(mockRefs[0]);
});
it('should handle dependency assets correctly', () => {
const zip = new AdmZip();
const mockAsset = { kind: 'validKind', metadata: { name: 'validName' } } as BaseAsset;
const mockRefAsset = { kind: 'refKind', metadata: { name: 'refName' } } as BaseAsset;
const mockRefs = [mockRefAsset];
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'asset.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('valid yaml')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock).mockReturnValue([mockAsset]);
(isValidAsset as jest.Mock).mockReturnValue(true);
(getRefsFromAsset as jest.Mock).mockReturnValue(mockRefs);
loadCacheWithProject(zip);
expect(readMultiYaml).toHaveBeenCalledWith('asset.yml', 'valid yaml');
expect(isValidAsset).toHaveBeenCalledWith(mockAsset);
expect(AssetCache.getInstance().markAsProcessed).toHaveBeenCalledWith(mockAsset);
expect(getRefsFromAsset).toHaveBeenCalledWith(mockAsset);
expect(AssetCache.getInstance().checkAndMarkAsUnProcessed).toHaveBeenCalledWith(mockRefAsset);
});
it('should process multiple valid assets in one YAML file', () => {
const zip = new AdmZip();
const mockAssets = [
{ kind: 'validKind1', metadata: { name: 'validName1' } } as BaseAsset,
{ kind: 'validKind2', metadata: { name: 'validName2' } } as BaseAsset,
];
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'assets.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('multiple valid yaml')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock).mockReturnValue(mockAssets);
(isValidAsset as jest.Mock).mockReturnValue(true);
loadCacheWithProject(zip);
mockAssets.forEach(asset => {
expect(isValidAsset).toHaveBeenCalledWith(asset);
expect(AssetCache.getInstance().markAsProcessed).toHaveBeenCalledWith(asset);
});
expect(readMultiYaml).toHaveBeenCalledWith('assets.yml', 'multiple valid yaml');
});
it('should process valid assets and skip invalid ones in one YAML file', () => {
const zip = new AdmZip();
const validAsset = { kind: 'validKind', metadata: { name: 'validName' } } as BaseAsset;
const invalidAsset = { kind: 'invalidKind', metadata: { name: 'invalidName' } } as BaseAsset;
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'assets.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('valid and invalid yaml')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock).mockReturnValue([validAsset, invalidAsset]);
(isValidAsset as jest.Mock)
.mockReturnValueOnce(true) // validAsset
.mockReturnValueOnce(false); // invalidAsset
loadCacheWithProject(zip);
expect(readMultiYaml).toHaveBeenCalledWith('assets.yml', 'valid and invalid yaml');
expect(isValidAsset).toHaveBeenCalledWith(validAsset);
expect(isValidAsset).toHaveBeenCalledWith(invalidAsset);
expect(AssetCache.getInstance().markAsProcessed).toHaveBeenCalledWith(validAsset);
expect(AssetCache.getInstance().markAsProcessed).not.toHaveBeenCalledWith(invalidAsset);
});
it('should extract and pass source project name from entry path', () => {
const zip = new AdmZip();
const mockAsset = { kind: 'Product', metadata: { name: 'TestProduct' } } as BaseAsset;
const mockRef = { kind: 'API', ref: 'dev:PaymentAPI:1.0', isNewlyAdded: true };
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'ProjectA/api-assets/product.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('yaml content')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock).mockReturnValue([mockAsset]);
(isValidAsset as jest.Mock).mockReturnValue(true);
(getRefsFromAsset as jest.Mock).mockReturnValue([mockRef]);
loadCacheWithProject(zip);
// Verify that checkForDependencyAssets was called with the source project
expect(getRefsFromAsset).toHaveBeenCalledWith(mockAsset);
expect(AssetCache.getInstance().checkAndMarkAsUnProcessed).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'API',
ref: 'dev:PaymentAPI:1.0',
isNewlyAdded: true,
sourceProject: 'ProjectA'
})
);
});
it('should handle multiple projects with different source projects', () => {
const zip = new AdmZip();
const mockAssetA = { kind: 'Product', metadata: { name: 'ProductA' } } as BaseAsset;
const mockAssetB = { kind: 'Product', metadata: { name: 'ProductB' } } as BaseAsset;
const mockRefA = { kind: 'API', ref: 'dev:API_A:1.0', isNewlyAdded: true };
const mockRefB = { kind: 'API', ref: 'dev:API_B:1.0', isNewlyAdded: true };
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'ProjectA/api-assets/product.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('yaml A')) } as any,
{ entryName: 'ProjectC/api-assets/product.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('yaml B')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock)
.mockReturnValueOnce([mockAssetA])
.mockReturnValueOnce([mockAssetB]);
(isValidAsset as jest.Mock).mockReturnValue(true);
(getRefsFromAsset as jest.Mock)
.mockReturnValueOnce([mockRefA])
.mockReturnValueOnce([mockRefB]);
loadCacheWithProject(zip);
// Verify each dependency has correct source project
expect(AssetCache.getInstance().checkAndMarkAsUnProcessed).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'API',
ref: 'dev:API_A:1.0',
sourceProject: 'ProjectA'
})
);
expect(AssetCache.getInstance().checkAndMarkAsUnProcessed).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'API',
ref: 'dev:API_B:1.0',
sourceProject: 'ProjectC'
})
);
});
it('should handle nested folder structure in entry path', () => {
const zip = new AdmZip();
const mockAsset = { kind: 'Product', metadata: { name: 'TestProduct' } } as BaseAsset;
const mockRef = { kind: 'API', ref: 'dev:API:1.0', isNewlyAdded: true };
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'ProjectA/nested/folder/product.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('yaml')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock).mockReturnValue([mockAsset]);
(isValidAsset as jest.Mock).mockReturnValue(true);
(getRefsFromAsset as jest.Mock).mockReturnValue([mockRef]);
loadCacheWithProject(zip);
// Should extract 'ProjectA' as source project even with nested folders
expect(AssetCache.getInstance().checkAndMarkAsUnProcessed).toHaveBeenCalledWith(
expect.objectContaining({
sourceProject: 'ProjectA'
})
);
});
it('should handle entry path without project folder', () => {
const zip = new AdmZip();
const mockAsset = { kind: 'Product', metadata: { name: 'TestProduct' } } as BaseAsset;
const mockRef = { kind: 'API', ref: 'dev:API:1.0', isNewlyAdded: true };
jest.spyOn(zip, 'getEntries').mockReturnValue([
{ entryName: 'product.yml', isDirectory: false, getData: jest.fn().mockReturnValue(Buffer.from('yaml')) } as any,
]);
(isYamlFile as jest.Mock).mockReturnValue(true);
(readMultiYaml as jest.Mock).mockReturnValue([mockAsset]);
(isValidAsset as jest.Mock).mockReturnValue(true);
(getRefsFromAsset as jest.Mock).mockReturnValue([mockRef]);
loadCacheWithProject(zip);
// Should use the filename as source project when no folder structure
expect(AssetCache.getInstance().checkAndMarkAsUnProcessed).toHaveBeenCalledWith(
expect.objectContaining({
sourceProject: 'product.yml'
})
);
});
});