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

feat: [M3-7160] - Add AGLB "Edit Certificate" drawer #9723

Merged
merged 15 commits into from
Oct 6, 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
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 @@ -141,6 +141,7 @@ type CertificateType = 'ca' | 'downstream';
export interface Certificate {
id: number;
label: string;
certificate: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The API spec has been updated to indicate that certificate will now be returned in GET /v4beta/aglb/{id}/certificates.

type: CertificateType;
}

Expand All @@ -150,3 +151,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 @@ -13,7 +13,7 @@ import {
} from '@linode/api-v4/lib/aglb/types';
import * as Factory from 'factory.ts';

const certificate = `
export const mockCertificate = `
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I preemptively renamed in hope of making it clear that these are not real values that are a cause of security concern.

-----BEGIN CERTIFICATE-----
MIID0DCCArigAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJGUjET
MBEGA1UECAwKU29tZS1TdGF0ZTEOMAwGA1UEBwwFUGFyaXMxDTALBgNVBAoMBERp
Expand All @@ -39,7 +39,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);
mjac0bs marked this conversation as resolved.
Show resolved Hide resolved
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]}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the Create Service Target Cert drawer we say "TLS Certificate" but in the edit drawer we say "Service Certificate". I guess we should say "Service Certificate" in both places?

Daniel updated to mocks so that the Create drawer now labels the field as "Server Certificate" for ca/service target certificates and is "TLS Certificate" for downstream/TLS, so I made the change to dynamically render the label based on type in the Create drawer here.

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