Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/commands/bookmarks/bookmarks-command.service.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { Context } from "../../core/command/cli-context";
import { BookmarksApi } from "./bookmarks-api";
import { fileService, FileService } from "../../core/utils/file-service";
import { CuiFileService } from "../../core/utils/cui-file-service";
import { logger } from "../../core/utils/logger";

export class BookmarksCommandService {

private readonly bookmarksApi: BookmarksApi;
private readonly cuiFileService: CuiFileService;

constructor(context: Context) {
this.bookmarksApi = new BookmarksApi(context);
this.cuiFileService = new CuiFileService(context);
}

public async exportBookmarks(packageKey: string, file?: string): Promise<void> {
const exportData = await this.bookmarksApi.exportBookmarks(packageKey);

const fileName = file ?? `bookmarks-${packageKey}.json`;
fileService.writeToFileWithGivenName(JSON.stringify(exportData, null, 4), fileName);
logger.info(FileService.fileDownloadedMessage + fileName);
const writtenFilename = await this.cuiFileService.writeToFileWithGivenName(JSON.stringify(exportData, null, 4), fileName);
logger.info(FileService.fileDownloadedMessage + writtenFilename);
}

public async importBookmarks(packageKey: string, file: string): Promise<void> {
Expand Down
45 changes: 13 additions & 32 deletions src/core/http/http-shared/base.manager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import { FatalError, logger } from "../../utils/logger";
import { ManagerConfig } from "./manager-config.interface";
import { HttpClient } from "../http-client";
import { Context } from "../../command/cli-context";
import { FileConstants } from "../../utils/file.constants";
import { CuiFileService } from "../../utils/cui-file-service";

export abstract class BaseManager {
Expand All @@ -18,24 +15,14 @@ export abstract class BaseManager {
}

public async pull(): Promise<any> {
Comment thread
dwoditsch marked this conversation as resolved.
return new Promise<void>((resolve, reject) => {
this.httpClient()
.get(this.getConfig().pullUrl)
.then(data => {
try {
const filename = this.writeToFile(data);
logger.info(this.fileDownloadedMessage + filename);
resolve();
} catch (e) {
logger.error(new FatalError(e));
reject();
}
})
.catch(err => {
logger.error(new FatalError(err));
reject();
});
});
try {
const data = await this.httpClient().get(this.getConfig().pullUrl);
const filename = await this.writeToFile(data);
logger.info(this.fileDownloadedMessage + filename);
} catch (err) {
logger.error(new FatalError(err));
throw err;
}
}

public async pullFile(): Promise<any> {
Expand Down Expand Up @@ -90,23 +77,17 @@ export abstract class BaseManager {
}
}

protected writeToFile(data: any): string {
const filename = this.getConfig().exportFileName;
this.writeToFileWithGivenName(data, filename);
return filename;
protected async writeToFile(data: any): Promise<string> {
return this.cuiFileService.writeToFileWithGivenName(
this.getSerializedFileContent(data),
this.getConfig().exportFileName
);
}

protected async writeStreamToFile(data: Buffer): Promise<string> {
return this.cuiFileService.writeZipToFileWithGivenName(data, this.getConfig().exportFileName);
}

protected writeToFileWithGivenName(data: any, filename: string): void {
fs.writeFileSync(path.resolve(process.cwd(), filename), this.getSerializedFileContent(data), {
encoding: "utf-8",
mode: FileConstants.DEFAULT_FILE_PERMISSIONS,
});
}

protected abstract getConfig(): ManagerConfig;

protected abstract getBody(): object;
Expand Down
116 changes: 116 additions & 0 deletions tests/commands/cui-marking-single-file-exports.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { resolve } from "node:path";
import { readFileSync } from "node:fs";
import AdmZip = require("adm-zip");
import { mockAxiosGet, mockAxiosGetWithStatus } 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 { parse } from "../../src/core/utils/yaml";
import { AssetCommandService } from "../../src/commands/studio/command-service/asset-command.service";
import { SkillCommandService } from "../../src/commands/action-flows/skill/skill-command.service";
import { DataPoolCommandService } from "../../src/commands/data-pipeline/data-pool/data-pool-command.service";
import { ViewBookmarksCommandService } from "../../src/commands/view/view-bookmarks-command.service";
import { AnalysisBookmarksCommandService } from "../../src/commands/analysis/analysis-bookmarks-command.service";
import { BookmarksCommandService } from "../../src/commands/bookmarks/bookmarks-command.service";

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 PROJECT_ID = "project-1";
const SKILL_ID = "skill-1";
const POOL_ID = "pool-1";
const BOARD_ID = "board-1";
const ANALYSIS_ID = "analysis-1";
const PACKAGE_KEY = "my-package";

function markAsClassified(): void {
mockAxiosGetWithStatus(COVER_URL, 200, {
resolvedCuiMarking: { categories: [{ code: "PRVCY", name: "Privacy" }] },
coverPage: { pdfContent: PDF_BYTES.toString("base64"), encoding: "base64" },
});
}

function loggedFileName(): string {
const prefix = FileService.fileDownloadedMessage;
const message = loggingTestTransport.logMessages.map(entry => entry.message).find(entry => entry.includes(prefix));
return message.split(prefix)[1];
}

function markedArchive(expectedName: string): AdmZip {
const filename = loggedFileName();
expect(filename).toEqual(`${CuiFileService.CLASSIFIED_PREFIX}${expectedName}.zip`);

const archive = new AdmZip(readFileSync(resolve(process.cwd(), filename)));
expect(archive.getEntry(CuiFileService.COVER_SHEET_FILE_NAME).getData().equals(PDF_BYTES)).toBe(true);
return archive;
}

function markedEntry(expectedName: string, entryName: string): string {
return markedArchive(expectedName).getEntry(entryName).getData().toString();
}

function markedJson(expectedName: string, entryName: string): any {
return JSON.parse(markedEntry(expectedName, entryName));
}

describe("CUI marking of single-file exports", () => {

beforeEach(() => {
markAsClassified();
});

it("Should mark the exported asset as YAML", async () => {
const asset = { key: "asset-1", name: "My Asset", rootNodeKey: PACKAGE_KEY };
mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/asset/export/${PACKAGE_KEY}.asset-1`, asset);

await new AssetCommandService(testContext).pullAsset(`${PACKAGE_KEY}.asset-1`);

expect(parse(markedEntry("asset_asset-1", "asset_asset-1.yml"))).toEqual(asset);
});

it("Should mark the exported skill", async () => {
const skill = { id: SKILL_ID, name: "My Skill" };
mockAxiosGet(`https://myTeam.celonis.cloud/action-engine/api/projects/${PROJECT_ID}/skills/${SKILL_ID}/export`, skill);

await new SkillCommandService(testContext).pullSkill(null, PROJECT_ID, SKILL_ID);

expect(markedJson(`skill_${SKILL_ID}`, `skill_${SKILL_ID}.json`)).toEqual(skill);
});

it("Should mark the exported data pool", async () => {
const dataPool = { id: POOL_ID, name: "Pool 1" };
mockAxiosGet(`https://myTeam.celonis.cloud/integration/api/pools/${POOL_ID}/export`, dataPool);

await new DataPoolCommandService(testContext).pullDataPool(POOL_ID);

expect(markedJson(`data-pool_${POOL_ID}`, `data-pool_${POOL_ID}.json`)).toEqual(dataPool);
});

it("Should mark the exported view bookmarks", async () => {
const bookmarks = [{ bookmark: { name: "My View Bookmark" } }];
mockAxiosGet(`https://myTeam.celonis.cloud/blueprint/api/bookmarks/export?boardId=${BOARD_ID}&type=USER`, bookmarks);

await new ViewBookmarksCommandService(testContext).pullViewBookmarks(BOARD_ID, undefined);

expect(markedJson(`studio_view_bookmarks_${BOARD_ID}`, `studio_view_bookmarks_${BOARD_ID}.json`)).toEqual(bookmarks);
});

it("Should mark the exported analysis bookmarks", async () => {
const bookmarks = [{ bookmark: { name: "My Analysis Bookmark" } }];
mockAxiosGet(`https://myTeam.celonis.cloud/process-analytics/api/bookmarks/export?analysisId=${ANALYSIS_ID}&type=USER`, bookmarks);

await new AnalysisBookmarksCommandService(testContext).pullAnalysisBookmarks(ANALYSIS_ID, "USER");

expect(markedJson(`studio_analysis_bookmarks_${ANALYSIS_ID}`, `studio_analysis_bookmarks_${ANALYSIS_ID}.json`)).toEqual(bookmarks);
});

it("Should mark the exported package bookmarks under the user-chosen name", async () => {
const bookmarks = { packageKey: PACKAGE_KEY, entries: [] };
mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/packages/${PACKAGE_KEY}/bookmarks/export`, bookmarks);

await new BookmarksCommandService(testContext).exportBookmarks(PACKAGE_KEY, "custom.json");

expect(markedJson("custom", "custom.json")).toEqual(bookmarks);
});
});
52 changes: 52 additions & 0 deletions tests/commands/data-pipeline/data-pool-update.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { mockAxiosPut, mockAxiosPutError, mockedAxiosInstance, mockedPostRequestBodyByUrl } from "../../utls/http-requests-mock";
import { DataPoolCommandService } from "../../../src/commands/data-pipeline/data-pool/data-pool-command.service";
import { testContext } from "../../utls/test-context";
import { loggingTestTransport } from "../../jest.setup";
import { writeJsonTempFile } from "../../utls/fs-utils";

describe("Update data pool", () => {

const poolId = "pool-1";
const updateUrl = `https://myTeam.celonis.cloud/integration/api/pools/${poolId}`;
const file = "data-pool-update.json";

const dataPool = {
id: poolId,
name: "Updated Pool",
objects: [],
};

beforeEach(() => {
writeJsonTempFile(file, { dataPool });
});

it("Should call the update API and log success", async () => {
mockAxiosPut(updateUrl, dataPool);

await new DataPoolCommandService(testContext).updateDataPool(poolId, file);

expect(mockedAxiosInstance.put).toHaveBeenCalledWith(updateUrl, expect.anything(), expect.anything());
expect(loggingTestTransport.logMessages).toHaveLength(1);
expect(loggingTestTransport.logMessages[0].message).toContain("Data Pool was updated successfully!");
});

it("Should send the data pool from the file as request body", async () => {
mockAxiosPut(updateUrl, dataPool);

await new DataPoolCommandService(testContext).updateDataPool(poolId, file);

expect(JSON.parse(mockedPostRequestBodyByUrl.get(updateUrl))).toEqual(dataPool);
});

it("Should log a fatal error and reject when the update API fails", async () => {
const exitSpy = jest.spyOn(process, "exit").mockImplementation((() => undefined) as never);
mockAxiosPutError(updateUrl, 500, { message: "Internal Server Error" });

await expect(new DataPoolCommandService(testContext).updateDataPool(poolId, file)).rejects.toBeUndefined();

expect(exitSpy).toHaveBeenCalledWith(1);
expect(loggingTestTransport.logMessages[0].message).toContain("Internal Server Error");

exitSpy.mockRestore();
});
});
89 changes: 89 additions & 0 deletions tests/core/http/base.manager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { BaseManager } from "../../../src/core/http/http-shared/base.manager";
import { ManagerConfig } from "../../../src/core/http/http-shared/manager-config.interface";
import { FatalError } from "../../../src/core/utils/logger";
import { loggingTestTransport } from "../../jest.setup";
import { mockAxiosGetError, mockAxiosPostError } from "../../utls/http-requests-mock";
import { testContext } from "../../utls/test-context";

const TEAM_URL = "https://myTeam.celonis.cloud";
const PULL_PATH = "/api/test/pull";
const PUSH_PATH = "/api/test/push";
const FIND_ALL_PATH = "/api/test/find-all";
const ERROR_BODY = { message: "Internal Server Error" };

class TestManager extends BaseManager {
public constructor() {
super(testContext);
}

protected getConfig(): ManagerConfig {
return {
pullUrl: PULL_PATH,
pushUrl: PUSH_PATH,
findAllUrl: FIND_ALL_PATH,
exportFileName: "test-export.json",
onPushSuccessMessage: () => "Pushed successfully",
onFindAll: () => undefined,
};
}

protected getBody(): object {
return { key: "test" };
}

protected getSerializedFileContent(data: any): string {
return JSON.stringify(data);
}
}

describe("BaseManager error handling", () => {

let manager: TestManager;
let exitSpy: jest.SpyInstance;

beforeEach(() => {
manager = new TestManager();
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => undefined) as never);
});

afterEach(() => {
exitSpy.mockRestore();
});

const expectFatalErrorLogged = (): void => {
expect(exitSpy).toHaveBeenCalledWith(1);
expect(loggingTestTransport.logMessages[0].message).toContain("Internal Server Error");
};

it("Should log a fatal error and reject when the pull API fails", async () => {
mockAxiosGetError(TEAM_URL + PULL_PATH, 500, ERROR_BODY);

await expect(manager.pull()).rejects.toThrow(FatalError);

expectFatalErrorLogged();
});

it("Should log a fatal error and reject when the file download fails", async () => {
mockAxiosPostError(TEAM_URL + PULL_PATH, 500, ERROR_BODY);

await expect(manager.pullFile()).rejects.toEqual(JSON.stringify(ERROR_BODY));

expectFatalErrorLogged();
});

it("Should log a fatal error and reject when the push API fails", async () => {
mockAxiosPostError(TEAM_URL + PUSH_PATH, 500, ERROR_BODY);

await expect(manager.push()).rejects.toBeUndefined();

expectFatalErrorLogged();
});

it("Should log a fatal error and reject when the find all API fails", async () => {
mockAxiosGetError(TEAM_URL + FIND_ALL_PATH, 500, ERROR_BODY);

await expect(manager.findAll()).rejects.toThrow(FatalError);

expectFatalErrorLogged();
});
});
Loading
Loading