From 23ba938e409634c697d97d2e76006f7478caeebf Mon Sep 17 00:00:00 2001 From: denniswo Date: Thu, 13 Aug 2026 12:10:18 +0200 Subject: [PATCH 1/2] SP-1173: say what to do when CUI marking cannot be resolved The failure surfaced as "Problem fetching cui pdf cover", which named neither the cause nor the consequence. All three CUI failures now share one message that says marking could not be resolved and no file was written, with the specifics kept as debug logs. Also pins the shape production takes: axios resolves the 403 rather than rejecting, because getStatusAndData disables the status check, and no test covered that branch directly. Includes-AI-Code: true Co-authored-by: Cursor --- src/core/utils/cui-api.ts | 5 ++++- src/core/utils/cui-file-service.ts | 10 ++++++---- tests/core/utils/cui-file-service.spec.ts | 20 +++++++++++++++----- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/core/utils/cui-api.ts b/src/core/utils/cui-api.ts index 9011c568..f8f8af2c 100644 --- a/src/core/utils/cui-api.ts +++ b/src/core/utils/cui-api.ts @@ -2,6 +2,8 @@ import { HttpClient } from "../http/http-client"; import { FatalError, logger } from "./logger"; import { Context } from "../command/cli-context"; +export const CUI_MARKING_FAILED_MESSAGE = "Could not resolve CUI marking. No file was written"; + export interface CuiPdfCoverResponse { coverPage?: { pdfContent: string; encoding: string }; } @@ -39,6 +41,7 @@ export class CuiApi { return { marking: CuiMarking.CLASSIFIED, cover: data as CuiPdfCoverResponse }; } - throw new FatalError("Problem fetching cui pdf cover"); + logger.debug(`The CUI cover call answered with status ${status}`); + throw new FatalError(CUI_MARKING_FAILED_MESSAGE); } } diff --git a/src/core/utils/cui-file-service.ts b/src/core/utils/cui-file-service.ts index 5b069ccf..84ec3f04 100644 --- a/src/core/utils/cui-file-service.ts +++ b/src/core/utils/cui-file-service.ts @@ -1,10 +1,10 @@ import * as path from "node:path"; import AdmZip = require("adm-zip"); import { Context } from "../command/cli-context"; -import { CuiApi, CuiMarking, CuiPdfCoverResponse } from "./cui-api"; +import { CUI_MARKING_FAILED_MESSAGE, CuiApi, CuiMarking, CuiPdfCoverResponse } from "./cui-api"; import { fileService } from "./file-service"; import { FileConstants } from "./file.constants"; -import { FatalError } from "./logger"; +import { FatalError, logger } from "./logger"; export class CuiFileService { public static readonly COVER_SHEET_FILE_NAME = "CUI_Cover_Sheet.pdf"; @@ -96,10 +96,12 @@ export class CuiFileService { private decodeCoverPage(cover: CuiPdfCoverResponse): Buffer { const coverPage = cover.coverPage; if (!coverPage?.pdfContent) { - throw new FatalError("CUI marking applies but the response contained no cover page."); + logger.debug("CUI marking applies but the response contained no cover page"); + throw new FatalError(CUI_MARKING_FAILED_MESSAGE); } if (coverPage.encoding !== CuiFileService.BASE64_ENCODING) { - throw new FatalError(`Unsupported CUI cover page encoding: ${coverPage.encoding}`); + logger.debug(`Unsupported CUI cover page encoding: ${coverPage.encoding}`); + throw new FatalError(CUI_MARKING_FAILED_MESSAGE); } return Buffer.from(coverPage.pdfContent, CuiFileService.BASE64_ENCODING); diff --git a/tests/core/utils/cui-file-service.spec.ts b/tests/core/utils/cui-file-service.spec.ts index 826b9925..2aa99586 100644 --- a/tests/core/utils/cui-file-service.spec.ts +++ b/tests/core/utils/cui-file-service.spec.ts @@ -35,6 +35,15 @@ describe("CuiFileService", () => { expect(filename).toEqual("report.json"); expect(readFile("report.json").toString()).toEqual(PAYLOAD); }); + + it("Should keep the original filename when axios resolves the 403 instead of rejecting", async () => { + mockAxiosGetWithStatus(COVER_URL, 403, ""); + + const filename = await cuiFileService.writeToFileWithGivenName(PAYLOAD, "resolved-403.json"); + + expect(filename).toEqual("resolved-403.json"); + expect(readFile("resolved-403.json").toString()).toEqual(PAYLOAD); + }); }); describe("when the cover response cannot be used", () => { @@ -55,7 +64,8 @@ describe("CuiFileService", () => { it("Should fail when the backend answers with no content", async () => { mockAxiosGetWithStatus(COVER_URL, 204, ""); - await expect(cuiFileService.writeToFileWithGivenName(PAYLOAD, "no-content.json")).rejects.toThrow(FatalError); + await expect(cuiFileService.writeToFileWithGivenName(PAYLOAD, "no-content.json")) + .rejects.toThrow("Could not resolve CUI marking. No file was written"); expect(() => accessSync(resolve(process.cwd(), "no-content.json"))).toThrow(); }); }); @@ -81,14 +91,14 @@ describe("CuiFileService", () => { mockAxiosGetWithStatus(COVER_URL, 200, response); await expect(cuiFileService.writeToFileWithGivenName(PAYLOAD, "packages.json")) - .rejects.toThrow("Unsupported CUI cover page encoding: hex"); + .rejects.toThrow("Could not resolve CUI marking. No file was written"); }); it("Should fail when the marking applies but no cover page was returned", async () => { mockAxiosGetWithStatus(COVER_URL, 200, { teamId: "team-1" }); await expect(cuiFileService.writeToFileWithGivenName(PAYLOAD, "packages.json")) - .rejects.toThrow("CUI marking applies but the response contained no cover page."); + .rejects.toThrow("Could not resolve CUI marking. No file was written"); }); }); @@ -134,7 +144,7 @@ describe("CuiFileService", () => { mockAxiosGetWithStatus(COVER_URL, 200, { teamId: "team-1" }); await expect(cuiFileService.writeZipToFileWithGivenName(buildExportZip(), "export.zip")) - .rejects.toThrow("CUI marking applies but the response contained no cover page."); + .rejects.toThrow("Could not resolve CUI marking. No file was written"); }); }); @@ -174,7 +184,7 @@ describe("CuiFileService", () => { mockAxiosGetWithStatus(COVER_URL, 200, { teamId: "team-1" }); await expect(cuiFileService.writeDirectoryWithGivenName(writeTree, "broken-export")) - .rejects.toThrow("CUI marking applies but the response contained no cover page."); + .rejects.toThrow("Could not resolve CUI marking. No file was written"); expect(exists("broken-export")).toBe(false); expect(exists("CUI - broken-export")).toBe(false); }); From 52496d856a03aba2f65252fa3e516bd35962ed9d Mon Sep 17 00:00:00 2001 From: denniswo Date: Thu, 13 Aug 2026 15:52:29 +0200 Subject: [PATCH 2/2] SP-1173: assert the CUI marking decision at the API level The three outcomes of getCuiMarking were only reachable through CuiFileService, so nothing pinned the decision itself and the new message and status log went uncovered. Includes-AI-Code: true Co-authored-by: Cursor --- tests/core/utils/cui-api.spec.ts | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/core/utils/cui-api.spec.ts diff --git a/tests/core/utils/cui-api.spec.ts b/tests/core/utils/cui-api.spec.ts new file mode 100644 index 00000000..320732de --- /dev/null +++ b/tests/core/utils/cui-api.spec.ts @@ -0,0 +1,50 @@ +import { CUI_MARKING_FAILED_MESSAGE, CuiApi, CuiMarking } from "../../../src/core/utils/cui-api"; +import { FatalError } from "../../../src/core/utils/logger"; +import { testContext } from "../../utls/test-context"; +import { mockAxiosGetWithStatus } from "../../utls/http-requests-mock"; + +describe("CuiApi", () => { + const COVER_URL = "https://myTeam.celonis.cloud/api/team/cui-settings/cui-pdf-cover"; + + const coverResponse = () => ({ + coverPage: { + pdfContent: Buffer.from("%PDF-1.4 cover sheet").toString("base64"), + encoding: "base64", + }, + }); + + let cuiApi: CuiApi; + + beforeEach(() => { + cuiApi = new CuiApi(testContext); + }); + + it("Should report the content as classified and pass the cover response on", async () => { + const cover = coverResponse(); + mockAxiosGetWithStatus(COVER_URL, 200, cover); + + await expect(cuiApi.getCuiMarking()).resolves.toEqual({ + marking: CuiMarking.CLASSIFIED, + cover, + }); + }); + + it("Should report that marking does not apply when the feature flag is disabled", async () => { + mockAxiosGetWithStatus(COVER_URL, 403, ""); + + await expect(cuiApi.getCuiMarking()).resolves.toEqual({ marking: CuiMarking.DISABLED }); + }); + + it("Should fail when the marking applies but the response body is empty", async () => { + mockAxiosGetWithStatus(COVER_URL, 200, ""); + + await expect(cuiApi.getCuiMarking()).rejects.toThrow(FatalError); + await expect(cuiApi.getCuiMarking()).rejects.toThrow(CUI_MARKING_FAILED_MESSAGE); + }); + + it("Should fail when the backend answers with an unexpected status", async () => { + mockAxiosGetWithStatus(COVER_URL, 204, ""); + + await expect(cuiApi.getCuiMarking()).rejects.toThrow(CUI_MARKING_FAILED_MESSAGE); + }); +});