Skip to content

Commit

Permalink
test(GDB-11138) Cypress Tests for Cluster Multi-Region Support
Browse files Browse the repository at this point in the history
## WHAT:
Refactored and expanded Cypress tests and UI components for cluster configuration and multi-region support.11

## WHY
To test common use cases

## HOW
- Added helper methods in ClusterConfigurationSteps for interacting with the modal dialog and multi-region tab elements.
- Enhanced test configuration with new stubs
- Updated templates for cluster-properties, cluster-nodes, and multi-region tabs with unique identifiers and modular class names
- Modified existing Cypress fixtures and added new fixtures to support primary and secondary cluster mode scenarios
  • Loading branch information
teodossidossev committed Oct 31, 2024
1 parent f298e2c commit 58c0d6b
Show file tree
Hide file tree
Showing 12 changed files with 557 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ <h3 class="hovered-parent">
{{'cluster_management.cluster_configuration.label' | translate}}
</h3>

<ul class="nav nav-tabs acl-tabs">
<ul class="nav nav-tabs configuration-tabs">
<li class="properties-tab nav-item">
<a href="#" class="nav-link"
ng-click="switchTab($event, CONFIGURATION_TABS.PROPERTIES)"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<div class="cluster-nodes">
<h4>{{'cluster_management.cluster_configuration.nodes_list_label' | translate}}</h4>
<h4 class="title">{{'cluster_management.cluster_configuration.nodes_list_label' | translate}}</h4>
<ul class="nodes-list">
<li ng-repeat="node in clusterModel.nodes">
<a class="data-value" href="{{node.endpoint}}" target="_blank">{{node.endpoint}}</a>
<a class="data-value endpoint" href="{{node.endpoint}}" target="_blank">{{node.endpoint}}</a>
<div class="small rpc-address">{{ 'cluster_management.cluster_status.node.rpc_address' | translate }}: <span
class="data-value">{{node.address}}</span>
</div>
<div class="small">{{ 'cluster_management.cluster_status.node.state' | translate }}: <span
<div class="small state">{{ 'cluster_management.cluster_status.node.state' | translate }}: <span
class="data-value">{{node.nodeState}}</span>
<span ng-if="currentNode.endpoint === node.endpoint">({{ 'cluster_management.cluster_configuration_nodes.local' | translate }})</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="cluster-properties">
<div class="mb-2" ng-if="isAdmin">
<button type="button" class="btn btn-primary edit-cluster-configuration"
<button type="button" class="btn btn-primary edit-properties-btn"
ng-click="showEditConfigurationDialog()"
gdb-tooltip="{{'cluster_management.buttons.edit_cluster_tooltip' | translate}}"
tooltip-placement="bottom">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<div class="multi-region">
<div ng-if="topology.state === TopologyState.PRIMARY_NODE">
<h4>{{'cluster_management.cluster_configuration_multi_region.primary_cluster' | translate}}</h4>
<h4 class="title">{{'cluster_management.cluster_configuration_multi_region.primary_cluster' | translate}}</h4>
<div class="mb-2" ng-if="isAdmin">
<button type="button" class="btn btn-primary create-tag-btn"
ng-click="add()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ <h3 class="modal-title">{{pageTitle}}</h3>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" ng-click="cancel()">{{'common.cancel.btn' | translate}}</button>
<button type="button" class="btn btn-secondary cancel-cluster-configuration-btn" ng-click="cancel()">{{'common.cancel.btn' | translate}}</button>
<button type="submit" ng-click="ok()" ng-disabled="!isClusterConfigurationValid()"
form="clusterConfigurationForm"
class="btn btn-primary create-connector-btn">{{'cluster_management.cluster_page.save_btn' | translate}}
class="btn btn-primary save-cluster-configuration-btn">{{'cluster_management.cluster_page.save_btn' | translate}}
</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[
{
"address": "pc-desktop:7300",
"nodeState": "FOLLOWER",
"term": 2,
"syncStatus": {},
"lastLogTerm": 0,
"lastLogIndex": 0,
"endpoint": "http://pc-desktop:7200",
"recoveryStatus": {},
"topologyStatus": {
"state": "PRIMARY_NODE"
}
},
{
"address": "pc-desktop:7301",
"nodeState": "LEADER",
"term": 2,
"syncStatus": {
"pc-desktop:7300": "IN_SYNC",
"pc-desktop:7302": "IN_SYNC"
},
"lastLogTerm": 0,
"lastLogIndex": 0,
"endpoint": "http://pc-desktop:7201",
"recoveryStatus": {},
"topologyStatus": {
"state": "PRIMARY_NODE",
"primaryTags": {
"test": 0
}
}
},
{
"address": "pc-desktop:7302",
"nodeState": "FOLLOWER",
"term": 2,
"syncStatus": {},
"lastLogTerm": 0,
"lastLogIndex": 0,
"endpoint": "http://pc-desktop:7202",
"recoveryStatus": {},
"topologyStatus": {
"state": "PRIMARY_NODE"
}
}
]
3 changes: 2 additions & 1 deletion test-cypress/fixtures/cluster/no-cluster-group-status.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"lastLogTerm": 0,
"lastLogIndex": 0,
"endpoint": "http://pc-desktop:7200",
"recoveryStatus": {}
"recoveryStatus": {},
"topologyStatus": {}
}
]
3 changes: 2 additions & 1 deletion test-cypress/fixtures/cluster/no-cluster-node-status.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"lastLogTerm": 0,
"lastLogIndex": 0,
"endpoint": "http://pc-desktop:7200",
"recoveryStatus": {}
"recoveryStatus": {},
"topologyStatus": {}
}
277 changes: 277 additions & 0 deletions test-cypress/integration/cluster/cluster-configuration.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import {ClusterPageSteps} from "../../steps/cluster/cluster-page-steps";
import {GlobalOperationsStatusesStub} from "../../stubs/global-operations-statuses-stub";
import {ClusterStubs} from "../../stubs/cluster/cluster-stubs";
import {RemoteLocationStubs} from "../../stubs/cluster/remote-location-stubs";
import {DeleteClusterDialogSteps} from "../../steps/cluster/delete-cluster-dialog-steps";
import {ClusterConfigurationSteps} from "../../steps/cluster/cluster-configuration-steps";
import {ModalDialogSteps} from "../../steps/modal-dialog-steps";
import {ApplicationSteps} from "../../steps/application-steps";

