Skip to content

Commit

Permalink
feat: [M3-7160] - Add AGLB "Edit Certificate" drawer (#9723)
Browse files Browse the repository at this point in the history
* Initial set up, open edit drawer action

* Update mock data, dynamic drawer display, and unit test

* Update tests to add TODOs and skip correctly

* Update payload for PUT request, update unit test

* Only include key in errorFields for a downstream cert type

* Add validation

* Fix schema

* Address feedback

* Re-enable cert and key fields; add cert field validation

* Add placeholder text to private key field

* Address Slack feedback

* Address feedback, but WIP validation

* Add type so validation triggers correctly; thanks @bnussman-akamai

* Address PR and UX feedback

* Fix failure in load-balancer-certificates.spec.ts due to label change
  • Loading branch information
mjac0bs authored Oct 6, 2023
1 parent 5a07117 commit 459f7e7
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 13 deletions.
17 changes: 12 additions & 5 deletions packages/api-v4/src/aglb/certificates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ import Request, {
} from '../request';
import { BETA_API_ROOT } from '../constants';
import { Filter, Params, ResourcePage } from '../types';
import { Certificate, CreateCertificatePayload } from './types';
import { CreateCertificateSchema } from '@linode/validation';
import {
Certificate,
CreateCertificatePayload,
UpdateCertificatePayload,
} from './types';
import {
CreateCertificateSchema,
UpdateCertificateSchema,
} from '@linode/validation';

/**
* getLoadbalancerCertificates
Expand Down Expand Up @@ -67,12 +74,12 @@ export const createLoadbalancerCertificate = (
/**
* updateLoadbalancerCertificate
*
* Creates an Akamai Global Load Balancer certificate
* Updates an Akamai Global Load Balancer certificate
*/
export const updateLoadbalancerCertificate = (
loadbalancerId: number,
certificateId: number,
data: Partial<CreateCertificatePayload>
data: Partial<UpdateCertificatePayload>
) =>
Request<Certificate>(
setURL(
Expand All @@ -81,7 +88,7 @@ export const updateLoadbalancerCertificate = (
)}/certificates/${encodeURIComponent(certificateId)}`
),
setMethod('PUT'),
setData(data)
setData(data, UpdateCertificateSchema)
);

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/api-v4/src/aglb/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ type CertificateType = 'ca' | 'downstream';
export interface Certificate {
id: number;
label: string;
certificate: string;
type: CertificateType;
}

Expand All @@ -169,3 +170,10 @@ export interface CreateCertificatePayload {
label: string;
type: CertificateType;
}

export interface UpdateCertificatePayload {
key?: string;
certificate?: string;
label?: string;
type?: CertificateType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('Akamai Global Load Balancer certificates page', () => {
.should('be.visible')
.type(mockLoadBalancerCertServiceTarget.label);

cy.findByLabelText('TLS Certificate')
cy.findByLabelText('Server Certificate')
.should('be.visible')
.type(randomString(32));

Expand Down
9 changes: 5 additions & 4 deletions packages/manager/src/factories/aglb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import * as Factory from 'factory.ts';
import { pickRandom } from 'src/utilities/random';

const certificate = `
export const mockCertificate = `
-----BEGIN CERTIFICATE-----
MIID0DCCArigAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJGUjET
MBEGA1UECAwKU29tZS1TdGF0ZTEOMAwGA1UEBwwFUGFyaXMxDTALBgNVBAoMBERp
Expand All @@ -40,7 +40,7 @@ cbTV5RDkrlaYwm5yqlTIglvCv7o=
-----END CERTIFICATE-----
`;

const key = `
const mockKey = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAvpnaPKLIKdvx98KW68lz8pGaRRcYersNGqPjpifMVjjE8LuC
oXgPU0HePnNTUjpShBnynKCvrtWhN+haKbSp+QWXSxiTrW99HBfAl1MDQyWcukoE
Expand Down Expand Up @@ -275,15 +275,16 @@ export const createServiceTargetFactory = Factory.Sync.makeFactory<ServiceTarget
// Certificate endpoints
// *********************
export const certificateFactory = Factory.Sync.makeFactory<Certificate>({
certificate: mockCertificate,
id: Factory.each((i) => i),
label: Factory.each((i) => `certificate-${i}`),
type: 'ca',
});

export const createCertificateFactory = Factory.Sync.makeFactory<CreateCertificatePayload>(
{
certificate,
key,
certificate: mockCertificate,
key: mockKey,
label: 'my-cert',
type: 'downstream',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useLoadBalancerCertificatesQuery } from 'src/queries/aglb/certificates'

import { CreateCertificateDrawer } from './CreateCertificateDrawer';
import { DeleteCertificateDialog } from './DeleteCertificateDialog';
import { EditCertificateDrawer } from './EditCertificateDrawer';

import type { Certificate, Filter } from '@linode/api-v4';

Expand All @@ -40,6 +41,7 @@ export const Certificates = () => {
const history = useHistory();

const isCreateDrawerOpen = location.pathname.endsWith('/create');
const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false);
const [isDeleteDrawerOpen, setIsDeleteDrawerOpen] = useState(false);

const [selectedCertificateId, setSelectedCertificateId] = useState<number>();
Expand Down Expand Up @@ -69,6 +71,11 @@ export const Certificates = () => {
filter
);

const onEditCertificate = (certificate: Certificate) => {
setIsEditDrawerOpen(true);
setSelectedCertificateId(certificate.id);
};

const onDeleteCertificate = (certificate: Certificate) => {
setIsDeleteDrawerOpen(true);
setSelectedCertificateId(certificate.id);
Expand Down Expand Up @@ -147,7 +154,10 @@ export const Certificates = () => {
<TableCell actionCell>
<ActionMenu
actionsList={[
{ onClick: () => null, title: 'Edit' },
{
onClick: () => onEditCertificate(certificate),
title: 'Edit',
},
{
onClick: () => onDeleteCertificate(certificate),
title: 'Delete',
Expand Down Expand Up @@ -175,6 +185,12 @@ export const Certificates = () => {
open={isCreateDrawerOpen}
type={certType}
/>
<EditCertificateDrawer
certificate={selectedCertificate}
loadbalancerId={id}
onClose={() => setIsEditDrawerOpen(false)}
open={isEditDrawerOpen}
/>
<DeleteCertificateDialog
certificate={selectedCertificate}
loadbalancerId={id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Typography } from 'src/components/Typography';
import { useLoadBalancerCertificateCreateMutation } from 'src/queries/aglb/certificates';
import { getErrorMap } from 'src/utilities/errorUtils';

import { labelMap } from './EditCertificateDrawer';

import type { Certificate, CreateCertificatePayload } from '@linode/api-v4';

interface Props {
Expand Down Expand Up @@ -91,7 +93,7 @@ export const CreateCertificateDrawer = (props: Props) => {
/>
<TextField
errorText={errorMap.certificate}
label="TLS Certificate"
label={labelMap[type]}
labelTooltipText="TODO"
multiline
name="certificate"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Certificate } from '@linode/api-v4';
import { act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { mockCertificate } from 'src/factories';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { EditCertificateDrawer } from './EditCertificateDrawer';

const mockTLSCertificate: Certificate = {
certificate: mockCertificate,
id: 0,
label: 'test-tls-cert',
type: 'downstream',
};
const mockCACertificate: Certificate = {
certificate: mockCertificate,
id: 0,
label: 'test-ca-cert',
type: 'ca',
};

describe('EditCertificateDrawer', () => {
it('should contain the name of the cert in the drawer title and label field', () => {
const onClose = jest.fn();

const { getByLabelText, getByTestId } = renderWithTheme(
<EditCertificateDrawer
certificate={mockTLSCertificate}
loadbalancerId={0}
onClose={onClose}
open
/>
);

const labelInput = getByLabelText('Certificate Label');

expect(getByTestId('drawer-title')).toHaveTextContent(
`Edit ${mockTLSCertificate.label}`
);
expect(labelInput).toHaveDisplayValue(mockTLSCertificate.label);
});

it('should contain the cert in the cert field and placeholder text in the private key for a downstream cert', () => {
const onClose = jest.fn();

const { getByLabelText } = renderWithTheme(
<EditCertificateDrawer
certificate={mockTLSCertificate}
loadbalancerId={0}
onClose={onClose}
open
/>
);

const certInput = getByLabelText('TLS Certificate');
const keyInput = getByLabelText('Private Key');

expect(certInput).toHaveDisplayValue(mockTLSCertificate.certificate.trim());
expect(keyInput).toHaveAttribute(
'placeholder',
'Private key is redacted for security.'
);
});

it('should submit and close drawer when only the label of the certificate is edited', async () => {
const onClose = jest.fn();

const { getByLabelText, getByTestId } = renderWithTheme(
<EditCertificateDrawer
certificate={mockCACertificate}
loadbalancerId={0}
onClose={onClose}
open
/>
);

const labelInput = getByLabelText('Certificate Label');
const certInput = getByLabelText('Server Certificate');

expect(labelInput).toHaveDisplayValue(mockCACertificate.label);
expect(certInput).toHaveDisplayValue(mockCACertificate.certificate.trim());

act(() => {
userEvent.type(labelInput, 'my-updated-cert-0');
userEvent.click(getByTestId('submit'));
});

await waitFor(() => expect(onClose).toBeCalled());
});

it('should submit and close drawer when both a certificate and key are included', async () => {
const onClose = jest.fn();

const { getByLabelText, getByTestId } = renderWithTheme(
<EditCertificateDrawer
certificate={mockTLSCertificate}
loadbalancerId={0}
onClose={onClose}
open
/>
);
const labelInput = getByLabelText('Certificate Label');
const certInput = getByLabelText('TLS Certificate');
const keyInput = getByLabelText('Private Key');

act(() => {
userEvent.type(labelInput, 'my-cert-0');
userEvent.type(certInput, 'massive cert');
userEvent.type(keyInput, 'massive key');

userEvent.click(getByTestId('submit'));
});

await waitFor(() => expect(onClose).toBeCalled());
});
});
Loading

0 comments on commit 459f7e7

Please sign in to comment.