diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index cc0bf3d..114dce4 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -15,6 +15,9 @@ - [List all packages in Studio](#list-all-packages-in-studio) - [List assignments](#list-assignments) - [Asset options for Analysis](#asset-options-for-analysis) + - [Action Flows commands](#action-flows-commands) + - [Analyze Action Flows](#analyze-action-flows) + - [Export Action Flows](#export-action-flows) - [Data Pool export / import commands](#data-pool-export--import-commands) - [Export Data Pool](#export-data-pool) - [Batch Import multiple Data Pools](#batch-import-multiple-data-pools) @@ -459,6 +462,36 @@ the same command as with pushing other assets to Studio: content-cli push bookmarks -p my-profile-name --id 73d39112-73ae-4bbe-8051-3c0f14e065ec --file studio_analysis_bookmarks_39c5bb7b-b486-4230-ab01-854a17ddbff2.json ``` +### Action Flows commands + +#### Analyze Action Flows + +The analyze operation returns the metadata of Action Flows for one package together with their dependencies. Dependencies +could be webhooks, data structures, variables and other. + +In order to analyze Action Flows you can execute the following command: + +``` +content-cli analyze action-flows -p my-profile-name --packageId --outputToJsonFile +``` + +The ```--outputToJsonFile``` is optional. If specified, the analyze result is saved in a JSON file (```metadata.json```). The +command output will give you all the details. + +#### Export Action Flows + +The export operation allows export of Action Flows of one package together with their dependencies. Dependencies +could be webhooks, data structures, variables and other. + +In order to pull Action Flows you can execute the following command: + +``` +content-cli export action-flows -p my-profile-name --packageId -f +``` + +_Note_: The ```-f``` is optional. If specified, it will attach the metadata file to the exported zip. +This file is expected to be received by the ```action-flows analyze``` command, and manually be populated with the mappings source to target package. + ### Data Pool export / import commands #### Export Data Pool diff --git a/package.json b/package.json index a3a5976..716595e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@celonis/content-cli", - "version": "0.11.0", + "version": "0.11.1", "description": "CLI Tool to help manage content in Celonis EMS", "main": "content-cli.js", "bin": { diff --git a/src/api/action-flow-api.ts b/src/api/action-flow-api.ts new file mode 100644 index 0000000..ad60200 --- /dev/null +++ b/src/api/action-flow-api.ts @@ -0,0 +1,31 @@ +import { httpClientV2 } from "../services/http-client-service.v2"; +import { FatalError } from "../util/logger"; +import * as FormData from "form-data"; + +class ActionFlowApi { + public static readonly INSTANCE = new ActionFlowApi(); + + public async exportRawAssets(packageId: string): Promise { + return httpClientV2.getFile(`/ems-automation/api/root/${packageId}/export/assets`).catch(e => { + throw new FatalError(`Problem getting Action Flow assets: ${e}`); + }); + } + + public async analyzeAssets(packageId: string): Promise { + return httpClientV2.get(`/ems-automation/api/root/${packageId}/export/assets/analyze`).catch(e => { + throw new FatalError(`Problem analyzing Action Flow assets: ${e}`); + }); + } + + public async importAssets(packageId: string, data: FormData, dryRun: boolean): Promise { + const params = { + dryRun: dryRun, + }; + + return httpClientV2.postFile(`/ems-automation/api/root/${packageId}/import/assets`, data, params).catch(e => { + throw new FatalError(`Problem importing Action Flow assets: ${e}`); + }); + } +} + +export const actionFlowApi = ActionFlowApi.INSTANCE; \ No newline at end of file diff --git a/src/commands/action-flow.command.ts b/src/commands/action-flow.command.ts new file mode 100644 index 0000000..6d0b431 --- /dev/null +++ b/src/commands/action-flow.command.ts @@ -0,0 +1,16 @@ +import { actionFlowService } from "../services/action-flow/action-flow-service"; + +export class ActionFlowCommand { + + public async exportActionFlows(packageId: string, metadataFile: string): Promise { + await actionFlowService.exportActionFlows(packageId, metadataFile); + } + + public async analyzeActionFlows(packageId: string, outputToJsonFile: boolean): Promise { + await actionFlowService.analyzeActionFlows(packageId, outputToJsonFile); + } + + public async importActionFlows(packageId: string, filePath: string, dryRun: boolean, outputToJsonFile: boolean): Promise { + await actionFlowService.importActionFlows(packageId, filePath, dryRun, outputToJsonFile); + } +} \ No newline at end of file diff --git a/src/content-cli-analyze.ts b/src/content-cli-analyze.ts new file mode 100644 index 0000000..ccbf822 --- /dev/null +++ b/src/content-cli-analyze.ts @@ -0,0 +1,43 @@ +import { ContextInitializer } from "./util/context-initializer"; +import { logger } from "./util/logger"; +import { ActionFlowCommand } from "./commands/action-flow.command"; +import commander = require("commander"); + +type CommanderStatic = commander.CommanderStatic; + +class Analyze { + public static actionFlows(program: CommanderStatic): CommanderStatic { + program + .command("action-flows") + .description("Analyze Action Flows dependencies for a certain package") + .option("-p, --profile ", "Profile which you want to use to analyze Action Flows") + .requiredOption("--packageId ", "ID of the package from which you want to export Action Flows") + .option("-o, --outputToJsonFile", "Output the analyze result in a JSON file") + .action(async cmd => { + await new ActionFlowCommand().analyzeActionFlows(cmd.packageId, cmd.outputToJsonFile); + process.exit(); + }); + return program; + } +} + +const loadCommands = () => { + getAllCommands(); +}; + +ContextInitializer.initContext() + .then(loadCommands, loadCommands) + .catch(e => { + logger.error(e); + }); + +if (!process.argv.slice(2).length) { + commander.outputHelp(); + process.exit(1); +} + +function getAllCommands(): void { + Analyze.actionFlows(commander); + + commander.parse(process.argv); +} diff --git a/src/content-cli-export.ts b/src/content-cli-export.ts index d6b6c42..d0032b2 100644 --- a/src/content-cli-export.ts +++ b/src/content-cli-export.ts @@ -4,6 +4,7 @@ import { PackageCommand } from "./commands/package.command"; import { logger } from "./util/logger"; import { DataPoolCommand } from "./commands/data-pool.command"; import { ContextInitializer } from "./util/context-initializer"; +import { ActionFlowCommand } from "./commands/action-flow.command"; export class Export { public static packages(program: CommanderStatic): CommanderStatic { @@ -36,6 +37,21 @@ export class Export { return program; } + + public static actionFlows(program: CommanderStatic): CommanderStatic { + program + .command("action-flows") + .description("Command to export all Action Flows in a package with their objects and dependencies") + .option("-p, --profile ", "Profile which you want to use to export Action Flows") + .requiredOption("--packageId ", "ID of the package from which you want to export Action Flows") + .option("-f, --file ", "Action flows metadata file (relative path)") + .action(async cmd => { + await new ActionFlowCommand().exportActionFlows(cmd.packageId, cmd.file); + process.exit(); + }); + + return program; + } } const loadCommands = () => { @@ -56,6 +72,7 @@ if (!process.argv.slice(2).length) { function getAllCommands(): void { Export.packages(commander); Export.dataPool(commander); + Export.actionFlows(commander); commander.parse(process.argv); } diff --git a/src/content-cli-import.ts b/src/content-cli-import.ts index 5c326eb..df66889 100644 --- a/src/content-cli-import.ts +++ b/src/content-cli-import.ts @@ -4,6 +4,7 @@ import { PackageCommand } from "./commands/package.command"; import { DataPoolCommand } from "./commands/data-pool.command"; import { ContextInitializer } from "./util/context-initializer"; import { logger } from "./util/logger"; +import { ActionFlowCommand } from "./commands/action-flow.command"; export class Import { public static packages(program: CommanderStatic): CommanderStatic { @@ -41,6 +42,23 @@ export class Import { return program; } + + public static actionFlows(program: CommanderStatic): CommanderStatic { + program + .command("action-flows") + .description("Command to import all Action Flows in a package with their objects and dependencies") + .option("-p, --profile ", "Profile which you want to use to import Action Flows") + .requiredOption("--packageId ", "ID of the package to which you want to export Action Flows") + .requiredOption("-f, --file ", "Exported Action Flows file (relative path)") + .requiredOption("-d, --dryRun ", "Execute the import on dry run mode") + .option("-o, --outputToJsonFile", "Output the import result in a JSON file") + .action(async cmd => { + await new ActionFlowCommand().importActionFlows(cmd.packageId, cmd.file, cmd.dryRun, cmd.outputToJsonFile); + process.exit(); + }); + + return program; + } } const loadCommands = () => { @@ -61,6 +79,7 @@ if (!process.argv.slice(2).length) { function getAllCommands(): void { Import.packages(commander); Import.dataPools(commander); + Import.actionFlows(commander); commander.parse(process.argv); } diff --git a/src/content-cli.ts b/src/content-cli.ts index 21fa9e6..e83d272 100644 --- a/src/content-cli.ts +++ b/src/content-cli.ts @@ -18,7 +18,7 @@ program.command("profile", "Commands related to profiles."); program.command("pull", "Commands to pull content."); program.command("export", "Commands to export content.") -program.command("import", "Commands to export content.") +program.command("import", "Commands to import content.") program.command("push", "Commands to push content."); @@ -32,6 +32,8 @@ program.command("set", "Commands to set configuration properties."); program.command("config", "Commands related to config management.") +program.command("analyze", "Commands to analyze assets dependencies."); + program.version(VersionUtils.getCurrentCliVersion()); program.parse(process.argv); diff --git a/src/services/action-flow/action-flow-service.ts b/src/services/action-flow/action-flow-service.ts new file mode 100644 index 0000000..c3e1c16 --- /dev/null +++ b/src/services/action-flow/action-flow-service.ts @@ -0,0 +1,75 @@ +import { logger } from "../../util/logger"; +import { v4 as uuidv4 } from "uuid"; +import { FileService, fileService } from "../file-service"; +import { actionFlowApi } from "../../api/action-flow-api"; +import * as AdmZip from "adm-zip"; +import * as FormData from "form-data"; +import * as fs from "fs"; + +class ActionFlowService { + public static readonly METADATA_FILE_NAME = "metadata.json"; + + public async exportActionFlows(packageId: string, metadataFilePath: string): Promise { + const exportedActionFlowsData = await actionFlowApi.exportRawAssets(packageId); + const tmpZip: AdmZip = new AdmZip(exportedActionFlowsData); + + const zip = new AdmZip(); + tmpZip.getEntries().forEach(entry => { + zip.addFile(entry.entryName, entry.getData()); + }); + + if (metadataFilePath) { + this.attachMetadataFile(metadataFilePath, zip); + } + + const fileName = "action-flows_export_" + uuidv4() + ".zip"; + zip.writeZip(fileName); + logger.info(FileService.fileDownloadedMessage + fileName); + } + + public async analyzeActionFlows(packageId: string, outputToJsonFile: boolean): Promise { + const actionFlowsMetadata = await actionFlowApi.analyzeAssets(packageId); + const actionFlowsMetadataString = JSON.stringify(actionFlowsMetadata, null, 4); + + if (outputToJsonFile) { + const metadataFileName = "action-flows_metadata_" + uuidv4() + ".json"; + fileService.writeToFileWithGivenName(actionFlowsMetadataString, metadataFileName); + logger.info(FileService.fileDownloadedMessage + metadataFileName); + } else { + logger.info("Action flows analyze metadata: \n" + actionFlowsMetadataString); + } + } + + public async importActionFlows(packageId: string, filePath: string, dryRun: boolean, outputToJsonFile: boolean): Promise { + const actionFlowsZip = this.createBodyForImport(filePath); + const eventLog = await actionFlowApi.importAssets(packageId, actionFlowsZip, dryRun); + const eventLogString = JSON.stringify(eventLog, null, 4); + + if (outputToJsonFile) { + const eventLogFileName = "action-flows_import_event_log_" + uuidv4() + ".json"; + fileService.writeToFileWithGivenName(eventLogString, eventLogFileName); + logger.info(FileService.fileDownloadedMessage + eventLogFileName); + } else { + logger.info("Action flows import event log: \n" + eventLogString); + } + } + + private createBodyForImport(fileName: string): FormData { + fileName = fileName + (fileName.endsWith(".zip") ? "" : ".zip"); + + const formData = new FormData(); + formData.append("file", fs.createReadStream(fileName, { encoding: null }), { filename: fileName }); + + return formData; + } + + private attachMetadataFile(fileName: string, zip: AdmZip): void { + fileName = fileName + (fileName.endsWith(".json") ? "" : ".json"); + const metadata = fileService.readFile(fileName); + + zip.addFile(ActionFlowService.METADATA_FILE_NAME, Buffer.from(metadata)); + } +} + +export const actionFlowService = new ActionFlowService(); +export const metadataFileName = ActionFlowService.METADATA_FILE_NAME; \ No newline at end of file diff --git a/tests/analyze/action-flows.spec.ts b/tests/analyze/action-flows.spec.ts new file mode 100644 index 0000000..8cb1f7a --- /dev/null +++ b/tests/analyze/action-flows.spec.ts @@ -0,0 +1,69 @@ +import { FileService } from "../../src/services/file-service"; +import * as path from "path"; +import { mockWriteFileSync, testTransport } from "../jest.setup"; +import { mockedAxiosInstance } from "../utls/http-requests-mock"; +import { ActionFlowCommand } from "../../src/commands/action-flow.command"; + +describe("Analyze action-flows", () => { + + const packageId = "123-456-789"; + const mockAnalyzeResponse = { + "actionFlows": [ + { + "key": "987_asset_key", + "rootNodeKey": "123_root_key_node", + "parentNodeKey": "555_parent_node_key", + "name": "T2T - simple package Automation", + "scenarioId": "321", + "webHookUrl": null, + "version": "10", + "sensorType": null, + "schedule": { + "type": "indefinitely", + "interval": 900, + }, + "teamSpecific": { + "connections": [], + "variables": [], + "celonisApps": [], + "callingOtherAf": [], + "datastructures": [], + }, + }, + ], + "connections": [], + "dataPools": [], + "dataModels": [], + "skills": [], + "analyses": [], + "datastructures": [], + "mappings": [], + "actionFlowsTeamId": "1234", + }; + + it("Should call import API and return non-json response", async () => { + const resp = { data: mockAnalyzeResponse }; + (mockedAxiosInstance.get as jest.Mock).mockResolvedValue(resp); + + await new ActionFlowCommand().analyzeActionFlows(packageId, false); + + expect(testTransport.logMessages.length).toBe(1); + expect(testTransport.logMessages[0].message).toContain(JSON.stringify(mockAnalyzeResponse, null, 4)); + + expect(mockedAxiosInstance.get).toHaveBeenCalledWith(`https://myTeam.celonis.cloud/ems-automation/api/root/${packageId}/export/assets/analyze`, expect.anything()); + }); + + it("Should call import API and return json response", async () => { + const resp = { data: mockAnalyzeResponse }; + (mockedAxiosInstance.get as jest.Mock).mockResolvedValue(resp); + + await new ActionFlowCommand().analyzeActionFlows(packageId, true); + + expect(testTransport.logMessages.length).toBe(1); + expect(testTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); + const expectedFileName = testTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(mockAnalyzeResponse, null, 4), { encoding: "utf-8" }); + expect(mockedAxiosInstance.get).toHaveBeenCalledWith(`https://myTeam.celonis.cloud/ems-automation/api/root/${packageId}/export/assets/analyze`, expect.anything()); + }); +}); \ No newline at end of file diff --git a/tests/export/action-flows.spec.ts b/tests/export/action-flows.spec.ts new file mode 100644 index 0000000..f050391 --- /dev/null +++ b/tests/export/action-flows.spec.ts @@ -0,0 +1,181 @@ +import { FileService } from "../../src/services/file-service"; +import { mockWriteSync, testTransport } from "../jest.setup"; +import { mockAxiosGet } from "../utls/http-requests-mock"; +import { ActionFlowCommand } from "../../src/commands/action-flow.command"; +import * as AdmZip from "adm-zip"; +import { mockExistsSyncOnce, mockReadFileSync } from "../utls/fs-mock-utils"; +import { stringify } from "../../src/util/yaml"; +import * as fs from "fs"; +import { parse } from "yaml"; +import { metadataFileName } from "../../src/services/action-flow/action-flow-service"; + +describe("Export action-flows", () => { + + const packageId = "123-456-789"; + const actionFlowFileName = "20240711-scenario-1234.json"; + const actionFlowConfig = { + "name": "T2T - simple package Automation", + "flow": [ + { + "id": 6, + "module": "util:FunctionSleep", + "version": 1, + "parameters": {}, + "metadata": { + "expect": [ + { + "name": "duration", + "type": "uinteger", + "label": "Delay", + "required": true, + "validate": { + "max": 300, + "min": 1, + }, + }, + ], + "restore": {}, + "designer": { + "x": 300, + "y": 0, + }, + }, + "mapper": { + "duration": "1", + }, + }, + ], + "metadata": { + "instant": false, + "version": 1, + "designer": { + "orphans": [], + }, + "scenario": { + "dlq": false, + "dataloss": false, + "maxErrors": 3, + "autoCommit": true, + "roundtrips": 1, + "sequential": false, + "confidential": false, + "freshVariables": false, + "autoCommitTriggerLast": true, + }, + }, + "io": { + "output_spec": [], + "input_spec": [], + }, + }; + + beforeEach(() => { + (fs.openSync as jest.Mock).mockReturnValue(100); + }); + + it("Should call export API and return the zip response", async () => { + const zipExport = new AdmZip(); + zipExport.addFile(actionFlowFileName, Buffer.from(stringify(actionFlowConfig))); + + mockAxiosGet(`https://myTeam.celonis.cloud/ems-automation/api/root/${packageId}/export/assets`, zipExport.toBuffer()); + + await new ActionFlowCommand().exportActionFlows(packageId, null); + + expect(testTransport.logMessages.length).toBe(1); + const expectedZipFileName = testTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + expect(fs.openSync).toHaveBeenCalledWith(expectedZipFileName, expect.anything(), expect.anything()); + expect(mockWriteSync).toHaveBeenCalled(); + + const fileBuffer = mockWriteSync.mock.calls[0][1]; + const receivedZip = new AdmZip(fileBuffer); + + expect(receivedZip.getEntries().length).toBe(1); + const receivedZipEntry = receivedZip.getEntries()[0]; + const receivedActionFlowConfig = parse(receivedZipEntry.getData().toString()); + expect(receivedZipEntry.name).toEqual(actionFlowFileName); + expect(receivedActionFlowConfig).toEqual(actionFlowConfig); + }); + + it("Should call export API and attach the provided file to the zip response", async () => { + const metadata = { + "actionFlows": [ + { + "key": "123_asset_key", + "rootNodeKey": "543_root_key", + "parentNodeKey": "9099_parent_key", + "name": "T2T - simple package Automation", + "scenarioId": "1234", + "webHookUrl": null, + "version": "10", + "sensorType": null, + "schedule": { + "type": "indefinitely", + "interval": 900, + }, + "teamSpecific": { + "connections": [], + "variables": [], + "celonisApps": [], + "callingOtherAf": [], + "datastructures": [], + }, + }, + ], + "connections": [], + "dataPools": [], + "dataModels": [], + "skills": [], + "analyses": [], + "datastructures": [], + "mappings": [], + "actionFlowsTeamId": "555", + }; + + mockExistsSyncOnce(); + mockReadFileSync(stringify(metadata)); + const zipExport = new AdmZip(); + zipExport.addFile(actionFlowFileName, Buffer.from(stringify(actionFlowConfig))); + + mockAxiosGet(`https://myTeam.celonis.cloud/ems-automation/api/root/${packageId}/export/assets`, zipExport.toBuffer()); + await new ActionFlowCommand().exportActionFlows(packageId, metadataFileName); + + expect(testTransport.logMessages.length).toBe(1); + const expectedZipFileName = testTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + expect(fs.openSync).toHaveBeenCalledWith(expectedZipFileName, expect.anything(), expect.anything()); + expect(mockWriteSync).toHaveBeenCalled(); + + const fileBuffer = mockWriteSync.mock.calls[0][1]; + const receivedZip = new AdmZip(fileBuffer); + + expect(receivedZip.getEntries().length).toBe(2); + expect(receivedZip.getEntries().filter(entry => entry.name === metadataFileName).length).toBe(1); + + const receivedMetadataZipEntry = receivedZip.getEntries().filter(entry => entry.name === metadataFileName)[0]; + const receivedActionFlowZipEntry = receivedZip.getEntries().filter(entry => entry.name !== metadataFileName)[0]; + const receivedMetadataFile = parse(receivedMetadataZipEntry.getData().toString()); + const receivedActionFlowFile = parse(receivedActionFlowZipEntry.getData().toString()); + + expect(receivedActionFlowZipEntry.name).toEqual(actionFlowFileName); + expect(receivedActionFlowFile).toEqual(actionFlowConfig); + expect(receivedMetadataFile).toEqual(metadata); + }); + + it("Should throw error if metadata files does not exist", async () => { + const zipExport = new AdmZip(); + zipExport.addFile(actionFlowFileName, Buffer.from(stringify(actionFlowConfig))); + + mockAxiosGet(`https://myTeam.celonis.cloud/ems-automation/api/root/${packageId}/export/assets`, zipExport.toBuffer()); + + const error = new Error("mock exit"); + jest.spyOn(process, "exit").mockImplementation(() => { + throw error; + }); + + try { + await new ActionFlowCommand().exportActionFlows(packageId, metadataFileName); + } catch (e) { + expect(e).toBe(error); + expect(process.exit).toHaveBeenCalledWith(1); + } + }); +}); \ No newline at end of file diff --git a/tests/import/action-flows.spec.ts b/tests/import/action-flows.spec.ts new file mode 100644 index 0000000..3dea247 --- /dev/null +++ b/tests/import/action-flows.spec.ts @@ -0,0 +1,55 @@ +import { FileService } from "../../src/services/file-service"; +import * as path from "path"; +import { mockWriteFileSync, testTransport } from "../jest.setup"; +import { mockedAxiosInstance } from "../utls/http-requests-mock"; +import { ActionFlowCommand } from "../../src/commands/action-flow.command"; +import * as AdmZip from "adm-zip"; +import { mockCreateReadStream } from "../utls/fs-mock-utils"; + +describe("Import action-flows", () => { + + const packageId = "123-456-789"; + const mockImportResponse = { + "status": "SUCCESS", + "eventLog": [ + { + "status": "SUCCESS", + "assetType": "SCENARIO", + "assetId": "asset-id-automation", + "eventType": "IMPORT", + "log": "updated action flow with key [asset-id-automation]", + "mapping": null, + }, + ], + }; + + it("Should call import API and return non-json response", async () => { + const resp = { data: mockImportResponse }; + (mockedAxiosInstance.post as jest.Mock).mockResolvedValue(resp); + const zip = new AdmZip(); + mockCreateReadStream(zip.toBuffer()); + + await new ActionFlowCommand().importActionFlows(packageId, "tmp", true, false); + + expect(testTransport.logMessages.length).toBe(1); + expect(testTransport.logMessages[0].message).toContain(JSON.stringify(mockImportResponse, null, 4)); + + expect(mockedAxiosInstance.post).toHaveBeenCalledWith(`https://myTeam.celonis.cloud/ems-automation/api/root/${packageId}/import/assets`, expect.anything(), expect.anything()); + }); + + it("Should call import API and return json response", async () => { + const resp = { data: mockImportResponse }; + (mockedAxiosInstance.post as jest.Mock).mockResolvedValue(resp); + const zip = new AdmZip(); + mockCreateReadStream(zip.toBuffer()); + + await new ActionFlowCommand().importActionFlows(packageId, "tmp", true, true); + + expect(testTransport.logMessages.length).toBe(1); + expect(testTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); + const expectedFileName = testTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(mockImportResponse, null, 4), { encoding: "utf-8" }); + expect(mockedAxiosInstance.post).toHaveBeenCalledWith(`https://myTeam.celonis.cloud/ems-automation/api/root/${packageId}/import/assets`, expect.anything(), expect.anything()); + }); +}); \ No newline at end of file diff --git a/tests/utls/fs-mock-utils.ts b/tests/utls/fs-mock-utils.ts index add8872..2c01cea 100644 --- a/tests/utls/fs-mock-utils.ts +++ b/tests/utls/fs-mock-utils.ts @@ -14,4 +14,8 @@ export function mockCreateReadStream(data: any): void { export function mockExistsSync(): void { (fs.existsSync as jest.Mock).mockReturnValue(true); +} + +export function mockExistsSyncOnce(): void { + (fs.existsSync as jest.Mock).mockReturnValueOnce(true); } \ No newline at end of file