Skip to content
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13107-tests-1763492424120.md
Original file line number Diff line number Diff line change
@@ -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))
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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');
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags';

describe('NodeBalancer create page validation', () => {
/**
* This test ensures that the user sees a uniqueness error when
* - they configure many TCP, HTTP, or HTTPS configs to use the same port
* - 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
Expand Down
6 changes: 5 additions & 1 deletion packages/manager/cypress/support/api/linodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export const deleteAllTestLinodes = async (): Promise<void> => {
);

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);
Expand Down
6 changes: 4 additions & 2 deletions packages/manager/cypress/support/intercepts/lke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
latestEnterpriseTierKubernetesVersion,
latestStandardTierKubernetesVersion,
} from 'support/constants/lke';
import { makeErrorResponse } from 'support/util/errors';
import { APIErrorContents, makeErrorResponse } from 'support/util/errors';

Check warning on line 13 in packages/manager/cypress/support/intercepts/lke.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Imports "APIErrorContents" are only used as type. Raw Output: {"ruleId":"@typescript-eslint/consistent-type-imports","severity":1,"message":"Imports \"APIErrorContents\" are only used as type.","line":13,"column":1,"nodeType":"ImportDeclaration","messageId":"someImportsAreOnlyTypes","endLine":13,"endColumn":75,"fix":{"range":[292,318],"text":"import type { APIErrorContents} from 'support/util/errors';
import { apiMatcher } from 'support/util/intercepts';
import { paginateResponse } from 'support/util/paginate';
import { randomDomainName } from 'support/util/random';
Expand Down Expand Up @@ -185,7 +185,9 @@
* @returns Cypress chainable.
*/
export const mockCreateClusterError = (
errorMessage: string = 'An unknown error occurred.',
errorMessage:
| APIErrorContents
| APIErrorContents[] = 'An unknown error occurred.',

Check warning on line 190 in packages/manager/cypress/support/intercepts/lke.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 5 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 5 times.","line":190,"column":28,"nodeType":"Literal","endLine":190,"endColumn":56}
statusCode: number = 500
): Cypress.Chainable<null> => {
return cy.intercept(
Expand Down
145 changes: 1 addition & 144 deletions packages/manager/cypress/support/setup/defer-command.ts
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved these API error handling utils into support/util/api.ts so they could be used by our clean up util too

Original file line number Diff line number Diff line change
@@ -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<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 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.
*/
Expand Down
Loading
Loading