diff --git a/cypress/e2e/update-cluster.cy.ts b/cypress/e2e/update-cluster.cy.ts new file mode 100644 index 00000000..fa3ea37a --- /dev/null +++ b/cypress/e2e/update-cluster.cy.ts @@ -0,0 +1,491 @@ +import root from '../fixtures/api/role-root.json'; +import guest from '../fixtures/api/role-guest.json'; +import user from '../fixtures/api/user.json'; +import guestUser from '../fixtures/api/guest-user.json'; +import cluster from '../fixtures/api/clusters/cluster/cluster.json'; +import updateCluster from '../fixtures/api/clusters/cluster/update-cluster.json'; +import _ from 'lodash'; + +describe('Update cluster', () => { + beforeEach(() => { + cy.signin(); + + cy.intercept( + { + method: 'GET', + url: '/api/v1/users/1', + }, + (req) => { + req.reply({ + statusCode: 200, + body: user, + }); + }, + ); + cy.intercept( + { + method: 'GET', + url: '/api/v1/users/1/roles', + }, + (req) => { + req.reply({ + statusCode: 200, + body: root, + }); + }, + ); + cy.intercept( + { + method: 'GET', + url: '/api/v1/clusters/1', + }, + (req) => { + req.reply({ + statusCode: 200, + body: cluster, + }); + }, + ); + cy.intercept( + { + method: 'GET', + url: '/api/v1/seed-peers?page=1&per_page=10000000&seed_peer_cluster_id=1', + }, + (req) => { + req.reply({ + statusCode: 200, + body: [], + }); + }, + ); + cy.intercept( + { + method: 'GET', + url: '/api/v1/schedulers?page=1&per_page=10000000&scheduler_cluster_id=1', + }, + (req) => { + req.reply({ + statusCode: 200, + body: [], + }); + }, + ); + + cy.visit('/clusters/1/edit'); + cy.viewport(1440, 1080); + }); + + it('when data is loaded', () => { + // Show cluster information. + cy.get('.MuiPaper-root > .css-0 > :nth-child(2)').should('contain', 'cluster-1'); + + cy.get('.PrivateSwitchBase-input').should('be.checked').check({ force: true }); + + // Show scopes. + cy.get('#location').should('have.value', 'China|Hang|Zhou'); + + cy.get(':nth-child(2) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root') + .should('contain', 'Hangzhou') + .and('contain', 'Shanghai') + .and('contain', 'Beijing'); + + cy.get(':nth-child(3) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root') + .should('contain', '10.0.0.0/8') + .and('contain', '192.168.0.0/16') + .and('contain', '172.16.0.0/12'); + + // Show config. + cy.get('#seedPeerLoadLimit').should('have.value', 300); + cy.get('#peerLoadLimit').should('have.value', 51); + cy.get('#numberOfConcurrentDownloadPieces').should('have.value', 4); + cy.get('#candidateParentLimit').should('have.value', 4); + cy.get('#filterParentLimit').should('have.value', 40); + }); + + it('when no data is loaded', () => { + cy.intercept( + { + method: 'GET', + url: '/api/v1/clusters/1', + }, + (req) => { + req.reply({ + statusCode: 401, + body: { message: 'Not Found' }, + }); + }, + ); + // Show cluster information. + + cy.get('.MuiPaper-outlined > .css-0 > :nth-child(1)').should('not.contain', '1'); + + cy.get('.MuiPaper-root > .css-0 > :nth-child(2)').should('not.contain', 'cluster-1'); + + cy.get('.PrivateSwitchBase-input').should('not.be.checked').check({ force: false }); + + // Show scopes. + cy.get('#location').should('have.value', ''); + + cy.get(':nth-child(2) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root').should( + 'have.value', + '', + ); + + cy.get(':nth-child(3) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root').should( + 'have.value', + '', + ); + + // Show config. + cy.get('#seedPeerLoadLimit').should('have.value', 0); + cy.get('#peerLoadLimit').should('have.value', 0); + cy.get('#numberOfConcurrentDownloadPieces').should('have.value', 0); + cy.get('#candidateParentLimit').should('have.value', 0); + cy.get('#filterParentLimit').should('have.value', 0); + }); + + it('can update cluster', () => { + cy.intercept({ method: 'PATCH', url: '/api/v1/clusters/1' }, (req) => { + (req.body = ''), + req.reply({ + statusCode: 200, + body: [], + }); + }); + cy.visit('/clusters/1'); + + // Click update cluster button. + cy.get('.css-bbra84-MuiButtonBase-root-MuiButton-root').click(); + + // Show cluster name. + cy.get('.MuiPaper-root > .css-0 > :nth-child(2)').should('be.visible').and('contain', 'cluster-1'); + + cy.get('.PrivateSwitchBase-input').click(); + + // Update cluster description. + cy.get('#description').clear(); + cy.get('#description').type('update cluster-1'); + + // Update cluster Scopes. + cy.get('#location').clear(); + cy.get('#location').type('China|Shang|Hai'); + + cy.get(':nth-child(2) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root') + .type('{selectall}') + .type('{backspace}'); + + cy.get(':nth-child(3) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root').type( + '192.168.20.2{enter}', + ); + + // Update cluster Config. + cy.get('#seedPeerLoadLimit').clear(); + cy.get('#seedPeerLoadLimit').type('400'); + + cy.get('#peerLoadLimit').clear(); + cy.get('#peerLoadLimit').type('50'); + + cy.get('#numberOfConcurrentDownloadPieces').clear(); + cy.get('#numberOfConcurrentDownloadPieces').type('8'); + + cy.get('#candidateParentLimit').clear(); + cy.get('#candidateParentLimit').type('5'); + + cy.get('#filterParentLimit').clear(); + cy.get('#filterParentLimit').type('50'); + + // Click save button. + cy.get('#save').click(); + + cy.intercept( + { + method: 'GET', + url: '/api/v1/clusters/1', + }, + (req) => { + req.reply({ + statusCode: 200, + body: updateCluster, + }); + }, + ); + + // Then I see that the current page is the clusters/1! + cy.url().should('include', '/clusters/1'); + + cy.get('.show_container__osP4U > .MuiTypography-root').scrollIntoView(); + + // Check whether the cluster information is updated successfully. + cy.get('.information_clusterContainer__l8H8p > :nth-child(3) > .MuiTypography-subtitle1') + .should('be.visible') + .and('contain', 'update cluster-1'); + + cy.get('.information_clusterContainer__l8H8p > :nth-child(5) > .MuiTypography-subtitle1') + .should('be.visible') + .and('contain', 'No'); + + cy.get('#cidrs').click(); + + cy.get('.MuiDialogContent-root > :nth-child(4)').should('be.visible').and('contain', '192.168.20.2'); + + cy.get('body').click('topLeft'); + + cy.get('.MuiPaper-root > :nth-child(1) > .MuiTypography-body1') + .scrollIntoView() + .should('be.visible') + .and('contain', '400'); + }); + + it('click the `CANCEL button', () => { + cy.get('#cancel').click(); + + // Then I see that the current page is the clusters/1! + cy.url().should('include', '/clusters/1'); + }); + + it('try to update cluster with guest user', () => { + cy.intercept( + { + method: 'GET', + url: '/api/v1/users/2', + }, + (req) => { + req.reply({ + statusCode: 200, + body: guestUser, + }); + }, + ); + cy.intercept( + { + method: 'GET', + url: '/api/v1/users/2/roles', + }, + (req) => { + req.reply({ + statusCode: 200, + body: guest, + }); + }, + ); + cy.intercept({ method: 'PATCH', url: '/api/v1/clusters/1' }, (req) => { + (req.body = ''), + req.reply({ + statusCode: 401, + body: { message: 'permission deny' }, + }); + }); + + cy.guestSignin(); + + cy.get('#save').click(); + + // Show error message. + cy.get('.MuiAlert-message').should('be.visible').and('contain', 'permission deny'); + + // Close error message. + cy.get(':nth-child(1) > .MuiSnackbar-root > .MuiPaper-root > .MuiAlert-action > .MuiButtonBase-root').click(); + cy.get('.MuiAlert-message').should('not.exist'); + }); + + describe('should handle API error response', () => { + it('get cluster API error response', () => { + cy.intercept( + { + method: 'GET', + url: '/api/v1/clusters/1', + }, + (req) => { + req.reply({ + forceNetworkError: true, + }); + }, + ); + + cy.get('.MuiAlert-message').should('be.visible').and('contain', 'Failed to fetch'); + cy.get('.MuiAlert-action > .MuiButtonBase-root').click(); + cy.get('.MuiSnackbar-root > .MuiPaper-root').should('not.exist'); + }); + + it('update cluster API error response', () => { + cy.intercept({ method: 'PATCH', url: '/api/v1/clusters/1' }, (req) => { + (req.body = ''), + req.reply({ + forceNetworkError: true, + }); + }); + + cy.get('#description').clear(); + cy.get('#description').type('update cluster-1'); + + cy.get('#save').click(); + cy.get('.MuiAlert-message').should('be.visible').and('contain', 'Failed to fetch'); + }); + }); + + describe('cannot update cluster with invalid attributes', () => { + it('try to verify information', () => { + const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + const description = _.times(1001, () => _.sample(characters)).join(''); + + cy.get('#description').clear(); + cy.get('#description').type(description); + + // Show verification error message. + cy.get('#description-helper-text') + .should('be.visible') + .and('contain', 'Fill in the characters, the length is 0-1000.'); + + // Submit form when validation fails. + cy.get('#save').click(); + cy.url().should('include', '/clusters/1/edit'); + cy.get('#description').clear(); + + // Verification passed. + cy.get('#description').type('cluster description'); + }); + + it('try to verify scopes', () => { + const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + const location = _.times(101, () => _.sample(characters)).join(''); + + // Should display location the validation error message. + cy.get('#location').type(location); + + // Show verification error message. + cy.get('#location-helper-text') + .should('be.visible') + .and('contain', 'Fill in the characters, the length is 0-100.'); + + // Submit form when validation fails. + cy.get('#save').click(); + cy.url().should('include', '/clusters/1/edit'); + cy.get('#location').clear(); + + // Verification passed. + cy.get('#location').type('Beijing'); + cy.get('#location-helper-text').should('not.exist'); + + // Should display idc the validation error message. + cy.get(':nth-child(2) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root').type('hz'); + cy.get('#save').click(); + cy.url().should('include', '/clusters/1/edit'); + cy.get('#idc-helper-text').should('be.visible').and('contain', `Please press ENTER to end the IDC creation.`); + + // Verification passed. + cy.get(':nth-child(2) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root').type('hz{enter}'); + cy.get('#idc-helper-text').should('not.exist'); + + // Should display cidrs the validation error message. + cy.get(':nth-child(3) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root').type( + '192.168.40.0/24', + ); + cy.get('#save').click(); + cy.url().should('include', '/clusters/1/edit'); + // Show verification error message. + cy.get('#cidrs-helper-text').should('be.visible').and('contain', `Please press ENTER to end the CIDRs creation.`); + + cy.get(':nth-child(3) > .MuiAutocomplete-root > .MuiFormControl-root > .MuiInputBase-root').type( + '192.168.40.0/24{enter}', + ); + cy.get('#cidrs-helper-text').should('not.exist'); + }); + + it('try to verify config', () => { + // Should display seed peer load limit the validation error message. + cy.get('#seedPeerLoadLimit').type('5000'); + + // Show verification error message. + cy.get('#seedPeerLoadLimit-helper-text') + .should('be.visible') + .and('contain', `Fill in the number, the length is 0-5000.`); + + // Submit form when validation fails. + cy.get('#save').click(); + + // cluster creation failed, the page is still in cluster/new! + cy.url().should('include', '/clusters/1/edit'); + cy.get('#seedPeerLoadLimit').clear(); + cy.get('#seedPeerLoadLimit').type('400'); + + // Verification passed. + cy.get('#seedPeerLoadLimit-helper-text').should('not.exist'); + + // Should display peer load limit the validation error message. + cy.get('#peerLoadLimit').clear(); + cy.get('#peerLoadLimit').type('2001'); + + // Show verification error message. + cy.get('#peerLoadLimit-helper-text') + .should('be.visible') + .and('contain', `Fill in the number, the length is 0-2000.`); + cy.get('#save').click(); + cy.url().should('include', '/clusters/1/edit'); + cy.get('#peerLoadLimit').clear(); + cy.get('#peerLoadLimit').type('50'); + + // Verification passed. + cy.get('#peerLoadLimit-helper-text').should('not.exist'); + + // Should display number of concurrent download pieces the validation error message. + cy.get('#numberOfConcurrentDownloadPieces').clear(); + cy.get('#numberOfConcurrentDownloadPieces').type('51'); + + // Show verification error message. + cy.get('#numberOfConcurrentDownloadPieces-helper-text') + .should('be.visible') + .and('contain', `Fill in the number, the length is 0-50.`); + cy.get('#save').click(); + cy.url().should('include', '/clusters/1/edit'); + cy.get('#numberOfConcurrentDownloadPieces').clear(); + cy.get('#numberOfConcurrentDownloadPieces').type('10'); + + // Verification passed. + cy.get('#numberOfConcurrentDownloadPieces-helper-text').should('not.exist'); + + // Should display candidate parent limit the validation error message. + cy.get('#candidateParentLimit').clear(); + cy.get('#candidateParentLimit').type('21'); + + // Show verification error message. + cy.get('#candidateParentLimit-helper-text') + .should('be.visible') + .and('contain', `Fill in the number, the length is 1-20.`); + cy.get('#save').click(); + cy.url().should('include', '/clusters/1/edit'); + cy.get('#candidateParentLimit').clear(); + cy.get('#candidateParentLimit').type('5'); + cy.get('#candidateParentLimit-helper-text').should('not.exist'); + + // Should display filter parent limit the validation error message. + cy.get('#filterParentLimit').clear(); + + // Minimum validation range not reached. + cy.get('#filterParentLimit').type('9'); + + // Show verification error message. + cy.get('#filterParentLimit-helper-text') + .should('be.visible') + .and('contain', `Fill in the number, the length is 10-1000.`); + cy.get('#save').click(); + cy.url().should('include', '/clusters/1/edit'); + cy.get('#filterParentLimit').clear(); + + // Maximum verification range exceeded. + cy.get('#filterParentLimit').type('1001'); + + // Show verification error message. + cy.get('#filterParentLimit-helper-text') + .should('be.visible') + .and('contain', `Fill in the number, the length is 10-1000.`); + cy.get('#save').click(); + + cy.url().should('include', '/clusters/1/edit'); + cy.get('#filterParentLimit').clear(); + cy.get('#filterParentLimit').type('100'); + + // Verification passed. + cy.get('#filterParentLimit-helper-text').should('not.exist'); + }); + }); +}); diff --git a/cypress/fixtures/api/clusters/cluster/update-cluster.json b/cypress/fixtures/api/clusters/cluster/update-cluster.json new file mode 100644 index 00000000..961e615d --- /dev/null +++ b/cypress/fixtures/api/clusters/cluster/update-cluster.json @@ -0,0 +1,26 @@ +{ + "id": 1, + "name": "cluster-1", + "bio": "update cluster-1", + "scopes": { + "idc": "Hangzhou|Shanghai", + "location": "China|Shang|Hai", + "cidrs": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "192.168.20.2"] + }, + "scheduler_cluster_id": 1, + "seed_peer_cluster_id": 1, + "scheduler_cluster_config": { + "candidate_parent_limit": 5, + "filter_parent_limit": 50 + }, + "seed_peer_cluster_config": { + "load_limit": 400 + }, + "peer_cluster_config": { + "load_limit": 50, + "concurrent_piece_count": 8 + }, + "created_at": "2023-10-31T07:48:35Z", + "updated_at": "2023-10-31T07:48:35Z", + "is_default": false +} diff --git a/package.json b/package.json index c4d0ee95..15f9f85b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "eject": "react-scripts eject", "lint": "react-scripts lint", "cy:open": "npx cypress open", - "cy:run": "npx cypress run --browser chrome", + "cy:run": "npx cypress run --headed", "coverage:verify": "npx nyc report --check-coverage true --lines 10" }, "dependencies": { diff --git a/src/components/clusters/edit.tsx b/src/components/clusters/edit.tsx index d4552eac..5f9ee34b 100644 --- a/src/components/clusters/edit.tsx +++ b/src/components/clusters/edit.tsx @@ -17,7 +17,7 @@ import { LoadingButton } from '@mui/lab'; import styles from './edit.module.css'; import HelpIcon from '@mui/icons-material/Help'; import { useEffect, useState } from 'react'; -import { getCluster, updateCluster } from '../../lib/api'; +import { getCluster, updateCluster, getClusterResponse } from '../../lib/api'; import CancelIcon from '@mui/icons-material/Cancel'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import { useNavigate, useParams } from 'react-router-dom'; @@ -36,14 +36,14 @@ export default function EditCluster() { const [idcError, setIDCError] = useState(false); const [cidrsError, setCIDRsError] = useState(false); const [loadingButton, setLoadingButton] = useState(false); - const [cluster, setCluster] = useState({ + const [cluster, setCluster] = useState({ id: 0, name: '', bio: '', scopes: { idc: '', location: '', - cidrs: [''], + cidrs: [], }, scheduler_cluster_id: 0, seed_peer_cluster_id: 0, @@ -98,9 +98,9 @@ export default function EditCluster() { const informationForm = [ { formProps: { - id: 'bio', + id: 'description', label: 'Description', - name: 'bio', + name: 'description', autoComplete: 'family-name', value: bio, placeholder: 'Please enter description', @@ -254,7 +254,7 @@ export default function EditCluster() { const configForm = [ { formProps: { - id: 'seed peer load limit', + id: 'seedPeerLoadLimit', label: 'Seed Peer load limit', name: 'seedPeerLoadLimit', type: 'number', @@ -292,7 +292,7 @@ export default function EditCluster() { }, { formProps: { - id: 'peer load limit', + id: 'peerLoadLimit', label: 'Peer load limit', name: 'peerLoadLimit', type: 'number', @@ -332,7 +332,7 @@ export default function EditCluster() { }, { formProps: { - id: 'number of concurrent download pieces', + id: 'numberOfConcurrentDownloadPieces', label: 'Number of concurrent download pieces', name: 'numberOfConcurrentDownloadPieces', type: 'number', @@ -371,7 +371,7 @@ export default function EditCluster() { }, { formProps: { - id: 'candidate parent limit', + id: 'candidateParentLimit', label: 'Candidate parent limit', name: 'candidateParentLimit', type: 'number', @@ -413,9 +413,9 @@ export default function EditCluster() { }, { formProps: { - id: 'filter parent limit', + id: 'filterParentLimit', label: 'Filter parent limit', - name: 'filter parent limit', + name: 'filterParentLimit', type: 'number', autoComplete: 'family-name', placeholder: 'Please enter Filter parent limit', @@ -476,7 +476,7 @@ export default function EditCluster() { const cidrsText = event.currentTarget.elements.cidrs.value; if (idcText) { - setIDCHelperText('Please press ENTER to end the IDC creation'); + setIDCHelperText('Please press ENTER to end the IDC creation.'); setIDCError(true); } else { setIDCError(false); @@ -484,7 +484,7 @@ export default function EditCluster() { } if (cidrsText) { - setCIDRsHelperText('Please press ENTER to end the CIDRs creation'); + setCIDRsHelperText('Please press ENTER to end the CIDRs creation.'); setCIDRsError(true); } else { setCIDRsError(false); @@ -638,7 +638,7 @@ export default function EditCluster() { ))} - scopes + Scopes