Skip to content

Commit

Permalink
test: Improve Cypress error messages when Linode API errors occur (#9777
Browse files Browse the repository at this point in the history
)

* Improve error messages when Linode API errors occur

* Added changeset: Improve error message display when Linode API error occurs in Cypress test
  • Loading branch information
jdamore-linode authored Oct 16, 2023
1 parent cdc51b6 commit cc73646
Showing 2 changed files with 150 additions and 28 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-9777-tests-1696958372039.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Improve error message display when Linode API error occurs in Cypress test ([#9777](https://github.com/linode/manager/pull/9777))
173 changes: 145 additions & 28 deletions packages/manager/cypress/support/setup/defer-command.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,145 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
import type { AxiosError } from 'axios';
import type { APIError } from '@linode/api-v4';

type LinodeApiV4Error = {
errors: APIError[];
};

/**
* Returns `true` if the given error is a Linode API schema validation error.
*
* Type guards `e` as an array of `APIError` objects.
*
* @param e - Error.
*
* @returns `true` if `e` is a Linode API schema validation error.
*/
const isValidationError = (e: any): e is APIError[] => {
// When a Linode APIv4 schema validation error occurs, an array of `APIError`
// objects is thrown rather than a typical `Error` type.
return (
Array.isArray(e) &&
e.every((item: any) => {
return 'reason' in item;
})
);
};

/**
* Returns `true` if the given error is an Axios error.
*
* Type guards `e` as an `AxiosError` instance.
*
* @param e - Error.
*
* @returns `true` if `e` is an `AxiosError`.
*/
const isAxiosError = (e: any): e is AxiosError => {
return !!e.isAxiosError;
};

/**
* Returns `true` if the given error is a Linode API v4 request error.
*
* Type guards `e` as an `AxiosError<LinodeApiV4Error>` instance.
*
* @param e - Error.
*
* @returns `true` if `e` is a Linode API v4 request error.
*/
const isLinodeApiError = (e: any): e is AxiosError<LinodeApiV4Error> => {
if (isAxiosError(e)) {
const errorData = e.response?.data?.errors;
return (
Array.isArray(errorData) &&
errorData.every((item: any) => {
return 'reason' in item;
})
);
}
return false;
};

/**
* Detects known error types and returns a new Error with more detailed message.
*
* Unknown error types are returned without modification.
*
* @param e - Error.
*
* @returns A new error with added information in message, or `e`.
*/
const enhanceError = (e: any) => {
// Check for most specific error types first.
if (isLinodeApiError(e)) {
// If `e` is a Linode APIv4 error response, show the status code, error messages,
// and request URL when applicable.
const summary = !!e.response?.status
? `Linode APIv4 request failed with status code ${e.response.status}`
: `Linode APIv4 request failed`;

const errorDetails = e.response!.data.errors.map((error: APIError) => {
return error.field
? `- ${error.reason} (field '${error.field}')`
: `- ${error.reason}`;
});

const requestInfo =
!!e.request?.responseURL && !!e.config.method
? `\nRequest: ${e.config.method.toUpperCase()} ${e.request.responseURL}`
: '';

return new Error(`${summary}\n${errorDetails.join('\n')}${requestInfo}`);
}

if (isAxiosError(e)) {
// If `e` is an Axios error (but not a Linode API error specifically), show the
// status code, error messages, and request URL when applicable.
const summary = !!e.response?.status
? `Request failed with status code ${e.response.status}`
: `Request failed`;

const requestInfo =
!!e.request?.responseURL && !!e.config.method
? `\nRequest: ${e.config.method.toUpperCase()} ${e.request.responseURL}`
: '';

return new Error(`${summary}${requestInfo}`);
}

// Handle cases where a validation error is thrown.
// These are arrays containing `APIError` objects; no additional request context
// is included so we only have the validation error messages themselves to work with.
if (isValidationError(e)) {
// Validation errors do not contain any additional context (request URL, payload, etc.).
// Show the validation error messages instead.
const multipleErrors = e.length > 1;
const summary = multipleErrors
? 'Request failed with Linode schema validation errors'
: 'Request failed with Linode schema validation error';

// Format, accounting for 0, 1, or more errors.
const validationErrorMessage = multipleErrors
? e
.map((error) =>
error.field
? `- ${error.reason} (field '${error.field}')`
: `- ${error.reason}`
)
.join('\n')
: e
.map((error) =>
error.field
? `${error.reason} (field '${error.field}')`
: `${error.reason}`
)
.join('\n');

return new Error(`${summary}\n${validationErrorMessage}`);
}
// Return `e` unmodified if it's not handled by any of the above cases.
return e;
};

/**
* Describes an object which can contain a label.
@@ -87,9 +204,9 @@ Cypress.Commands.add(
let result: T;
try {
result = await promise;
} catch (e) {
commandLog.end();
throw e;
} catch (e: any) {
commandLog.error(e);
throw enhanceError(e);
}
commandLog.end();
return result;

0 comments on commit cc73646

Please sign in to comment.