diff --git a/packages/manager/.changeset/pr-10521-tests-1717099871186.md b/packages/manager/.changeset/pr-10521-tests-1717099871186.md new file mode 100644 index 00000000000..d8d8076b57c --- /dev/null +++ b/packages/manager/.changeset/pr-10521-tests-1717099871186.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add unit tests for CreateImageFromDiskDialog and EnableBackupsDialog and LDE-related E2E assertions for Create Image flow ([#10521](https://github.com/linode/manager/pull/10521)) diff --git a/packages/manager/.changeset/pr-10521-upcoming-features-1717099430648.md b/packages/manager/.changeset/pr-10521-upcoming-features-1717099430648.md new file mode 100644 index 00000000000..95186f0e1a6 --- /dev/null +++ b/packages/manager/.changeset/pr-10521-upcoming-features-1717099430648.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add warning notices regarding non-encryption when creating Images and enabling Backups ([#10521](https://github.com/linode/manager/pull/10521)) diff --git a/packages/manager/cypress/e2e/core/images/create-image.spec.ts b/packages/manager/cypress/e2e/core/images/create-image.spec.ts index b4e73064099..fdffbf581d4 100644 --- a/packages/manager/cypress/e2e/core/images/create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-image.spec.ts @@ -1,9 +1,50 @@ -import type { Linode } from '@linode/api-v4'; +import type { Linode, Region } from '@linode/api-v4'; +import { accountFactory, linodeFactory, regionFactory } from 'src/factories'; import { authenticate } from 'support/api/authentication'; +import { mockGetAccount } from 'support/intercepts/account'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomPhrase } from 'support/util/random'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { + mockGetLinodeDetails, + mockGetLinodes, +} from 'support/intercepts/linodes'; + +const mockRegions: Region[] = [ + regionFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + id: 'us-east', + label: 'Newark, NJ', + site_type: 'core', + }), + regionFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + id: 'us-den-edge-1', + label: 'Edge - Denver, CO', + site_type: 'edge', + }), +]; + +const mockLinodes: Linode[] = [ + linodeFactory.build({ + label: 'core-region-linode', + region: mockRegions[0].id, + }), + linodeFactory.build({ + label: 'edge-region-linode', + region: mockRegions[1].id, + }), +]; + +const DISK_ENCRYPTION_IMAGES_CAVEAT_COPY = + 'Virtual Machine Images are not encrypted.'; authenticate(); describe('create image (e2e)', () => { @@ -84,4 +125,137 @@ describe('create image (e2e)', () => { }); }); }); + + it('displays notice informing user that Images are not encrypted, provided the LDE feature is enabled and the selected linode is not in an Edge region', () => { + // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out + mockAppendFeatureFlags({ + linodeDiskEncryption: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + // Mock responses + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + }); + + mockGetAccount(mockAccount).as('getAccount'); + mockGetRegions(mockRegions).as('getRegions'); + mockGetLinodes(mockLinodes).as('getLinodes'); + + // intercept request + cy.visitWithLogin('/images/create'); + cy.wait([ + '@getFeatureFlags', + '@getClientStream', + '@getAccount', + '@getLinodes', + '@getRegions', + ]); + + // Find the Linode select and open it + cy.findByLabelText('Linode') + .should('be.visible') + .should('be.enabled') + .should('have.attr', 'placeholder', 'Select a Linode') + .click(); + + // Select the Linode + ui.autocompletePopper + .findByTitle(mockLinodes[0].label) + .should('be.visible') + .should('be.enabled') + .click(); + + // Check if notice is visible + cy.findByText(DISK_ENCRYPTION_IMAGES_CAVEAT_COPY).should('be.visible'); + }); + + it('does not display a notice informing user that Images are not encrypted if the LDE feature is disabled', () => { + // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out + mockAppendFeatureFlags({ + linodeDiskEncryption: makeFeatureFlagData(false), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + // Mock responses + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + }); + + mockGetAccount(mockAccount).as('getAccount'); + mockGetRegions(mockRegions).as('getRegions'); + mockGetLinodes(mockLinodes).as('getLinodes'); + + // intercept request + cy.visitWithLogin('/images/create'); + cy.wait([ + '@getFeatureFlags', + '@getClientStream', + '@getAccount', + '@getLinodes', + '@getRegions', + ]); + + // Find the Linode select and open it + cy.findByLabelText('Linode') + .should('be.visible') + .should('be.enabled') + .should('have.attr', 'placeholder', 'Select a Linode') + .click(); + + // Select the Linode + ui.autocompletePopper + .findByTitle(mockLinodes[0].label) + .should('be.visible') + .should('be.enabled') + .click(); + + // Check if notice is visible + cy.findByText(DISK_ENCRYPTION_IMAGES_CAVEAT_COPY).should('not.exist'); + }); + + it('does not display a notice informing user that Images are not encrypted if the selected linode is in an Edge region', () => { + // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out + mockAppendFeatureFlags({ + linodeDiskEncryption: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + // Mock responses + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + }); + + mockGetAccount(mockAccount).as('getAccount'); + mockGetRegions(mockRegions).as('getRegions'); + mockGetLinodes(mockLinodes).as('getLinodes'); + mockGetLinodeDetails(mockLinodes[1].id, mockLinodes[1]); + + // intercept request + cy.visitWithLogin('/images/create'); + cy.wait([ + '@getFeatureFlags', + '@getClientStream', + '@getAccount', + '@getRegions', + '@getLinodes', + ]); + + // Find the Linode select and open it + cy.findByLabelText('Linode') + .should('be.visible') + .should('be.enabled') + .should('have.attr', 'placeholder', 'Select a Linode') + .click(); + + // Select the Linode + ui.autocompletePopper + .findByTitle(mockLinodes[1].label) + .should('be.visible') + .should('be.enabled') + .click(); + + // Check if notice is visible + cy.findByText(DISK_ENCRYPTION_IMAGES_CAVEAT_COPY).should('not.exist'); + }); }); diff --git a/packages/manager/src/components/DiskEncryption/constants.tsx b/packages/manager/src/components/DiskEncryption/constants.tsx index 5d0ffe10ec8..a9799ec1c80 100644 --- a/packages/manager/src/components/DiskEncryption/constants.tsx +++ b/packages/manager/src/components/DiskEncryption/constants.tsx @@ -22,3 +22,6 @@ export const DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY = export const DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY = 'To enable disk encryption, delete the node pool and create a new node pool. New node pools are always encrypted.'; + +export const DISK_ENCRYPTION_IMAGES_CAVEAT_COPY = + 'Virtual Machine Images are not encrypted.'; diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index 273e0abf6c3..d83f4af3fc1 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -10,9 +10,12 @@ import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; import { Checkbox } from 'src/components/Checkbox'; +import { DISK_ENCRYPTION_IMAGES_CAVEAT_COPY } from 'src/components/DiskEncryption/constants'; +import { useIsDiskEncryptionFeatureEnabled } from 'src/components/DiskEncryption/utils'; import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; +import { getIsEdgeRegion } from 'src/components/RegionSelect/RegionSelect.utils'; import { Stack } from 'src/components/Stack'; import { SupportLink } from 'src/components/SupportLink'; import { TagsInput } from 'src/components/TagsInput/TagsInput'; @@ -25,7 +28,9 @@ import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGran import { useEventsPollingActions } from 'src/queries/events/events'; import { useCreateImageMutation } from 'src/queries/images'; import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useGrants } from 'src/queries/profile'; +import { useRegionsQuery } from 'src/queries/regions/regions'; export const CreateImageTab = () => { const [selectedLinodeId, setSelectedLinodeId] = React.useState( @@ -59,6 +64,10 @@ export const CreateImageTab = () => { globalGrantType: 'add_images', }); + const { + isDiskEncryptionFeatureEnabled, + } = useIsDiskEncryptionFeatureEnabled(); + const onSubmit = handleSubmit(async (values) => { try { await createImage(values); @@ -92,6 +101,23 @@ export const CreateImageTab = () => { const isRawDisk = selectedDisk?.filesystem === 'raw'; + /* + We only want to display the notice about disk encryption if: + 1. the Disk Encryption feature is enabled + 2. the selected linode is not in an Edge region + */ + const { data: regionsData } = useRegionsQuery(); + + const { data: linode } = useLinodeQuery( + selectedLinodeId ?? -1, + Boolean(selectedLinodeId) && isDiskEncryptionFeatureEnabled + ); + + const linodeIsInEdgeRegion = getIsEdgeRegion( + regionsData ?? [], + linode?.region ?? '' + ); + return (
@@ -113,7 +139,7 @@ export const CreateImageTab = () => { Select Linode & Disk By default, Linode images are limited to 6144 MB of data per disk. - Ensure your content doesn't exceed this limit, or{' '} + Ensure your content doesn’t exceed this limit, or{' '} { text="open a support ticket" title="Request to increase Image size limit when capturing from Linode disk" />{' '} - to request a higher limit. Additionally, images can't be created - from a raw disk or a disk that's formatted using a custom file - system. + to request a higher limit. Additionally, images can’t be + created from a raw disk or a disk that’s formatted using a + custom file system. { required value={selectedLinodeId} /> + {isDiskEncryptionFeatureEnabled && + !linodeIsInEdgeRegion && + selectedLinodeId !== null && ( + + ({ fontFamily: theme.font.normal })} + > + {DISK_ENCRYPTION_IMAGES_CAVEAT_COPY} + + + )} ( ({ useLinodeQuery: vi.fn().mockReturnValue({ @@ -30,12 +33,16 @@ vi.mock('src/queries/types', async () => { }; }); +const diskEncryptionEnabledMock = vi.hoisted(() => { + return { + useIsDiskEncryptionFeatureEnabled: vi.fn(), + }; +}); + describe('EnableBackupsDialog component', () => { beforeEach(() => { queryMocks.useTypeQuery.mockReturnValue({ data: typeFactory.build({ - id: 'mock-linode-type', - label: 'Mock Linode Type', addons: { backups: { price: { @@ -51,17 +58,36 @@ describe('EnableBackupsDialog component', () => { ], }, }, + id: 'mock-linode-type', + label: 'Mock Linode Type', }), }); }); + vi.mock('src/components/DiskEncryption/utils.ts', async () => { + const actual = await vi.importActual( + 'src/components/DiskEncryption/utils.ts' + ); + return { + ...actual, + __esModule: true, + useIsDiskEncryptionFeatureEnabled: diskEncryptionEnabledMock.useIsDiskEncryptionFeatureEnabled.mockImplementation( + () => { + return { + isDiskEncryptionFeatureEnabled: false, // indicates the feature flag is off or account capability is absent + }; + } + ), + }; + }); + it('Displays the monthly backup price', async () => { queryMocks.useLinodeQuery.mockReturnValue({ data: linodeFactory.build({ id: 1, label: 'Mock Linode', - type: 'mock-linode-type', region: 'us-east', + type: 'mock-linode-type', }), }); @@ -84,12 +110,12 @@ describe('EnableBackupsDialog component', () => { data: linodeFactory.build({ id: 1, label: 'Mock Linode', - type: 'mock-linode-type', region: 'es-mad', + type: 'mock-linode-type', }), }); - const { getByTestId, findByText, queryByText } = renderWithTheme( + const { findByText, getByTestId, queryByText } = renderWithTheme( ); @@ -118,12 +144,12 @@ describe('EnableBackupsDialog component', () => { data: linodeFactory.build({ id: 1, label: 'Mock Linode', - type: 'mock-linode-type', region: 'es-mad', + type: 'mock-linode-type', }), }); - const { getByTestId, findByText } = renderWithTheme( + const { findByText, getByTestId } = renderWithTheme( ); @@ -133,4 +159,38 @@ describe('EnableBackupsDialog component', () => { // Confirm that "Enable Backups" button is disabled. expect(getByTestId('confirm-enable-backups')).toBeDisabled(); }); + + it('does not display a notice regarding Backups not being encrypted if the Disk Encryption feature is disabled', () => { + const { queryByText } = renderWithTheme( + + ); + + const encryptionBackupsCaveatNotice = queryByText( + DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY + ); + + expect(encryptionBackupsCaveatNotice).not.toBeInTheDocument(); + }); + + it('displays a notice regarding Backups not being encrypted if the Disk Encryption feature is enabled', () => { + diskEncryptionEnabledMock.useIsDiskEncryptionFeatureEnabled.mockImplementationOnce( + () => { + return { + isDiskEncryptionFeatureEnabled: true, + }; + } + ); + + const { queryByText } = renderWithTheme( + + ); + + const encryptionBackupsCaveatNotice = queryByText( + DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY + ); + + expect(encryptionBackupsCaveatNotice).toBeInTheDocument(); + + diskEncryptionEnabledMock.useIsDiskEncryptionFeatureEnabled.mockRestore(); + }); }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx index 9c28b43666f..ce79d0d47c1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.tsx @@ -5,6 +5,8 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Currency } from 'src/components/Currency'; +import { DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY } from 'src/components/DiskEncryption/constants'; +import { useIsDiskEncryptionFeatureEnabled } from 'src/components/DiskEncryption/utils'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; import { useEventsPollingActions } from 'src/queries/events/events'; @@ -40,6 +42,10 @@ export const EnableBackupsDialog = (props: Props) => { Boolean(linode?.type) ); + const { + isDiskEncryptionFeatureEnabled, + } = useIsDiskEncryptionFeatureEnabled(); + const backupsMonthlyPrice: | PriceObject['monthly'] | undefined = getMonthlyBackupsPrice({ @@ -95,6 +101,13 @@ export const EnableBackupsDialog = (props: Props) => { open={open} title="Enable backups?" > + {isDiskEncryptionFeatureEnabled && ( + + )} {!hasBackupsMonthlyPriceError ? ( Are you sure you want to enable backups on this Linode?{` `} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateImageFromDiskDialog.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateImageFromDiskDialog.test.tsx new file mode 100644 index 00000000000..423eaaeb977 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateImageFromDiskDialog.test.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; + +import { DISK_ENCRYPTION_IMAGES_CAVEAT_COPY } from 'src/components/DiskEncryption/constants'; +import { linodeDiskFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { CreateImageFromDiskDialog } from './CreateImageFromDiskDialog'; + +const diskEncryptionEnabledMock = vi.hoisted(() => { + return { + useIsDiskEncryptionFeatureEnabled: vi.fn(), + }; +}); + +describe('CreateImageFromDiskDialog component', () => { + const mockDisk = linodeDiskFactory.build(); + + vi.mock('src/components/DiskEncryption/utils.ts', async () => { + const actual = await vi.importActual( + 'src/components/DiskEncryption/utils.ts' + ); + return { + ...actual, + __esModule: true, + useIsDiskEncryptionFeatureEnabled: diskEncryptionEnabledMock.useIsDiskEncryptionFeatureEnabled.mockImplementation( + () => { + return { + isDiskEncryptionFeatureEnabled: false, // indicates the feature flag is off or account capability is absent + }; + } + ), + }; + }); + + it('does not display a notice regarding Images not being encrypted if the Disk Encryption feature is disabled', () => { + const { queryByText } = renderWithTheme( + + ); + + const encryptionImagesCaveatNotice = queryByText( + DISK_ENCRYPTION_IMAGES_CAVEAT_COPY + ); + + expect(encryptionImagesCaveatNotice).not.toBeInTheDocument(); + }); + + it('displays a notice regarding Images not being encrypted if the Disk Encryption feature is enabled', () => { + diskEncryptionEnabledMock.useIsDiskEncryptionFeatureEnabled.mockImplementationOnce( + () => { + return { + isDiskEncryptionFeatureEnabled: true, + }; + } + ); + + const { queryByText } = renderWithTheme( + + ); + + const encryptionImagesCaveatNotice = queryByText( + DISK_ENCRYPTION_IMAGES_CAVEAT_COPY + ); + + expect(encryptionImagesCaveatNotice).toBeInTheDocument(); + + diskEncryptionEnabledMock.useIsDiskEncryptionFeatureEnabled.mockRestore(); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateImageFromDiskDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateImageFromDiskDialog.tsx index f14d1a48988..a268ad7d764 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateImageFromDiskDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateImageFromDiskDialog.tsx @@ -5,6 +5,9 @@ import React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { DISK_ENCRYPTION_IMAGES_CAVEAT_COPY } from 'src/components/DiskEncryption/constants'; +import { useIsDiskEncryptionFeatureEnabled } from 'src/components/DiskEncryption/utils'; +import { Notice } from 'src/components/Notice/Notice'; import { SupportLink } from 'src/components/SupportLink/SupportLink'; import { useCreateImageMutation } from 'src/queries/images'; @@ -26,6 +29,10 @@ export const CreateImageFromDiskDialog = (props: Props) => { reset, } = useCreateImageMutation(); + const { + isDiskEncryptionFeatureEnabled, + } = useIsDiskEncryptionFeatureEnabled(); + React.useEffect(() => { if (open) { reset(); @@ -63,6 +70,13 @@ export const CreateImageFromDiskDialog = (props: Props) => { open={open} title={`Create Image from ${disk?.label}?`} > + {isDiskEncryptionFeatureEnabled && ( + + )} Linode Images are limited to 6144 MB of data per disk by default. Please ensure that your disk content does not exceed this size limit, or{' '}