From f29b34158890fd0c20097ad7ba150b4e4ea46bda Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Tue, 15 Jul 2025 16:37:22 +0530 Subject: [PATCH 01/18] adding build:approve command --- packages/cli-build/src/approve.js | 93 +++++++++++++++++++++++++++++++ packages/cli-build/src/build.js | 3 +- packages/client/src/client.js | 50 ++++++++++++++++- 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 packages/cli-build/src/approve.js diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js new file mode 100644 index 000000000..82b0ed3e4 --- /dev/null +++ b/packages/cli-build/src/approve.js @@ -0,0 +1,93 @@ +import command from '@percy/cli-command'; + +/** + * Constants for environment variable names and error messages + */ +const ENV_VARS = { + PERCY_USERNAME: 'PERCY_USERNAME', + PERCY_ACCESS_KEY: 'PERCY_ACCESS_KEY' +}; + +/** + * Validates that required authentication credentials are present + * @param {Object} flags - Command flags object + * @returns {Object} Validated credentials object + */ +function validateCredentials(flags) { + // Use flags if provided, otherwise fallback to environment variables + const username = flags.username || process.env[ENV_VARS.PERCY_USERNAME]; + const accessKey = flags.accessKey || process.env[ENV_VARS.PERCY_ACCESS_KEY]; + + return { username, accessKey }; +} + +/** + * Approve command definition for Percy builds + * Allows users to approve builds using build ID and authentication credentials + */ +export const approve = command('approve', { + description: 'Approve Percy builds', + + args: [ + { + name: 'build-id', + description: 'Build ID to approve', + type: 'id', + required: true + } + ], + + flags: [ + { + name: 'username', + description: 'Username for authentication (can also be set via PERCY_USERNAME env var)', + type: 'string' + }, + { + name: 'access-key', + description: 'Access key for authentication (can also be set via PERCY_ACCESS_KEY env var)', + type: 'string' + } + ], + + examples: [ + '$0 ', + '$0 --username username --access-key **key**' + ], + + percy: true +}, async ({ flags, args, percy, log, exit }) => { + // Early return if Percy is disabled + if (!percy) { + exit(0, 'Percy is disabled'); + } + + // Validate and get authentication credentials + const { username, accessKey } = validateCredentials(flags); + + if (!username || !accessKey) { + exit(1, 'Username and access key are required to approve builds.'); + } + + log.info('Approving build...'); + + try { + // Call the Percy API to approve the build + const buildApprovalResponse = await percy.client.approveBuild( + args.buildId, + username, + accessKey + ); + + log.debug(`Build approved successfully: ${JSON.stringify(buildApprovalResponse)}`); + log.info('Build approved successfully'); + } catch (error) { + // Log detailed error information for debugging + log.debug(`Failed to approve build. Error: ${error.message || error}`); + + // Provide user-friendly error message + exit(1, 'Failed to approve the build'); + } +}); + +export default approve; \ No newline at end of file diff --git a/packages/cli-build/src/build.js b/packages/cli-build/src/build.js index b3f00a928..672008385 100644 --- a/packages/cli-build/src/build.js +++ b/packages/cli-build/src/build.js @@ -2,10 +2,11 @@ import command from '@percy/cli-command'; import finalize from './finalize.js'; import wait from './wait.js'; import id from './id.js'; +import approve from './approve.js'; export const build = command('build', { description: 'Finalize and wait on Percy builds', - commands: [finalize, wait, id] + commands: [finalize, wait, id, approve] }); export default build; diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 1afbd5292..bd387b06f 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -154,10 +154,13 @@ export class PercyClient { } // Performs a POST request to a JSON API endpoint with appropriate headers. - post(path, body = {}, { ...meta } = {}) { + post(path, body = {}, { ...meta } = {}, customHeaders = {}) { return logger.measure('client:post', meta.identifier || 'Unknown', meta, () => { return request(`${this.apiUrl}/${path}`, { - headers: this.headers({ 'Content-Type': 'application/vnd.api+json' }), + headers: this.headers({ + 'Content-Type': 'application/vnd.api+json', + ...customHeaders, + }), method: 'POST', body, meta @@ -710,6 +713,49 @@ export class PercyClient { }, { identifier: 'error.analysis.get', ...meta }); } + // Performs a review action (approve, unapprove, reject) on a specific build. + // This function handles the common logic for sending review requests. + async reviewBuild(buildId, action, username, accessKey) { + validateId('build', buildId); + this.log.debug(`Sending ${action} action for build ${buildId}...`); + + const customHeaders = { + 'bstack-username': username, + 'bstack-access-key': accessKey + }; + + const requestBody = { + data: { + attributes: { + action: action, + }, + relationships: { + build: { + data: { + type: 'builds', + id: buildId + } + } + }, + type: 'reviews' + } + }; + + return this.post(`reviews`, requestBody, { identifier: `build.${action}` }, customHeaders); + } + + async approveBuild(buildId, username, accessKey) { + return this.reviewBuild(buildId, 'approve', username, accessKey); + } + + async unapproveBuild(buildId, username, accessKey) { + return this.reviewBuild(buildId, 'unapprove', username, accessKey); + } + + async rejectBuild(buildId, username, accessKey) { + return this.reviewBuild(buildId, 'reject', username, accessKey); + } + mayBeLogUploadSize(contentSize, meta = {}) { if (contentSize >= 25 * 1024 * 1024) { this.log.error('Uploading resource above 25MB might fail the build...', meta); From fad34eb7c95182a911d7bbaa286a890fd28a275d Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Tue, 15 Jul 2025 17:37:57 +0530 Subject: [PATCH 02/18] adding unapprove and reject --- packages/cli-build/package.json | 5 +- packages/cli-build/src/approve.js | 23 +-------- packages/cli-build/src/reject.js | 74 +++++++++++++++++++++++++++++ packages/cli-build/src/unapprove.js | 74 +++++++++++++++++++++++++++++ packages/cli-build/src/utils.js | 20 ++++++++ packages/client/src/client.js | 12 +++-- 6 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 packages/cli-build/src/reject.js create mode 100644 packages/cli-build/src/unapprove.js create mode 100644 packages/cli-build/src/utils.js diff --git a/packages/cli-build/package.json b/packages/cli-build/package.json index caaf7c99f..63d8ae88b 100644 --- a/packages/cli-build/package.json +++ b/packages/cli-build/package.json @@ -19,7 +19,10 @@ ], "main": "./dist/index.js", "type": "module", - "exports": "./dist/index.js", + "exports": { + ".": "./dist/index.js", + "./utils": "./dist/utils.js" + }, "scripts": { "build": "node ../../scripts/build", "lint": "eslint --ignore-path ../../.gitignore .", diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index 82b0ed3e4..0691b6fca 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -1,25 +1,5 @@ import command from '@percy/cli-command'; - -/** - * Constants for environment variable names and error messages - */ -const ENV_VARS = { - PERCY_USERNAME: 'PERCY_USERNAME', - PERCY_ACCESS_KEY: 'PERCY_ACCESS_KEY' -}; - -/** - * Validates that required authentication credentials are present - * @param {Object} flags - Command flags object - * @returns {Object} Validated credentials object - */ -function validateCredentials(flags) { - // Use flags if provided, otherwise fallback to environment variables - const username = flags.username || process.env[ENV_VARS.PERCY_USERNAME]; - const accessKey = flags.accessKey || process.env[ENV_VARS.PERCY_ACCESS_KEY]; - - return { username, accessKey }; -} +import { validateCredentials } from './utils.js'; /** * Approve command definition for Percy builds @@ -80,6 +60,7 @@ export const approve = command('approve', { ); log.debug(`Build approved successfully: ${JSON.stringify(buildApprovalResponse)}`); + // To add Approved by name here once that changes are deployed from API log.info('Build approved successfully'); } catch (error) { // Log detailed error information for debugging diff --git a/packages/cli-build/src/reject.js b/packages/cli-build/src/reject.js new file mode 100644 index 000000000..09571ad3f --- /dev/null +++ b/packages/cli-build/src/reject.js @@ -0,0 +1,74 @@ +import command from '@percy/cli-command'; +import { validateCredentials } from './utils.js'; + +/** + * Reject command definition for Percy builds + * Allows users to reject builds using build ID and authentication credentials + */ +export const reject = command('reject', { + description: 'Reject Percy builds', + + args: [ + { + name: 'build-id', + description: 'Build ID to reject', + type: 'id', + required: true + } + ], + + flags: [ + { + name: 'username', + description: 'Username for authentication (can also be set via PERCY_USERNAME env var)', + type: 'string' + }, + { + name: 'access-key', + description: 'Access key for authentication (can also be set via PERCY_ACCESS_KEY env var)', + type: 'string' + } + ], + + examples: [ + '$0 ', + '$0 --username username --access-key **key**' + ], + + percy: true +}, async ({ flags, args, percy, log, exit }) => { + // Early return if Percy is disabled + if (!percy) { + exit(0, 'Percy is disabled'); + } + + // Validate and get authentication credentials + const { username, accessKey } = validateCredentials(flags); + + if (!username || !accessKey) { + exit(1, 'Username and access key are required to reject builds.'); + } + + log.info('Rejecting build...'); + + try { + // Call the Percy API to reject the build + const buildRejectionResponse = await percy.client.rejectBuild( + args.buildId, + username, + accessKey + ); + + log.debug(`Build rejected successfully: ${JSON.stringify(buildRejectionResponse)}`); + // To add Rejected by name here once that changes are deployed from API + log.info('Build rejected successfully'); + } catch (error) { + // Log detailed error information for debugging + log.debug(`Failed to reject build. Error: ${error.message || error}`); + + // Provide user-friendly error message + exit(1, 'Failed to reject the build'); + } +}); + +export default reject; diff --git a/packages/cli-build/src/unapprove.js b/packages/cli-build/src/unapprove.js new file mode 100644 index 000000000..01c154bb9 --- /dev/null +++ b/packages/cli-build/src/unapprove.js @@ -0,0 +1,74 @@ +import command from '@percy/cli-command'; +import { validateCredentials } from './utils.js'; + +/** + * Unapprove command definition for Percy builds + * Allows users to unapprove builds using build ID and authentication credentials + */ +export const unapprove = command('unapprove', { + description: 'Unapprove Percy builds', + + args: [ + { + name: 'build-id', + description: 'Build ID to unapprove', + type: 'id', + required: true + } + ], + + flags: [ + { + name: 'username', + description: 'Username for authentication (can also be set via PERCY_USERNAME env var)', + type: 'string' + }, + { + name: 'access-key', + description: 'Access key for authentication (can also be set via PERCY_ACCESS_KEY env var)', + type: 'string' + } + ], + + examples: [ + '$0 ', + '$0 --username username --access-key **key**' + ], + + percy: true +}, async ({ flags, args, percy, log, exit }) => { + // Early return if Percy is disabled + if (!percy) { + exit(0, 'Percy is disabled'); + } + + // Validate and get authentication credentials + const { username, accessKey } = validateCredentials(flags); + + if (!username || !accessKey) { + exit(1, 'Username and access key are required to unapprove builds.'); + } + + log.info('Unapproving build...'); + + try { + // Call the Percy API to unapprove the build + const buildUnapprovalResponse = await percy.client.unapproveBuild( + args.buildId, + username, + accessKey + ); + + log.debug(`Build unapproved successfully: ${JSON.stringify(buildUnapprovalResponse)}`); + // To add unApproved by name here once that changes are deployed from API + log.info('Build unapproved successfully'); + } catch (error) { + // Log detailed error information for debugging + log.debug(`Failed to unapprove build. Error: ${error.message || error}`); + + // Provide user-friendly error message + exit(1, 'Failed to unapprove the build'); + } +}); + +export default unapprove; diff --git a/packages/cli-build/src/utils.js b/packages/cli-build/src/utils.js new file mode 100644 index 000000000..a5a348a8b --- /dev/null +++ b/packages/cli-build/src/utils.js @@ -0,0 +1,20 @@ +/** + * Constants for environment variable names and error messages + */ +const ENV_VARS = { + PERCY_USERNAME: 'PERCY_USERNAME', + PERCY_ACCESS_KEY: 'PERCY_ACCESS_KEY' +}; + +/** + * Validates that required authentication credentials are present + * @param {Object} flags - Command flags object + * @returns {Object} Validated credentials object + */ +export function validateCredentials(flags) { + // Use flags if provided, otherwise fallback to environment variables + const username = flags.username || process.env[ENV_VARS.PERCY_USERNAME]; + const accessKey = flags.accessKey || process.env[ENV_VARS.PERCY_ACCESS_KEY]; + + return { username, accessKey }; +} \ No newline at end of file diff --git a/packages/client/src/client.js b/packages/client/src/client.js index bd387b06f..f9bebae9b 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -133,9 +133,9 @@ export class PercyClient { // Returns common headers used for each request with additional // headers. Throws an error when the token is missing, which is a required // authorization header. - headers(headers) { + headers(headers, projectTokenRequired = true) { return Object.assign({ - Authorization: `Token token=${this.getToken()}`, + Authorization: `Token token=${this.getToken(projectTokenRequired)}`, 'User-Agent': this.userAgent() }, headers); } @@ -154,13 +154,13 @@ export class PercyClient { } // Performs a POST request to a JSON API endpoint with appropriate headers. - post(path, body = {}, { ...meta } = {}, customHeaders = {}) { + post(path, body = {}, { ...meta } = {}, customHeaders = {}, projectTokenRequired = true) { return logger.measure('client:post', meta.identifier || 'Unknown', meta, () => { return request(`${this.apiUrl}/${path}`, { headers: this.headers({ 'Content-Type': 'application/vnd.api+json', ...customHeaders, - }), + }, projectTokenRequired), method: 'POST', body, meta @@ -741,7 +741,9 @@ export class PercyClient { } }; - return this.post(`reviews`, requestBody, { identifier: `build.${action}` }, customHeaders); + // For the review action, we use accessKey and username in custom headers + // and do not require a project token. + return this.post(`reviews`, requestBody, { identifier: `build.${action}` }, customHeaders, false); } async approveBuild(buildId, username, accessKey) { From e7edbb1f22b1804a5434603fba6e8a48825f896f Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Tue, 15 Jul 2025 18:03:55 +0530 Subject: [PATCH 03/18] adding command in build --- packages/cli-build/src/build.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli-build/src/build.js b/packages/cli-build/src/build.js index 672008385..0e98aa3ba 100644 --- a/packages/cli-build/src/build.js +++ b/packages/cli-build/src/build.js @@ -3,10 +3,12 @@ import finalize from './finalize.js'; import wait from './wait.js'; import id from './id.js'; import approve from './approve.js'; +import reject from './reject.js'; +import unapprove from './unapprove.js'; export const build = command('build', { description: 'Finalize and wait on Percy builds', - commands: [finalize, wait, id, approve] + commands: [finalize, wait, id, approve, unapprove, reject], }); export default build; From c19def525f14ec3a412731a318f801b005c7cb2e Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Tue, 15 Jul 2025 18:09:20 +0530 Subject: [PATCH 04/18] fixing lint and updating readme --- packages/cli-app/README.md | 1 + packages/cli-build/README.md | 111 +++++++++++++++++++++++++--- packages/cli-build/src/approve.js | 8 +- packages/cli-build/src/build.js | 2 +- packages/cli-build/src/reject.js | 6 +- packages/cli-build/src/unapprove.js | 6 +- packages/cli-build/src/utils.js | 4 +- packages/cli-config/README.md | 47 ++++++------ packages/cli-exec/README.md | 24 +++--- packages/cli-snapshot/README.md | 1 + packages/cli-upload/README.md | 1 + 11 files changed, 156 insertions(+), 55 deletions(-) diff --git a/packages/cli-app/README.md b/packages/cli-app/README.md index 8d752e374..d0de3713d 100644 --- a/packages/cli-app/README.md +++ b/packages/cli-app/README.md @@ -33,6 +33,7 @@ Global options: -v, --verbose Log everything -q, --quiet Log errors only -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) -h, --help Display command help ``` diff --git a/packages/cli-build/README.md b/packages/cli-build/README.md index a6f2ab2d3..7d633c0f2 100644 --- a/packages/cli-build/README.md +++ b/packages/cli-build/README.md @@ -7,6 +7,9 @@ Commands for interacting with Percy builds * [`percy build:finalize`](#percy-buildfinalize) * [`percy build:wait`](#percy-buildwait) * [`percy build:id`](#percy-buildid) +* [`percy build:approve`](#percy-buildapprove) +* [`percy build:unapprove`](#percy-buildunapprove) +* [`percy build:reject`](#percy-buildreject) ### `percy build:finalize` @@ -17,10 +20,11 @@ Usage: $ percy build:finalize [options] Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help ``` ### `percy build:wait` @@ -38,12 +42,14 @@ Options: -t, --timeout Timeout before exiting without updates, defaults to 10 minutes -i, --interval Interval at which to poll for updates, defaults to 10 second -f, --fail-on-changes Exit with an error when diffs are found - --pass-if-approved Doesn't Exit with an error if the build is approved, requires '--fail-on-changes' + --pass-if-approved Doesn't exit with an error if the build is approved, regardless of if + diffs are found. Global options: -v, --verbose Log everything -q, --quiet Log errors only -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) -h, --help Display command help Examples: @@ -60,12 +66,97 @@ Usage: $ percy build:id [options] Percy options: - -P, --port [number] Local CLI server port (default: 5338) + -P, --port [number] Local CLI server port (default: 5338) + +Global options: + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help +``` + +### `percy build:approve` + +Approve Percy builds + +``` +Usage: + $ percy build:approve [options] + +Arguments: + build-id Build ID to approve + +Options: + --username Username for authentication (can also be set via PERCY_USERNAME env var) + --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env + var) Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help + +Examples: + $ percy build:approve + $ percy build:approve --username username --access-key **key** +``` + +### `percy build:unapprove` + +Unapprove Percy builds + +``` +Usage: + $ percy build:unapprove [options] + +Arguments: + build-id Build ID to unapprove + +Options: + --username Username for authentication (can also be set via PERCY_USERNAME env var) + --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env + var) + +Global options: + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help + +Examples: + $ percy build:unapprove + $ percy build:unapprove --username username --access-key **key** +``` + +### `percy build:reject` + +Reject Percy builds + +``` +Usage: + $ percy build:reject [options] + +Arguments: + build-id Build ID to reject + +Options: + --username Username for authentication (can also be set via PERCY_USERNAME env var) + --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env + var) + +Global options: + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help + +Examples: + $ percy build:reject + $ percy build:reject --username username --access-key **key** ``` diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index 0691b6fca..3a7f04115 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -44,7 +44,7 @@ export const approve = command('approve', { // Validate and get authentication credentials const { username, accessKey } = validateCredentials(flags); - + if (!username || !accessKey) { exit(1, 'Username and access key are required to approve builds.'); } @@ -58,17 +58,17 @@ export const approve = command('approve', { username, accessKey ); - + log.debug(`Build approved successfully: ${JSON.stringify(buildApprovalResponse)}`); // To add Approved by name here once that changes are deployed from API log.info('Build approved successfully'); } catch (error) { // Log detailed error information for debugging log.debug(`Failed to approve build. Error: ${error.message || error}`); - + // Provide user-friendly error message exit(1, 'Failed to approve the build'); } }); -export default approve; \ No newline at end of file +export default approve; diff --git a/packages/cli-build/src/build.js b/packages/cli-build/src/build.js index 0e98aa3ba..e8ed42827 100644 --- a/packages/cli-build/src/build.js +++ b/packages/cli-build/src/build.js @@ -8,7 +8,7 @@ import unapprove from './unapprove.js'; export const build = command('build', { description: 'Finalize and wait on Percy builds', - commands: [finalize, wait, id, approve, unapprove, reject], + commands: [finalize, wait, id, approve, unapprove, reject] }); export default build; diff --git a/packages/cli-build/src/reject.js b/packages/cli-build/src/reject.js index 09571ad3f..c53754437 100644 --- a/packages/cli-build/src/reject.js +++ b/packages/cli-build/src/reject.js @@ -44,7 +44,7 @@ export const reject = command('reject', { // Validate and get authentication credentials const { username, accessKey } = validateCredentials(flags); - + if (!username || !accessKey) { exit(1, 'Username and access key are required to reject builds.'); } @@ -58,14 +58,14 @@ export const reject = command('reject', { username, accessKey ); - + log.debug(`Build rejected successfully: ${JSON.stringify(buildRejectionResponse)}`); // To add Rejected by name here once that changes are deployed from API log.info('Build rejected successfully'); } catch (error) { // Log detailed error information for debugging log.debug(`Failed to reject build. Error: ${error.message || error}`); - + // Provide user-friendly error message exit(1, 'Failed to reject the build'); } diff --git a/packages/cli-build/src/unapprove.js b/packages/cli-build/src/unapprove.js index 01c154bb9..9e0fc5409 100644 --- a/packages/cli-build/src/unapprove.js +++ b/packages/cli-build/src/unapprove.js @@ -44,7 +44,7 @@ export const unapprove = command('unapprove', { // Validate and get authentication credentials const { username, accessKey } = validateCredentials(flags); - + if (!username || !accessKey) { exit(1, 'Username and access key are required to unapprove builds.'); } @@ -58,14 +58,14 @@ export const unapprove = command('unapprove', { username, accessKey ); - + log.debug(`Build unapproved successfully: ${JSON.stringify(buildUnapprovalResponse)}`); // To add unApproved by name here once that changes are deployed from API log.info('Build unapproved successfully'); } catch (error) { // Log detailed error information for debugging log.debug(`Failed to unapprove build. Error: ${error.message || error}`); - + // Provide user-friendly error message exit(1, 'Failed to unapprove the build'); } diff --git a/packages/cli-build/src/utils.js b/packages/cli-build/src/utils.js index a5a348a8b..d47ebf824 100644 --- a/packages/cli-build/src/utils.js +++ b/packages/cli-build/src/utils.js @@ -15,6 +15,6 @@ export function validateCredentials(flags) { // Use flags if provided, otherwise fallback to environment variables const username = flags.username || process.env[ENV_VARS.PERCY_USERNAME]; const accessKey = flags.accessKey || process.env[ENV_VARS.PERCY_ACCESS_KEY]; - + return { username, accessKey }; -} \ No newline at end of file +} diff --git a/packages/cli-config/README.md b/packages/cli-config/README.md index a3068ae69..1eaa0652b 100644 --- a/packages/cli-config/README.md +++ b/packages/cli-config/README.md @@ -18,20 +18,21 @@ Usage: $ percy config:create [options] [filepath] Arguments: - filepath Optional config filepath + filepath Optional config filepath Options: - --rc Create a .percyrc file - --yaml Create a .percy.yaml file - --yml Create a .percy.yml file - --json Create a .percy.json file - --js Create a .percy.js file + --rc Create a .percyrc file + --yaml Create a .percy.yaml file + --yml Create a .percy.yml file + --json Create a .percy.json file + --js Create a .percy.js file Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help Examples: $ percy config:create @@ -51,13 +52,14 @@ Usage: $ percy config:validate [options] [filepath] Arguments: - filepath Config filepath, detected by default + filepath Config filepath, detected by default Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help Examples: $ percy config:validate @@ -73,17 +75,18 @@ Usage: $ percy config:migrate [options] [filepath] [output] Arguments: - filepath Current config filepath, detected by default - output New config filepath to write to, defaults to 'filepath' + filepath Current config filepath, detected by default + output New config filepath to write to, defaults to 'filepath' Options: - -d, --dry-run Print the new config without writing it + -d, --dry-run Print the new config without writing it Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help Examples: $ percy config:migrate diff --git a/packages/cli-exec/README.md b/packages/cli-exec/README.md index 92d9c1c38..93ef12762 100644 --- a/packages/cli-exec/README.md +++ b/packages/cli-exec/README.md @@ -41,6 +41,7 @@ Global options: -v, --verbose Log everything -q, --quiet Log errors only -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) --help Display command help Examples: @@ -70,6 +71,7 @@ Global options: -v, --verbose Log everything -q, --quiet Log errors only -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) --help Display command help Examples: @@ -85,13 +87,14 @@ Usage: $ percy exec:stop [options] Percy options: - -P, --port [number] Local CLI server port (default: 5338) + -P, --port [number] Local CLI server port (default: 5338) Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help ``` ### `percy exec:ping` @@ -103,12 +106,13 @@ Usage: $ percy exec:ping [options] Percy options: - -P, --port [number] Local CLI server port (default: 5338) + -P, --port [number] Local CLI server port (default: 5338) Global options: - -v, --verbose Log everything - -q, --quiet Log errors only - -s, --silent Log nothing - -h, --help Display command help + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help ``` diff --git a/packages/cli-snapshot/README.md b/packages/cli-snapshot/README.md index 93e37d4c8..4426c078e 100644 --- a/packages/cli-snapshot/README.md +++ b/packages/cli-snapshot/README.md @@ -38,6 +38,7 @@ Global options: -v, --verbose Log everything -q, --quiet Log errors only -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) --help Display command help Examples: diff --git a/packages/cli-upload/README.md b/packages/cli-upload/README.md index 1265bd8e1..faf6e6ddd 100644 --- a/packages/cli-upload/README.md +++ b/packages/cli-upload/README.md @@ -31,6 +31,7 @@ Global options: -v, --verbose Log everything -q, --quiet Log errors only -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) -h, --help Display command help Examples: From f953fda047a609dd4bfad5f25bf2333cdfb3f7b6 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Tue, 15 Jul 2025 18:14:26 +0530 Subject: [PATCH 05/18] lint fix --- packages/client/src/client.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/client.js b/packages/client/src/client.js index f9bebae9b..1a34bd50f 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -159,7 +159,7 @@ export class PercyClient { return request(`${this.apiUrl}/${path}`, { headers: this.headers({ 'Content-Type': 'application/vnd.api+json', - ...customHeaders, + ...customHeaders }, projectTokenRequired), method: 'POST', body, @@ -727,7 +727,7 @@ export class PercyClient { const requestBody = { data: { attributes: { - action: action, + action: action }, relationships: { build: { @@ -743,7 +743,7 @@ export class PercyClient { // For the review action, we use accessKey and username in custom headers // and do not require a project token. - return this.post(`reviews`, requestBody, { identifier: `build.${action}` }, customHeaders, false); + return this.post('reviews', requestBody, { identifier: `build.${action}` }, customHeaders, false); } async approveBuild(buildId, username, accessKey) { From 66443d96a0b98ad2941059ae1d664e00858374d2 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Tue, 15 Jul 2025 19:23:27 +0530 Subject: [PATCH 06/18] logging error message received from api --- packages/cli-build/src/approve.js | 3 +-- packages/cli-build/src/reject.js | 3 +-- packages/cli-build/src/unapprove.js | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index 3a7f04115..1dbc4ec6e 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -63,8 +63,7 @@ export const approve = command('approve', { // To add Approved by name here once that changes are deployed from API log.info('Build approved successfully'); } catch (error) { - // Log detailed error information for debugging - log.debug(`Failed to approve build. Error: ${error.message || error}`); + log.error(error); // Provide user-friendly error message exit(1, 'Failed to approve the build'); diff --git a/packages/cli-build/src/reject.js b/packages/cli-build/src/reject.js index c53754437..5549d9cba 100644 --- a/packages/cli-build/src/reject.js +++ b/packages/cli-build/src/reject.js @@ -63,8 +63,7 @@ export const reject = command('reject', { // To add Rejected by name here once that changes are deployed from API log.info('Build rejected successfully'); } catch (error) { - // Log detailed error information for debugging - log.debug(`Failed to reject build. Error: ${error.message || error}`); + log.error(error); // Provide user-friendly error message exit(1, 'Failed to reject the build'); diff --git a/packages/cli-build/src/unapprove.js b/packages/cli-build/src/unapprove.js index 9e0fc5409..e0b27eeb6 100644 --- a/packages/cli-build/src/unapprove.js +++ b/packages/cli-build/src/unapprove.js @@ -63,8 +63,7 @@ export const unapprove = command('unapprove', { // To add unApproved by name here once that changes are deployed from API log.info('Build unapproved successfully'); } catch (error) { - // Log detailed error information for debugging - log.debug(`Failed to unapprove build. Error: ${error.message || error}`); + log.error(error); // Provide user-friendly error message exit(1, 'Failed to unapprove the build'); From fd0898e3e565b891ddc32283344c519d5f39bcca Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Tue, 15 Jul 2025 20:42:45 +0530 Subject: [PATCH 07/18] adding tests for approve --- packages/cli-build/src/index.js | 3 + packages/cli-build/test/approve.test.js | 398 ++++++++++++++++++++++++ 2 files changed, 401 insertions(+) create mode 100644 packages/cli-build/test/approve.test.js diff --git a/packages/cli-build/src/index.js b/packages/cli-build/src/index.js index 1b03bf77b..5b59ed45b 100644 --- a/packages/cli-build/src/index.js +++ b/packages/cli-build/src/index.js @@ -2,3 +2,6 @@ export { default, build } from './build.js'; export { finalize } from './finalize.js'; export { wait } from './wait.js'; export { id } from './id.js'; +export { approve } from './approve.js'; +export { reject } from './reject.js'; +export { unapprove } from './unapprove.js'; diff --git a/packages/cli-build/test/approve.test.js b/packages/cli-build/test/approve.test.js new file mode 100644 index 000000000..437a2c09c --- /dev/null +++ b/packages/cli-build/test/approve.test.js @@ -0,0 +1,398 @@ +import { logger, setupTest } from '@percy/cli-command/test/helpers'; +import api from '@percy/client/test/helpers'; +import { approve } from '@percy/cli-build'; + +describe('percy build:approve', () => { + beforeEach(async () => { + await setupTest(); + }); + + afterEach(() => { + delete process.env.PERCY_ENABLE; + delete process.env.PERCY_USERNAME; + delete process.env.PERCY_ACCESS_KEY; + delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; + }); + + it('does nothing and logs when percy is not enabled', async () => { + process.env.PERCY_ENABLE = '0'; + await approve(['123']); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Percy is disabled' + ]); + }); + + it('logs an error when build ID is not provided', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + await expectAsync(approve([])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + "[percy] ParseError: Missing required argument 'build-id'" + ]); + }); + + it('logs an error when username is missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_ACCESS_KEY = 'test-access-key'; + await expectAsync(approve(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to approve builds.' + ]); + }); + + it('logs an error when access key is missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'test-username'; + await expectAsync(approve(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to approve builds.' + ]); + }); + + it('logs an error when both username and access key are missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + await expectAsync(approve(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to approve builds.' + ]); + }); + + it('uses username and access key from environment variables', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('env-username'); + expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + return [200, { success: true }]; + }); + + await approve(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Approving build...', + '[percy] Build approved successfully' + ]); + }); + + it('doesnot require percy token', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('env-username'); + expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + return [200, { success: true }]; + }); + + await approve(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Approving build...', + '[percy] Build approved successfully' + ]); + }); + + it('uses username and access key from flags over environment variables', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('flag-username'); + expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + return [200, { success: true }]; + }); + + await approve(['123', '--username=flag-username', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Approving build...', + '[percy] Build approved successfully' + ]); + }); + + it('handles mixed flag and environment variable usage', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + // Only access key from flag + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('env-username'); + expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + return [200, { success: true }]; + }); + + await approve(['123', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Approving build...', + '[percy] Build approved successfully' + ]); + }); + + it('handles username from flag and access key from environment', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + // Only username from flag + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('flag-username'); + expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + return [200, { success: true }]; + }); + + await approve(['123', '--username=flag-username']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Approving build...', + '[percy] Build approved successfully' + ]); + }); + + it('successfully approves a build with valid credentials', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'test-username'; + process.env.PERCY_ACCESS_KEY = 'test-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('test-username'); + expect(req.headers['bstack-access-key']).toEqual('test-access-key'); + return [200, { success: true }]; + }); + + await approve(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Approving build...', + '[percy] Build approved successfully' + ]); + }); + + it('logs an error when build approval fails with 401 Unauthorized', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'invalid-username'; + process.env.PERCY_ACCESS_KEY = 'invalid-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('invalid-username'); + expect(req.headers['bstack-access-key']).toEqual('invalid-access-key'); + return [401, { errors: [{ detail: 'Unauthorized' }] }]; + }); + + await expectAsync(approve(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Error: Unauthorized', + '[percy] Error: Failed to approve the build' + ]); + }); + + it('logs an error when build approval fails with 403 Forbidden', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'test-username'; + process.env.PERCY_ACCESS_KEY = 'test-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + return [403, { errors: [{ detail: 'Forbidden' }] }]; + }); + + await expectAsync(approve(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Error: Forbidden', + '[percy] Error: Failed to approve the build' + ]); + }); + + it('logs an error when build approval fails with 500 Internal Server Error', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'test-username'; + process.env.PERCY_ACCESS_KEY = 'test-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + return [500, { errors: [{ detail: 'Internal Server Error' }] }]; + }); + + await expectAsync(approve(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Error: Internal Server Error', + '[percy] Error: Failed to approve the build' + ]); + }); +}); From a428a136aace9af6407a331bdae6cab7318533e5 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Tue, 15 Jul 2025 22:58:34 +0530 Subject: [PATCH 08/18] adding test for unapprove and reject --- packages/cli-build/test/approve.test.js | 125 +--------- packages/cli-build/test/reject.test.js | 275 ++++++++++++++++++++++ packages/cli-build/test/unapprove.test.js | 275 ++++++++++++++++++++++ 3 files changed, 551 insertions(+), 124 deletions(-) create mode 100644 packages/cli-build/test/reject.test.js create mode 100644 packages/cli-build/test/unapprove.test.js diff --git a/packages/cli-build/test/approve.test.js b/packages/cli-build/test/approve.test.js index 437a2c09c..cdf217e98 100644 --- a/packages/cli-build/test/approve.test.js +++ b/packages/cli-build/test/approve.test.js @@ -113,27 +113,7 @@ describe('percy build:approve', () => { process.env.PERCY_USERNAME = 'env-username'; process.env.PERCY_ACCESS_KEY = 'env-access-key'; - api.reply('/reviews', (req) => { - expect(req.body).toEqual({ - data: { - type: 'reviews', - attributes: { - action: 'approve' - }, - relationships: { - build: { - data: { - type: 'builds', - id: '123' - } - } - } - } - }); - expect(req.headers['bstack-username']).toEqual('env-username'); - expect(req.headers['bstack-access-key']).toEqual('env-access-key'); - return [200, { success: true }]; - }); + api.reply('/reviews', (req) => [200, { success: true }]); await approve(['123']); @@ -151,22 +131,6 @@ describe('percy build:approve', () => { process.env.PERCY_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { - expect(req.body).toEqual({ - data: { - type: 'reviews', - attributes: { - action: 'approve' - }, - relationships: { - build: { - data: { - type: 'builds', - id: '123' - } - } - } - } - }); expect(req.headers['bstack-username']).toEqual('flag-username'); expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); return [200, { success: true }]; @@ -188,22 +152,6 @@ describe('percy build:approve', () => { // Only access key from flag api.reply('/reviews', (req) => { - expect(req.body).toEqual({ - data: { - type: 'reviews', - attributes: { - action: 'approve' - }, - relationships: { - build: { - data: { - type: 'builds', - id: '123' - } - } - } - } - }); expect(req.headers['bstack-username']).toEqual('env-username'); expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); return [200, { success: true }]; @@ -255,43 +203,6 @@ describe('percy build:approve', () => { ]); }); - it('successfully approves a build with valid credentials', async () => { - process.env.PERCY_TOKEN = '<>'; - process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'test-username'; - process.env.PERCY_ACCESS_KEY = 'test-access-key'; - - api.reply('/reviews', (req) => { - expect(req.body).toEqual({ - data: { - type: 'reviews', - attributes: { - action: 'approve' - }, - relationships: { - build: { - data: { - type: 'builds', - id: '123' - } - } - } - } - }); - expect(req.headers['bstack-username']).toEqual('test-username'); - expect(req.headers['bstack-access-key']).toEqual('test-access-key'); - return [200, { success: true }]; - }); - - await approve(['123']); - - expect(logger.stderr).toEqual([]); - expect(logger.stdout).toEqual([ - '[percy] Approving build...', - '[percy] Build approved successfully' - ]); - }); - it('logs an error when build approval fails with 401 Unauthorized', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); @@ -361,38 +272,4 @@ describe('percy build:approve', () => { '[percy] Error: Failed to approve the build' ]); }); - - it('logs an error when build approval fails with 500 Internal Server Error', async () => { - process.env.PERCY_TOKEN = '<>'; - process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'test-username'; - process.env.PERCY_ACCESS_KEY = 'test-access-key'; - - api.reply('/reviews', (req) => { - expect(req.body).toEqual({ - data: { - type: 'reviews', - attributes: { - action: 'approve' - }, - relationships: { - build: { - data: { - type: 'builds', - id: '123' - } - } - } - } - }); - return [500, { errors: [{ detail: 'Internal Server Error' }] }]; - }); - - await expectAsync(approve(['123'])).toBeRejected(); - - expect(logger.stderr).toEqual([ - '[percy] Error: Internal Server Error', - '[percy] Error: Failed to approve the build' - ]); - }); }); diff --git a/packages/cli-build/test/reject.test.js b/packages/cli-build/test/reject.test.js new file mode 100644 index 000000000..f571dd810 --- /dev/null +++ b/packages/cli-build/test/reject.test.js @@ -0,0 +1,275 @@ +import { logger, setupTest } from '@percy/cli-command/test/helpers'; +import api from '@percy/client/test/helpers'; +import { reject } from '@percy/cli-build'; + +describe('percy build:reject', () => { + beforeEach(async () => { + await setupTest(); + }); + + afterEach(() => { + delete process.env.PERCY_ENABLE; + delete process.env.PERCY_USERNAME; + delete process.env.PERCY_ACCESS_KEY; + delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; + }); + + it('does nothing and logs when percy is not enabled', async () => { + process.env.PERCY_ENABLE = '0'; + await reject(['123']); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Percy is disabled' + ]); + }); + + it('logs an error when build ID is not provided', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + await expectAsync(reject([])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + "[percy] ParseError: Missing required argument 'build-id'" + ]); + }); + + it('logs an error when username is missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_ACCESS_KEY = 'test-access-key'; + await expectAsync(reject(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to reject builds.' + ]); + }); + + it('logs an error when access key is missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'test-username'; + await expectAsync(reject(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to reject builds.' + ]); + }); + + it('logs an error when both username and access key are missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + await expectAsync(reject(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to reject builds.' + ]); + }); + + it('uses username and access key from environment variables', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'reject' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('env-username'); + expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + return [200, { success: true }]; + }); + + await reject(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Rejecting build...', + '[percy] Build rejected successfully' + ]); + }); + + it('doesnot require percy token', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => [200, { success: true }]); + + await reject(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Rejecting build...', + '[percy] Build rejected successfully' + ]); + }); + + it('uses username and access key from flags over environment variables', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => { + expect(req.headers['bstack-username']).toEqual('flag-username'); + expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + return [200, { success: true }]; + }); + + await reject(['123', '--username=flag-username', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Rejecting build...', + '[percy] Build rejected successfully' + ]); + }); + + it('handles mixed flag and environment variable usage', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + // Only access key from flag + + api.reply('/reviews', (req) => { + expect(req.headers['bstack-username']).toEqual('env-username'); + expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + return [200, { success: true }]; + }); + + await reject(['123', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Rejecting build...', + '[percy] Build rejected successfully' + ]); + }); + + it('handles username from flag and access key from environment', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + // Only username from flag + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'reject' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('flag-username'); + expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + return [200, { success: true }]; + }); + + await reject(['123', '--username=flag-username']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Rejecting build...', + '[percy] Build rejected successfully' + ]); + }); + + it('logs an error when build approval fails with 401 Unauthorized', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'invalid-username'; + process.env.PERCY_ACCESS_KEY = 'invalid-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'reject' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('invalid-username'); + expect(req.headers['bstack-access-key']).toEqual('invalid-access-key'); + return [401, { errors: [{ detail: 'Unauthorized' }] }]; + }); + + await expectAsync(reject(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Error: Unauthorized', + '[percy] Error: Failed to reject the build' + ]); + }); + + it('logs an error when build approval fails with 403 Forbidden', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'test-username'; + process.env.PERCY_ACCESS_KEY = 'test-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'reject' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + return [403, { errors: [{ detail: 'Forbidden' }] }]; + }); + + await expectAsync(reject(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Error: Forbidden', + '[percy] Error: Failed to reject the build' + ]); + }); +}); diff --git a/packages/cli-build/test/unapprove.test.js b/packages/cli-build/test/unapprove.test.js new file mode 100644 index 000000000..100f3c251 --- /dev/null +++ b/packages/cli-build/test/unapprove.test.js @@ -0,0 +1,275 @@ +import { logger, setupTest } from '@percy/cli-command/test/helpers'; +import api from '@percy/client/test/helpers'; +import { unapprove } from '@percy/cli-build'; + +describe('percy build:unapprove', () => { + beforeEach(async () => { + await setupTest(); + }); + + afterEach(() => { + delete process.env.PERCY_ENABLE; + delete process.env.PERCY_USERNAME; + delete process.env.PERCY_ACCESS_KEY; + delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; + }); + + it('does nothing and logs when percy is not enabled', async () => { + process.env.PERCY_ENABLE = '0'; + await unapprove(['123']); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Percy is disabled' + ]); + }); + + it('logs an error when build ID is not provided', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + await expectAsync(unapprove([])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + "[percy] ParseError: Missing required argument 'build-id'" + ]); + }); + + it('logs an error when username is missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_ACCESS_KEY = 'test-access-key'; + await expectAsync(unapprove(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to unapprove builds.' + ]); + }); + + it('logs an error when access key is missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'test-username'; + await expectAsync(unapprove(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to unapprove builds.' + ]); + }); + + it('logs an error when both username and access key are missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + await expectAsync(unapprove(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to unapprove builds.' + ]); + }); + + it('uses username and access key from environment variables', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'unapprove' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('env-username'); + expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + return [200, { success: true }]; + }); + + await unapprove(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Unapproving build...', + '[percy] Build unapproved successfully' + ]); + }); + + it('doesnot require percy token', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => [200, { success: true }]); + + await unapprove(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Unapproving build...', + '[percy] Build unapproved successfully' + ]); + }); + + it('uses username and access key from flags over environment variables', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + + api.reply('/reviews', (req) => { + expect(req.headers['bstack-username']).toEqual('flag-username'); + expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + return [200, { success: true }]; + }); + + await unapprove(['123', '--username=flag-username', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Unapproving build...', + '[percy] Build unapproved successfully' + ]); + }); + + it('handles mixed flag and environment variable usage', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'env-username'; + // Only access key from flag + + api.reply('/reviews', (req) => { + expect(req.headers['bstack-username']).toEqual('env-username'); + expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + return [200, { success: true }]; + }); + + await unapprove(['123', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Unapproving build...', + '[percy] Build unapproved successfully' + ]); + }); + + it('handles username from flag and access key from environment', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_ACCESS_KEY = 'env-access-key'; + // Only username from flag + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'unapprove' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('flag-username'); + expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + return [200, { success: true }]; + }); + + await unapprove(['123', '--username=flag-username']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Unapproving build...', + '[percy] Build unapproved successfully' + ]); + }); + + it('logs an error when build approval fails with 401 Unauthorized', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'invalid-username'; + process.env.PERCY_ACCESS_KEY = 'invalid-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'unapprove' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + expect(req.headers['bstack-username']).toEqual('invalid-username'); + expect(req.headers['bstack-access-key']).toEqual('invalid-access-key'); + return [401, { errors: [{ detail: 'Unauthorized' }] }]; + }); + + await expectAsync(unapprove(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Error: Unauthorized', + '[percy] Error: Failed to unapprove the build' + ]); + }); + + it('logs an error when build approval fails with 403 Forbidden', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.PERCY_USERNAME = 'test-username'; + process.env.PERCY_ACCESS_KEY = 'test-access-key'; + + api.reply('/reviews', (req) => { + expect(req.body).toEqual({ + data: { + type: 'reviews', + attributes: { + action: 'unapprove' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + } + } + }); + return [403, { errors: [{ detail: 'Forbidden' }] }]; + }); + + await expectAsync(unapprove(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Error: Forbidden', + '[percy] Error: Failed to unapprove the build' + ]); + }); +}); From 073c32f44a1ed0e76b318110ae2be1983eb839e5 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 16 Jul 2025 12:34:47 +0530 Subject: [PATCH 09/18] updating the env values --- packages/cli-build/README.md | 9 +- packages/cli-build/src/approve.js | 4 +- packages/cli-build/src/reject.js | 4 +- packages/cli-build/src/unapprove.js | 4 +- packages/cli-build/src/utils.js | 8 +- packages/cli-build/test/approve.test.js | 32 +-- packages/cli-build/test/reject.test.js | 32 +-- packages/cli-build/test/unapprove.test.js | 32 +-- packages/client/test/client.test.js | 240 ++++++++++++++++++++++ 9 files changed, 304 insertions(+), 61 deletions(-) diff --git a/packages/cli-build/README.md b/packages/cli-build/README.md index 7d633c0f2..0e460e384 100644 --- a/packages/cli-build/README.md +++ b/packages/cli-build/README.md @@ -88,7 +88,8 @@ Arguments: build-id Build ID to approve Options: - --username Username for authentication (can also be set via PERCY_USERNAME env var) + --username Username for authentication (can also be set via BROWSERSTACK_USERNAME env + var) --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env var) @@ -116,7 +117,8 @@ Arguments: build-id Build ID to unapprove Options: - --username Username for authentication (can also be set via PERCY_USERNAME env var) + --username Username for authentication (can also be set via BROWSERSTACK_USERNAME env + var) --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env var) @@ -144,7 +146,8 @@ Arguments: build-id Build ID to reject Options: - --username Username for authentication (can also be set via PERCY_USERNAME env var) + --username Username for authentication (can also be set via BROWSERSTACK_USERNAME env + var) --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env var) diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index 1dbc4ec6e..7083c2dbd 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -20,12 +20,12 @@ export const approve = command('approve', { flags: [ { name: 'username', - description: 'Username for authentication (can also be set via PERCY_USERNAME env var)', + description: 'Username for authentication (can also be set via BROWSERSTACK_USERNAME env var)', type: 'string' }, { name: 'access-key', - description: 'Access key for authentication (can also be set via PERCY_ACCESS_KEY env var)', + description: 'Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY env var)', type: 'string' } ], diff --git a/packages/cli-build/src/reject.js b/packages/cli-build/src/reject.js index 5549d9cba..458193f6f 100644 --- a/packages/cli-build/src/reject.js +++ b/packages/cli-build/src/reject.js @@ -20,12 +20,12 @@ export const reject = command('reject', { flags: [ { name: 'username', - description: 'Username for authentication (can also be set via PERCY_USERNAME env var)', + description: 'Username for authentication (can also be set via BROWSERSTACK_USERNAME env var)', type: 'string' }, { name: 'access-key', - description: 'Access key for authentication (can also be set via PERCY_ACCESS_KEY env var)', + description: 'Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY env var)', type: 'string' } ], diff --git a/packages/cli-build/src/unapprove.js b/packages/cli-build/src/unapprove.js index e0b27eeb6..e9bb79254 100644 --- a/packages/cli-build/src/unapprove.js +++ b/packages/cli-build/src/unapprove.js @@ -20,12 +20,12 @@ export const unapprove = command('unapprove', { flags: [ { name: 'username', - description: 'Username for authentication (can also be set via PERCY_USERNAME env var)', + description: 'Username for authentication (can also be set via BROWSERSTACK_USERNAME env var)', type: 'string' }, { name: 'access-key', - description: 'Access key for authentication (can also be set via PERCY_ACCESS_KEY env var)', + description: 'Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY env var)', type: 'string' } ], diff --git a/packages/cli-build/src/utils.js b/packages/cli-build/src/utils.js index d47ebf824..35a6a6ba3 100644 --- a/packages/cli-build/src/utils.js +++ b/packages/cli-build/src/utils.js @@ -2,8 +2,8 @@ * Constants for environment variable names and error messages */ const ENV_VARS = { - PERCY_USERNAME: 'PERCY_USERNAME', - PERCY_ACCESS_KEY: 'PERCY_ACCESS_KEY' + BROWSERSTACK_USERNAME: 'BROWSERSTACK_USERNAME', + BROWSERSTACK_ACCESS_KEY: 'BROWSERSTACK_ACCESS_KEY' }; /** @@ -13,8 +13,8 @@ const ENV_VARS = { */ export function validateCredentials(flags) { // Use flags if provided, otherwise fallback to environment variables - const username = flags.username || process.env[ENV_VARS.PERCY_USERNAME]; - const accessKey = flags.accessKey || process.env[ENV_VARS.PERCY_ACCESS_KEY]; + const username = flags.username || process.env[ENV_VARS.BROWSERSTACK_USERNAME]; + const accessKey = flags.accessKey || process.env[ENV_VARS.BROWSERSTACK_ACCESS_KEY]; return { username, accessKey }; } diff --git a/packages/cli-build/test/approve.test.js b/packages/cli-build/test/approve.test.js index cdf217e98..ae5b06707 100644 --- a/packages/cli-build/test/approve.test.js +++ b/packages/cli-build/test/approve.test.js @@ -9,8 +9,8 @@ describe('percy build:approve', () => { afterEach(() => { delete process.env.PERCY_ENABLE; - delete process.env.PERCY_USERNAME; - delete process.env.PERCY_ACCESS_KEY; + delete process.env.BROWSERSTACK_USERNAME; + delete process.env.BROWSERSTACK_ACCESS_KEY; delete process.env.PERCY_TOKEN; delete process.env.PERCY_FORCE_PKG_VALUE; }); @@ -39,7 +39,7 @@ describe('percy build:approve', () => { it('logs an error when username is missing', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_ACCESS_KEY = 'test-access-key'; + process.env.BROWSERSTACK_ACCESS_KEY = 'test-access-key'; await expectAsync(approve(['123'])).toBeRejected(); expect(logger.stdout).toEqual([]); @@ -51,7 +51,7 @@ describe('percy build:approve', () => { it('logs an error when access key is missing', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'test-username'; + process.env.BROWSERSTACK_USERNAME = 'test-username'; await expectAsync(approve(['123'])).toBeRejected(); expect(logger.stdout).toEqual([]); @@ -74,8 +74,8 @@ describe('percy build:approve', () => { it('uses username and access key from environment variables', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ @@ -110,8 +110,8 @@ describe('percy build:approve', () => { it('doesnot require percy token', async () => { process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => [200, { success: true }]); @@ -127,8 +127,8 @@ describe('percy build:approve', () => { it('uses username and access key from flags over environment variables', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('flag-username'); @@ -148,7 +148,7 @@ describe('percy build:approve', () => { it('handles mixed flag and environment variable usage', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; // Only access key from flag api.reply('/reviews', (req) => { @@ -169,7 +169,7 @@ describe('percy build:approve', () => { it('handles username from flag and access key from environment', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; // Only username from flag api.reply('/reviews', (req) => { @@ -206,8 +206,8 @@ describe('percy build:approve', () => { it('logs an error when build approval fails with 401 Unauthorized', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'invalid-username'; - process.env.PERCY_ACCESS_KEY = 'invalid-access-key'; + process.env.BROWSERSTACK_USERNAME = 'invalid-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'invalid-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ @@ -242,8 +242,8 @@ describe('percy build:approve', () => { it('logs an error when build approval fails with 403 Forbidden', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'test-username'; - process.env.PERCY_ACCESS_KEY = 'test-access-key'; + process.env.BROWSERSTACK_USERNAME = 'test-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'test-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ diff --git a/packages/cli-build/test/reject.test.js b/packages/cli-build/test/reject.test.js index f571dd810..827f788d7 100644 --- a/packages/cli-build/test/reject.test.js +++ b/packages/cli-build/test/reject.test.js @@ -9,8 +9,8 @@ describe('percy build:reject', () => { afterEach(() => { delete process.env.PERCY_ENABLE; - delete process.env.PERCY_USERNAME; - delete process.env.PERCY_ACCESS_KEY; + delete process.env.BROWSERSTACK_USERNAME; + delete process.env.BROWSERSTACK_ACCESS_KEY; delete process.env.PERCY_TOKEN; delete process.env.PERCY_FORCE_PKG_VALUE; }); @@ -39,7 +39,7 @@ describe('percy build:reject', () => { it('logs an error when username is missing', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_ACCESS_KEY = 'test-access-key'; + process.env.BROWSERSTACK_ACCESS_KEY = 'test-access-key'; await expectAsync(reject(['123'])).toBeRejected(); expect(logger.stdout).toEqual([]); @@ -51,7 +51,7 @@ describe('percy build:reject', () => { it('logs an error when access key is missing', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'test-username'; + process.env.BROWSERSTACK_USERNAME = 'test-username'; await expectAsync(reject(['123'])).toBeRejected(); expect(logger.stdout).toEqual([]); @@ -74,8 +74,8 @@ describe('percy build:reject', () => { it('uses username and access key from environment variables', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ @@ -110,8 +110,8 @@ describe('percy build:reject', () => { it('doesnot require percy token', async () => { process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => [200, { success: true }]); @@ -127,8 +127,8 @@ describe('percy build:reject', () => { it('uses username and access key from flags over environment variables', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('flag-username'); @@ -148,7 +148,7 @@ describe('percy build:reject', () => { it('handles mixed flag and environment variable usage', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; // Only access key from flag api.reply('/reviews', (req) => { @@ -169,7 +169,7 @@ describe('percy build:reject', () => { it('handles username from flag and access key from environment', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; // Only username from flag api.reply('/reviews', (req) => { @@ -206,8 +206,8 @@ describe('percy build:reject', () => { it('logs an error when build approval fails with 401 Unauthorized', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'invalid-username'; - process.env.PERCY_ACCESS_KEY = 'invalid-access-key'; + process.env.BROWSERSTACK_USERNAME = 'invalid-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'invalid-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ @@ -242,8 +242,8 @@ describe('percy build:reject', () => { it('logs an error when build approval fails with 403 Forbidden', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'test-username'; - process.env.PERCY_ACCESS_KEY = 'test-access-key'; + process.env.BROWSERSTACK_USERNAME = 'test-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'test-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ diff --git a/packages/cli-build/test/unapprove.test.js b/packages/cli-build/test/unapprove.test.js index 100f3c251..2a5d19cf8 100644 --- a/packages/cli-build/test/unapprove.test.js +++ b/packages/cli-build/test/unapprove.test.js @@ -9,8 +9,8 @@ describe('percy build:unapprove', () => { afterEach(() => { delete process.env.PERCY_ENABLE; - delete process.env.PERCY_USERNAME; - delete process.env.PERCY_ACCESS_KEY; + delete process.env.BROWSERSTACK_USERNAME; + delete process.env.BROWSERSTACK_ACCESS_KEY; delete process.env.PERCY_TOKEN; delete process.env.PERCY_FORCE_PKG_VALUE; }); @@ -39,7 +39,7 @@ describe('percy build:unapprove', () => { it('logs an error when username is missing', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_ACCESS_KEY = 'test-access-key'; + process.env.BROWSERSTACK_ACCESS_KEY = 'test-access-key'; await expectAsync(unapprove(['123'])).toBeRejected(); expect(logger.stdout).toEqual([]); @@ -51,7 +51,7 @@ describe('percy build:unapprove', () => { it('logs an error when access key is missing', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'test-username'; + process.env.BROWSERSTACK_USERNAME = 'test-username'; await expectAsync(unapprove(['123'])).toBeRejected(); expect(logger.stdout).toEqual([]); @@ -74,8 +74,8 @@ describe('percy build:unapprove', () => { it('uses username and access key from environment variables', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ @@ -110,8 +110,8 @@ describe('percy build:unapprove', () => { it('doesnot require percy token', async () => { process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => [200, { success: true }]); @@ -127,8 +127,8 @@ describe('percy build:unapprove', () => { it('uses username and access key from flags over environment variables', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('flag-username'); @@ -148,7 +148,7 @@ describe('percy build:unapprove', () => { it('handles mixed flag and environment variable usage', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'env-username'; + process.env.BROWSERSTACK_USERNAME = 'env-username'; // Only access key from flag api.reply('/reviews', (req) => { @@ -169,7 +169,7 @@ describe('percy build:unapprove', () => { it('handles username from flag and access key from environment', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_ACCESS_KEY = 'env-access-key'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; // Only username from flag api.reply('/reviews', (req) => { @@ -206,8 +206,8 @@ describe('percy build:unapprove', () => { it('logs an error when build approval fails with 401 Unauthorized', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'invalid-username'; - process.env.PERCY_ACCESS_KEY = 'invalid-access-key'; + process.env.BROWSERSTACK_USERNAME = 'invalid-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'invalid-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ @@ -242,8 +242,8 @@ describe('percy build:unapprove', () => { it('logs an error when build approval fails with 403 Forbidden', async () => { process.env.PERCY_TOKEN = '<>'; process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); - process.env.PERCY_USERNAME = 'test-username'; - process.env.PERCY_ACCESS_KEY = 'test-access-key'; + process.env.BROWSERSTACK_USERNAME = 'test-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'test-access-key'; api.reply('/reviews', (req) => { expect(req.body).toEqual({ diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 311e2f8a9..21d1245f1 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -165,6 +165,32 @@ describe('PercyClient', () => { expect(() => new PercyClient().post('foobar', {})) .toThrowError('Missing Percy token'); }); + + it('sends a POST request with both custom headers and projectTokenRequired=false', async () => { + const customHeaders = { + 'X-Custom-Header': 'custom-value', + 'Content-Type': 'application/json' + }; + + spyOn(client, 'headers').and.callThrough(); + + await expectAsync(client.post('foobar', { test: '123' }, {}, customHeaders, false)).toBeResolved(); + + expect(client.headers).toHaveBeenCalledWith( + jasmine.objectContaining({ + 'Content-Type': 'application/json', + 'X-Custom-Header': 'custom-value' + }), + false + ); + + expect(api.requests['/foobar'][0].headers).toEqual( + jasmine.objectContaining({ + 'Content-Type': 'application/json', + 'X-Custom-Header': 'custom-value' + }) + ); + }); }); describe('#createBuild()', () => { @@ -2173,4 +2199,218 @@ describe('PercyClient', () => { expect(client.getToken()).toBe('USE_THIS_TOKEN'); }); }); + + describe('#headers()', () => { + it('returns default headers with Authorization and User-Agent', () => { + const headers = client.headers(); + + expect(headers).toEqual({ + Authorization: 'Token token=PERCY_TOKEN', + 'User-Agent': jasmine.stringMatching(/^Percy\/v1 @percy\/client\/\S+ \(node\/v[\d.]+.*\)$/) + }); + }); + + it('merges additional headers with default headers', () => { + const additionalHeaders = { + 'Content-Type': 'application/json', + 'X-Custom-Header': 'custom-value' + }; + + const headers = client.headers(additionalHeaders); + + expect(headers).toEqual({ + Authorization: 'Token token=PERCY_TOKEN', + 'User-Agent': jasmine.stringMatching(/^Percy\/v1 @percy\/client\/\S+ \(node\/v[\d.]+.*\)$/), + 'Content-Type': 'application/json', + 'X-Custom-Header': 'custom-value' + }); + }); + + it('calls getToken with projectTokenRequired=true by default', () => { + spyOn(client, 'getToken').and.returnValue('TEST_TOKEN'); + + client.headers(); + + expect(client.getToken).toHaveBeenCalledWith(true); + }); + + it('calls getToken with projectTokenRequired=false when specified', () => { + spyOn(client, 'getToken').and.returnValue('TEST_TOKEN'); + + client.headers({}, false); + + expect(client.getToken).toHaveBeenCalledWith(false); + }); + }); + + describe('#reviewBuild()', () => { + it('sends a review request with correct parameters', async () => { + await expectAsync(client.reviewBuild('123', 'approve', 'testuser', 'testkey')).toBeResolved(); + + expect(api.requests['/reviews'][0].method).toBe('POST'); + expect(api.requests['/reviews'][0].headers).toEqual( + jasmine.objectContaining({ + 'bstack-username': 'testuser', + 'bstack-access-key': 'testkey' + }) + ); + expect(api.requests['/reviews'][0].body).toEqual({ + data: { + attributes: { + action: 'approve' + }, + relationships: { + build: { + data: { + type: 'builds', + id: '123' + } + } + }, + type: 'reviews' + } + }); + }); + + it('calls post with projectTokenRequired=false', async () => { + spyOn(client, 'post').and.callThrough(); + + await expectAsync(client.reviewBuild('123', 'reject', 'testuser', 'testkey')).toBeResolved(); + + expect(client.post).toHaveBeenCalledWith( + 'reviews', + jasmine.any(Object), + { identifier: 'build.reject' }, + { + 'bstack-username': 'testuser', + 'bstack-access-key': 'testkey' + }, + false + ); + }); + + it('validates build ID', async () => { + await expectAsync(client.reviewBuild(null, 'approve', 'testuser', 'testkey')) + .toBeRejectedWithError('Missing build ID'); + + await expectAsync(client.reviewBuild('', 'approve', 'testuser', 'testkey')) + .toBeRejectedWithError('Missing build ID'); + + await expectAsync(client.reviewBuild({}, 'approve', 'testuser', 'testkey')) + .toBeRejectedWithError('Invalid build ID'); + }); + + it('logs debug message with action and build ID', async () => { + spyOn(client.log, 'debug'); + + await expectAsync(client.reviewBuild('456', 'unapprove', 'testuser', 'testkey')).toBeResolved(); + + expect(client.log.debug).toHaveBeenCalledWith('Sending unapprove action for build 456...'); + }); + + it('works with different action types', async () => { + spyOn(client, 'post').and.callThrough(); + + await expectAsync(client.reviewBuild('123', 'custom-action', 'testuser', 'testkey')).toBeResolved(); + + expect(api.requests['/reviews'][0].body.data.attributes.action).toBe('custom-action'); + expect(client.post).toHaveBeenCalledWith( + 'reviews', + jasmine.any(Object), + { identifier: 'build.custom-action' }, + jasmine.any(Object), + false + ); + }); + + it('accepts numeric build ID', async () => { + await expectAsync(client.reviewBuild(123, 'approve', 'testuser', 'testkey')).toBeResolved(); + + expect(api.requests['/reviews'][0].body.data.relationships.build.data.id).toBe(123); + }); + }); + + describe('#approveBuild()', () => { + it('calls reviewBuild with approve action', async () => { + spyOn(client, 'reviewBuild').and.returnValue(Promise.resolve({ success: true })); + + const result = await client.approveBuild('123', 'testuser', 'testkey'); + + expect(client.reviewBuild).toHaveBeenCalledWith('123', 'approve', 'testuser', 'testkey'); + expect(result).toEqual({ success: true }); + }); + + it('sends approve request to API', async () => { + await expectAsync(client.approveBuild('123', 'testuser', 'testkey')).toBeResolved(); + + expect(api.requests['/reviews'][0].body.data.attributes.action).toBe('approve'); + expect(api.requests['/reviews'][0].headers).toEqual( + jasmine.objectContaining({ + 'bstack-username': 'testuser', + 'bstack-access-key': 'testkey' + }) + ); + }); + + it('validates build ID', async () => { + await expectAsync(client.approveBuild(null, 'testuser', 'testkey')) + .toBeRejectedWithError('Missing build ID'); + }); + }); + + describe('#unapproveBuild()', () => { + it('calls reviewBuild with unapprove action', async () => { + spyOn(client, 'reviewBuild').and.returnValue(Promise.resolve({ success: true })); + + const result = await client.unapproveBuild('456', 'testuser', 'testkey'); + + expect(client.reviewBuild).toHaveBeenCalledWith('456', 'unapprove', 'testuser', 'testkey'); + expect(result).toEqual({ success: true }); + }); + + it('sends unapprove request to API', async () => { + await expectAsync(client.unapproveBuild('456', 'testuser', 'testkey')).toBeResolved(); + + expect(api.requests['/reviews'][0].body.data.attributes.action).toBe('unapprove'); + expect(api.requests['/reviews'][0].headers).toEqual( + jasmine.objectContaining({ + 'bstack-username': 'testuser', + 'bstack-access-key': 'testkey' + }) + ); + }); + + it('validates build ID', async () => { + await expectAsync(client.unapproveBuild('', 'testuser', 'testkey')) + .toBeRejectedWithError('Missing build ID'); + }); + }); + + describe('#rejectBuild()', () => { + it('calls reviewBuild with reject action', async () => { + spyOn(client, 'reviewBuild').and.returnValue(Promise.resolve({ success: true })); + + const result = await client.rejectBuild('789', 'testuser', 'testkey'); + + expect(client.reviewBuild).toHaveBeenCalledWith('789', 'reject', 'testuser', 'testkey'); + expect(result).toEqual({ success: true }); + }); + + it('sends reject request to API', async () => { + await expectAsync(client.rejectBuild('789', 'testuser', 'testkey')).toBeResolved(); + + expect(api.requests['/reviews'][0].body.data.attributes.action).toBe('reject'); + expect(api.requests['/reviews'][0].headers).toEqual( + jasmine.objectContaining({ + 'bstack-username': 'testuser', + 'bstack-access-key': 'testkey' + }) + ); + }); + + it('validates build ID', async () => { + await expectAsync(client.rejectBuild({}, 'testuser', 'testkey')) + .toBeRejectedWithError('Invalid build ID'); + }); + }); }); From c5370d32f1d536bbe622da015431a1d216579e2f Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 16 Jul 2025 16:54:10 +0530 Subject: [PATCH 10/18] refactoring logging for review command --- packages/cli-build/src/approve.js | 26 +++++++++--- packages/cli-build/src/reject.js | 26 +++++++++--- packages/cli-build/src/unapprove.js | 26 +++++++++--- packages/cli-build/src/utils.js | 2 +- packages/cli-build/test/approve.test.js | 49 ++++++++++++++++------- packages/cli-build/test/reject.test.js | 49 ++++++++++++++++------- packages/cli-build/test/unapprove.test.js | 49 ++++++++++++++++------- 7 files changed, 163 insertions(+), 64 deletions(-) diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index 7083c2dbd..cc01961d1 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -1,5 +1,5 @@ import command from '@percy/cli-command'; -import { validateCredentials } from './utils.js'; +import { fetchCredentials } from './utils.js'; /** * Approve command definition for Percy builds @@ -43,13 +43,13 @@ export const approve = command('approve', { } // Validate and get authentication credentials - const { username, accessKey } = validateCredentials(flags); + const { username, accessKey } = fetchCredentials(flags); if (!username || !accessKey) { exit(1, 'Username and access key are required to approve builds.'); } - log.info('Approving build...'); + log.info(`Approving build ${args.buildId}...`); try { // Call the Percy API to approve the build @@ -59,10 +59,24 @@ export const approve = command('approve', { accessKey ); - log.debug(`Build approved successfully: ${JSON.stringify(buildApprovalResponse)}`); - // To add Approved by name here once that changes are deployed from API - log.info('Build approved successfully'); + // Mocking the response for testing purposes + // The API changes are not implemented yet, so we simulate the response + // This will be removed before merging + if (!buildApprovalResponse.data.attributes['latest-action-performed-by']) { + buildApprovalResponse.data.attributes['latest-action-performed-by'] = { + user_email: 'moin@test.com', + user_name: 'moin' + }; + } + + const approvedBy = buildApprovalResponse.data.attributes['latest-action-performed-by'] || { + user_email: 'unknown@example.com', + user_name: username + }; + log.info(`Build ${args.buildId} approved successfully!`); + log.info(`Approved by: ${approvedBy.user_name} (${approvedBy.user_email})`); } catch (error) { + log.error(`Failed to approve build ${args.buildId}`); log.error(error); // Provide user-friendly error message diff --git a/packages/cli-build/src/reject.js b/packages/cli-build/src/reject.js index 458193f6f..189fdfd0d 100644 --- a/packages/cli-build/src/reject.js +++ b/packages/cli-build/src/reject.js @@ -1,5 +1,5 @@ import command from '@percy/cli-command'; -import { validateCredentials } from './utils.js'; +import { fetchCredentials } from './utils.js'; /** * Reject command definition for Percy builds @@ -43,13 +43,13 @@ export const reject = command('reject', { } // Validate and get authentication credentials - const { username, accessKey } = validateCredentials(flags); + const { username, accessKey } = fetchCredentials(flags); if (!username || !accessKey) { exit(1, 'Username and access key are required to reject builds.'); } - log.info('Rejecting build...'); + log.info(`Rejecting build ${args.buildId}...`); try { // Call the Percy API to reject the build @@ -59,10 +59,24 @@ export const reject = command('reject', { accessKey ); - log.debug(`Build rejected successfully: ${JSON.stringify(buildRejectionResponse)}`); - // To add Rejected by name here once that changes are deployed from API - log.info('Build rejected successfully'); + // Mocking the response for testing purposes + // The API changes are not implemented yet, so we simulate the response + // This will be removed before merging + if (!buildRejectionResponse.data.attributes['latest-action-performed-by']) { + buildRejectionResponse.data.attributes['latest-action-performed-by'] = { + user_email: 'moin@test.com', + user_name: 'moin' + }; + } + + const rejectedBy = buildRejectionResponse.data.attributes['latest-action-performed-by'] || { + user_email: 'unknown@example.com', + user_name: username + }; + log.info(`Build ${args.buildId} rejected successfully!`); + log.info(`Rejected by: ${rejectedBy.user_name} (${rejectedBy.user_email})`); } catch (error) { + log.error(`Failed to reject build ${args.buildId}`); log.error(error); // Provide user-friendly error message diff --git a/packages/cli-build/src/unapprove.js b/packages/cli-build/src/unapprove.js index e9bb79254..2fc3c18fc 100644 --- a/packages/cli-build/src/unapprove.js +++ b/packages/cli-build/src/unapprove.js @@ -1,5 +1,5 @@ import command from '@percy/cli-command'; -import { validateCredentials } from './utils.js'; +import { fetchCredentials } from './utils.js'; /** * Unapprove command definition for Percy builds @@ -43,13 +43,13 @@ export const unapprove = command('unapprove', { } // Validate and get authentication credentials - const { username, accessKey } = validateCredentials(flags); + const { username, accessKey } = fetchCredentials(flags); if (!username || !accessKey) { exit(1, 'Username and access key are required to unapprove builds.'); } - log.info('Unapproving build...'); + log.info(`Unapproving build ${args.buildId}...`); try { // Call the Percy API to unapprove the build @@ -59,10 +59,24 @@ export const unapprove = command('unapprove', { accessKey ); - log.debug(`Build unapproved successfully: ${JSON.stringify(buildUnapprovalResponse)}`); - // To add unApproved by name here once that changes are deployed from API - log.info('Build unapproved successfully'); + // Mocking the response for testing purposes + // The API changes are not implemented yet, so we simulate the response + // This will be removed before merging + if (!buildUnapprovalResponse.data.attributes['latest-action-performed-by']) { + buildUnapprovalResponse.data.attributes['latest-action-performed-by'] = { + user_email: 'moin@test.com', + user_name: 'moin' + }; + } + + const unapprovedBy = buildUnapprovalResponse.data.attributes['latest-action-performed-by'] || { + user_email: 'unknown@example.com', + user_name: username + }; + log.info(`Build ${args.buildId} unapproved successfully!`); + log.info(`Unapproved by: ${unapprovedBy.user_name} (${unapprovedBy.user_email})`); } catch (error) { + log.error(`Failed to unapprove build ${args.buildId}`); log.error(error); // Provide user-friendly error message diff --git a/packages/cli-build/src/utils.js b/packages/cli-build/src/utils.js index 35a6a6ba3..99820a14e 100644 --- a/packages/cli-build/src/utils.js +++ b/packages/cli-build/src/utils.js @@ -11,7 +11,7 @@ const ENV_VARS = { * @param {Object} flags - Command flags object * @returns {Object} Validated credentials object */ -export function validateCredentials(flags) { +export function fetchCredentials(flags) { // Use flags if provided, otherwise fallback to environment variables const username = flags.username || process.env[ENV_VARS.BROWSERSTACK_USERNAME]; const accessKey = flags.accessKey || process.env[ENV_VARS.BROWSERSTACK_ACCESS_KEY]; diff --git a/packages/cli-build/test/approve.test.js b/packages/cli-build/test/approve.test.js index ae5b06707..63bd23f48 100644 --- a/packages/cli-build/test/approve.test.js +++ b/packages/cli-build/test/approve.test.js @@ -3,6 +3,18 @@ import api from '@percy/client/test/helpers'; import { approve } from '@percy/cli-build'; describe('percy build:approve', () => { + let successResponse = { + data: { + attributes: { + action: 'approve', + 'latest-action-performed-by': { + user_email: 'test@test.com', + user_name: 'testuser' + } + } + } + }; + beforeEach(async () => { await setupTest(); }); @@ -96,15 +108,16 @@ describe('percy build:approve', () => { }); expect(req.headers['bstack-username']).toEqual('env-username'); expect(req.headers['bstack-access-key']).toEqual('env-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await approve(['123']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Approving build...', - '[percy] Build approved successfully' + '[percy] Approving build 123...', + '[percy] Build 123 approved successfully!', + '[percy] Approved by: testuser (test@test.com)' ]); }); @@ -113,14 +126,15 @@ describe('percy build:approve', () => { process.env.BROWSERSTACK_USERNAME = 'env-username'; process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; - api.reply('/reviews', (req) => [200, { success: true }]); + api.reply('/reviews', (req) => [200, successResponse]); await approve(['123']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Approving build...', - '[percy] Build approved successfully' + '[percy] Approving build 123...', + '[percy] Build 123 approved successfully!', + '[percy] Approved by: testuser (test@test.com)' ]); }); @@ -133,15 +147,16 @@ describe('percy build:approve', () => { api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('flag-username'); expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await approve(['123', '--username=flag-username', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Approving build...', - '[percy] Build approved successfully' + '[percy] Approving build 123...', + '[percy] Build 123 approved successfully!', + '[percy] Approved by: testuser (test@test.com)' ]); }); @@ -154,15 +169,16 @@ describe('percy build:approve', () => { api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('env-username'); expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await approve(['123', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Approving build...', - '[percy] Build approved successfully' + '[percy] Approving build 123...', + '[percy] Build 123 approved successfully!', + '[percy] Approved by: testuser (test@test.com)' ]); }); @@ -191,15 +207,16 @@ describe('percy build:approve', () => { }); expect(req.headers['bstack-username']).toEqual('flag-username'); expect(req.headers['bstack-access-key']).toEqual('env-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await approve(['123', '--username=flag-username']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Approving build...', - '[percy] Build approved successfully' + '[percy] Approving build 123...', + '[percy] Build 123 approved successfully!', + '[percy] Approved by: testuser (test@test.com)' ]); }); @@ -234,6 +251,7 @@ describe('percy build:approve', () => { await expectAsync(approve(['123'])).toBeRejected(); expect(logger.stderr).toEqual([ + '[percy] Failed to approve build 123', '[percy] Error: Unauthorized', '[percy] Error: Failed to approve the build' ]); @@ -268,6 +286,7 @@ describe('percy build:approve', () => { await expectAsync(approve(['123'])).toBeRejected(); expect(logger.stderr).toEqual([ + '[percy] Failed to approve build 123', '[percy] Error: Forbidden', '[percy] Error: Failed to approve the build' ]); diff --git a/packages/cli-build/test/reject.test.js b/packages/cli-build/test/reject.test.js index 827f788d7..c8bd66b62 100644 --- a/packages/cli-build/test/reject.test.js +++ b/packages/cli-build/test/reject.test.js @@ -3,6 +3,18 @@ import api from '@percy/client/test/helpers'; import { reject } from '@percy/cli-build'; describe('percy build:reject', () => { + let successResponse = { + data: { + attributes: { + action: 'approve', + 'latest-action-performed-by': { + user_email: 'test@test.com', + user_name: 'testuser' + } + } + } + }; + beforeEach(async () => { await setupTest(); }); @@ -96,15 +108,16 @@ describe('percy build:reject', () => { }); expect(req.headers['bstack-username']).toEqual('env-username'); expect(req.headers['bstack-access-key']).toEqual('env-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await reject(['123']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Rejecting build...', - '[percy] Build rejected successfully' + '[percy] Rejecting build 123...', + '[percy] Build 123 rejected successfully!', + '[percy] Rejected by: testuser (test@test.com)' ]); }); @@ -113,14 +126,15 @@ describe('percy build:reject', () => { process.env.BROWSERSTACK_USERNAME = 'env-username'; process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; - api.reply('/reviews', (req) => [200, { success: true }]); + api.reply('/reviews', (req) => [200, successResponse]); await reject(['123']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Rejecting build...', - '[percy] Build rejected successfully' + '[percy] Rejecting build 123...', + '[percy] Build 123 rejected successfully!', + '[percy] Rejected by: testuser (test@test.com)' ]); }); @@ -133,15 +147,16 @@ describe('percy build:reject', () => { api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('flag-username'); expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await reject(['123', '--username=flag-username', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Rejecting build...', - '[percy] Build rejected successfully' + '[percy] Rejecting build 123...', + '[percy] Build 123 rejected successfully!', + '[percy] Rejected by: testuser (test@test.com)' ]); }); @@ -154,15 +169,16 @@ describe('percy build:reject', () => { api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('env-username'); expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await reject(['123', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Rejecting build...', - '[percy] Build rejected successfully' + '[percy] Rejecting build 123...', + '[percy] Build 123 rejected successfully!', + '[percy] Rejected by: testuser (test@test.com)' ]); }); @@ -191,15 +207,16 @@ describe('percy build:reject', () => { }); expect(req.headers['bstack-username']).toEqual('flag-username'); expect(req.headers['bstack-access-key']).toEqual('env-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await reject(['123', '--username=flag-username']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Rejecting build...', - '[percy] Build rejected successfully' + '[percy] Rejecting build 123...', + '[percy] Build 123 rejected successfully!', + '[percy] Rejected by: testuser (test@test.com)' ]); }); @@ -234,6 +251,7 @@ describe('percy build:reject', () => { await expectAsync(reject(['123'])).toBeRejected(); expect(logger.stderr).toEqual([ + '[percy] Failed to reject build 123', '[percy] Error: Unauthorized', '[percy] Error: Failed to reject the build' ]); @@ -268,6 +286,7 @@ describe('percy build:reject', () => { await expectAsync(reject(['123'])).toBeRejected(); expect(logger.stderr).toEqual([ + '[percy] Failed to reject build 123', '[percy] Error: Forbidden', '[percy] Error: Failed to reject the build' ]); diff --git a/packages/cli-build/test/unapprove.test.js b/packages/cli-build/test/unapprove.test.js index 2a5d19cf8..e900c6e8f 100644 --- a/packages/cli-build/test/unapprove.test.js +++ b/packages/cli-build/test/unapprove.test.js @@ -3,6 +3,18 @@ import api from '@percy/client/test/helpers'; import { unapprove } from '@percy/cli-build'; describe('percy build:unapprove', () => { + let successResponse = { + data: { + attributes: { + action: 'unapprove', + 'latest-action-performed-by': { + user_email: 'test@test.com', + user_name: 'testuser' + } + } + } + }; + beforeEach(async () => { await setupTest(); }); @@ -96,15 +108,16 @@ describe('percy build:unapprove', () => { }); expect(req.headers['bstack-username']).toEqual('env-username'); expect(req.headers['bstack-access-key']).toEqual('env-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await unapprove(['123']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Unapproving build...', - '[percy] Build unapproved successfully' + '[percy] Unapproving build 123...', + '[percy] Build 123 unapproved successfully!', + '[percy] Unapproved by: testuser (test@test.com)' ]); }); @@ -113,14 +126,15 @@ describe('percy build:unapprove', () => { process.env.BROWSERSTACK_USERNAME = 'env-username'; process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; - api.reply('/reviews', (req) => [200, { success: true }]); + api.reply('/reviews', (req) => [200, successResponse]); await unapprove(['123']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Unapproving build...', - '[percy] Build unapproved successfully' + '[percy] Unapproving build 123...', + '[percy] Build 123 unapproved successfully!', + '[percy] Unapproved by: testuser (test@test.com)' ]); }); @@ -133,15 +147,16 @@ describe('percy build:unapprove', () => { api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('flag-username'); expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await unapprove(['123', '--username=flag-username', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Unapproving build...', - '[percy] Build unapproved successfully' + '[percy] Unapproving build 123...', + '[percy] Build 123 unapproved successfully!', + '[percy] Unapproved by: testuser (test@test.com)' ]); }); @@ -154,15 +169,16 @@ describe('percy build:unapprove', () => { api.reply('/reviews', (req) => { expect(req.headers['bstack-username']).toEqual('env-username'); expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await unapprove(['123', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Unapproving build...', - '[percy] Build unapproved successfully' + '[percy] Unapproving build 123...', + '[percy] Build 123 unapproved successfully!', + '[percy] Unapproved by: testuser (test@test.com)' ]); }); @@ -191,15 +207,16 @@ describe('percy build:unapprove', () => { }); expect(req.headers['bstack-username']).toEqual('flag-username'); expect(req.headers['bstack-access-key']).toEqual('env-access-key'); - return [200, { success: true }]; + return [200, successResponse]; }); await unapprove(['123', '--username=flag-username']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ - '[percy] Unapproving build...', - '[percy] Build unapproved successfully' + '[percy] Unapproving build 123...', + '[percy] Build 123 unapproved successfully!', + '[percy] Unapproved by: testuser (test@test.com)' ]); }); @@ -234,6 +251,7 @@ describe('percy build:unapprove', () => { await expectAsync(unapprove(['123'])).toBeRejected(); expect(logger.stderr).toEqual([ + '[percy] Failed to unapprove build 123', '[percy] Error: Unauthorized', '[percy] Error: Failed to unapprove the build' ]); @@ -268,6 +286,7 @@ describe('percy build:unapprove', () => { await expectAsync(unapprove(['123'])).toBeRejected(); expect(logger.stderr).toEqual([ + '[percy] Failed to unapprove build 123', '[percy] Error: Forbidden', '[percy] Error: Failed to unapprove the build' ]); From e71ff9a3e8bc97c63e593762600910cdf83cf2d7 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 16 Jul 2025 17:06:02 +0530 Subject: [PATCH 11/18] adding temp test for code coverage --- packages/cli-build/test/approve.test.js | 22 ++++++++++++++++++++++ packages/cli-build/test/reject.test.js | 22 ++++++++++++++++++++++ packages/cli-build/test/unapprove.test.js | 22 ++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/packages/cli-build/test/approve.test.js b/packages/cli-build/test/approve.test.js index 63bd23f48..e82f12dab 100644 --- a/packages/cli-build/test/approve.test.js +++ b/packages/cli-build/test/approve.test.js @@ -291,4 +291,26 @@ describe('percy build:approve', () => { '[percy] Error: Failed to approve the build' ]); }); + + it('temp test - adding it for covergae will remove it before merging', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + + api.reply('/reviews', (req) => [200, { + data: { + attributes: { + action: 'approve' + } + } + }] + ); + + await approve(['123', '--username=flag-username', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Approving build 123...', + '[percy] Build 123 approved successfully!', + '[percy] Approved by: moin (moin@test.com)' + ]); + }); }); diff --git a/packages/cli-build/test/reject.test.js b/packages/cli-build/test/reject.test.js index c8bd66b62..afb69754b 100644 --- a/packages/cli-build/test/reject.test.js +++ b/packages/cli-build/test/reject.test.js @@ -291,4 +291,26 @@ describe('percy build:reject', () => { '[percy] Error: Failed to reject the build' ]); }); + + it('temp test - adding it for covergae will remove it before merging', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + + api.reply('/reviews', (req) => [200, { + data: { + attributes: { + action: 'reject' + } + } + }] + ); + + await reject(['123', '--username=flag-username', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Rejecting build 123...', + '[percy] Build 123 rejected successfully!', + '[percy] Rejected by: moin (moin@test.com)' + ]); + }); }); diff --git a/packages/cli-build/test/unapprove.test.js b/packages/cli-build/test/unapprove.test.js index e900c6e8f..f1ed19028 100644 --- a/packages/cli-build/test/unapprove.test.js +++ b/packages/cli-build/test/unapprove.test.js @@ -291,4 +291,26 @@ describe('percy build:unapprove', () => { '[percy] Error: Failed to unapprove the build' ]); }); + + it('temp test - adding it for covergae will remove it before merging', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + + api.reply('/reviews', (req) => [200, { + data: { + attributes: { + action: 'unapprove' + } + } + }] + ); + + await unapprove(['123', '--username=flag-username', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Unapproving build 123...', + '[percy] Build 123 unapproved successfully!', + '[percy] Unapproved by: moin (moin@test.com)' + ]); + }); }); From c2d1e50c0d43d8e23949ac0806afb247daa6992d Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 16 Jul 2025 17:33:51 +0530 Subject: [PATCH 12/18] fixing test for code coverage --- packages/cli-build/src/approve.js | 18 +++++++++--------- packages/cli-build/src/reject.js | 18 +++++++++--------- packages/cli-build/src/unapprove.js | 18 +++++++++--------- packages/cli-build/test/approve.test.js | 6 +++--- packages/cli-build/test/reject.test.js | 6 +++--- packages/cli-build/test/unapprove.test.js | 6 +++--- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index cc01961d1..c801ea36c 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -59,15 +59,15 @@ export const approve = command('approve', { accessKey ); - // Mocking the response for testing purposes - // The API changes are not implemented yet, so we simulate the response - // This will be removed before merging - if (!buildApprovalResponse.data.attributes['latest-action-performed-by']) { - buildApprovalResponse.data.attributes['latest-action-performed-by'] = { - user_email: 'moin@test.com', - user_name: 'moin' - }; - } + // // Mocking the response for testing purposes + // // The API changes are not implemented yet, so we simulate the response + // // This will be removed before merging + // if (!buildApprovalResponse.data.attributes['latest-action-performed-by']) { + // buildApprovalResponse.data.attributes['latest-action-performed-by'] = { + // user_email: 'moin@test.com', + // user_name: 'moin' + // }; + // } const approvedBy = buildApprovalResponse.data.attributes['latest-action-performed-by'] || { user_email: 'unknown@example.com', diff --git a/packages/cli-build/src/reject.js b/packages/cli-build/src/reject.js index 189fdfd0d..0ec3b6d81 100644 --- a/packages/cli-build/src/reject.js +++ b/packages/cli-build/src/reject.js @@ -59,15 +59,15 @@ export const reject = command('reject', { accessKey ); - // Mocking the response for testing purposes - // The API changes are not implemented yet, so we simulate the response - // This will be removed before merging - if (!buildRejectionResponse.data.attributes['latest-action-performed-by']) { - buildRejectionResponse.data.attributes['latest-action-performed-by'] = { - user_email: 'moin@test.com', - user_name: 'moin' - }; - } + // // Mocking the response for testing purposes + // // The API changes are not implemented yet, so we simulate the response + // // This will be removed before merging + // if (!buildRejectionResponse.data.attributes['latest-action-performed-by']) { + // buildRejectionResponse.data.attributes['latest-action-performed-by'] = { + // user_email: 'moin@test.com', + // user_name: 'moin' + // }; + // } const rejectedBy = buildRejectionResponse.data.attributes['latest-action-performed-by'] || { user_email: 'unknown@example.com', diff --git a/packages/cli-build/src/unapprove.js b/packages/cli-build/src/unapprove.js index 2fc3c18fc..f047ec285 100644 --- a/packages/cli-build/src/unapprove.js +++ b/packages/cli-build/src/unapprove.js @@ -59,15 +59,15 @@ export const unapprove = command('unapprove', { accessKey ); - // Mocking the response for testing purposes - // The API changes are not implemented yet, so we simulate the response - // This will be removed before merging - if (!buildUnapprovalResponse.data.attributes['latest-action-performed-by']) { - buildUnapprovalResponse.data.attributes['latest-action-performed-by'] = { - user_email: 'moin@test.com', - user_name: 'moin' - }; - } + // // Mocking the response for testing purposes + // // The API changes are not implemented yet, so we simulate the response + // // This will be removed before merging + // if (!buildUnapprovalResponse.data.attributes['latest-action-performed-by']) { + // buildUnapprovalResponse.data.attributes['latest-action-performed-by'] = { + // user_email: 'moin@test.com', + // user_name: 'moin' + // }; + // } const unapprovedBy = buildUnapprovalResponse.data.attributes['latest-action-performed-by'] || { user_email: 'unknown@example.com', diff --git a/packages/cli-build/test/approve.test.js b/packages/cli-build/test/approve.test.js index e82f12dab..ce77ccbf5 100644 --- a/packages/cli-build/test/approve.test.js +++ b/packages/cli-build/test/approve.test.js @@ -292,7 +292,7 @@ describe('percy build:approve', () => { ]); }); - it('temp test - adding it for covergae will remove it before merging', async () => { + it('uses fallback user info when latest-action-performed-by is not in response', async () => { process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); api.reply('/reviews', (req) => [200, { @@ -304,13 +304,13 @@ describe('percy build:approve', () => { }] ); - await approve(['123', '--username=flag-username', '--access-key=flag-access-key']); + await approve(['123', '--username=temp-username', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ '[percy] Approving build 123...', '[percy] Build 123 approved successfully!', - '[percy] Approved by: moin (moin@test.com)' + '[percy] Approved by: temp-username (unknown@example.com)' ]); }); }); diff --git a/packages/cli-build/test/reject.test.js b/packages/cli-build/test/reject.test.js index afb69754b..ad23e9d9a 100644 --- a/packages/cli-build/test/reject.test.js +++ b/packages/cli-build/test/reject.test.js @@ -292,7 +292,7 @@ describe('percy build:reject', () => { ]); }); - it('temp test - adding it for covergae will remove it before merging', async () => { + it('uses fallback user info when latest-action-performed-by is not in response', async () => { process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); api.reply('/reviews', (req) => [200, { @@ -304,13 +304,13 @@ describe('percy build:reject', () => { }] ); - await reject(['123', '--username=flag-username', '--access-key=flag-access-key']); + await reject(['123', '--username=temp-username', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ '[percy] Rejecting build 123...', '[percy] Build 123 rejected successfully!', - '[percy] Rejected by: moin (moin@test.com)' + '[percy] Rejected by: temp-username (unknown@example.com)' ]); }); }); diff --git a/packages/cli-build/test/unapprove.test.js b/packages/cli-build/test/unapprove.test.js index f1ed19028..11895e651 100644 --- a/packages/cli-build/test/unapprove.test.js +++ b/packages/cli-build/test/unapprove.test.js @@ -292,7 +292,7 @@ describe('percy build:unapprove', () => { ]); }); - it('temp test - adding it for covergae will remove it before merging', async () => { + it('uses fallback user info when latest-action-performed-by is not in response', async () => { process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); api.reply('/reviews', (req) => [200, { @@ -304,13 +304,13 @@ describe('percy build:unapprove', () => { }] ); - await unapprove(['123', '--username=flag-username', '--access-key=flag-access-key']); + await unapprove(['123', '--username=temp-username', '--access-key=flag-access-key']); expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ '[percy] Unapproving build 123...', '[percy] Build 123 unapproved successfully!', - '[percy] Unapproved by: moin (moin@test.com)' + '[percy] Unapproved by: temp-username (unknown@example.com)' ]); }); }); From 7577d053db1441bb1a1df9813ddd8726bfa9a630 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 16 Jul 2025 22:04:48 +0530 Subject: [PATCH 13/18] updating sending on creds --- packages/cli-build/src/approve.js | 12 +----------- packages/cli-build/src/reject.js | 12 +----------- packages/cli-build/src/unapprove.js | 12 +----------- packages/client/src/client.js | 21 +++++++++++---------- 4 files changed, 14 insertions(+), 43 deletions(-) diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index c801ea36c..5b3995aff 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -59,17 +59,7 @@ export const approve = command('approve', { accessKey ); - // // Mocking the response for testing purposes - // // The API changes are not implemented yet, so we simulate the response - // // This will be removed before merging - // if (!buildApprovalResponse.data.attributes['latest-action-performed-by']) { - // buildApprovalResponse.data.attributes['latest-action-performed-by'] = { - // user_email: 'moin@test.com', - // user_name: 'moin' - // }; - // } - - const approvedBy = buildApprovalResponse.data.attributes['latest-action-performed-by'] || { + const approvedBy = buildApprovalResponse.data.attributes['action-performed-by'] || { user_email: 'unknown@example.com', user_name: username }; diff --git a/packages/cli-build/src/reject.js b/packages/cli-build/src/reject.js index 0ec3b6d81..faf94ed6e 100644 --- a/packages/cli-build/src/reject.js +++ b/packages/cli-build/src/reject.js @@ -59,17 +59,7 @@ export const reject = command('reject', { accessKey ); - // // Mocking the response for testing purposes - // // The API changes are not implemented yet, so we simulate the response - // // This will be removed before merging - // if (!buildRejectionResponse.data.attributes['latest-action-performed-by']) { - // buildRejectionResponse.data.attributes['latest-action-performed-by'] = { - // user_email: 'moin@test.com', - // user_name: 'moin' - // }; - // } - - const rejectedBy = buildRejectionResponse.data.attributes['latest-action-performed-by'] || { + const rejectedBy = buildRejectionResponse.data.attributes['action-performed-by'] || { user_email: 'unknown@example.com', user_name: username }; diff --git a/packages/cli-build/src/unapprove.js b/packages/cli-build/src/unapprove.js index f047ec285..deb82d66f 100644 --- a/packages/cli-build/src/unapprove.js +++ b/packages/cli-build/src/unapprove.js @@ -59,17 +59,7 @@ export const unapprove = command('unapprove', { accessKey ); - // // Mocking the response for testing purposes - // // The API changes are not implemented yet, so we simulate the response - // // This will be removed before merging - // if (!buildUnapprovalResponse.data.attributes['latest-action-performed-by']) { - // buildUnapprovalResponse.data.attributes['latest-action-performed-by'] = { - // user_email: 'moin@test.com', - // user_name: 'moin' - // }; - // } - - const unapprovedBy = buildUnapprovalResponse.data.attributes['latest-action-performed-by'] || { + const unapprovedBy = buildUnapprovalResponse.data.attributes['action-performed-by'] || { user_email: 'unknown@example.com', user_name: username }; diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 1a34bd50f..a6011013d 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -133,9 +133,9 @@ export class PercyClient { // Returns common headers used for each request with additional // headers. Throws an error when the token is missing, which is a required // authorization header. - headers(headers, projectTokenRequired = true) { + headers(headers, raiseIfMissing = true) { return Object.assign({ - Authorization: `Token token=${this.getToken(projectTokenRequired)}`, + Authorization: `Token token=${this.getToken(raiseIfMissing)}`, 'User-Agent': this.userAgent() }, headers); } @@ -154,13 +154,13 @@ export class PercyClient { } // Performs a POST request to a JSON API endpoint with appropriate headers. - post(path, body = {}, { ...meta } = {}, customHeaders = {}, projectTokenRequired = true) { + post(path, body = {}, { ...meta } = {}, customHeaders = {}, raiseIfMissing = true) { return logger.measure('client:post', meta.identifier || 'Unknown', meta, () => { return request(`${this.apiUrl}/${path}`, { headers: this.headers({ 'Content-Type': 'application/vnd.api+json', ...customHeaders - }, projectTokenRequired), + }, raiseIfMissing), method: 'POST', body, meta @@ -719,11 +719,6 @@ export class PercyClient { validateId('build', buildId); this.log.debug(`Sending ${action} action for build ${buildId}...`); - const customHeaders = { - 'bstack-username': username, - 'bstack-access-key': accessKey - }; - const requestBody = { data: { attributes: { @@ -743,7 +738,13 @@ export class PercyClient { // For the review action, we use accessKey and username in custom headers // and do not require a project token. - return this.post('reviews', requestBody, { identifier: `build.${action}` }, customHeaders, false); + return this.post( + 'reviews', + requestBody, + { identifier: `build.${action}` }, + { Authorization: `Basic ${base64encode(`${username}:${accessKey}`)}` }, + false + ); } async approveBuild(buildId, username, accessKey) { From 3d790684fc90f4ab32764105997c0cd5ead5fcc9 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 16 Jul 2025 22:18:11 +0530 Subject: [PATCH 14/18] refactoring --- packages/cli-build/src/approve.js | 32 ++------------------------- packages/cli-build/src/reject.js | 32 ++------------------------- packages/cli-build/src/unapprove.js | 32 ++------------------------- packages/cli-build/src/utils.js | 34 +++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 90 deletions(-) diff --git a/packages/cli-build/src/approve.js b/packages/cli-build/src/approve.js index 5b3995aff..afcd95495 100644 --- a/packages/cli-build/src/approve.js +++ b/packages/cli-build/src/approve.js @@ -1,5 +1,5 @@ import command from '@percy/cli-command'; -import { fetchCredentials } from './utils.js'; +import { fetchCredentials, reviewCommandConfig } from './utils.js'; /** * Approve command definition for Percy builds @@ -7,35 +7,7 @@ import { fetchCredentials } from './utils.js'; */ export const approve = command('approve', { description: 'Approve Percy builds', - - args: [ - { - name: 'build-id', - description: 'Build ID to approve', - type: 'id', - required: true - } - ], - - flags: [ - { - name: 'username', - description: 'Username for authentication (can also be set via BROWSERSTACK_USERNAME env var)', - type: 'string' - }, - { - name: 'access-key', - description: 'Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY env var)', - type: 'string' - } - ], - - examples: [ - '$0 ', - '$0 --username username --access-key **key**' - ], - - percy: true + ...reviewCommandConfig }, async ({ flags, args, percy, log, exit }) => { // Early return if Percy is disabled if (!percy) { diff --git a/packages/cli-build/src/reject.js b/packages/cli-build/src/reject.js index faf94ed6e..1d8fc1325 100644 --- a/packages/cli-build/src/reject.js +++ b/packages/cli-build/src/reject.js @@ -1,5 +1,5 @@ import command from '@percy/cli-command'; -import { fetchCredentials } from './utils.js'; +import { fetchCredentials, reviewCommandConfig } from './utils.js'; /** * Reject command definition for Percy builds @@ -7,35 +7,7 @@ import { fetchCredentials } from './utils.js'; */ export const reject = command('reject', { description: 'Reject Percy builds', - - args: [ - { - name: 'build-id', - description: 'Build ID to reject', - type: 'id', - required: true - } - ], - - flags: [ - { - name: 'username', - description: 'Username for authentication (can also be set via BROWSERSTACK_USERNAME env var)', - type: 'string' - }, - { - name: 'access-key', - description: 'Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY env var)', - type: 'string' - } - ], - - examples: [ - '$0 ', - '$0 --username username --access-key **key**' - ], - - percy: true + ...reviewCommandConfig }, async ({ flags, args, percy, log, exit }) => { // Early return if Percy is disabled if (!percy) { diff --git a/packages/cli-build/src/unapprove.js b/packages/cli-build/src/unapprove.js index deb82d66f..207d9d6fa 100644 --- a/packages/cli-build/src/unapprove.js +++ b/packages/cli-build/src/unapprove.js @@ -1,5 +1,5 @@ import command from '@percy/cli-command'; -import { fetchCredentials } from './utils.js'; +import { fetchCredentials, reviewCommandConfig } from './utils.js'; /** * Unapprove command definition for Percy builds @@ -7,35 +7,7 @@ import { fetchCredentials } from './utils.js'; */ export const unapprove = command('unapprove', { description: 'Unapprove Percy builds', - - args: [ - { - name: 'build-id', - description: 'Build ID to unapprove', - type: 'id', - required: true - } - ], - - flags: [ - { - name: 'username', - description: 'Username for authentication (can also be set via BROWSERSTACK_USERNAME env var)', - type: 'string' - }, - { - name: 'access-key', - description: 'Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY env var)', - type: 'string' - } - ], - - examples: [ - '$0 ', - '$0 --username username --access-key **key**' - ], - - percy: true + ...reviewCommandConfig }, async ({ flags, args, percy, log, exit }) => { // Early return if Percy is disabled if (!percy) { diff --git a/packages/cli-build/src/utils.js b/packages/cli-build/src/utils.js index 99820a14e..fd15b5387 100644 --- a/packages/cli-build/src/utils.js +++ b/packages/cli-build/src/utils.js @@ -18,3 +18,37 @@ export function fetchCredentials(flags) { return { username, accessKey }; } + +/** * Configuration for review commands (approve, reject, unapprove) + * Contains common arguments and flags used across these commands + */ +export const reviewCommandConfig = { + args: [ + { + name: 'build-id', + description: 'Build ID to approve', + type: 'id', + required: true + } + ], + + flags: [ + { + name: 'username', + description: 'Username for authentication (can also be set via BROWSERSTACK_USERNAME env var)', + type: 'string' + }, + { + name: 'access-key', + description: 'Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY env var)', + type: 'string' + } + ], + + examples: [ + '$0 ', + '$0 --username username --access-key **key**' + ], + + percy: true +}; From f76a6db0511e90da799183947ef9c77f2678a598 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 16 Jul 2025 22:18:59 +0530 Subject: [PATCH 15/18] updating readme --- packages/cli-build/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cli-build/README.md b/packages/cli-build/README.md index 0e460e384..a721652cb 100644 --- a/packages/cli-build/README.md +++ b/packages/cli-build/README.md @@ -90,8 +90,8 @@ Arguments: Options: --username Username for authentication (can also be set via BROWSERSTACK_USERNAME env var) - --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env - var) + --access-key Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY + env var) Global options: -v, --verbose Log everything @@ -114,13 +114,13 @@ Usage: $ percy build:unapprove [options] Arguments: - build-id Build ID to unapprove + build-id Build ID to approve Options: --username Username for authentication (can also be set via BROWSERSTACK_USERNAME env var) - --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env - var) + --access-key Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY + env var) Global options: -v, --verbose Log everything @@ -143,13 +143,13 @@ Usage: $ percy build:reject [options] Arguments: - build-id Build ID to reject + build-id Build ID to approve Options: --username Username for authentication (can also be set via BROWSERSTACK_USERNAME env var) - --access-key Access key for authentication (can also be set via PERCY_ACCESS_KEY env - var) + --access-key Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY + env var) Global options: -v, --verbose Log everything From 163b74773b4219676745ec24714041bbf9be1e7c Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 16 Jul 2025 23:11:25 +0530 Subject: [PATCH 16/18] fixing test --- packages/cli-build/test/approve.test.js | 18 +++++++----------- packages/cli-build/test/reject.test.js | 18 +++++++----------- packages/cli-build/test/unapprove.test.js | 18 +++++++----------- packages/client/test/client.test.js | 17 +++++------------ 4 files changed, 26 insertions(+), 45 deletions(-) diff --git a/packages/cli-build/test/approve.test.js b/packages/cli-build/test/approve.test.js index ce77ccbf5..3ba7e200e 100644 --- a/packages/cli-build/test/approve.test.js +++ b/packages/cli-build/test/approve.test.js @@ -1,4 +1,5 @@ import { logger, setupTest } from '@percy/cli-command/test/helpers'; +import { base64encode } from '@percy/client/utils'; import api from '@percy/client/test/helpers'; import { approve } from '@percy/cli-build'; @@ -7,7 +8,7 @@ describe('percy build:approve', () => { data: { attributes: { action: 'approve', - 'latest-action-performed-by': { + 'action-performed-by': { user_email: 'test@test.com', user_name: 'testuser' } @@ -106,8 +107,7 @@ describe('percy build:approve', () => { } } }); - expect(req.headers['bstack-username']).toEqual('env-username'); - expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('env-username:env-access-key')}`); return [200, successResponse]; }); @@ -145,8 +145,7 @@ describe('percy build:approve', () => { process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { - expect(req.headers['bstack-username']).toEqual('flag-username'); - expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('flag-username:flag-access-key')}`); return [200, successResponse]; }); @@ -167,8 +166,7 @@ describe('percy build:approve', () => { // Only access key from flag api.reply('/reviews', (req) => { - expect(req.headers['bstack-username']).toEqual('env-username'); - expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('env-username:flag-access-key')}`); return [200, successResponse]; }); @@ -205,8 +203,7 @@ describe('percy build:approve', () => { } } }); - expect(req.headers['bstack-username']).toEqual('flag-username'); - expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('flag-username:env-access-key')}`); return [200, successResponse]; }); @@ -243,8 +240,7 @@ describe('percy build:approve', () => { } } }); - expect(req.headers['bstack-username']).toEqual('invalid-username'); - expect(req.headers['bstack-access-key']).toEqual('invalid-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('invalid-username:invalid-access-key')}`); return [401, { errors: [{ detail: 'Unauthorized' }] }]; }); diff --git a/packages/cli-build/test/reject.test.js b/packages/cli-build/test/reject.test.js index ad23e9d9a..0602d6e3d 100644 --- a/packages/cli-build/test/reject.test.js +++ b/packages/cli-build/test/reject.test.js @@ -1,4 +1,5 @@ import { logger, setupTest } from '@percy/cli-command/test/helpers'; +import { base64encode } from '@percy/client/utils'; import api from '@percy/client/test/helpers'; import { reject } from '@percy/cli-build'; @@ -7,7 +8,7 @@ describe('percy build:reject', () => { data: { attributes: { action: 'approve', - 'latest-action-performed-by': { + 'action-performed-by': { user_email: 'test@test.com', user_name: 'testuser' } @@ -106,8 +107,7 @@ describe('percy build:reject', () => { } } }); - expect(req.headers['bstack-username']).toEqual('env-username'); - expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('env-username:env-access-key')}`); return [200, successResponse]; }); @@ -145,8 +145,7 @@ describe('percy build:reject', () => { process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { - expect(req.headers['bstack-username']).toEqual('flag-username'); - expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('flag-username:flag-access-key')}`); return [200, successResponse]; }); @@ -167,8 +166,7 @@ describe('percy build:reject', () => { // Only access key from flag api.reply('/reviews', (req) => { - expect(req.headers['bstack-username']).toEqual('env-username'); - expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('env-username:flag-access-key')}`); return [200, successResponse]; }); @@ -205,8 +203,7 @@ describe('percy build:reject', () => { } } }); - expect(req.headers['bstack-username']).toEqual('flag-username'); - expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('flag-username:env-access-key')}`); return [200, successResponse]; }); @@ -243,8 +240,7 @@ describe('percy build:reject', () => { } } }); - expect(req.headers['bstack-username']).toEqual('invalid-username'); - expect(req.headers['bstack-access-key']).toEqual('invalid-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('invalid-username:invalid-access-key')}`); return [401, { errors: [{ detail: 'Unauthorized' }] }]; }); diff --git a/packages/cli-build/test/unapprove.test.js b/packages/cli-build/test/unapprove.test.js index 11895e651..101fe978c 100644 --- a/packages/cli-build/test/unapprove.test.js +++ b/packages/cli-build/test/unapprove.test.js @@ -1,4 +1,5 @@ import { logger, setupTest } from '@percy/cli-command/test/helpers'; +import { base64encode } from '@percy/client/utils'; import api from '@percy/client/test/helpers'; import { unapprove } from '@percy/cli-build'; @@ -7,7 +8,7 @@ describe('percy build:unapprove', () => { data: { attributes: { action: 'unapprove', - 'latest-action-performed-by': { + 'action-performed-by': { user_email: 'test@test.com', user_name: 'testuser' } @@ -106,8 +107,7 @@ describe('percy build:unapprove', () => { } } }); - expect(req.headers['bstack-username']).toEqual('env-username'); - expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('env-username:env-access-key')}`); return [200, successResponse]; }); @@ -145,8 +145,7 @@ describe('percy build:unapprove', () => { process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; api.reply('/reviews', (req) => { - expect(req.headers['bstack-username']).toEqual('flag-username'); - expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('flag-username:flag-access-key')}`); return [200, successResponse]; }); @@ -167,8 +166,7 @@ describe('percy build:unapprove', () => { // Only access key from flag api.reply('/reviews', (req) => { - expect(req.headers['bstack-username']).toEqual('env-username'); - expect(req.headers['bstack-access-key']).toEqual('flag-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('env-username:flag-access-key')}`); return [200, successResponse]; }); @@ -205,8 +203,7 @@ describe('percy build:unapprove', () => { } } }); - expect(req.headers['bstack-username']).toEqual('flag-username'); - expect(req.headers['bstack-access-key']).toEqual('env-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('flag-username:env-access-key')}`); return [200, successResponse]; }); @@ -243,8 +240,7 @@ describe('percy build:unapprove', () => { } } }); - expect(req.headers['bstack-username']).toEqual('invalid-username'); - expect(req.headers['bstack-access-key']).toEqual('invalid-access-key'); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('invalid-username:invalid-access-key')}`); return [401, { errors: [{ detail: 'Unauthorized' }] }]; }); diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 21d1245f1..d2cbc3c73 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -2250,8 +2250,7 @@ describe('PercyClient', () => { expect(api.requests['/reviews'][0].method).toBe('POST'); expect(api.requests['/reviews'][0].headers).toEqual( jasmine.objectContaining({ - 'bstack-username': 'testuser', - 'bstack-access-key': 'testkey' + Authorization: `Basic ${base64encode('testuser:testkey')}` }) ); expect(api.requests['/reviews'][0].body).toEqual({ @@ -2281,10 +2280,7 @@ describe('PercyClient', () => { 'reviews', jasmine.any(Object), { identifier: 'build.reject' }, - { - 'bstack-username': 'testuser', - 'bstack-access-key': 'testkey' - }, + { Authorization: `Basic ${base64encode('testuser:testkey')}` }, false ); }); @@ -2346,8 +2342,7 @@ describe('PercyClient', () => { expect(api.requests['/reviews'][0].body.data.attributes.action).toBe('approve'); expect(api.requests['/reviews'][0].headers).toEqual( jasmine.objectContaining({ - 'bstack-username': 'testuser', - 'bstack-access-key': 'testkey' + Authorization: `Basic ${base64encode('testuser:testkey')}` }) ); }); @@ -2374,8 +2369,7 @@ describe('PercyClient', () => { expect(api.requests['/reviews'][0].body.data.attributes.action).toBe('unapprove'); expect(api.requests['/reviews'][0].headers).toEqual( jasmine.objectContaining({ - 'bstack-username': 'testuser', - 'bstack-access-key': 'testkey' + Authorization: `Basic ${base64encode('testuser:testkey')}` }) ); }); @@ -2402,8 +2396,7 @@ describe('PercyClient', () => { expect(api.requests['/reviews'][0].body.data.attributes.action).toBe('reject'); expect(api.requests['/reviews'][0].headers).toEqual( jasmine.objectContaining({ - 'bstack-username': 'testuser', - 'bstack-access-key': 'testkey' + Authorization: `Basic ${base64encode('testuser:testkey')}` }) ); }); From 75e9bf04635296f6135dadb747620cded6360085 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Thu, 17 Jul 2025 23:34:25 +0530 Subject: [PATCH 17/18] adding delete command --- packages/cli-build/README.md | 30 ++++ packages/cli-build/src/build.js | 3 +- packages/cli-build/src/delete.js | 44 +++++ packages/cli-build/src/index.js | 1 + packages/cli-build/test/delete.test.js | 221 +++++++++++++++++++++++++ packages/client/src/client.js | 15 ++ packages/client/test/client.test.js | 53 ++++++ 7 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 packages/cli-build/src/delete.js create mode 100644 packages/cli-build/test/delete.test.js diff --git a/packages/cli-build/README.md b/packages/cli-build/README.md index a721652cb..230f90c73 100644 --- a/packages/cli-build/README.md +++ b/packages/cli-build/README.md @@ -10,6 +10,7 @@ Commands for interacting with Percy builds * [`percy build:approve`](#percy-buildapprove) * [`percy build:unapprove`](#percy-buildunapprove) * [`percy build:reject`](#percy-buildreject) +* [`percy build:delete`](#percy-builddelete) ### `percy build:finalize` @@ -162,4 +163,33 @@ Examples: $ percy build:reject $ percy build:reject --username username --access-key **key** ``` + +### `percy build:delete` + +Delete Percy builds + +``` +Usage: + $ percy build:delete [options] + +Arguments: + build-id Build ID to approve + +Options: + --username Username for authentication (can also be set via BROWSERSTACK_USERNAME env + var) + --access-key Access key for authentication (can also be set via BROWSERSTACK_ACCESS_KEY + env var) + +Global options: + -v, --verbose Log everything + -q, --quiet Log errors only + -s, --silent Log nothing + -l, --labels Associates labels to the build (ex: --labels=dev,prod ) + -h, --help Display command help + +Examples: + $ percy build:delete + $ percy build:delete --username username --access-key **key** +``` diff --git a/packages/cli-build/src/build.js b/packages/cli-build/src/build.js index e8ed42827..8b4549086 100644 --- a/packages/cli-build/src/build.js +++ b/packages/cli-build/src/build.js @@ -5,10 +5,11 @@ import id from './id.js'; import approve from './approve.js'; import reject from './reject.js'; import unapprove from './unapprove.js'; +import deleteBuild from './delete.js'; export const build = command('build', { description: 'Finalize and wait on Percy builds', - commands: [finalize, wait, id, approve, unapprove, reject] + commands: [finalize, wait, id, approve, unapprove, reject, deleteBuild] }); export default build; diff --git a/packages/cli-build/src/delete.js b/packages/cli-build/src/delete.js new file mode 100644 index 000000000..7ae2f8abd --- /dev/null +++ b/packages/cli-build/src/delete.js @@ -0,0 +1,44 @@ +import command from '@percy/cli-command'; +import { fetchCredentials, reviewCommandConfig } from './utils.js'; + +/** + * Delete command definition for Percy builds + * Allows users to delete builds using build ID and authentication credentials + */ +export const deleteBuild = command('delete', { + description: 'Delete Percy builds', + ...reviewCommandConfig +}, async ({ flags, args, percy, log, exit }) => { + // Early return if Percy is disabled + if (!percy) { + exit(0, 'Percy is disabled'); + } + + // Validate and get authentication credentials + const { username, accessKey } = fetchCredentials(flags); + + if (!username || !accessKey) { + exit(1, 'Username and access key are required to delete builds.'); + } + + log.info(`Deleting build ${args.buildId}...`); + + try { + // Call the Percy API to delete the build + await percy.client.deleteBuild( + args.buildId, + username, + accessKey + ); + + log.info(`Build ${args.buildId} deleted successfully!`); + } catch (error) { + log.error(`Failed to delete build ${args.buildId}`); + log.error(error); + + // Provide user-friendly error message + exit(1, 'Failed to delete the build'); + } +}); + +export default deleteBuild; diff --git a/packages/cli-build/src/index.js b/packages/cli-build/src/index.js index 5b59ed45b..c9b63c428 100644 --- a/packages/cli-build/src/index.js +++ b/packages/cli-build/src/index.js @@ -5,3 +5,4 @@ export { id } from './id.js'; export { approve } from './approve.js'; export { reject } from './reject.js'; export { unapprove } from './unapprove.js'; +export { deleteBuild } from './delete.js'; diff --git a/packages/cli-build/test/delete.test.js b/packages/cli-build/test/delete.test.js new file mode 100644 index 000000000..8cf218c76 --- /dev/null +++ b/packages/cli-build/test/delete.test.js @@ -0,0 +1,221 @@ +import { logger, setupTest } from '@percy/cli-command/test/helpers'; +import { base64encode } from '@percy/client/utils'; +import api from '@percy/client/test/helpers'; +import { deleteBuild } from '@percy/cli-build'; + +describe('percy build:delete', () => { + let successResponse = { + data: { + attributes: { + action: 'delete' + } + } + }; + + beforeEach(async () => { + await setupTest(); + }); + + afterEach(() => { + delete process.env.PERCY_ENABLE; + delete process.env.BROWSERSTACK_USERNAME; + delete process.env.BROWSERSTACK_ACCESS_KEY; + delete process.env.PERCY_TOKEN; + delete process.env.PERCY_FORCE_PKG_VALUE; + }); + + it('does nothing and logs when percy is not enabled', async () => { + process.env.PERCY_ENABLE = '0'; + await deleteBuild(['123']); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Percy is disabled' + ]); + }); + + it('logs an error when build ID is not provided', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + await expectAsync(deleteBuild([])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + "[percy] ParseError: Missing required argument 'build-id'" + ]); + }); + + it('logs an error when username is missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_ACCESS_KEY = 'test-access-key'; + await expectAsync(deleteBuild(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to delete builds.' + ]); + }); + + it('logs an error when access key is missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'test-username'; + await expectAsync(deleteBuild(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to delete builds.' + ]); + }); + + it('logs an error when both username and access key are missing', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + await expectAsync(deleteBuild(['123'])).toBeRejected(); + + expect(logger.stdout).toEqual([]); + expect(logger.stderr).toEqual([ + '[percy] Error: Username and access key are required to delete builds.' + ]); + }); + + it('uses username and access key from environment variables', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; + + api.reply('/builds/123/delete', (req) => { + expect(req.body).toEqual({}); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('env-username:env-access-key')}`); + return [200, successResponse]; + }); + + await deleteBuild(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Deleting build 123...', + '[percy] Build 123 deleted successfully!' + ]); + }); + + it('doesnot require percy token', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; + + api.reply('/builds/123/delete', (req) => [200, successResponse]); + + await deleteBuild(['123']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Deleting build 123...', + '[percy] Build 123 deleted successfully!' + ]); + }); + + it('uses username and access key from flags over environment variables', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'env-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; + + api.reply('/builds/123/delete', (req) => { + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('flag-username:flag-access-key')}`); + return [200, successResponse]; + }); + + await deleteBuild(['123', '--username=flag-username', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Deleting build 123...', + '[percy] Build 123 deleted successfully!' + ]); + }); + + it('handles mixed flag and environment variable usage', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'env-username'; + // Only access key from flag + + api.reply('/builds/123/delete', (req) => { + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('env-username:flag-access-key')}`); + return [200, successResponse]; + }); + + await deleteBuild(['123', '--access-key=flag-access-key']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Deleting build 123...', + '[percy] Build 123 deleted successfully!' + ]); + }); + + it('handles username from flag and access key from environment', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_ACCESS_KEY = 'env-access-key'; + // Only username from flag + + api.reply('/builds/123/delete', (req) => { + expect(req.body).toEqual({}); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('flag-username:env-access-key')}`); + return [200, successResponse]; + }); + + await deleteBuild(['123', '--username=flag-username']); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Deleting build 123...', + '[percy] Build 123 deleted successfully!' + ]); + }); + + it('logs an error when build deletion fails with 401 Unauthorized', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'invalid-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'invalid-access-key'; + + api.reply('/builds/123/delete', (req) => { + expect(req.body).toEqual({}); + expect(req.headers.Authorization).toEqual(`Basic ${base64encode('invalid-username:invalid-access-key')}`); + return [401, { errors: [{ detail: 'Unauthorized' }] }]; + }); + + await expectAsync(deleteBuild(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Failed to delete build 123', + '[percy] Error: Unauthorized', + '[percy] Error: Failed to delete the build' + ]); + }); + + it('logs an error when build deletion fails with 403 Forbidden', async () => { + process.env.PERCY_TOKEN = '<>'; + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + process.env.BROWSERSTACK_USERNAME = 'test-username'; + process.env.BROWSERSTACK_ACCESS_KEY = 'test-access-key'; + + api.reply('/builds/123/delete', (req) => { + expect(req.body).toEqual({}); + return [403, { errors: [{ detail: 'Forbidden' }] }]; + }); + + await expectAsync(deleteBuild(['123'])).toBeRejected(); + + expect(logger.stderr).toEqual([ + '[percy] Failed to delete build 123', + '[percy] Error: Forbidden', + '[percy] Error: Failed to delete the build' + ]); + }); +}); diff --git a/packages/client/src/client.js b/packages/client/src/client.js index a6011013d..c361626c4 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -759,6 +759,21 @@ export class PercyClient { return this.reviewBuild(buildId, 'reject', username, accessKey); } + async deleteBuild(buildId, username, accessKey) { + validateId('build', buildId); + this.log.debug(`Sending Delete action for build ${buildId}...`); + + // For the delete action, we use accessKey and username in custom headers + // and do not require a project token. + return this.post( + `builds/${buildId}/delete`, + {}, + { identifier: 'build.delete' }, + { Authorization: `Basic ${base64encode(`${username}:${accessKey}`)}` }, + false + ); + } + mayBeLogUploadSize(contentSize, meta = {}) { if (contentSize >= 25 * 1024 * 1024) { this.log.error('Uploading resource above 25MB might fail the build...', meta); diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index d2cbc3c73..3a65231fc 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -2406,4 +2406,57 @@ describe('PercyClient', () => { .toBeRejectedWithError('Invalid build ID'); }); }); + + describe('#deleteBuild()', () => { + it('sends a delete request with correct parameters', async () => { + await expectAsync(client.deleteBuild('123', 'testuser', 'testkey')).toBeResolved(); + + expect(api.requests['/builds/123/delete'][0].method).toBe('POST'); + expect(api.requests['/builds/123/delete'][0].headers).toEqual( + jasmine.objectContaining({ + Authorization: `Basic ${base64encode('testuser:testkey')}` + }) + ); + expect(api.requests['/builds/123/delete'][0].body).toEqual({}); + }); + + it('calls post with projectTokenRequired=false', async () => { + spyOn(client, 'post').and.callThrough(); + + await expectAsync(client.deleteBuild('123', 'testuser', 'testkey')).toBeResolved(); + + expect(client.post).toHaveBeenCalledWith( + 'builds/123/delete', + {}, + { identifier: 'build.delete' }, + { Authorization: `Basic ${base64encode('testuser:testkey')}` }, + false + ); + }); + + it('validates build ID', async () => { + await expectAsync(client.deleteBuild(null, 'testuser', 'testkey')) + .toBeRejectedWithError('Missing build ID'); + + await expectAsync(client.deleteBuild('', 'testuser', 'testkey')) + .toBeRejectedWithError('Missing build ID'); + + await expectAsync(client.deleteBuild({}, 'testuser', 'testkey')) + .toBeRejectedWithError('Invalid build ID'); + }); + + it('logs debug message with build ID', async () => { + spyOn(client.log, 'debug'); + + await expectAsync(client.deleteBuild('456', 'testuser', 'testkey')).toBeResolved(); + + expect(client.log.debug).toHaveBeenCalledWith('Sending Delete action for build 456...'); + }); + + it('accepts numeric build ID', async () => { + await expectAsync(client.deleteBuild(123, 'testuser', 'testkey')).toBeResolved(); + + expect(api.requests['/builds/123/delete'][0].method).toBe('POST'); + }); + }); }); From 6f6cdb78dcca55cdac88314c7a1cc972d9b6d899 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Fri, 18 Jul 2025 14:57:43 +0530 Subject: [PATCH 18/18] adding deleted by user details --- packages/cli-build/src/delete.js | 7 ++++- packages/cli-build/test/delete.test.js | 40 +++++++++++++++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/cli-build/src/delete.js b/packages/cli-build/src/delete.js index 7ae2f8abd..75b1e56a0 100644 --- a/packages/cli-build/src/delete.js +++ b/packages/cli-build/src/delete.js @@ -25,13 +25,18 @@ export const deleteBuild = command('delete', { try { // Call the Percy API to delete the build - await percy.client.deleteBuild( + const buildDeletionResponse = await percy.client.deleteBuild( args.buildId, username, accessKey ); + const deletedBy = buildDeletionResponse['action-performed-by'] || { + user_email: 'unknown@example.com', + user_name: username + }; log.info(`Build ${args.buildId} deleted successfully!`); + log.info(`Deleted by: ${deletedBy.user_name} (${deletedBy.user_email})`); } catch (error) { log.error(`Failed to delete build ${args.buildId}`); log.error(error); diff --git a/packages/cli-build/test/delete.test.js b/packages/cli-build/test/delete.test.js index 8cf218c76..3d0817182 100644 --- a/packages/cli-build/test/delete.test.js +++ b/packages/cli-build/test/delete.test.js @@ -5,11 +5,8 @@ import { deleteBuild } from '@percy/cli-build'; describe('percy build:delete', () => { let successResponse = { - data: { - attributes: { - action: 'delete' - } - } + success: true, + 'action-performed-by': { user_email: 'test@test.com', user_name: 'testuser' } }; beforeEach(async () => { @@ -97,7 +94,8 @@ describe('percy build:delete', () => { expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ '[percy] Deleting build 123...', - '[percy] Build 123 deleted successfully!' + '[percy] Build 123 deleted successfully!', + '[percy] Deleted by: testuser (test@test.com)' ]); }); @@ -113,7 +111,8 @@ describe('percy build:delete', () => { expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ '[percy] Deleting build 123...', - '[percy] Build 123 deleted successfully!' + '[percy] Build 123 deleted successfully!', + '[percy] Deleted by: testuser (test@test.com)' ]); }); @@ -133,7 +132,8 @@ describe('percy build:delete', () => { expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ '[percy] Deleting build 123...', - '[percy] Build 123 deleted successfully!' + '[percy] Build 123 deleted successfully!', + '[percy] Deleted by: testuser (test@test.com)' ]); }); @@ -153,7 +153,8 @@ describe('percy build:delete', () => { expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ '[percy] Deleting build 123...', - '[percy] Build 123 deleted successfully!' + '[percy] Build 123 deleted successfully!', + '[percy] Deleted by: testuser (test@test.com)' ]); }); @@ -174,7 +175,8 @@ describe('percy build:delete', () => { expect(logger.stderr).toEqual([]); expect(logger.stdout).toEqual([ '[percy] Deleting build 123...', - '[percy] Build 123 deleted successfully!' + '[percy] Build 123 deleted successfully!', + '[percy] Deleted by: testuser (test@test.com)' ]); }); @@ -218,4 +220,22 @@ describe('percy build:delete', () => { '[percy] Error: Failed to delete the build' ]); }); + + it('uses fallback user info when latest-action-performed-by is not in response', async () => { + process.env.PERCY_FORCE_PKG_VALUE = JSON.stringify({ name: '@percy/client', version: '1.0.0' }); + + api.reply('/builds/123/delete', (req) => { + expect(req.body).toEqual({}); + return [200, { success: true }]; + }); + + await expectAsync(deleteBuild(['123', '--username=temp-username', '--access-key=flag-access-key'])).toBeResolved(); + + expect(logger.stderr).toEqual([]); + expect(logger.stdout).toEqual([ + '[percy] Deleting build 123...', + '[percy] Build 123 deleted successfully!', + '[percy] Deleted by: temp-username (unknown@example.com)' + ]); + }); });