diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8a121244..1dd3e0d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,7 @@ /src/core @celonis/astro /src/commands/configuration-management/ @celonis/astro /src/commands/profile/ @celonis/astro +/src/commands/deployment/ @celonis/astro /src/commands/action-flows/ @celonis/process-automation /tests/commands/action-flows/ @celonis/process-automation /src/commands/analysis/ @celonis/process-analytics diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index bb4d79f8..c66fce2e 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -569,6 +569,76 @@ This mapping should be saved and then used during import. Since the format of the variables.json file on import is the same JSON structure as the list variables result, you can either map the values to the variables.json file for each variable, or replace the variables.json file with the result of the listing & mapping altogether. If the mapping of variables is skipped, you should delete the variables.json file before importing. +### Deployment commands (beta) +The **deployment** command group allows you to create deployments, list their history, check active deployments, and retrieve deployables and targets. + +#### Create Deployment +Create a new deployment for a given package version in a target, based on the deployable type. +``` +content-cli deployment create --packageKey myPackage --packageVersion 1.0.0 --deployableType app-package --targetId targetId +``` +The command will return the created deployment, or an error if the deployment could not be created. +It is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. + +#### List Deployment History +List the deployment history, optionally filtered by package, target, type, status, or creator. +``` +content-cli deployment list history +``` + +Additional filtering options: +- `--packageKey ` – Filter deployment history by package key +- `--targetId ` – Filter deployment history by target ID +- `--deployableType ` – Filter deployment history by deployable type +- `--status ` – Filter deployment history by status +- `--createdBy ` – Filter deployment history by creator + +The response is paginated, and the page size can be controlled with the `--limit` and `--offset` options (defaults to 100 and 0 respectively). + +It is also possible to use the '--json' option for writing the extended response to a file that gets created in the working directory. + +#### List Active Deployments +Get the active deployment(s) for a given target or package. + +For getting the active deployment for a target the `--targetId` option should be used: +``` +content-cli deployment list active --targetId targetId +``` + +For getting the active deployment for a package the `--packageKey` option should be used: +``` +content-cli deployment list active --packageKey myPackage +``` + +The `--targetIds` option can be used to filter the response by multiple target IDs. The option should be used with a comma-separated list of target IDs. + +The response of listing active deployments for a package will return all active deployments for that package across all targets. +The response is paginated, and the page size can be controlled with the `--limit` and `--offset` options (defaults to 100 and 0 respectively). + +For both options, it is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. + +Note: You must provide either `--packageKey` or `--targetId`, not both. + +#### List Deployables + +List all available deployables. + +``` +content-cli deployment list deployables --flavor STUDIO +``` + +The `--flavor` option can be used to filter the deployables by flavor. The available flavors are: **STUDIO** and **OCDM**. +It is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. + +#### List Targets +List all targets for a given deployable type (and optionally a package key). + +``` +content-cli deployment list targets --deployableType app-package +``` + +The `--packageKey` option can be used to get targets for a specific package. +It is also possible to use the `--json` option for writing the extended response to a file that gets created in the working directory. ### Action Flows commands diff --git a/package.json b/package.json index 54489ddc..66e7f91d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@celonis/content-cli", - "version": "1.1.3", + "version": "1.2.0", "description": "CLI Tool to help manage content in Celonis Platform", "main": "content-cli.js", "bin": { diff --git a/src/commands/deployment/deployment-api.ts b/src/commands/deployment/deployment-api.ts new file mode 100644 index 00000000..822d93ec --- /dev/null +++ b/src/commands/deployment/deployment-api.ts @@ -0,0 +1,121 @@ +import { HttpClient } from "../../core/http/http-client"; +import { Context } from "../../core/command/cli-context"; +import { + CreateDeploymentRequest, + DeployableTransport, + DeploymentStatus, + DeploymentTransport, + GetDeploymentsRequest, + TargetTransport, +} from "./deployment.interfaces"; +import { FatalError } from "../../core/utils/logger"; + +export class DeploymentApi { + private httpClient: () => HttpClient; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async createDeployment(request: CreateDeploymentRequest): Promise { + return this.httpClient() + .post("/pacman/api/deployments", request) + .catch((e) => { + throw new FatalError(`Problem creating deployment: ${e}`); + }); + } + + public async getDeployments(filter: GetDeploymentsRequest, limit?: string, offset?: string): Promise { + const queryParams = new URLSearchParams(); + this.addPaginationParams(queryParams, limit, offset); + + if (filter.packageKey) { + queryParams.set("packageKey", filter.packageKey); + } + if (filter.targetId) { + queryParams.set("targetId", filter.targetId); + } + if (filter.deployableType) { + queryParams.set("deployableType", filter.deployableType); + } + if (filter.status && filter.status !== DeploymentStatus.UNKNOWN) { + queryParams.set("status", filter.status); + } + if (filter.createdBy) { + queryParams.set("createdBy", filter.createdBy); + } + + return this.httpClient() + .get(`/pacman/api/deployments/history?${queryParams.toString()}`) + .catch((e) => { + throw new FatalError(`Problem getting deployments: ${e}`); + }); + } + + public async getActiveDeploymentsForPackage( + packageKey: string, + targetIds?: string[], + limit?: string, + offset?: string + ): Promise { + const queryParams = new URLSearchParams(); + this.addPaginationParams(queryParams, limit, offset); + + if (targetIds && targetIds.length > 0) { + queryParams.set("targetIds", targetIds.join(",")); + } + + return this.httpClient() + .get(`/pacman/api/deployments/packages/${packageKey}/active?${queryParams.toString()}`) + .catch((e) => { + throw new FatalError(`Problem getting active deployments for package ${packageKey}: ${e}`); + }); + } + + public async getActiveDeploymentForTarget(targetId: string): Promise { + return this.httpClient() + .get(`/pacman/api/deployments/targets/${targetId}/active`) + .catch((e) => { + throw new FatalError(`Problem getting active deployment for target ${targetId}: ${e}`); + }); + } + + public async getTargets(deployableType: string, packageKey?: string): Promise { + const queryParams = new URLSearchParams(); + queryParams.set("deployableType", deployableType); + + if (packageKey) { + queryParams.set("packageKey", packageKey); + } + + return this.httpClient() + .get(`/pacman/api/deployments/targets?${queryParams.toString()}`) + .catch((e) => { + throw new FatalError(`Problem getting targets: ${e}`); + }); + } + + public async getDeployables(flavor?: string): Promise { + const queryParams = new URLSearchParams(); + if (flavor) { + queryParams.set("flavor", flavor); + } + + return this.httpClient() + .get(`/pacman/api/deployments/deployables?${queryParams.toString()}`) + .catch((e) => { + throw new FatalError(`Problem getting deployables: ${e}`); + }); + } + + private addPaginationParams(queryParams: URLSearchParams, limit?: string, offset?: string): void { + const defaultLimit = 100; + const defaultOffset = 0; + + const parsedLimit = limit !== undefined && !isNaN(Number(limit)) ? Number(limit) : defaultLimit; + queryParams.set("limit", parsedLimit.toString()); + + const parsedOffset = offset !== undefined && !isNaN(Number(offset)) ? Number(offset) : defaultOffset; + queryParams.set("offset", parsedOffset.toString()); + } +} \ No newline at end of file diff --git a/src/commands/deployment/deployment.interfaces.ts b/src/commands/deployment/deployment.interfaces.ts new file mode 100644 index 00000000..3fffc3c5 --- /dev/null +++ b/src/commands/deployment/deployment.interfaces.ts @@ -0,0 +1,53 @@ +export interface DeploymentTransport { + id: string; + packageKey: string; + packageVersion: string; + deployableType: string; + target: TargetTransport; + status: DeploymentStatus; + statusMessage: string; + createdBy: string; + createdAt: string; + deployedAt: string; +} + +export interface TargetTransport { + id: string; + name: string; +} + +export interface DeployableTransport { + name: string; + type: string; + targetType: string; + flavor: string; +} + +export enum DeploymentStatus { + UNKNOWN = "unknown", + PENDING = "PENDING", + IN_PROGRESS = "IN_PROGRESS", + SUCCESS = "SUCCESS", + FAILED = "FAILED", +} + +export interface CreateDeploymentRequest { + packageKey: string; + packageVersion: string; + deployableType: string; + targetId: string; +} + +export interface GetDeploymentsRequest { + packageKey: string; + targetId: string; + deployableType: string; + status: DeploymentStatus; + createdBy: string; +} + +export function fromString(value: string): DeploymentStatus { + return (Object.values(DeploymentStatus) as string[]).includes(value) + ? (value as DeploymentStatus) + : DeploymentStatus.UNKNOWN; +} \ No newline at end of file diff --git a/src/commands/deployment/deployment.service.ts b/src/commands/deployment/deployment.service.ts new file mode 100644 index 00000000..a50ba4f2 --- /dev/null +++ b/src/commands/deployment/deployment.service.ts @@ -0,0 +1,115 @@ +import { DeploymentApi } from "./deployment-api"; +import { CreateDeploymentRequest, fromString, GetDeploymentsRequest } from "./deployment.interfaces"; +import { Context } from "../../core/command/cli-context"; +import { fileService, FileService } from "../../core/utils/file-service"; +import { logger } from "../../core/utils/logger"; +import { v4 as uuidv4 } from "uuid"; + +export class DeploymentService { + private deploymentApi: DeploymentApi; + + constructor(context: Context) { + this.deploymentApi = new DeploymentApi(context); + } + + public async createDeployment(packageKey: string, packageVersion: string, deployableType: string, targetId: string, jsonResponse: boolean): Promise { + const request: CreateDeploymentRequest = { + packageKey, + packageVersion, + deployableType, + targetId + }; + + const deployment = await this.deploymentApi.createDeployment(request); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployment), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + logger.info(`Deployment created with ID: ${deployment.id}, Status: ${deployment.status}`); + } + } + + public async getDeployments(jsonResponse: boolean + ,packageKey?: string + ,targetId?: string + ,deployableType?: string + ,status?: string + ,createdBy?: string + ,limit?: string + ,offset?: string): Promise { + const filter: GetDeploymentsRequest = { + packageKey, + targetId, + deployableType, + status: fromString(status), + createdBy + }; + const deployments = await this.deploymentApi.getDeployments(filter, limit, offset); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployments), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + deployments.forEach(deployment => { + logger.info(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Created at: ${new Date(deployment.createdAt).toISOString()}`); + }); + } + } + + public async getActiveDeploymentForTarget(targetId: string, jsonResponse: boolean): Promise { + const deployment = await this.deploymentApi.getActiveDeploymentForTarget(targetId); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployment), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + logger.info(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + } + } + + public async getActiveDeploymentsForPackage(packageKey: string, jsonResponse: boolean, targetIds?: string[], limit?: string, offset?: string): Promise { + const deployments = await this.deploymentApi.getActiveDeploymentsForPackage(packageKey, targetIds, limit, offset); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployments), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + deployments.forEach(deployment => { + logger.info(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + } + } + + public async getTargets(jsonResponse: boolean, deployableType: string, packageKey?: string): Promise { + const targets = await this.deploymentApi.getTargets(deployableType, packageKey); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(targets), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + targets.forEach(target => { + logger.info(`ID: ${target.id}, Name: ${target.name}`); + }); + } + } + + public async getDeployables(jsonResponse: boolean, flavor?: string): Promise { + const deployables = await this.deploymentApi.getDeployables(flavor); + + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(deployables), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + deployables.forEach(deployable => { + logger.info(`Name: ${deployable.name}, Type: ${deployable.type}`); + }); + } + } +} \ No newline at end of file diff --git a/src/commands/deployment/module.ts b/src/commands/deployment/module.ts new file mode 100644 index 00000000..fe4dcda8 --- /dev/null +++ b/src/commands/deployment/module.ts @@ -0,0 +1,100 @@ +import { Configurator, IModule } from "../../core/command/module-handler"; +import { Context } from "../../core/command/cli-context"; +import { Command, OptionValues } from "commander"; +import { DeploymentService } from "./deployment.service"; + +class Module extends IModule { + + public register(context: Context, configurator: Configurator): void { + const deploymentCommand = configurator.command("deployment").beta(); + + deploymentCommand.command("create") + .beta() + .description("Create a new deployment") + .requiredOption("--packageKey ", "Identifier of the package to deploy") + .requiredOption("--packageVersion ", "Version of the package to deploy") + .requiredOption("--deployableType ", "The type of the deployable") + .requiredOption("--targetId ", "Identifier of the target to deploy to") + .option("--json", "Return the response as a JSON file") + .action(this.createDeployment); + + const listCommand = deploymentCommand.command("list").beta(); + + listCommand.command("history") + .beta() + .description("List deployment history") + .option("--packageKey ", "Filter deployment history by package key") + .option("--targetId ", "Filter deployment history by target ID") + .option("--deployableType ", "Filter deployment history by deployable type") + .option("--status ", "Filter deployment history by status") + .option("--createdBy ", "Filter deployment history by creator") + .option("--limit ", "Limit the number of results returned") + .option("--offset ", "Offset for pagination") + .option("--json", "Return the response as a JSON file") + .action(this.listDeploymentHistory); + + listCommand.command("active") + .beta() + .description("Get the active deployment(s) for a given target or package.\n"+ + "You can use the command to list the active deployment(s) for a specific target or for a specific package.\n" + + "The targetIds filter is available only for getting the active deployments for a given package. \n" + + "Listing active deployments for a package returns paginated results. Use the limit and offset parameters to navigate through the results.") + .option("--packageKey ", "Identifier of the package to get the active deployment for") + .option("--targetId ", "Identifier of the target to get the active deployment for") + .option("--targetIds ", "Comma-separated list of target IDs to get the active deployments for (only with packageKey)") + .option("--limit ", "Limit the number of results returned") + .option("--offset ", "Offset for pagination") + .option("--json", "Return the response as a JSON file") + .action(this.listActiveDeployments); + + listCommand.command("deployables") + .beta() + .description("List all deployables") + .option("--flavor ", "Filter deployables by flavor") + .option("--json", "Return the response as a JSON file") + .action(this.listDeployables); + + listCommand.command("targets") + .beta() + .description("List all targets for a given deployable type and package key") + .requiredOption("--deployableType ", "The type of the deployable") + .option("--packageKey ", "Identifier of the package to list targets for") + .option("--json", "Return the response as a JSON file") + .action(this.listTargets); + } + + private async createDeployment(context: Context, command: Command, options: OptionValues): Promise { + await new DeploymentService(context).createDeployment(options.packageKey, options.packageVersion, options.deployableType, options.targetId, options.json); + } + + private async listDeploymentHistory(context: Context, command: Command, options: OptionValues): Promise { + await new DeploymentService(context).getDeployments( + options.json, options.packageKey, options.targetId, options.deployableType + , options.status, options.createdBy, options.limit, options.offset); + } + + private async listActiveDeployments(context: Context, command: Command, options: OptionValues): Promise { + if (options.packageKey && options.targetId) { + throw new Error("Please provide either --packageKey or --targetId, not both."); + } + + if (options.packageKey) { + const targetIds: string[] = options.targetIds ? options.targetIds.split(",") : []; + await new DeploymentService(context).getActiveDeploymentsForPackage(options.packageKey, options.json, targetIds, options.limit, options.offset); + } else if (options.targetId) { + await new DeploymentService(context).getActiveDeploymentForTarget(options.targetId, options.json); + } else { + throw new Error("Either --packageKey or --targetId must be provided."); + } + } + + private async listDeployables(context: Context, command: Command, options: OptionValues): Promise { + await new DeploymentService(context).getDeployables(options.json, options.flavor); + } + + private async listTargets(context: Context, command: Command, options: OptionValues): Promise { + await new DeploymentService(context).getTargets(options.json, options.deployableType, options.packageKey); + } +} + +export = Module; \ No newline at end of file diff --git a/tests/commands/deployment/deployment-create.spec.ts b/tests/commands/deployment/deployment-create.spec.ts new file mode 100644 index 00000000..2485a723 --- /dev/null +++ b/tests/commands/deployment/deployment-create.spec.ts @@ -0,0 +1,48 @@ +import { DeploymentStatus, DeploymentTransport } from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosPost } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployment create", () => { + const deployment: DeploymentTransport = { + id: "deployment-id", + packageKey: "package-key", + packageVersion: "1.0.0", + deployableType: "app-package", + status: DeploymentStatus.IN_PROGRESS, + createdAt: new Date().toISOString(), + createdBy: "user-id", + deployedAt: new Date().toISOString(), + statusMessage: "Deployment in progress", + target: { + id: "target-id", + name: "Target Name", + }, + }; + + it("Should return created deployment", async () => { + mockAxiosPost("https://myTeam.celonis.cloud/pacman/api/deployments", deployment); + + await new DeploymentService(testContext).createDeployment(deployment.packageKey, deployment.packageVersion, deployment.deployableType, deployment.target.id, false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`Deployment created with ID: ${deployment.id}, Status: ${deployment.status}`); + }); + + it("Should return created deployment as JSON", async () => { + mockAxiosPost("https://myTeam.celonis.cloud/pacman/api/deployments", deployment); + + await new DeploymentService(testContext).createDeployment(deployment.packageKey, deployment.packageVersion, deployment.deployableType, deployment.target.id, true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deploymentTransport = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeploymentTransport; + + expect(deploymentTransport).toEqual(deployment); + }); +}) \ No newline at end of file diff --git a/tests/commands/deployment/deployment-list-active.spec.ts b/tests/commands/deployment/deployment-list-active.spec.ts new file mode 100644 index 00000000..838bf2fb --- /dev/null +++ b/tests/commands/deployment/deployment-list-active.spec.ts @@ -0,0 +1,99 @@ +import { DeploymentStatus, DeploymentTransport} from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployment list active", () => { + const deployment: DeploymentTransport = { + id: "deployment-id", + packageKey: "package-key", + packageVersion: "1.0.0", + deployableType: "app-package", + status: DeploymentStatus.SUCCESS, + createdAt: new Date().getDate() + "", + createdBy: "user-id", + deployedAt: new Date().getDate() + "", + statusMessage: "Deployment in progress", + target: { + id: "target-id", + name: "Target Name", + }, + }; + + it("Should list active deployment for target", async () => { + const targetId = "target-id"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/targets/${targetId}/active`, deployment); + + await new DeploymentService(testContext).getActiveDeploymentForTarget(targetId, false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + + it("Should list active deployment for target as JSON", async () => { + const targetId = "target-id"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/targets/${targetId}/active`, deployment); + + await new DeploymentService(testContext).getActiveDeploymentForTarget(targetId, true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deploymentTransport = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeploymentTransport; + + expect(deploymentTransport).toEqual(deployment); + }); + + it("Should list active deployments for package", async () => { + const packageKey = "package-key"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=100&offset=0`, [deployment]); + + await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + + it("Should list active deployments for package with targetIds filter", async () => { + const packageKey = "package-key"; + const targetIds = ["target-id-1", "target-id-2"]; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=100&offset=0&targetIds=${targetIds.join("%2C")}`, [deployment]); + + await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, false, targetIds); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + + it("Should list active deployments for package with pagination filters", async () => { + const packageKey = "package-key"; + const limit = "99"; + const offset = "1"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=${limit}&offset=${offset}`, [deployment]); + + await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, false, undefined, limit, offset); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deployment.id}, Package: ${deployment.packageKey}, Version: ${deployment.packageVersion}, Status: ${deployment.status}, Deployed at: ${new Date(deployment.deployedAt).toISOString()}`); + }); + + it("Should list active deployments for package as JSON", async () => { + const packageKey = "package-key"; + mockAxiosGet(`https://myTeam.celonis.cloud/pacman/api/deployments/packages/${packageKey}/active?limit=100&offset=0`, [deployment]); + + await new DeploymentService(testContext).getActiveDeploymentsForPackage(packageKey, true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deploymentTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeploymentTransport[]; + expect(deploymentTransports.length).toBe(1); + + expect(deploymentTransports[0]).toEqual(deployment); + }); +}); \ No newline at end of file diff --git a/tests/commands/deployment/deployment-list-deployables.spec.ts b/tests/commands/deployment/deployment-list-deployables.spec.ts new file mode 100644 index 00000000..33648489 --- /dev/null +++ b/tests/commands/deployment/deployment-list-deployables.spec.ts @@ -0,0 +1,61 @@ +import { DeployableTransport } from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployment list deployables", () => { + const appPackage: DeployableTransport = { + name: "App package", + type: "app-package", + targetType: "apps-portal", + flavor: "STUDIO", + }; + + const pigData: DeployableTransport = { + name: "Objects and Events", + type: "pig-data", + targetType: "data-pool", + flavor: "OCDM", + } + + it("Should list deployables", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/deployables?", [appPackage, pigData]); + + await new DeploymentService(testContext).getDeployables(false); + + expect(loggingTestTransport.logMessages.length).toBe(2); + expect(loggingTestTransport.logMessages[0].message).toContain(`Name: ${appPackage.name}, Type: ${appPackage.type}`); + expect(loggingTestTransport.logMessages[1].message).toContain(`Name: ${pigData.name}, Type: ${pigData.type}`); + }); + + it("Should list deployables with flavor filter", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/deployables?flavor=STUDIO", [appPackage]); + + await new DeploymentService(testContext).getDeployables(false, "STUDIO"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`Name: ${appPackage.name}, Type: ${appPackage.type}`); + }); + + it("Should list deployables as JSON", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/deployables?", [appPackage, pigData]); + + await new DeploymentService(testContext).getDeployables(true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deployableTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeployableTransport[]; + expect(deployableTransports.length).toBe(2); + + const appPackageTransport = deployableTransports.filter(transport => transport.name === appPackage.name)[0]; + const pigDataTransport = deployableTransports.filter(transport => transport.name === pigData.name)[0]; + + expect(appPackageTransport).toEqual(appPackage); + expect(pigDataTransport).toEqual(pigData); + }); +}) \ No newline at end of file diff --git a/tests/commands/deployment/deployment-list-history.spec.ts b/tests/commands/deployment/deployment-list-history.spec.ts new file mode 100644 index 00000000..bd7a3b38 --- /dev/null +++ b/tests/commands/deployment/deployment-list-history.spec.ts @@ -0,0 +1,70 @@ +import { + DeploymentStatus, + DeploymentTransport, +} from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployments list history", () => { + const deploymentTransport: DeploymentTransport = { + id: "deployment-id", + packageKey: "package-key", + packageVersion: "1.0.0", + deployableType: "app-package", + status: DeploymentStatus.IN_PROGRESS, + createdAt: new Date().toISOString(), + createdBy: "user-id", + deployedAt: new Date().toISOString(), + statusMessage: "Deployment in progress", + target: { + id: "target-id", + name: "Target Name", + }, + }; + + it("Should list deployment history", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?limit=100&offset=0", [deploymentTransport]); + + await new DeploymentService(testContext).getDeployments(false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deploymentTransport.id}, Package: ${deploymentTransport.packageKey}, Version: ${deploymentTransport.packageVersion}, Status: ${deploymentTransport.status}, Created at: ${new Date(deploymentTransport.createdAt).toISOString()}`); + }); + + it("Should list deployment history with filter params", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?limit=100&offset=0&packageKey=package-key&targetId=target-id&deployableType=app-package&status=IN_PROGRESS&createdBy=user-id", [deploymentTransport]); + + await new DeploymentService(testContext).getDeployments(false, "package-key", "target-id", "app-package", "IN_PROGRESS", "user-id"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deploymentTransport.id}, Package: ${deploymentTransport.packageKey}, Version: ${deploymentTransport.packageVersion}, Status: ${deploymentTransport.status}, Created at: ${new Date(deploymentTransport.createdAt).toISOString()}`); + }); + + it("Should list deployment history with pagination params", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?limit=3&offset=3", [deploymentTransport]); + + await new DeploymentService(testContext).getDeployments(false, undefined, undefined, undefined, undefined, undefined, "3", "3"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${deploymentTransport.id}, Package: ${deploymentTransport.packageKey}, Version: ${deploymentTransport.packageVersion}, Status: ${deploymentTransport.status}, Created at: ${new Date(deploymentTransport.createdAt).toISOString()}`); + }); + + it("Should list deployment history as JSON", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/history?limit=100&offset=0", [deploymentTransport]); + + await new DeploymentService(testContext).getDeployments(true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const deploymentTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeploymentTransport[]; + expect(deploymentTransports.length).toBe(1); + + expect(deploymentTransports[0]).toEqual(deploymentTransport); + }); +}); \ No newline at end of file diff --git a/tests/commands/deployment/deployment-list-targets.spec.ts b/tests/commands/deployment/deployment-list-targets.spec.ts new file mode 100644 index 00000000..2cae472d --- /dev/null +++ b/tests/commands/deployment/deployment-list-targets.spec.ts @@ -0,0 +1,57 @@ +import { DeployableTransport, TargetTransport } from "../../../src/commands/deployment/deployment.interfaces"; +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { DeploymentService } from "../../../src/commands/deployment/deployment.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Deployment list targets", () => { + const firstTarget: TargetTransport = { + id: "target-1", + name: "First target", + }; + + const secondTarget: TargetTransport = { + id: "target-2", + name: "Second target", + } + + it("Should list targets", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/targets?deployableType=app-package", [firstTarget, secondTarget]); + + await new DeploymentService(testContext).getTargets(false, "app-package"); + + expect(loggingTestTransport.logMessages.length).toBe(2); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${firstTarget.id}, Name: ${firstTarget.name}`); + expect(loggingTestTransport.logMessages[1].message).toContain(`ID: ${secondTarget.id}, Name: ${secondTarget.name}`); + }); + + it("Should list targets with package key filter", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/targets?deployableType=app-package&packageKey=package-key", [firstTarget]); + + await new DeploymentService(testContext).getTargets(false, "app-package", "package-key"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(`ID: ${firstTarget.id}, Name: ${firstTarget.name}`); + }); + + it("Should list targets as JSON", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/deployments/targets?deployableType=app-package", [firstTarget, secondTarget]); + + await new DeploymentService(testContext).getTargets(true, "app-package"); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + + expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), expect.any(String), {encoding: "utf-8"}); + + const targetTransports = JSON.parse(mockWriteFileSync.mock.calls[0][1]) as DeployableTransport[]; + expect(targetTransports.length).toBe(2); + + const firstTargetTransport = targetTransports.filter(transport => transport.name === firstTarget.name)[0]; + const secondTargetTransport = targetTransports.filter(transport => transport.name === secondTarget.name)[0]; + + expect(firstTargetTransport).toEqual(firstTarget); + expect(secondTargetTransport).toEqual(secondTarget); + }); +}) \ No newline at end of file