Skip to content

Commit

Permalink
fix(cf-deploy-config-writer): add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
longieirl committed Jan 24, 2025
1 parent 7f781fe commit a002ed3
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 36 deletions.
10 changes: 9 additions & 1 deletion packages/cf-deploy-config-writer/src/cf-writer/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,14 @@ async function updateMtaConfig(cfConfig: CFConfig, fs: Editor): Promise<void> {
}
}

/**
*
* @param root0
* @param root0.rootPath
* @param root0.appId
* @param mtaInstance
* @param fs
*/
function cleanupStandaloneRoutes({ rootPath, appId }: CFConfig, mtaInstance: MtaConfig, fs: Editor): void {
// Cleanup standalone xs-app.json to reflect new application
const appRouterPath = mtaInstance.standaloneRouterPath;
Expand Down Expand Up @@ -399,7 +407,7 @@ async function updateHTML5AppPackage(cfConfig: CFConfig, fs: Editor): Promise<vo
* @param fs reference to a mem-fs editor
*/
async function updateRootPackage(cfConfig: CFConfig, fs: Editor): Promise<void> {
const packageExists = fs.exists(join(cfConfig.rootPath, 'package.json'));
const packageExists = fs.exists(join(cfConfig.rootPath, FileName.Package));
// Append mta scripts only if mta.yaml is at a different level to the HTML5 app
if (cfConfig.isMtaRoot && packageExists) {
await addPackageDevDependency(cfConfig.rootPath, Rimraf, RimrafVersion, fs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import LoggerHelper from '../logger-helper';
import { createMTA, validateMtaConfig } from '../mta-config';
import { type Logger } from '@sap-ux/logger';
import { type CFBaseConfig, type MTABaseConfig } from '../types';
import { join } from 'path';
import { t } from '../i18n';

/**
* Add a standalone | managed approuter to an empty target folder.
Expand All @@ -22,7 +24,10 @@ export async function generateBaseConfig(config: CFBaseConfig, fs?: Editor, logg
LoggerHelper.logger = logger;
}
logger?.debug(`Generate base configuration using: \n ${JSON.stringify(config)}`);
validateMtaConfig(config, fs);
validateMtaConfig(config);
if (fs.exists(join(config.mtaPath, config.mtaId))) {
throw new Error(t('error.mtaFolderAlreadyExists'));
}
createMTA(config as MTABaseConfig);
await addRoutingConfig(config, fs);
addSupportingConfig(config, fs);
Expand Down
23 changes: 21 additions & 2 deletions packages/cf-deploy-config-writer/src/cf-writer/cap-config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { create as createStorage } from 'mem-fs';
import { create, type Editor } from 'mem-fs-editor';
import { addSupportingConfig, addRoutingConfig } from '../utils';
import { createCAPMTA, validateMtaConfig } from '../mta-config';
import { createCAPMTA, validateMtaConfig, isMTAFound } from '../mta-config';
import LoggerHelper from '../logger-helper';
import type { Logger } from '@sap-ux/logger';
import type { CAPConfig, CFBaseConfig } from '../types';
import { CDSDestinationService, CDSHTML5RepoService, CDSXSUAAService } from '../constants';
import { t } from '../i18n';
import { getCapProjectType } from '@sap-ux/project-access';

/**
* Add a standalone | managed approuter to a CAP project.
Expand All @@ -23,11 +25,28 @@ export async function generateCAPConfig(config: CAPConfig, fs?: Editor, logger?:
LoggerHelper.logger = logger;
}
logger?.debug(`Generate CAP configuration using: \n ${JSON.stringify(config)}`);
validateMtaConfig(config as CFBaseConfig, fs);
await validateConfig(config);
const cdsOptionalParams: string[] = [CDSXSUAAService, CDSDestinationService, CDSHTML5RepoService];
createCAPMTA(config.mtaPath, cdsOptionalParams);
await addRoutingConfig(config, fs);
addSupportingConfig(config, fs);
LoggerHelper.logger?.debug(`CF CAP Config ${JSON.stringify(config, null, 2)}`);
return fs;
}

/**
* Ensure the configuration is valid, target folder exists and is a CAP Node.js app and mta.yaml does not already exist.
*
* @param config writer configuration
*/
async function validateConfig(config: CAPConfig): Promise<void> {
validateMtaConfig(config as CFBaseConfig);
// Check if the target directory contains a CAP Node.js project or exists!
if ((await getCapProjectType(config.mtaPath)) !== 'CAPNodejs') {
throw new Error(t('error.doesNotContainACapApp'));
}
// Check if the target directory contains an existing mta.yaml
if (isMTAFound(config.mtaPath)) {
throw new Error(t('error.mtaAlreadyExists'));
}
}
19 changes: 10 additions & 9 deletions packages/cf-deploy-config-writer/src/mta-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { type MTABaseConfig, type CFBaseConfig } from '../types';
import LoggerHelper from '../logger-helper';
import { sync } from 'hasbin';
import { spawnSync } from 'child_process';
import type { Editor } from 'mem-fs-editor';
import { t } from '../i18n';

/**
Expand Down Expand Up @@ -137,19 +136,24 @@ export function doesCDSBinaryExist(): void {
* @param options
*/
export function createCAPMTA(cwd: string, options?: string[]): void {
const result = spawnSync(CDSExecutable, [...CDSAddMtaParams, ...(options ?? [])], { cwd });
if (result.error) {
throw new Error(CDSBinNotFound);
let result = spawnSync(CDSExecutable, [...CDSAddMtaParams, ...(options ?? [])], { cwd });
if (result?.error) {
throw new Error(`Something went wrong creating mta.yaml! ${result.error}`);
}
// Ensure the package-lock is created otherwise mta build will fail
const cmd = process.platform === 'win32' ? `npm.cmd` : 'npm';
result = spawnSync(cmd, ['install', '--ignore-engines'], { cwd });
if (result?.error) {
throw new Error(`Something went wrong installing node modules! ${result.error}`);
}
}

/**
* Validate the writer configuration to ensure all required parameters are present.
*
* @param config writer configuration
* @param fs reference to a mem-fs editor
*/
export function validateMtaConfig(config: CFBaseConfig, fs: Editor): void {
export function validateMtaConfig(config: CFBaseConfig): void {
// We use mta-lib, which in turn relies on the mta executable being installed and available in the path
doesMTABinaryExist();

Expand All @@ -172,9 +176,6 @@ export function validateMtaConfig(config: CFBaseConfig, fs: Editor): void {
throw new Error(t('error.missingABAPServiceBindingDetails'));
}

if (fs.exists(join(config.mtaPath, config.mtaId))) {
throw new Error(t('error.mtaAlreadyExists'));
}
setMtaDefaults(config);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,25 @@
"ui5YamlDoesNotExist": "File ui5.yaml does not exist in the project"
},
"error": {
"unableToLoadMTA": "Unable to load mta.yaml configuration",
"updatingMTAExtensionFailed": "Unable to add mta extension configuration to file: {{mtaExtFilePath}}",
"cannotFindBinary": "Cannot find the \"{{bin}}\" executable. Please add it to the path or use \"npm i -g {{pkg}}\" to install it.",
"mtaExtensionFailed": "Unable to create or update the mta extension file for Api Hub Enterprise destination configuration: {{error}}",
"serviceKeyFailed": "Failed to fetch service key",
"missingMtaParameters": "Missing required parameters, MTA path, MTA ID or router type is missing",
"invalidMtaIdWithChars": "The MTA ID can only contain letters, numbers, dashes, periods, underscores",
"invalidMtaId": "The MTA ID must start with a letter or underscore and be less than 128 characters long",
"missingABAPServiceBindingDetails": "Missing ABAP service details for direct service binding",
"mtaAlreadyExists": "A folder with same name already exists in the target directory",
"cannotUpdateRouterXSApp": "Unable to update router xs-app.json welcome location."
"unableToLoadMTA": "Unable to load mta.yaml configuration.",
"updatingMTAExtensionFailed": "Unable to add mta extension configuration to file: {{mtaExtFilePath}}.",
"cannotFindBinary": "Cannot find the \"{{bin}}\" executable. Please add it to the path or use \"npm i -g {{- pkg}}\" to install it.",
"mtaExtensionFailed": "Unable to create or update the mta extension file for Api Hub Enterprise destination configuration: {{error}}.",
"serviceKeyFailed": "Failed to fetch service key.",
"missingMtaParameters": "Missing required parameters, MTA path, MTA ID or router type is missing.",
"invalidMtaIdWithChars": "The MTA ID can only contain letters, numbers, dashes, periods, underscores.",
"invalidMtaId": "The MTA ID must start with a letter or underscore and be less than 128 characters long.",
"missingABAPServiceBindingDetails": "Missing ABAP service details for direct service binding.",
"mtaFolderAlreadyExists": "A folder with same name already exists in the target directory.",
"mtaAlreadyExists": "An mta.yaml already exists in the target directory.",
"cannotUpdateRouterXSApp": "Unable to update router xs-app.json welcome location.",
"targetFolderDoesNotExist": "Target folder does not exist, {{targetFolder}}.",
"doesNotContainACapApp": "Target folder does not contain a Node.js CAP project."
},
"info":{
"existingMTAExtensionNotFound": "Cannot find a valid existing mta extension file, a new one will be created",
"existingDestinationNotFound": "A destination service resource cannot be found in the mta.yaml. An mta extension destination instance cannot be added",
"mtaExtensionCreated": "Created file: {{mtaExtFile}} to extend mta module {{appMtaId}} with destination configuration",
"mtaExtensionUpdated": "Updated file: {{mtaExtFile}} with module destination configuration"
"existingMTAExtensionNotFound": "Cannot find a valid existing mta extension file, a new one will be created.",
"existingDestinationNotFound": "A destination service resource cannot be found in the mta.yaml. An mta extension destination instance cannot be added.",
"mtaExtensionCreated": "Created file: {{mtaExtFile}} to extend mta module {{appMtaId}} with destination configuration.",
"mtaExtensionUpdated": "Updated file: {{mtaExtFile}} with module destination configuration."
}
}
6 changes: 3 additions & 3 deletions packages/cf-deploy-config-writer/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
type Authentication,
type Destinations
} from '@sap-ux/btp-utils';
import { addPackageDevDependency, type Manifest } from '@sap-ux/project-access';
import { addPackageDevDependency, FileName, type Manifest } from '@sap-ux/project-access';
import {
MTAVersion,
UI5BuilderWebIdePackage,
Expand Down Expand Up @@ -151,7 +151,7 @@ export function addGitIgnore(targetPath: string, fs: Editor): void {
* @param fs reference to a mem-fs editor
*/
export function addRootPackage({ mtaPath, mtaId }: MTABaseConfig, fs: Editor): void {
fs.copyTpl(getTemplatePath('package.json'), join(mtaPath, 'package.json'), {
fs.copyTpl(getTemplatePath('package.json'), join(mtaPath, FileName.Package), {
mtaId: mtaId
});
}
Expand Down Expand Up @@ -219,7 +219,7 @@ async function addStandaloneRouter(cfConfig: CFBaseConfig, mtaInstance: MtaConfi
await mtaInstance.addAbapService(abapServiceName, abapService);
}

fs.copyTpl(getTemplatePath(`router/package.json`), join(cfConfig.mtaPath, `${RouterModule}/package.json`));
fs.copyTpl(getTemplatePath(`router/package.json`), join(cfConfig.mtaPath, `${RouterModule}/${FileName.Package}`));

if (abapServiceName) {
let serviceKey;
Expand Down
4 changes: 2 additions & 2 deletions packages/cf-deploy-config-writer/test/unit/cap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('CF Writer', () => {
await expect(generateAppConfig({ appPath: capPath }, unitTestFs)).rejects.toThrowError(MTABinNotFound);
});

test('Validate dependency on CDS', async () => {
test('Validate error is thrown if cds fails', async () => {
spawnMock = jest.spyOn(childProcess, 'spawnSync').mockImplementation(() => ({ error: 1 } as any));
const capPath = join(outputDir, 'capcds');
fsExtra.mkdirSync(outputDir, { recursive: true });
Expand All @@ -101,7 +101,7 @@ describe('CF Writer', () => {
},
unitTestFs
)
).rejects.toThrowError(CDSBinNotFound);
).rejects.toThrowError(/Something went wrong creating mta.yaml!/);
expect(spawnMock).not.toHaveBeenCalledWith('');
});
});
Expand Down
53 changes: 50 additions & 3 deletions packages/cf-deploy-config-writer/test/unit/index-cap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ import hasbin from 'hasbin';
import { NullTransport, ToolsLogger } from '@sap-ux/logger';
import { generateCAPConfig, RouterModuleType } from '../../src';
import * as childProcess from 'child_process';
jest.mock('child_process');
import * as projectAccess from '@sap-ux/project-access';
import fs from 'fs';

jest.mock('child_process');
jest.mock('hasbin', () => ({
...(jest.requireActual('hasbin') as {}),
sync: jest.fn()
}));

let hasSyncMock: jest.SpyInstance;
let spawnMock: jest.SpyInstance;
const originalPlatform = process.platform;

jest.mock('child_process');

jest.mock('@sap/mta-lib', () => {
return {
Expand Down Expand Up @@ -44,11 +49,14 @@ describe('CF Writer CAP', () => {
sync: hasSyncMock
};
});
Object.defineProperty(process, 'platform', { value: 'win32' });
});

afterAll(() => {
jest.resetAllMocks();
// fsExtra.removeSync(outputDir);
Object.defineProperty(process, 'platform', {
value: originalPlatform
});
});

it.each([[RouterModuleType.Managed], [RouterModuleType.Standard]])(
Expand All @@ -58,13 +66,13 @@ describe('CF Writer CAP', () => {
const mtaPath = join(outputDir, routerType, mtaId);
fsExtra.mkdirSync(mtaPath, { recursive: true });
fsExtra.copySync(join(__dirname, `../sample/capcds`), mtaPath);
const getCapProjectTypeMock = jest.spyOn(projectAccess, 'getCapProjectType').mockResolvedValue('CAPNodejs');
// For testing purposes, an existing mta.yaml is copied to reflect the spawn command;
// `cds add mta xsuaa connectivity destination html5-repo`
spawnMock = jest.spyOn(childProcess, 'spawnSync').mockImplementation(() => {
fsExtra.copySync(join(__dirname, `fixtures/mta-types/cdsmta`), mtaPath);
return { status: 0 } as any;
});

const localFs = await generateCAPConfig(
{
mtaPath,
Expand All @@ -75,15 +83,54 @@ describe('CF Writer CAP', () => {
logger
);
expect(localFs.read(join(mtaPath, 'mta.yaml'))).toMatchSnapshot();
expect(getCapProjectTypeMock).toHaveBeenCalled();
expect(spawnMock.mock.calls).toHaveLength(2);
expect(spawnMock).toHaveBeenCalledWith(
'cds',
['add', 'mta', 'xsuaa', 'destination', 'html5-repo'],
expect.objectContaining({ cwd: expect.stringContaining(mtaId) })
);
expect(spawnMock.mock.calls[1][0]).toStrictEqual('npm.cmd'); // Just always test for windows!
expect(spawnMock.mock.calls[1][1]).toStrictEqual(['install', '--ignore-engines']);
if (RouterModuleType.Standard === routerType) {
expect(localFs.read(join(mtaPath, `router`, 'package.json'))).toMatchSnapshot();
expect(localFs.read(join(mtaPath, `router`, 'xs-app.json'))).toMatchSnapshot();
}
}
);

test('Validate CAP project type is correct when creating mta.yaml', async () => {
const mtaId = 'captestproject';
const mtaPath = join(outputDir, mtaId);
jest.spyOn(projectAccess, 'getCapProjectType').mockResolvedValue('CAPJava');
await expect(
generateCAPConfig(
{
mtaPath,
mtaId,
routerType: RouterModuleType.Managed
},
undefined,
logger
)
).rejects.toThrowError(/Target folder does not contain a Node.js CAP project./);
});

test('Validate CAP type if target contains mta.yaml', async () => {
const mtaId = 'captestproject';
const mtaPath = join(outputDir, mtaId);
jest.spyOn(projectAccess, 'getCapProjectType').mockResolvedValue('CAPNodejs');
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
await expect(
generateCAPConfig(
{
mtaPath,
mtaId,
routerType: RouterModuleType.Managed
},
undefined,
logger
)
).rejects.toThrowError(/An mta.yaml already exists in the target directory./);
});
});

0 comments on commit a002ed3

Please sign in to comment.