Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test, upcoming: [M3-7134, M3-7314] - Add integration test for AGLB Edit Certificate flow and improve form validation #9880

Merged
merged 8 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-9880-tests-1699311124062.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Add integration tests for AGLB Edit Certificate flow ([#9880](https://github.com/linode/manager/pull/9880))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add missing label field validation in AGLB Edit Certificate drawer ([#9880](https://github.com/linode/manager/pull/9880))
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ import {
mockGetFeatureFlagClientstream,
} from 'support/intercepts/feature-flags';
import { makeFeatureFlagData } from 'support/util/feature-flags';
import { loadbalancerFactory, certificateFactory } from '@src/factories';
import {
loadbalancerFactory,
certificateFactory,
mockCertificate,
} from '@src/factories';
import { ui } from 'support/ui';
import { randomItem, randomLabel, randomString } from 'support/util/random';
import {
mockDeleteLoadBalancerCertificate,
mockDeleteLoadBalancerCertificateError,
mockGetLoadBalancer,
mockGetLoadBalancerCertificates,
mockUpdateLoadBalancerCertificate,
mockUploadLoadBalancerCertificate,
} from 'support/intercepts/load-balancers';
import { Loadbalancer, Certificate } from '@linode/api-v4/types';
Expand Down Expand Up @@ -241,6 +246,221 @@ describe('Akamai Global Load Balancer certificates page', () => {
cy.findByText(mockLoadBalancerCertServiceTarget.label).should('be.visible');
});

/*
* - Confirms Load Balancer certificate edit UI flow using mocked API requests.
* - Confirms that TLS and Service Target certificates can be edited.
* - Confirms that certificates table updates to reflect edited certificates.
*/
it('can update a TLS certificate', () => {
const mockLoadBalancer = loadbalancerFactory.build();
const mockLoadBalancerCertTls = certificateFactory.build({
label: randomLabel(),
type: 'downstream',
certificate: mockCertificate.trim(),
});
const mockNewLoadBalancerCertTls = certificateFactory.build({
label: 'my-updated-tls-cert',
certificate: 'mock-new-cert',
key: 'mock-new-key',
type: 'downstream',
});

mockAppendFeatureFlags({
aglb: makeFeatureFlagData(true),
}).as('getFeatureFlags');
mockGetFeatureFlagClientstream().as('getClientStream');
mockGetLoadBalancer(mockLoadBalancer).as('getLoadBalancer');
mockGetLoadBalancerCertificates(
mockLoadBalancer.id,
mockLoadBalancerCertTls
).as('getCertificates');

cy.visitWithLogin(`/loadbalancers/${mockLoadBalancer.id}/certificates`);
cy.wait([
'@getFeatureFlags',
'@getClientStream',
'@getLoadBalancer',
'@getCertificates',
]);

// Edit a TLS certificate.
ui.actionMenu
.findByTitle(
`Action Menu for certificate ${mockLoadBalancerCertTls.label}`
)
.should('be.visible')
.click();
ui.actionMenuItem.findByTitle('Edit').should('be.visible').click();

mockUpdateLoadBalancerCertificate(
mockLoadBalancer.id,
mockLoadBalancerCertTls
).as('updateCertificate');

mockGetLoadBalancerCertificates(mockLoadBalancer.id, [
mockNewLoadBalancerCertTls,
]).as('getCertificates');

ui.drawer
.findByTitle(`Edit ${mockLoadBalancerCertTls.label}`)
.should('be.visible')
.within(() => {
// Confirm that drawer displays certificate data or indicates where data is redacted for security.
cy.findByLabelText('Certificate Label')
.should('be.visible')
.should('have.value', mockLoadBalancerCertTls.label);

cy.findByLabelText('TLS Certificate')
.should('be.visible')
.should('have.value', mockLoadBalancerCertTls.certificate);

cy.findByLabelText('Private Key')
.should('be.visible')
.should('have.value', '')
.invoke('attr', 'placeholder')
.should('contain', 'Private key is redacted for security.');

// Attempt to submit an incorrect form without a label or a new cert key.
cy.findByLabelText('Certificate Label').clear();
cy.findByLabelText('TLS Certificate').clear().type('my-new-cert');

ui.buttonGroup
.findButtonByTitle('Update Certificate')
.scrollIntoView()
.should('be.visible')
.should('be.enabled')
.click();

// Confirm that validation errors appear when drawer is not filled out correctly.
cy.findAllByText('Label must not be empty.').should('be.visible');
cy.findAllByText('Private Key is required').should('be.visible');

// Fix errors.
cy.findByLabelText('Certificate Label')
.click()
.type(mockNewLoadBalancerCertTls.label);

cy.findByLabelText('TLS Certificate')
.click()
.type(mockNewLoadBalancerCertTls.certificate);

cy.findByLabelText('Private Key')
.click()
.type(mockNewLoadBalancerCertTls.key);

ui.buttonGroup
.findButtonByTitle('Update Certificate')
.scrollIntoView()
.click();
});

cy.wait(['@updateCertificate', '@getCertificates']);

// Confirm that new certificate is listed in the table with expected info.
cy.findByText(mockNewLoadBalancerCertTls.label).should('be.visible');
});

it('can update a service target certificate', () => {
const mockLoadBalancer = loadbalancerFactory.build();
const mockLoadBalancerCertServiceTarget = certificateFactory.build({
label: randomLabel(),
type: 'ca',
certificate: mockCertificate.trim(),
});
const mockNewLoadBalancerCertServiceTarget = certificateFactory.build({
label: 'my-updated-ca-cert',
certificate: 'mock-new-cert',
type: 'ca',
});

mockAppendFeatureFlags({
aglb: makeFeatureFlagData(true),
}).as('getFeatureFlags');
mockGetFeatureFlagClientstream().as('getClientStream');
mockGetLoadBalancer(mockLoadBalancer).as('getLoadBalancer');
mockGetLoadBalancerCertificates(
mockLoadBalancer.id,
mockLoadBalancerCertServiceTarget
).as('getCertificates');

cy.visitWithLogin(`/loadbalancers/${mockLoadBalancer.id}/certificates`);
cy.wait([
'@getFeatureFlags',
'@getClientStream',
'@getLoadBalancer',
'@getCertificates',
]);

// Edit a CA certificate.
ui.actionMenu
.findByTitle(
`Action Menu for certificate ${mockLoadBalancerCertServiceTarget.label}`
)
.should('be.visible')
.click();
ui.actionMenuItem.findByTitle('Edit').should('be.visible').click();

mockUpdateLoadBalancerCertificate(
mockLoadBalancer.id,
mockLoadBalancerCertServiceTarget
).as('updateCertificate');

mockGetLoadBalancerCertificates(mockLoadBalancer.id, [
mockNewLoadBalancerCertServiceTarget,
]).as('getCertificates');

ui.drawer
.findByTitle(`Edit ${mockLoadBalancerCertServiceTarget.label}`)
.should('be.visible')
.within(() => {
// Confirm that drawer displays certificate data or indicates where data is redacted for security.
cy.findByLabelText('Certificate Label')
.should('be.visible')
.should('have.value', mockLoadBalancerCertServiceTarget.label);

cy.findByLabelText('Server Certificate')
.should('be.visible')
.should('have.value', mockLoadBalancerCertServiceTarget.certificate);

cy.findByLabelText('Private Key').should('not.exist');

// Attempt to submit an incorrect form without a label.
cy.findByLabelText('Certificate Label').clear();

ui.buttonGroup
.findButtonByTitle('Update Certificate')
.scrollIntoView()
.should('be.visible')
.should('be.enabled')
.click();

// Confirm that validation error appears when drawer is not filled out correctly.
cy.findAllByText('Label must not be empty.').should('be.visible');

// Fix error.
cy.findByLabelText('Certificate Label')
.click()
.type(mockNewLoadBalancerCertServiceTarget.label);

// Update certificate.
cy.findByLabelText('Server Certificate')
.click()
.type(mockNewLoadBalancerCertServiceTarget.certificate);

ui.buttonGroup
.findButtonByTitle('Update Certificate')
.scrollIntoView()
.click();
});

cy.wait(['@updateCertificate', '@getCertificates']);

// Confirm that new certificate is listed in the table with expected info.
cy.findByText(mockNewLoadBalancerCertServiceTarget.label).should(
'be.visible'
);
});

/*
* - Confirms Load Balancer certificate delete UI flow using mocked API requests.
* - Confirms that TLS and Service Target certificates can be deleted.
Expand Down
18 changes: 18 additions & 0 deletions packages/manager/cypress/support/intercepts/load-balancers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ export const mockDeleteLoadBalancerCertificateError = (
);
};

/**
* Intercepts PUT request to update an AGLB load balancer certificate and mocks a success response.
*
* @param loadBalancerId - ID of load balancer for which to mock certificates.
*
* @returns Cypress chainable.
*/
export const mockUpdateLoadBalancerCertificate = (
loadBalancerId: number,
certificate: Certificate
) => {
return cy.intercept(
'PUT',
apiMatcher(`/aglb/${loadBalancerId}/certificates/${certificate.id}`),
makeResponse(certificate)
);
};

/**
* Intercepts GET request to retrieve AGLB service targets and mocks response.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { Notice } from 'src/components/Notice/Notice';
import { TextField } from 'src/components/TextField';
import { Typography } from 'src/components/Typography';
import { useLoadBalancerCertificateCreateMutation } from 'src/queries/aglb/certificates';
import { getErrorMap } from 'src/utilities/errorUtils';
import { getFormikErrorsFromAPIErrors } from 'src/utilities/formikErrorUtils';
import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView';

import { labelMap } from './EditCertificateDrawer';

Expand Down Expand Up @@ -66,33 +67,38 @@ export const CreateCertificateDrawer = (props: Props) => {
type,
},
async onSubmit(values) {
await createCertificate(values);
onClose();
try {
await createCertificate(values);
onClose();
} catch (errors) {
formik.setErrors(getFormikErrorsFromAPIErrors(errors));
scrollErrorIntoView();
}
},
// Disabling validateOnBlur and validateOnChange when an API error is shown prevents
// all API errors from disappearing when one field is changed.
validateOnBlur: !error,
validateOnChange: !error,
});

const errorFields = ['label', 'certificate'];

if (type === 'downstream') {
errorFields.push('key');
}

const errorMap = getErrorMap(errorFields, error);
const generalError = error?.find((e) => !e.field)?.reason;

return (
<Drawer onClose={onClose} open={open} title={titleMap[type] ?? ''}>
<Drawer onClose={onClose} open={open} title={titleMap[type] ?? ''} wide>
<form onSubmit={formik.handleSubmit}>
{errorMap.none && <Notice text={errorMap.none} variant="error" />}
{generalError && <Notice text={generalError} variant="error" />}
<Typography>{descriptionMap[type]}</Typography>
<TextField
errorText={errorMap.label}
errorText={formik.errors.label}
expand
label="Label"
name="label"
onChange={formik.handleChange}
value={formik.values.label}
/>
<TextField
errorText={errorMap.certificate}
errorText={formik.errors.certificate}
expand
label={labelMap[type]}
labelTooltipText="TODO"
multiline
Expand All @@ -104,7 +110,8 @@ export const CreateCertificateDrawer = (props: Props) => {
/>
{type === 'downstream' && (
<TextField
errorText={errorMap.key}
errorText={formik.errors.key}
expand
label="Private Key"
labelTooltipText="TODO"
multiline
Expand Down
Loading