From 04ecab509f44fd69fdb1c76b545823bec973c5c6 Mon Sep 17 00:00:00 2001 From: denniswo Date: Mon, 10 Aug 2026 13:34:48 +0200 Subject: [PATCH 1/2] SP-1173: route --outputToJsonFile writes through CuiFileService The five report and export writers behind --outputToJsonFile now hand their payload to CuiFileService and log the filename it returns, since marking renames the artifact to "CUI - .zip". All five enclosing methods were already async, so no signatures change. The analyze action-flows spec stubbed axios.get wholesale, which swallowed the cover probe and returned an undefined status; it now mocks by URL so the cover endpoint keeps the default 204. Includes-AI-Code: true Co-authored-by: Cursor --- .../action-flows/action-flow/action-flow.service.ts | 11 +++++++---- .../data-pipeline/data-pool/data-pool-service.ts | 8 ++++---- src/commands/t2tc/t2tc-package.service.ts | 4 ++-- .../action-flows/analyze-action-flows.spec.ts | 9 ++++----- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/commands/action-flows/action-flow/action-flow.service.ts b/src/commands/action-flows/action-flow/action-flow.service.ts index e450734f..3da6dba8 100644 --- a/src/commands/action-flows/action-flow/action-flow.service.ts +++ b/src/commands/action-flows/action-flow/action-flow.service.ts @@ -5,6 +5,7 @@ import * as fs from "fs"; import { Context } from "../../../core/command/cli-context"; import { ActionFlowApi } from "./action-flow-api"; import { fileService, FileService } from "../../../core/utils/file-service"; +import { CuiFileService } from "../../../core/utils/cui-file-service"; import { logger } from "../../../core/utils/logger"; import { FileConstants } from "../../../core/utils/file.constants"; import { resolve } from "node:path"; @@ -13,9 +14,11 @@ export class ActionFlowService { public static readonly METADATA_FILE_NAME = "metadata.json"; private actionFlowApi: ActionFlowApi; + private readonly cuiFileService: CuiFileService; constructor(context: Context) { this.actionFlowApi = new ActionFlowApi(context); + this.cuiFileService = new CuiFileService(context); } public async exportActionFlows(packageId: string, metadataFilePath: string): Promise { @@ -43,8 +46,8 @@ export class ActionFlowService { if (outputToJsonFile) { const metadataFileName = "action-flows_metadata_" + uuidv4() + ".json"; - fileService.writeToFileWithGivenName(actionFlowsMetadataString, metadataFileName); - logger.info(FileService.fileDownloadedMessage + metadataFileName); + const writtenFilename = await this.cuiFileService.writeToFileWithGivenName(actionFlowsMetadataString, metadataFileName); + logger.info(FileService.fileDownloadedMessage + writtenFilename); } else { logger.info("Action flows analyze metadata: \n" + actionFlowsMetadataString); } @@ -57,8 +60,8 @@ export class ActionFlowService { if (outputToJsonFile) { const eventLogFileName = "action-flows_import_event_log_" + uuidv4() + ".json"; - fileService.writeToFileWithGivenName(eventLogString, eventLogFileName); - logger.info(FileService.fileDownloadedMessage + eventLogFileName); + const writtenFilename = await this.cuiFileService.writeToFileWithGivenName(eventLogString, eventLogFileName); + logger.info(FileService.fileDownloadedMessage + writtenFilename); } else { logger.info("Action flows import event log: \n" + eventLogString); } diff --git a/src/commands/data-pipeline/data-pool/data-pool-service.ts b/src/commands/data-pipeline/data-pool/data-pool-service.ts index a70c6404..5073d56f 100644 --- a/src/commands/data-pipeline/data-pool/data-pool-service.ts +++ b/src/commands/data-pipeline/data-pool/data-pool-service.ts @@ -23,8 +23,8 @@ export class DataPoolService { if (outputToJsonFile) { const reportFileName = "batch_import_report_" + uuidv4() + ".json"; - fileService.writeToFileWithGivenName(importReportString, reportFileName); - logger.info("Batch import report file: " + reportFileName); + const writtenFilename = await this.cuiFileService.writeToFileWithGivenName(importReportString, reportFileName); + logger.info("Batch import report file: " + writtenFilename); } else { logger.info("Batch import report: \n" + importReportString); } @@ -36,8 +36,8 @@ export class DataPoolService { if (outputToJsonFile) { const reportFileName = uuidv4() + "_data_pool_" + poolId + ".json"; - fileService.writeToFileWithGivenName(exportedDataPoolString, reportFileName); - logger.info(FileService.fileDownloadedMessage + reportFileName); + const writtenFilename = await this.cuiFileService.writeToFileWithGivenName(exportedDataPoolString, reportFileName); + logger.info(FileService.fileDownloadedMessage + writtenFilename); } else { logger.info("Exported Data Pool: \n" + exportedDataPoolString); } diff --git a/src/commands/t2tc/t2tc-package.service.ts b/src/commands/t2tc/t2tc-package.service.ts index 32de23fe..3cb96fb1 100644 --- a/src/commands/t2tc/t2tc-package.service.ts +++ b/src/commands/t2tc/t2tc-package.service.ts @@ -151,8 +151,8 @@ export class T2tcPackageService { fs.rmSync(sourceToBeImported); const reportFileName = "config_import_report_" + uuidv4() + ".json"; - fileService.writeToFileWithGivenName(JSON.stringify(postPackageImportData), reportFileName); - logger.info("Config import report file: " + reportFileName); + const writtenFilename = await this.cuiFileService.writeToFileWithGivenName(JSON.stringify(postPackageImportData), reportFileName); + logger.info("Config import report file: " + writtenFilename); } public async findAndExportListOfActivePackagesByVariableValue(flavors: string[], variableValue: string, variableType: string, includeBranches: boolean): Promise { diff --git a/tests/commands/action-flows/analyze-action-flows.spec.ts b/tests/commands/action-flows/analyze-action-flows.spec.ts index 42494709..b7764eba 100644 --- a/tests/commands/action-flows/analyze-action-flows.spec.ts +++ b/tests/commands/action-flows/analyze-action-flows.spec.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { mockedAxiosInstance } from "../../utls/http-requests-mock"; +import { mockAxiosGet, mockedAxiosInstance } from "../../utls/http-requests-mock"; import { loggingTestTransport } from "../../jest.setup"; import { FileService } from "../../../src/core/utils/file-service"; import { ActionFlowCommandService } from "../../../src/commands/action-flows/action-flow/action-flow-command.service"; @@ -9,6 +9,7 @@ import { getJsonFromDownloadedFile, getJsonFromFile } from "../../utls/fs-utils" describe("Analyze action-flows", () => { const packageId = "123-456-789"; + const analyzeUrl = `https://myTeam.celonis.cloud/ems-automation/api/root/${packageId}/export/assets/analyze`; const mockAnalyzeResponse = { "actionFlows": [ { @@ -44,8 +45,7 @@ describe("Analyze action-flows", () => { }; it("Should call import API and return non-json response", async () => { - const resp = { data: mockAnalyzeResponse }; - (mockedAxiosInstance.get as jest.Mock).mockResolvedValue(resp); + mockAxiosGet(analyzeUrl, mockAnalyzeResponse); await new ActionFlowCommandService(testContext).analyzeActionFlows(packageId, false); @@ -56,8 +56,7 @@ describe("Analyze action-flows", () => { }); it("Should call import API and return json response", async () => { - const resp = { data: mockAnalyzeResponse }; - (mockedAxiosInstance.get as jest.Mock).mockResolvedValue(resp); + mockAxiosGet(analyzeUrl, mockAnalyzeResponse); await new ActionFlowCommandService(testContext).analyzeActionFlows(packageId, true); From f9ab39c80c72f2bff89028fcd5a416b906120501 Mon Sep 17 00:00:00 2001 From: denniswo Date: Mon, 10 Aug 2026 13:34:56 +0200 Subject: [PATCH 2/2] SP-1173: cover CUI marking of --outputToJsonFile commands One case per write site, asserting the logged filename is a classified archive holding the payload plus the cover sheet. The two report writers log under their own prefix rather than the shared download message. Includes-AI-Code: true Co-authored-by: Cursor --- .../cui-marking-output-to-json-file.spec.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/commands/cui-marking-output-to-json-file.spec.ts diff --git a/tests/commands/cui-marking-output-to-json-file.spec.ts b/tests/commands/cui-marking-output-to-json-file.spec.ts new file mode 100644 index 00000000..0940a6fc --- /dev/null +++ b/tests/commands/cui-marking-output-to-json-file.spec.ts @@ -0,0 +1,106 @@ +import { resolve } from "node:path"; +import { readFileSync } from "node:fs"; +import AdmZip = require("adm-zip"); +import { mockAxiosGet, mockAxiosGetWithStatus, mockAxiosPost } from "../utls/http-requests-mock"; +import { testContext } from "../utls/test-context"; +import { loggingTestTransport } from "../jest.setup"; +import { FileService } from "../../src/core/utils/file-service"; +import { CuiFileService } from "../../src/core/utils/cui-file-service"; +import { ConfigUtils } from "../utls/config-utils"; +import { writeJsonTempFile, zipToTempFolder } from "../utls/fs-utils"; +import { ActionFlowCommandService } from "../../src/commands/action-flows/action-flow/action-flow-command.service"; +import { DataPoolCommandService } from "../../src/commands/data-pipeline/data-pool/data-pool-command.service"; +import { T2tcCommandService } from "../../src/commands/t2tc/t2tc-command.service"; +import { PackageManifestTransport } from "../../src/commands/configuration-management/interfaces/package-export.interfaces"; + +const COVER_URL = "https://myTeam.celonis.cloud/api/team/cui-settings/cui-pdf-cover"; +const PDF_BYTES = Buffer.from("%PDF-1.4 cover sheet"); + +const PACKAGE_ID = "123-456-789"; +const POOL_ID = "pool-1"; + +function markAsClassified(): void { + mockAxiosGetWithStatus(COVER_URL, 200, { + resolvedCuiMarking: { categories: [{ code: "PRVCY", name: "Privacy" }] }, + coverPage: { pdfContent: PDF_BYTES.toString("base64"), encoding: "base64" }, + }); +} + +function loggedFileName(prefix: string = FileService.fileDownloadedMessage): string { + const message = loggingTestTransport.logMessages.map(entry => entry.message).find(entry => entry.includes(prefix)); + return message.split(prefix)[1]; +} + +function payloadFromArchive(filename: string): any { + expect(filename.startsWith(CuiFileService.CLASSIFIED_PREFIX)).toBe(true); + expect(filename.endsWith(".zip")).toBe(true); + + const archive = new AdmZip(readFileSync(resolve(process.cwd(), filename))); + expect(archive.getEntry(CuiFileService.COVER_SHEET_FILE_NAME).getData().equals(PDF_BYTES)).toBe(true); + + const payloadEntry = archive.getEntries().map(entry => entry.entryName).find(entry => entry.endsWith(".json")); + return JSON.parse(archive.getEntry(payloadEntry).getData().toString()); +} + +function markedPayload(prefix?: string): any { + return payloadFromArchive(loggedFileName(prefix)); +} + +describe("CUI marking of --outputToJsonFile commands", () => { + + beforeEach(() => { + markAsClassified(); + }); + + it("Should mark the action flows analyze metadata", async () => { + const metadata = { actionFlows: [{ key: "987_asset_key", name: "Automation" }], connections: [] }; + mockAxiosGet(`https://myTeam.celonis.cloud/ems-automation/api/root/${PACKAGE_ID}/export/assets/analyze`, metadata); + + await new ActionFlowCommandService(testContext).analyzeActionFlows(PACKAGE_ID, true); + + expect(markedPayload()).toEqual(metadata); + }); + + it("Should mark the action flows import event log", async () => { + const eventLog = { status: "SUCCESS", eventLog: [{ status: "SUCCESS", assetType: "SCENARIO" }] }; + mockAxiosPost(`https://myTeam.celonis.cloud/ems-automation/api/root/${PACKAGE_ID}/import/assets`, eventLog); + + await new ActionFlowCommandService(testContext).importActionFlows(PACKAGE_ID, zipToTempFolder(new AdmZip()), true, true); + + expect(markedPayload()).toEqual(eventLog); + }); + + it("Should mark the exported data pool", async () => { + const dataPool = { id: POOL_ID, name: "Pool 1", objects: [] }; + mockAxiosGet(`https://myTeam.celonis.cloud/integration/api/pools/${POOL_ID}/v2/export`, dataPool); + + await new DataPoolCommandService(testContext).exportDataPool(POOL_ID, true); + + expect(markedPayload()).toEqual(dataPool); + }); + + it("Should mark the data pool batch import report", async () => { + const report = { installedVersions: [{ poolId: POOL_ID, version: "1.0.0" }] }; + mockAxiosPost("https://myTeam.celonis.cloud/integration/api/pool/batch-import", report); + + const requestFile = "data-pool-batch-import-request.json"; + writeJsonTempFile(requestFile, { pools: [{ id: POOL_ID }] }); + + await new DataPoolCommandService(testContext).batchImportDataPools(requestFile, true); + + expect(markedPayload("Batch import report file: ")).toEqual(report); + }); + + it("Should mark the t2tc package import report", async () => { + const manifest: PackageManifestTransport[] = [ConfigUtils.buildManifestForKeyAndFlavor("key-1", "TEST")]; + const zipPath = zipToTempFolder(ConfigUtils.buildBatchExportZip(manifest, [])); + + const importReport = [{ packageKey: "key-1", importedVersions: [{ oldVersion: "1.0.2", newVersion: "1.0.0" }] }]; + mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages", []); + mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importReport); + + await new T2tcCommandService(testContext).batchImportPackages(zipPath, null, true, null); + + expect(markedPayload("Config import report file: ")).toEqual(importReport); + }); +});