diff --git a/packages/manager/.changeset/pr-13107-tests-1763492424120.md b/packages/manager/.changeset/pr-13107-tests-1763492424120.md new file mode 100644 index 00000000000..5f1b879f190 --- /dev/null +++ b/packages/manager/.changeset/pr-13107-tests-1763492424120.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Fixed various test failures when running tests against Prod environment ([#13107](https://github.com/linode/manager/pull/13107)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-enterprise-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-enterprise-create.spec.ts index 32d93f7b6e7..b1a5509a488 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-enterprise-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-enterprise-create.spec.ts @@ -289,21 +289,19 @@ describe('LKE Cluster Creation with LKE-E', () => { */ it('surfaces field-level errors on VPC fields', () => { // Intercept the create cluster request and force an error response - cy.intercept('POST', '/v4beta/lke/clusters', { - statusCode: 400, - body: { - errors: [ - { - reason: 'There is an error configuring this VPC.', - field: 'vpc_id', - }, - { - reason: 'There is an error configuring this subnet.', - field: 'subnet_id', - }, - ], - }, - }).as('createClusterError'); + mockCreateClusterError( + [ + { + reason: 'There is an error configuring this VPC.', + field: 'vpc_id', + }, + { + reason: 'There is an error configuring this subnet.', + field: 'subnet_id', + }, + ], + 400 + ).as('createClusterError'); cy.findByLabelText('Cluster Label').type(clusterLabel); cy.findByText('LKE Enterprise').click(); diff --git a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts index 8814b12c700..50c52ea14bb 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts @@ -160,12 +160,10 @@ describe('linode storage tab', () => { }); /* - * - Confirms UI flow end-to-end when a user attempts to delete a Linode disk with encryption enabled. - * - Confirms that disk deletion fails and toast notification appears. + * - Confirms UI flow end-to-end when a user deletes a Linode disk with encryption enabled. + * - Confirms that disk deletion succeeds and toast notification appears. */ - // TODO: Disk cannot be deleted if disk_encryption is 'enabled' - // TODO: edit result of this test if/when behavior of backend is updated. uncertain what expected behavior is for this disk config - it('delete disk fails when Linode uses disk encryption', () => { + it('deletes a disk when Linode Disk Encryption is enabled', () => { const diskName = randomLabel(); cy.defer(() => createTestLinode({ @@ -195,19 +193,17 @@ describe('linode storage tab', () => { cy.wait('@deleteDisk').its('response.statusCode').should('eq', 200); cy.findByText('Deleting', { exact: false }).should('be.visible'); ui.button.findByTitle('Add a Disk').should('be.enabled'); - // ui.toast.assertMessage( - // `Disk ${diskName} on Linode ${linode.label} has been deleted.` - // ); + ui.toast.assertMessage( + `Disk ${diskName} on Linode ${linode.label} has been deleted.` + ); ui.toast .findByMessage( `Disk ${diskName} on Linode ${linode.label} has been deleted.` ) .should('not.exist'); - // cy.findByLabelText('List of Disks').within(() => { - // cy.contains(diskName).should('not.exist'); - // }); + cy.findByLabelText('List of Disks').within(() => { - cy.contains(diskName).should('be.visible'); + cy.contains(diskName).should('not.exist'); }); }); }); diff --git a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-create-validation.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-create-validation.spec.ts index 458e437b44f..6a2f6d0b78e 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-create-validation.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-create-validation.spec.ts @@ -1,3 +1,5 @@ +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; + describe('NodeBalancer create page validation', () => { /** * This test ensures that the user sees a uniqueness error when @@ -5,6 +7,9 @@ describe('NodeBalancer create page validation', () => { * - they configure many UDP configs to use the same port */ it('renders a port uniqueness errors when you try to create a nodebalancer with configs using the same port and protocol', () => { + mockAppendFeatureFlags({ + udp: true, + }); cy.visitWithLogin('/nodebalancers/create'); // Configure the first config to use TCP on port 8080 diff --git a/packages/manager/cypress/support/api/linodes.ts b/packages/manager/cypress/support/api/linodes.ts index 7d8a15e66e5..2e128b90ac0 100644 --- a/packages/manager/cypress/support/api/linodes.ts +++ b/packages/manager/cypress/support/api/linodes.ts @@ -30,7 +30,11 @@ export const deleteAllTestLinodes = async (): Promise => { ); const deletePromises = linodes - .filter((linode: Linode) => isTestLabel(linode.label)) + .filter( + (linode: Linode) => + isTestLabel(linode.label) && + ['offline', 'running'].includes(linode.status) + ) .map((linode: Linode) => deleteLinode(linode.id)); await Promise.all(deletePromises); diff --git a/packages/manager/cypress/support/intercepts/lke.ts b/packages/manager/cypress/support/intercepts/lke.ts index 4c2b6935e9a..280a5a7946e 100644 --- a/packages/manager/cypress/support/intercepts/lke.ts +++ b/packages/manager/cypress/support/intercepts/lke.ts @@ -10,7 +10,7 @@ import { latestEnterpriseTierKubernetesVersion, latestStandardTierKubernetesVersion, } from 'support/constants/lke'; -import { makeErrorResponse } from 'support/util/errors'; +import { APIErrorContents, makeErrorResponse } from 'support/util/errors'; import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; import { randomDomainName } from 'support/util/random'; @@ -185,7 +185,9 @@ export const mockCreateCluster = ( * @returns Cypress chainable. */ export const mockCreateClusterError = ( - errorMessage: string = 'An unknown error occurred.', + errorMessage: + | APIErrorContents + | APIErrorContents[] = 'An unknown error occurred.', statusCode: number = 500 ): Cypress.Chainable => { return cy.intercept( diff --git a/packages/manager/cypress/support/setup/defer-command.ts b/packages/manager/cypress/support/setup/defer-command.ts index 6196b6e976f..bc1c38bf225 100644 --- a/packages/manager/cypress/support/setup/defer-command.ts +++ b/packages/manager/cypress/support/setup/defer-command.ts @@ -1,150 +1,7 @@ import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { enhanceError, isAxiosError } from 'support/util/api'; import { timeout } from 'support/util/backoff'; -import type { APIError } from '@linode/api-v4'; -import type { AxiosError } from 'axios'; - -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` instance. - * - * @param e - Error. - * - * @returns `true` if `e` is a Linode API v4 request error. - */ -const isLinodeApiError = (e: any): e is AxiosError => { - if (isAxiosError(e)) { - const responseData = e.response?.data as any; - return ( - responseData.errors && - Array.isArray(responseData.errors) && - responseData.errors.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: Error) => { - // 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. */ diff --git a/packages/manager/cypress/support/util/api.ts b/packages/manager/cypress/support/util/api.ts index fd55777be19..b90bbaab130 100644 --- a/packages/manager/cypress/support/util/api.ts +++ b/packages/manager/cypress/support/util/api.ts @@ -2,14 +2,155 @@ * @file Utilities to help configure @linode/api-v4 package. */ -import { baseRequest } from '@linode/api-v4'; -import { AxiosHeaders } from 'axios'; +import { APIError, baseRequest } from '@linode/api-v4'; +import { AxiosError, AxiosHeaders } from 'axios'; // Note: This file is imported by Cypress plugins, and indirectly by Cypress // tests. Because Cypress has not been initiated when plugins are executed, we // cannot use any Cypress functionality in this module without causing a crash // at startup. +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. + */ +export 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`. + */ +export 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` instance. + * + * @param e - Error. + * + * @returns `true` if `e` is a Linode API v4 request error. + */ +export const isLinodeApiError = (e: any): e is AxiosError => { + if (isAxiosError(e)) { + const responseData = e.response?.data as any; + return ( + responseData.errors && + Array.isArray(responseData.errors) && + responseData.errors.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`. + */ +export const enhanceError = (e: Error) => { + // 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; +}; + /** * Default API root URL to use for replacement logic when using a URL override. * diff --git a/packages/manager/cypress/support/util/cleanup.ts b/packages/manager/cypress/support/util/cleanup.ts index 11432ce0ff7..847e1f79408 100644 --- a/packages/manager/cypress/support/util/cleanup.ts +++ b/packages/manager/cypress/support/util/cleanup.ts @@ -15,6 +15,7 @@ import { deleteAllTestSSHKeys } from 'support/api/profile'; import { deleteAllTestStackScripts } from 'support/api/stackscripts'; import { deleteAllTestTags } from 'support/api/tags'; import { deleteAllTestVolumes } from 'support/api/volumes'; +import { enhanceError } from 'support/util/api'; /** Types of resources that can be cleaned up. */ export type CleanUpResource = @@ -75,7 +76,20 @@ export const cleanUp = (resources: CleanUpResource | CleanUpResource[]) => { for (const resource of resourcesArray) { const cleanFunction = cleanUpMap[resource]; // Perform clean-up sequentially to avoid API rate limiting. - await cleanFunction(); + try { + await cleanFunction(); + } catch (e: any) { + // Log a warning but otherwise swallow errors if any resources fail to + // be cleaned up. There are a few cases where this is especially helpful: + // + // - Unplanned API issues or outages resulting in 5xx errors + // - 400 errors when inadevertently attempting to delete resources that are still busy (e.g. cleaning up a Linode that is the target of a clone operation) + const enhancedError = enhanceError(e); + console.warn( + 'An API error occurred while attempting to clean up one or more resources:' + ); + console.warn(enhancedError.message); + } } }; return cy.defer(