From c8e291e2624136967dfa4b3313a2099a2b8dd425 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:22:11 -0500 Subject: [PATCH] test: [M3-7435, M3-7637] - Cypress tests for OBJ Multicluster access key management (#10144) * Add QA data attribute for selection list component * Add tests for OBJ multi cluster access key edit and create flows --- .../pr-10144-tests-1707158192794.md | 5 + .../objectStorage/access-keys.smoke.spec.ts | 400 +++++++++++++++++- .../support/intercepts/object-storage.ts | 36 ++ .../RemovableSelectionsList.tsx | 6 +- 4 files changed, 444 insertions(+), 3 deletions(-) create mode 100644 packages/manager/.changeset/pr-10144-tests-1707158192794.md diff --git a/packages/manager/.changeset/pr-10144-tests-1707158192794.md b/packages/manager/.changeset/pr-10144-tests-1707158192794.md new file mode 100644 index 00000000000..3a43c6f51a3 --- /dev/null +++ b/packages/manager/.changeset/pr-10144-tests-1707158192794.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Cypress tests for OBJ Multicluster access key operations ([#10144](https://github.com/linode/manager/pull/10144)) diff --git a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts index ac17fe65c95..cc1f74b33b7 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts @@ -2,7 +2,10 @@ * @file Smoke tests for crucial Object Storage Access Keys operations. */ -import { objectStorageKeyFactory } from 'src/factories/objectStorage'; +import { + objectStorageKeyFactory, + objectStorageBucketFactory, +} from 'src/factories/objectStorage'; import { mockAppendFeatureFlags, mockGetFeatureFlagClientstream, @@ -11,10 +14,21 @@ import { mockCreateAccessKey, mockDeleteAccessKey, mockGetAccessKeys, + mockGetBucketsForRegion, + mockUpdateAccessKey, } from 'support/intercepts/object-storage'; import { makeFeatureFlagData } from 'support/util/feature-flags'; -import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { + randomDomainName, + randomLabel, + randomNumber, + randomString, +} from 'support/util/random'; import { ui } from 'support/ui'; +import { regionFactory } from 'src/factories'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { buildArray } from 'support/util/arrays'; +import { Scope } from '@linode/api-v4'; describe('object storage access keys smoke tests', () => { /* @@ -132,4 +146,386 @@ describe('object storage access keys smoke tests', () => { cy.wait(['@deleteKey', '@getKeys']); cy.findByText('No items to display.').should('be.visible'); }); + + describe('Object Storage Multicluster feature enabled', () => { + const mockRegionsObj = buildArray(3, () => { + return regionFactory.build({ + id: `us-${randomString(5)}`, + label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], + }); + }); + + const mockRegionsNoObj = regionFactory.buildList(3, { + capabilities: [], + }); + + const mockRegions = [...mockRegionsObj, ...mockRegionsNoObj]; + + beforeEach(() => { + mockAppendFeatureFlags({ + objMultiCluster: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + /* + * - Confirms user can create access keys with unlimited access when OBJ Multicluster is enabled. + * - Confirms multiple regions can be selected when creating an access key. + * - Confirms that UI updates to reflect created access key. + */ + it('can create unlimited access keys with OBJ Multicluster', () => { + const mockAccessKey = objectStorageKeyFactory.build({ + id: randomNumber(10000, 99999), + label: randomLabel(), + access_key: randomString(20), + secret_key: randomString(39), + regions: mockRegionsObj.map((mockObjRegion) => ({ + id: mockObjRegion.id, + s3_endpoint: randomDomainName(), + })), + }); + + mockGetAccessKeys([]); + mockCreateAccessKey(mockAccessKey).as('createAccessKey'); + mockGetRegions(mockRegions); + mockRegions.forEach((region) => { + mockGetBucketsForRegion(region.id, []); + }); + + cy.visitWithLogin('/object-storage/access-keys'); + + ui.button + .findByTitle('Create Access Key') + .should('be.visible') + .should('be.enabled') + .click(); + + mockGetAccessKeys([mockAccessKey]); + ui.drawer + .findByTitle('Create Access Key') + .should('be.visible') + .within(() => { + cy.contains('Label (required)') + .should('be.visible') + .click() + .type(mockAccessKey.label); + + cy.contains('Regions (required)').should('be.visible').click(); + + // Select each region with the OBJ capability. + mockRegionsObj.forEach((mockRegion) => { + cy.contains('Regions (required)').type(mockRegion.label); + ui.autocompletePopper + .findByTitle(`${mockRegion.label} (${mockRegion.id})`) + .should('be.visible') + .click(); + }); + + // Close the regions drop-down. + cy.contains('Regions (required)') + .should('be.visible') + .click() + .type('{esc}'); + + // TODO Confirm expected regions are shown. + ui.buttonGroup + .findButtonByTitle('Create Access Key') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@createAccessKey'); + ui.dialog + .findByTitle('Access Keys') + .should('be.visible') + .within(() => { + // TODO Add assertions for S3 hostnames + cy.get('input[id="access-key"]') + .should('be.visible') + .should('have.value', mockAccessKey.access_key); + cy.get('input[id="secret-key"]') + .should('be.visible') + .should('have.value', mockAccessKey.secret_key); + + ui.button + .findByTitle('I Have Saved My Secret Key') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.findByText(mockAccessKey.label) + .should('be.visible') + .closest('tr') + .within(() => { + // TODO Add assertions for regions/S3 hostnames + cy.findByText(mockAccessKey.access_key).should('be.visible'); + }); + }); + + /* + * - COnfirms user can create access keys with limited access when OBJ Multicluster is enabled. + * - Confirms that UI updates to reflect created access key. + * - Confirms that "Permissions" drawer contains expected scope and permission data. + */ + it('can create limited access keys with OBJ Multicluster', () => { + const mockRegion = regionFactory.build({ + id: `us-${randomString(5)}`, + label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], + }); + + const mockBuckets = objectStorageBucketFactory.buildList(2, { + region: mockRegion.id, + cluster: undefined, + }); + + const mockAccessKey = objectStorageKeyFactory.build({ + id: randomNumber(10000, 99999), + label: randomLabel(), + access_key: randomString(20), + secret_key: randomString(39), + regions: [ + { + id: mockRegion.id, + s3_endpoint: randomDomainName(), + }, + ], + limited: true, + bucket_access: mockBuckets.map( + (bucket): Scope => ({ + bucket_name: bucket.label, + cluster: '', + permissions: 'read_only', + region: mockRegion.id, + }) + ), + }); + + mockGetAccessKeys([]); + mockCreateAccessKey(mockAccessKey).as('createAccessKey'); + mockGetRegions([mockRegion]); + mockGetBucketsForRegion(mockRegion.id, mockBuckets); + + // Navigate to access keys page, click "Create Access Key" button. + cy.visitWithLogin('/object-storage/access-keys'); + ui.button + .findByTitle('Create Access Key') + .should('be.visible') + .should('be.enabled') + .click(); + + // Fill out form in "Create Access Key" drawer. + ui.drawer + .findByTitle('Create Access Key') + .should('be.visible') + .within(() => { + cy.contains('Label (required)') + .should('be.visible') + .click() + .type(mockAccessKey.label); + + cy.contains('Regions (required)') + .should('be.visible') + .click() + .type(`${mockRegion.label}{enter}`); + + ui.autocompletePopper + .findByTitle(`${mockRegion.label} (${mockRegion.id})`) + .should('be.visible'); + + // Dismiss region drop-down. + cy.contains('Regions (required)') + .should('be.visible') + .click() + .type('{esc}'); + + // Enable "Limited Access" toggle for access key, and select access rules. + cy.findByText('Limited Access').should('be.visible').click(); + + mockBuckets.forEach((mockBucket) => { + cy.findByText(mockBucket.label) + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByLabelText( + `read-only for ${mockRegion.id}-${mockBucket.label}` + ) + .should('be.enabled') + .click(); + }); + }); + + mockGetAccessKeys([mockAccessKey]); + ui.buttonGroup + .findButtonByTitle('Create Access Key') + .should('be.enabled') + .click(); + }); + + // Dismiss secrets dialog. + cy.wait('@createAccessKey'); + ui.buttonGroup + .findButtonByTitle('I Have Saved My Secret Key') + .should('be.visible') + .should('be.enabled') + .click(); + + // Open "Permissions" drawer for new access key. + cy.findByText(mockAccessKey.label) + .should('be.visible') + .closest('tr') + .within(() => { + ui.actionMenu + .findByTitle( + `Action menu for Object Storage Key ${mockAccessKey.label}` + ) + .should('be.visible') + .click(); + }); + + ui.actionMenuItem.findByTitle('Permissions').click(); + ui.drawer + .findByTitle(`Permissions for ${mockAccessKey.label}`) + .should('be.visible') + .within(() => { + mockBuckets.forEach((mockBucket) => { + // TODO M3-7733 Update this selector when ARIA label is fixed. + cy.findByLabelText( + `This token has read-only access for -${mockBucket.label}` + ); + }); + }); + }); + + /* + * - Confirms user can edit access key labels and regions when OBJ Multicluster is enabled. + * - Confirms that user can deselect regions via the region selection list. + * - Confirms that access keys landing page automatically updates to reflect edited access key. + */ + it('can update access keys with OBJ Multicluster', () => { + const mockInitialRegion = regionFactory.build({ + id: `us-${randomString(5)}`, + label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], + }); + + const mockUpdatedRegion = regionFactory.build({ + id: `us-${randomString(5)}`, + label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], + }); + + const mockRegions = [mockInitialRegion, mockUpdatedRegion]; + + const mockAccessKey = objectStorageKeyFactory.build({ + id: randomNumber(10000, 99999), + label: randomLabel(), + access_key: randomString(20), + secret_key: randomString(39), + regions: [ + { + id: mockInitialRegion.id, + s3_endpoint: randomDomainName(), + }, + ], + }); + + const mockUpdatedAccessKeyEndpoint = randomDomainName(); + + const mockUpdatedAccessKey = { + ...mockAccessKey, + label: randomLabel(), + regions: [ + { + id: mockUpdatedRegion.id, + s3_endpoint: mockUpdatedAccessKeyEndpoint, + }, + ], + }; + + mockGetAccessKeys([mockAccessKey]); + mockGetRegions(mockRegions); + cy.visitWithLogin('/object-storage/access-keys'); + + cy.findByText(mockAccessKey.label) + .should('be.visible') + .closest('tr') + .within(() => { + ui.actionMenu + .findByTitle( + `Action menu for Object Storage Key ${mockAccessKey.label}` + ) + .should('be.visible') + .click(); + }); + + ui.actionMenuItem + .findByTitle('Edit') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle('Edit Access Key') + .should('be.visible') + .within(() => { + cy.contains('Label (required)') + .should('be.visible') + .click() + .type('{selectall}{backspace}') + .type(mockUpdatedAccessKey.label); + + cy.contains('Regions (required)') + .should('be.visible') + .click() + .type(`${mockUpdatedRegion.label}{enter}{esc}`); + + cy.get('[data-qa-selection-list]') + .should('be.visible') + .within(() => { + // Confirm both regions are selected and present in selection list. + mockRegions.forEach((mockRegion) => { + cy.findByText(`${mockRegion.label} (${mockRegion.id})`).should( + 'be.visible' + ); + }); + + // Deselect initial region and confirm it's removed from list. + cy.findByLabelText( + `remove ${mockInitialRegion.label} (${mockInitialRegion.id})` + ) + .should('be.visible') + .should('be.enabled') + .click(); + + cy.findByText( + `${mockInitialRegion.label} (${mockInitialRegion.id})` + ).should('not.exist'); + }); + + mockUpdateAccessKey(mockUpdatedAccessKey).as('updateAccessKey'); + mockGetAccessKeys([mockUpdatedAccessKey]); + ui.button + .findByTitle('Save Changes') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateAccessKey'); + + // Confirm that access key landing page reflects updated key. + cy.findByText(mockAccessKey.label).should('not.exist'); + cy.findByText(mockUpdatedAccessKey.label) + .should('be.visible') + .closest('tr') + .within(() => { + cy.contains(mockUpdatedRegion.label).should('be.visible'); + cy.contains(mockUpdatedAccessKeyEndpoint).should('be.visible'); + }); + }); + }); }); diff --git a/packages/manager/cypress/support/intercepts/object-storage.ts b/packages/manager/cypress/support/intercepts/object-storage.ts index 3996a18a3e3..f993da79d73 100644 --- a/packages/manager/cypress/support/intercepts/object-storage.ts +++ b/packages/manager/cypress/support/intercepts/object-storage.ts @@ -51,6 +51,25 @@ export const mockGetBuckets = ( ); }; +/** + * Intercepts GET request to fetch buckets for a region and mocks response. + * + * @param regionId - ID of region for which to mock buckets. + * @param buckets - Array of Bucket objects with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetBucketsForRegion = ( + regionId: string, + buckets: ObjectStorageBucket[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher(`object-storage/buckets/${regionId}*`), + paginateResponse(buckets) + ); +}; + /** * Intercepts POST request to create bucket. * @@ -352,6 +371,23 @@ export const mockCreateAccessKey = ( ); }; +/** + * Intercepts request to update an Object Storage Access Key and mocks response. + * + * @param updatedAccessKey - Access key with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockUpdateAccessKey = ( + updatedAccessKey: ObjectStorageKey +): Cypress.Chainable => { + return cy.intercept( + 'PUT', + apiMatcher(`object-storage/keys/${updatedAccessKey.id}`), + makeResponse(updatedAccessKey) + ); +}; + /** * Intercepts object storage access key DELETE request and mocks success response. * diff --git a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx index 3b16017e78f..61988591ebe 100644 --- a/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx +++ b/packages/manager/src/components/RemovableSelectionsList/RemovableSelectionsList.tsx @@ -100,7 +100,11 @@ export const RemovableSelectionsList = ( maxWidth={maxWidth} > - + {selectionData.map((selection) => (