diff --git a/package-lock.json b/package-lock.json index e236d78..e4a0a72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11430,7 +11430,7 @@ }, "packages/client": { "name": "@cloudbeat/client", - "version": "2.1.0", + "version": "2.3.0", "license": "GPL-3.0", "dependencies": { "@cloudbeat/types": "1.0.8", @@ -11485,6 +11485,18 @@ "@cucumber/cucumber": "^12.0.0" } }, + "packages/cucumber/node_modules/@cloudbeat/client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@cloudbeat/client/-/client-2.1.0.tgz", + "integrity": "sha512-kC86sw1mkFavW9AlG1tgJu6FnHFydPv7AaH2/lC6lmkuDNAAy45L7WBOJkYjDifH6lEwEbDXacL24zRyFSz5qA==", + "license": "GPL-3.0", + "dependencies": { + "@cloudbeat/types": "1.0.8", + "axios": "^1.19.0", + "debug": "^4.4.3", + "moment": "^2.30.1" + } + }, "packages/cucumber/node_modules/@types/node": { "version": "24.0.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", @@ -11535,7 +11547,7 @@ }, "packages/playwright": { "name": "@cloudbeat/playwright", - "version": "2.2.1", + "version": "2.2.2", "license": "GPL-3.0", "dependencies": { "@cloudbeat/client": "2.1.0", @@ -11554,6 +11566,18 @@ "node": ">=18" } }, + "packages/playwright/node_modules/@cloudbeat/client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@cloudbeat/client/-/client-2.1.0.tgz", + "integrity": "sha512-kC86sw1mkFavW9AlG1tgJu6FnHFydPv7AaH2/lC6lmkuDNAAy45L7WBOJkYjDifH6lEwEbDXacL24zRyFSz5qA==", + "license": "GPL-3.0", + "dependencies": { + "@cloudbeat/types": "1.0.8", + "axios": "^1.19.0", + "debug": "^4.4.3", + "moment": "^2.30.1" + } + }, "packages/playwright/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/packages/client/package.json b/packages/client/package.json index 67caab0..0a5ab41 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@cloudbeat/client", - "version": "2.2.0", + "version": "2.3.0", "description": "CloudBeat API client library ", "keywords": [ "cloudbeat" diff --git a/packages/client/src/base/CbApiError.ts b/packages/client/src/base/CbApiError.ts index ec31c81..2aaa534 100644 --- a/packages/client/src/base/CbApiError.ts +++ b/packages/client/src/base/CbApiError.ts @@ -1,4 +1,5 @@ export class CbApiError extends Error { + public status?: number; private path?: string; constructor(msgOrError: string | Error) { @@ -7,6 +8,7 @@ export class CbApiError extends Error { if (msgOrError instanceof Error && (msgOrError as any).response) { const { response } = msgOrError as any; + this.status = response.status; if (response.status === 500) { this.message = 'Internal server error, please try again later.'; } @@ -44,10 +46,16 @@ export class CbApiError extends Error { this.message = message; } + else if ((response.status === 400 || response.status === 403) && typeof response.data === 'string' && response.data) { + this.message = response.data; + } else { this.message = response.statusText; } } + else if (msgOrError instanceof CbApiError) { + this.status = msgOrError.status; + } Error.captureStackTrace(this, this.constructor); } } diff --git a/packages/client/src/base/CbRestApiClient.ts b/packages/client/src/base/CbRestApiClient.ts index dc87597..ed246c5 100644 --- a/packages/client/src/base/CbRestApiClient.ts +++ b/packages/client/src/base/CbRestApiClient.ts @@ -30,28 +30,20 @@ export class CbRestApiClient { if (!config.headers['Content-Type'] && !config.headers['content-type']) { config.headers['Content-Type'] = 'application/json'; } - // TODO: - // Should add support for X-Api-Key header - // Should remove support for Bearer token (backend doesn't seem to support it?) - // Should eventually remove support for apiKey URL query parameter if (this.authType === AuthenticationType.Bearer) { config.headers.Authorization = `Bearer ${this.authToken}`; } else { - const params = new URLSearchParams({ - apiKey: this.authToken, - }); + // the key is sent as a header and never as a query parameter: URLs end up in access logs, proxies and browser history + config.headers['X-Api-Key'] = this.authToken; // append random parameter to each GET request to prevent caching proxies interfering with our requests if (config.method === 'get') { - params.append('rnd', Math.floor(Math.random() * 1000000000000).toString()); + const params = new URLSearchParams({ + rnd: Math.floor(Math.random() * 1000000000000).toString(), + }); + config.url += `${config.url?.includes('?') ? '&' : '?'}${params.toString()}`; } - - // append query string to the url - if (!config.url?.endsWith('?') && !config.url?.endsWith('&')) { - config.url += '?'; - } - config.url += params.toString(); } // log request debug(`REQ: ${config.method!} ${config.url!}`); diff --git a/packages/client/src/v1/ProjectApi.ts b/packages/client/src/v1/ProjectApi.ts index a0babc2..7f0f4d5 100644 --- a/packages/client/src/v1/ProjectApi.ts +++ b/packages/client/src/v1/ProjectApi.ts @@ -2,7 +2,7 @@ import Debug from 'debug'; import FormData from 'form-data'; import { CbApiError } from '../base/CbApiError'; import { ApiBaseClientV1 } from './ApiBaseClientV1'; -import { GetSyncStatusResponse, ProjectSyncStatus } from './dto/project/GetSyncStatusResponse'; +import { ProjectSyncStatus } from './dto/project/GetSyncStatusResponse'; import * as V1_API_ENDPOINTS from './endpoints'; const error = Debug('RuntimeApi:error'); @@ -41,15 +41,101 @@ export class ProjectApi extends ApiBaseClientV1 { public async getSyncStatus(projectId: string): Promise { const path = `${V1_API_ENDPOINTS.PROJECTS}/${projectId}/sync/status`; try { - const response = await this.instance.get(path); - if (!response.data) { + const response: any = await this.instance.get(path); + const status = response?.data ?? response; + if (!status) { throw new CbApiError('Invalid response, no data recieved.'); } - return new GetSyncStatusResponse(response).toModel(); + return new ProjectSyncStatus(status); } catch (e: any) { error(e); throw new CbApiError(e as Error); } } + + public async list(): Promise { + const path = `${V1_API_ENDPOINTS.PROJECTS}/list/details`; + try { + const response: any = await this.instance.get(path); + const projects = response?.data ?? response; + if (!Array.isArray(projects)) { + throw new CbApiError('Invalid response, project list expected.'); + } + return projects as ProjectInfo[]; + } + catch (e: any) { + error(e); + throw new CbApiError(e as Error); + } + } + + public async create(project: CreateProjectRequest, fileName?: string, fileContent?: Buffer): Promise { + try { + const formData = new FormData(); + formData.append('data', JSON.stringify(project), { contentType: 'application/json' }); + if (fileName && fileContent) { + formData.append('file', fileContent, { filename: fileName }); + } + const response: any = await this.instance.post( + V1_API_ENDPOINTS.PROJECTS, + formData, + { + headers: formData.getHeaders(), + }, + ); + const id = response?.data?.id ?? response?.id; + if (id === undefined) { + throw new CbApiError('Invalid response, project id expected.'); + } + return id as number; + } + catch (e: any) { + error(e); + throw new CbApiError(e as Error); + } + } + + public async triggerSync(projectId: string): Promise { + const path = `${V1_API_ENDPOINTS.PROJECTS}/${projectId}/sync`; + try { + await this.instance.post(path); + } + catch (e: any) { + error(e); + throw new CbApiError(e as Error); + } + } +} + +export type ProjectSyncType = 'Manual' | 'None' | 'Git'; + +export interface ProjectInfo { + id: number; + name: string; + type: string; +} + +export interface ProjectGitSettings { + url: string; + branchName?: string; + userName?: string; + password?: string; + token?: string; +} + +export interface CreateProjectRequest { + name: string; + type: string; + notes?: string; + settings: { + type: string; + syncType: ProjectSyncType; + execCommand?: string; + execOptions?: string; + assemblyNames?: string; + preExecCommands?: string[]; + postExecCommands?: string[]; + }; + gitSettings?: ProjectGitSettings; } diff --git a/packages/client/src/v1/dto/project/GetSyncStatusResponse.ts b/packages/client/src/v1/dto/project/GetSyncStatusResponse.ts index 51a9f80..4f3fe7a 100644 --- a/packages/client/src/v1/dto/project/GetSyncStatusResponse.ts +++ b/packages/client/src/v1/dto/project/GetSyncStatusResponse.ts @@ -24,6 +24,7 @@ export class ProjectSyncStatus { syncDate?: string; syncStatus?: string; message?: string; + details?: string; constructor(data: any) { Object.assign(this, data); diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 416b991..1f5f1c0 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -1,6 +1,6 @@ { "name": "@cloudbeat/playwright", - "version": "2.2.1", + "version": "2.2.2", "description": "CloudBeat Reporter for Playwright", "keywords": [ "cloudbeat", diff --git a/packages/playwright/src/cbUtils.ts b/packages/playwright/src/cbUtils.ts index 3382fa8..b22c4e6 100644 --- a/packages/playwright/src/cbUtils.ts +++ b/packages/playwright/src/cbUtils.ts @@ -20,5 +20,10 @@ export class cb { } const sendToReporter = (type: string, data: any ): void => { + // the reporter consumes these messages only when the test is executed by CloudBeat, + // otherwise they would just pollute the output of local and CI runs + if (!process.env.CB_RUN_ID) { + return; + } process.stdout.write(JSON.stringify({ type, data })); };