describe('Cluster configuration', () => {

let repositoryId;

beforeEach(() => {
repositoryId = 'cluster-repo' + Date.now();
GlobalOperationsStatusesStub.stubNoOperationsResponse(repositoryId);
});

it('Should display cluster configuration', () => {
// Given there is an existing cluster created
ClusterStubs.stubClusterConfig();
ClusterStubs.stubClusterGroupStatus();
ClusterStubs.stubClusterNodeStatus();
RemoteLocationStubs.stubRemoteLocationFilter();
RemoteLocationStubs.stubRemoteLocationStatusInCluster();
// Given I have opened the cluster management page
ClusterPageSteps.visit();
// When I click on edit properties
ClusterPageSteps.previewClusterConfig();
// Then I should see cluster configuration with 3 tabs
ClusterConfigurationSteps.getClusterConfig().should('be.visible');
ClusterConfigurationSteps.getTabs().should('have.length', 3);
ClusterConfigurationSteps.getActiveTab().should('have.text', 'Properties');
ClusterConfigurationSteps.getClusterPropertiesTabContent().should('be.visible');
ClusterConfigurationSteps.selectTab(1);
ClusterConfigurationSteps.getActiveTab().should('have.text', 'Nodes');
ClusterConfigurationSteps.getClusterNodesTabContent().should('be.visible');
ClusterConfigurationSteps.selectTab(2);
ClusterConfigurationSteps.getActiveTab().should('have.text', 'Multi-region');
ClusterConfigurationSteps.getMultiRegionTabContent().should('be.visible');
});

context('Properties', () => {
beforeEach(() => {
// Given there is an existing cluster created
ClusterStubs.stubClusterConfig();
ClusterStubs.stubClusterGroupStatus();
ClusterStubs.stubClusterNodeStatus();
RemoteLocationStubs.stubRemoteLocationFilter();
RemoteLocationStubs.stubRemoteLocationStatusInCluster();
});

it('Should be able to delete cluster', () => {
// Given I have opened the cluster management page
ClusterPageSteps.visit();

ClusterPageSteps.getClusterPage().should('be.visible');
ClusterPageSteps.getCreateClusterButton().should('not.have.class', 'no-cluster');
// When I click on delete cluster
ClusterPageSteps.previewClusterConfig();
ClusterConfigurationSteps.getClusterConfig().should('be.visible');
ClusterConfigurationSteps.deleteCluster();
// Then I expect a confirmation dialog to appear
DeleteClusterDialogSteps.getDialog().should('be.visible');
// When I confirm
ClusterStubs.stubDeleteCluster();
ClusterStubs.stubNoClusterGroupStatus();
ClusterStubs.stubNoClusterNodeStatus();
DeleteClusterDialogSteps.confirmDeleteCluster();
// Then Cluster should be deleted
ClusterStubs.stubNoClusterConfig();
RemoteLocationStubs.stubRemoteLocationStatusNotCluster();
DeleteClusterDialogSteps.getDialog().should('not.exist');
ClusterPageSteps.getRemoveNodesButton().should('not.exist');
ClusterPageSteps.getAddNodesButton().should('not.exist');
ClusterPageSteps.getReplaceNodesButton().should('not.exist');
ClusterPageSteps.getPreviewClusterConfigButton().should('not.exist');
ClusterPageSteps.getCreateClusterButton().should('have.class', 'no-cluster');
});

it('Should be able to edit cluster properties', () => {
// Given I have opened the cluster management page
ClusterPageSteps.visit();
// When I click on edit properties
ClusterPageSteps.previewClusterConfig();
ClusterConfigurationSteps.getClusterConfig().should('be.visible');
// When I press edit properties
ClusterConfigurationSteps.editProperties();
// Then I expect a modal to be shown
ClusterConfigurationSteps.getModal().should('be.visible');
// I can set values in form fields
ClusterConfigurationSteps.setFieldValue('electionMinTimeout', '5000');
ClusterConfigurationSteps.setFieldValue('electionRangeTimeout', '3000');
// Verify a field validation error appears if left empty
ClusterConfigurationSteps.getFieldByName('heartbeatInterval').clear();
ClusterConfigurationSteps.verifyFieldError('heartbeatInterval', 'This field is required');
// And Save button is disabled
ClusterConfigurationSteps.getSaveButton().should('be.disabled');
// Set value for required field to enable Save button
ClusterConfigurationSteps.setFieldValue('heartbeatInterval', '1500');
// And Save button is disabled
ClusterConfigurationSteps.getSaveButton().should('be.enabled');
ClusterConfigurationSteps.setFieldValue('messageSizeKB', '64');
ClusterConfigurationSteps.setFieldValue('verificationTimeout', '1200');
ClusterConfigurationSteps.setFieldValue('transactionLogMaximumSizeGB', '50');
ClusterConfigurationSteps.setFieldValue('batchUpdateInterval', '2000');

// Click Save button to submit
ClusterStubs.stubSaveClusterConfiguration();
ClusterConfigurationSteps.save();
const expectedRequestBody = {
electionMinTimeout: 5000,
electionRangeTimeout: 3000,
heartbeatInterval: 1500,
messageSizeKB: 64,
verificationTimeout: 1200,
transactionLogMaximumSizeGB: 50,
batchUpdateInterval: 2000
};
cy.wait('@save-cluster-properties').then((interception) => {
expect(interception.request.body).to.deep.equal(expectedRequestBody);
});
// And the modal is closed
ClusterConfigurationSteps.getModal().should('not.exist');
});
});

context('Nodes', () => {
beforeEach(() => {
// Given there is an existing cluster created
ClusterStubs.stubClusterConfig();
ClusterStubs.stubClusterGroupStatus();
ClusterStubs.stubClusterNodeStatus();
RemoteLocationStubs.stubRemoteLocationFilter();
RemoteLocationStubs.stubRemoteLocationStatusInCluster();
});
it('should display the nodes list with correct node information in the modal', () => {
// Given I have opened the cluster management page
ClusterPageSteps.visit();
// When I click on edit properties and open Nodes tab
ClusterPageSteps.previewClusterConfig();
ClusterConfigurationSteps.selectTab(1);
// I expect to see
ClusterConfigurationSteps.getNodesListHeader().should('contain.text', 'Nodes list');
ClusterConfigurationSteps.assertNodesCount(3);

const nodeData = [
{
url: 'http://pc-desktop:7200',
rpcAddress: 'pc-desktop:7300',
state: 'FOLLOWER',
local: true
},
{
url: 'http://pc-desktop:7201',
rpcAddress: 'pc-desktop:7301',
state: 'LEADER',
local: false
},
{
url: 'http://pc-desktop:7202',
rpcAddress: 'pc-desktop:7302',
state: 'FOLLOWER',
local: false
}
];

nodeData.forEach((data, index) => {
ClusterConfigurationSteps.getNodeLink(index)
.should('have.attr', 'href', data.url)
.and('contain.text', data.url);

ClusterConfigurationSteps.getNodeRPCAddress(index)
.should('contain.text', data.rpcAddress);

ClusterConfigurationSteps.getNodeState(index)
.should('contain.text', data.state);

if (data.local) {
ClusterConfigurationSteps.isNodeLocal(index).should('exist');
} else {
ClusterConfigurationSteps.isNodeLocal(index).should('not.exist');
}
});
});
});

context('Multi-region', () => {
beforeEach(() => {
// Given there is an existing cluster created
ClusterStubs.stubClusterConfig();
ClusterStubs.stubClusterGroupStatus();
ClusterStubs.stubClusterNodeStatus();
RemoteLocationStubs.stubRemoteLocationFilter();
RemoteLocationStubs.stubRemoteLocationStatusInCluster();
});

context('Primary cluster', () => {
it('should be able to add and delete tags', () => {
const tagName = 'test';
// Given I have opened the cluster management page
ClusterPageSteps.visit();
// When I click on edit properties and open Nodes tab
ClusterPageSteps.previewClusterConfig();
ClusterConfigurationSteps.selectTab(2);
// I expect to see
ClusterConfigurationSteps.getMultiRegionHeader().should('contain.text', 'Primary cluster');
ClusterConfigurationSteps.getAddTagButton().should('be.visible').and('not.be.disabled');
ClusterConfigurationSteps.getEnableSecondaryModeButton().should('be.visible').and('not.be.disabled');
ClusterConfigurationSteps.getTagsTable().should('not.exist');

ClusterStubs.stubAddTag(tagName);
ClusterConfigurationSteps.clickAddTagButton();
ClusterConfigurationSteps.typeTag(tagName);
ClusterConfigurationSteps.clickSubmitTagButton();
cy.wait('@add-tag').then((interception) => {
expect(interception.request.body).to.deep.equal({tag: tagName});
});

ClusterStubs.stubClusterGroupStatusWithTag();
cy.wait('@3-nodes-cluster-group-status-tag').then(() => {
// Assert the tags table contains the expected tag
ClusterConfigurationSteps.getTagsTable().should('be.visible');
ClusterConfigurationSteps.getTagsTableRows().should('contain.text', tagName);
});
// And expect success message to be displayed.
ApplicationSteps.getSuccessNotifications().contains(`Successfully added to cluster primary identifier tag: ${tagName}`);

//When I delete the tag
ClusterConfigurationSteps.clickDeleteTagButton();
// I expect to see deleting confirmation dialog.
ModalDialogSteps.getDialogHeader().should('contain', `Delete identifier tag ${tagName}`);
ModalDialogSteps.getDialogBody().should('contain', 'Deleting identifier tag would stop any secondary cluster identified with it from pulling updates.');

// When I confirm
ClusterStubs.stubDeleteTag(tagName);
ModalDialogSteps.getConfirmButton().click();
cy.wait('@delete-tag').then((interception) => {
expect(interception.request.body).to.deep.equal({tag: tagName});
});
});
});

context('Secondary cluster', () => {
it('should be able to switch modes', () => {
ClusterStubs.stubEnableSecondaryMode();
const rpcAddress = 'node-name:7300';
const tag = 'us-central';

// Given I have opened the cluster management page
ClusterPageSteps.visit();
// When I click on edit properties and open Nodes tab
ClusterPageSteps.previewClusterConfig();
ClusterConfigurationSteps.selectTab(2);
// I click enable secondary mode btn
ClusterConfigurationSteps.clickEnableSecondaryModeButton();
// I expect to see enable secondary mode confirmation dialog.
ModalDialogSteps.getDialogHeader().should('contain', `Enable secondary mode`);
ModalDialogSteps.getDialogBody().should('contain', 'By enabling secondary mode this cluster would become a read-only replica of the specified primary cluster.');
// When I confirm I expect to see configuration modal
ModalDialogSteps.getConfirmButton().click();
ModalDialogSteps.getDialogHeader().should('contain', `Secondary cluster settings`);
ClusterConfigurationSteps.getEnableButton().should('be.disabled');
ClusterConfigurationSteps.typeRpcAddress(rpcAddress);
ClusterConfigurationSteps.getEnableButton().should('be.disabled');
ClusterConfigurationSteps.typePrimaryTag(tag);
ClusterConfigurationSteps.getEnableButton().should('not.be.disabled');
ClusterConfigurationSteps.clickEnableButton();
cy.wait('@enable-secondary-mode').then((interception) => {
expect(interception.request.body).to.deep.equal({primaryNode: rpcAddress, tag});
});
// And expect success message to be displayed.
ApplicationSteps.getSuccessNotifications().contains(`Successfully enabled secondary mode`);
});
});
});
});
Loading

0 comments on commit 58c0d6b

Please sign in to comment.