Skip to content

Commit a791f04

Browse files
authored
test: [DBAAS1-1403] Cypress tests for Postgresql Synchronous Replication Advanced Configuration (#13440)
* test: [DBAAS1-1403] Add cypress test for synchronous replication advanced config, include nested/top level config * test: [DBAAS1-1403] Add negative cypress test for synchronous replication advanced config * test: fix for review comments * test: fix for failing cases * Added changeset: Cypress tests for Postgresql Synchronous Replication Advanced Configuration * test: add a pg advanced config for unit tests
1 parent 9065bc6 commit a791f04

5 files changed

Lines changed: 221 additions & 64 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Tests
3+
---
4+
5+
Cypress tests for Postgresql Synchronous Replication Advanced Configuration ([#13440](https://github.com/linode/manager/pull/13440))

packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

Lines changed: 178 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
mockGetDatabaseEngineConfigs,
1414
mockGetDatabaseTypes,
1515
mockUpdateDatabase,
16+
mockUpdateDatabaseError,
1617
} from 'support/intercepts/databases';
1718
import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags';
1819
import { ui } from 'support/ui';
@@ -40,15 +41,45 @@ import type { DatabaseClusterConfiguration } from 'support/constants/databases';
4041
*/
4142
const getFlattenDefaultConfigs = (
4243
engineConfig: Record<string, any>,
43-
prefix = ''
44+
prefix = '',
45+
includePrefix = true
4446
): string[] =>
4547
Object.entries(engineConfig).flatMap(([key, value]) => {
4648
const fullKey = prefix ? `${prefix}.${key}` : key;
4749
return typeof value === 'object' && value !== null && !Array.isArray(value)
48-
? getFlattenDefaultConfigs(value, fullKey)
49-
: [fullKey];
50+
? getFlattenDefaultConfigs(
51+
value,
52+
includePrefix ? fullKey : '',
53+
includePrefix
54+
)
55+
: [includePrefix ? fullKey : key];
5056
});
5157

58+
const flattenConfigsEngineLevel = (
59+
configs: Record<string, any>
60+
): Record<string, any> => {
61+
const result: Record<string, any> = {};
62+
Object.entries(configs).forEach(([key, value]) => {
63+
if (
64+
typeof value === 'object' &&
65+
value !== null &&
66+
// Only flatten if value is a config group (not a config leaf)
67+
Object.values(value).every(
68+
(v) => typeof v === 'object' && v !== null && !Array.isArray(v)
69+
)
70+
) {
71+
// Nested group (e.g., pg, mysql)
72+
Object.entries(value).forEach(([subKey, subValue]) => {
73+
result[subKey] = subValue;
74+
});
75+
} else {
76+
// Top-level config
77+
result[key] = value;
78+
}
79+
});
80+
return result;
81+
};
82+
5283
/**
5384
* Get list of advanced Configurations available for users to add/modify
5485
*
@@ -100,9 +131,14 @@ const addConfigsToUI = (
100131
? false
101132
: value.example;
102133

134+
// Get all existing config keys from engine_config (handles nested structures)
135+
const existingConfigKeys = new Set(
136+
getFlattenDefaultConfigs(database.engine_config, '', false)
137+
);
138+
103139
// Process new configs to be added
104140
const newEntries = Object.entries(configsList)
105-
.filter(([key]) => !database.engine_config[engineType][key])
141+
.filter(([key]) => !existingConfigKeys.has(key))
106142
.slice(0, addSingle ? 1 : undefined); // Limit to 1 if addSingle, otherwise all
107143

108144
if (newEntries.length > 0) {
@@ -121,8 +157,20 @@ const addConfigsToUI = (
121157
.within(() => {
122158
// Confirms configure drawer already renders default configs
123159
Object.keys(database.engine_config[engineType]).forEach((key) => {
160+
cy.findByText(`${engineType}.${key}`).scrollIntoView();
124161
cy.findByText(`${engineType}.${key}`).should('be.visible');
125162
});
163+
Object.keys(database.engine_config)
164+
.filter(
165+
(key) =>
166+
key !== 'pg' &&
167+
key !== 'mysql' &&
168+
typeof database.engine_config[key] !== 'object'
169+
)
170+
.forEach((key) => {
171+
cy.findByText(key).scrollIntoView();
172+
cy.findByText(key).should('be.visible');
173+
});
126174

127175
// Adding configs one at a time from the dropdown
128176
cy.get(
@@ -140,9 +188,24 @@ const addConfigsToUI = (
140188

141189
// Type value for non-boolean configs
142190
if (value.type !== 'boolean') {
143-
cy.get(`[name="${flatKey}"]`).scrollIntoView();
144-
cy.get(`[name="${flatKey}"]`).should('be.visible').clear();
145-
cy.get(`[name="${flatKey}"]`).type(additionalConfigs[flatKey]);
191+
if (value.enum) {
192+
cy.findByText(flatKey).scrollIntoView();
193+
cy.findByText(flatKey)
194+
.parent()
195+
.within(() => {
196+
ui.autocomplete.find().click();
197+
ui.autocomplete.find().clear();
198+
ui.autocomplete.find().type(`${additionalConfigs[flatKey]}`);
199+
});
200+
ui.autocompletePopper
201+
.findByTitle(`${additionalConfigs[flatKey]}`)
202+
.click();
203+
} else {
204+
cy.get(`[name="${flatKey}"]`).scrollIntoView();
205+
cy.get(`[name="${flatKey}"]`).should('be.visible');
206+
cy.get(`[name="${flatKey}"]`).clear();
207+
cy.get(`[name="${flatKey}"]`).type(additionalConfigs[flatKey]);
208+
}
146209
}
147210
});
148211
});
@@ -195,7 +258,7 @@ describe('Update database clusters', () => {
195258
);
196259

197260
mockGetAccount(accountFactory.build()).as('getAccount');
198-
mockGetDatabase(database).as('getDatabase').debug();
261+
mockGetDatabase(database).as('getDatabase');
199262
mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes');
200263
mockGetDatabaseEngineConfigs(database.engine, mockConfigs);
201264

@@ -215,6 +278,7 @@ describe('Update database clusters', () => {
215278
});
216279

217280
// Confirms all the buttons are in the initial state - enabled/disabled
281+
ui.cdsButton.findButtonByTitle('Configure').scrollIntoView();
218282
ui.cdsButton
219283
.findButtonByTitle('Configure')
220284
.should('be.visible')
@@ -226,18 +290,15 @@ describe('Update database clusters', () => {
226290
.findButtonByTitle('Add')
227291
.should('exist')
228292
.should('be.disabled');
229-
ui.button
230-
.findByTitle('Save')
231-
.scrollIntoView()
232-
.should('be.visible')
233-
.should('be.disabled');
293+
ui.button.findByTitle('Save').should('exist').should('be.disabled');
234294

235295
ui.button
236296
.findByTitle('Cancel')
237-
.scrollIntoView()
238-
.should('be.visible')
297+
.should('exist')
239298
.should('be.enabled')
240-
.click();
299+
.then((btn) => {
300+
btn[0].click();
301+
});
241302

242303
ui.cdsButton
243304
.findButtonByTitle('Configure')
@@ -247,9 +308,11 @@ describe('Update database clusters', () => {
247308

248309
ui.drawer.findByTitle('Advanced Configuration').should('be.visible');
249310
cy.get('[aria-label="Close drawer"]')
250-
.should('be.visible')
311+
.should('exist')
251312
.should('be.enabled')
252-
.click();
313+
.then((btn) => {
314+
btn[0].click();
315+
});
253316
});
254317

255318
/*
@@ -296,6 +359,7 @@ describe('Update database clusters', () => {
296359
cy.wait(['@getDatabase', '@getDatabaseTypes']);
297360

298361
// Expand configure drawer to add configs
362+
ui.cdsButton.findButtonByTitle('Configure').scrollIntoView();
299363
ui.cdsButton
300364
.findButtonByTitle('Configure')
301365
.should('be.visible')
@@ -313,31 +377,52 @@ describe('Update database clusters', () => {
313377
true
314378
);
315379

380+
const isSyncReplicationQuorum =
381+
singleConfig['synchronous_replication'] === 'quorum';
382+
const isInvaliClusterSize =
383+
database.cluster_size < 3 && isSyncReplicationQuorum;
384+
316385
// Update advanced configurations with the newly added config
317-
mockUpdateDatabase(database.id, database.engine, {
318-
...database,
319-
engine_config: {
320-
...(database.engine_config as ConfigCategoryValues),
321-
[engineType]: {
322-
...(existingConfig as ConfigCategoryValues),
323-
...singleConfig,
386+
if (isInvaliClusterSize) {
387+
mockUpdateDatabaseError(
388+
database.id,
389+
database.engine,
390+
'engine_config.synchronous_replication',
391+
'synchronous_replication is only supported for clusters with 3 nodes'
392+
).as('updateAdvancedConfiguration');
393+
} else {
394+
mockUpdateDatabase(database.id, database.engine, {
395+
...database,
396+
engine_config: {
397+
...(database.engine_config as ConfigCategoryValues),
398+
[engineType]: {
399+
...(existingConfig as ConfigCategoryValues),
400+
...singleConfig,
401+
},
324402
},
325-
},
326-
}).as('updateAdvancedConfiguration');
327-
403+
}).as('updateAdvancedConfiguration');
404+
}
328405
// Save or Save and Restart Services as per the config added
329406
ui.button
330407
.findByTitle(saveRestartButton)
331-
.scrollIntoView()
332-
.should('be.visible')
408+
.should('exist')
333409
.should('be.enabled')
334-
.click();
410+
.then((btn) => {
411+
btn[0].click();
412+
});
335413
cy.wait('@updateAdvancedConfiguration');
336414

337-
// Confirms newly added advacned Config on the Configuration tab tableview
338-
cy.findByText(`${engineType}.${Object.keys(singleConfig)[0]}`).should(
339-
'be.visible'
340-
);
415+
if (isInvaliClusterSize) {
416+
// Verify error message is displayed for invalid synchronous replication
417+
cy.findByText(
418+
/synchronous_replication is only supported for clusters with 3 nodes/i
419+
).should('be.visible');
420+
} else {
421+
// Confirms newly added advanced Config on the Configuration tab tableview
422+
cy.findByText(
423+
`${engineType}.${Object.keys(singleConfig)[0]}`
424+
).should('be.visible');
425+
}
341426
});
342427

343428
/*
@@ -384,48 +469,83 @@ describe('Update database clusters', () => {
384469
cy.wait(['@getDatabase', '@getDatabaseTypes']);
385470

386471
// Expand configure drawer to add configs
472+
ui.cdsButton.findButtonByTitle('Configure').scrollIntoView();
387473
ui.cdsButton
388474
.findButtonByTitle('Configure')
389475
.should('be.visible')
390476
.should('be.enabled')
391477
.click();
392478

479+
const flatMockConfigs = flattenConfigsEngineLevel(mockConfigs);
480+
393481
// Add configs from the configList to the existing database cluster
394482
const {
395483
additionalConfigs: allConfig,
396484
saveButton: saveRestartButton,
397-
} = addConfigsToUI(
398-
mockConfigs[engineType],
399-
database,
400-
engineType,
401-
false
402-
);
485+
} = addConfigsToUI(flatMockConfigs, database, engineType, false);
486+
487+
const nestedConfig: Record<string, any> = {};
488+
const topLevelConfig: Record<string, any> = {};
489+
// Separate nested engine configs and top-level configs
490+
Object.entries(allConfig).forEach(([key, value]) => {
491+
if (key in mockConfigs[engineType]) {
492+
nestedConfig[key] = value;
493+
} else {
494+
topLevelConfig[key] = value;
495+
}
496+
});
497+
498+
const isSyncReplicationQuorum =
499+
allConfig['synchronous_replication'] === 'quorum';
500+
const isInvalidClusterSize =
501+
database.cluster_size < 3 && isSyncReplicationQuorum;
403502

404503
// Update advanced configurations with the newly added config
405-
mockUpdateDatabase(database.id, database.engine, {
406-
...database,
407-
engine_config: {
408-
...(database.engine_config as ConfigCategoryValues),
409-
[engineType]: {
410-
...(existingConfig as ConfigCategoryValues),
411-
...allConfig,
504+
if (isInvalidClusterSize) {
505+
mockUpdateDatabaseError(
506+
database.id,
507+
database.engine,
508+
'engine_config.synchronous_replication',
509+
'synchronous_replication is only supported for clusters with 3 nodes'
510+
).as('updateAdvancedConfiguration');
511+
} else {
512+
mockUpdateDatabase(database.id, database.engine, {
513+
...database,
514+
engine_config: {
515+
...(database.engine_config as ConfigCategoryValues),
516+
[engineType]: {
517+
...(existingConfig as ConfigCategoryValues),
518+
...nestedConfig,
519+
},
520+
...topLevelConfig,
412521
},
413-
},
414-
}).as('updateAdvancedConfiguration');
522+
}).as('updateAdvancedConfiguration');
523+
}
415524

416525
// Save or Save and Restart Services as per the config added
417526
ui.button
418527
.findByTitle(saveRestartButton)
419-
.scrollIntoView()
420-
.should('be.visible')
528+
.should('exist')
421529
.should('be.enabled')
422-
.click();
530+
.then((btn) => {
531+
btn[0].click();
532+
});
423533
cy.wait('@updateAdvancedConfiguration');
424534

425-
// Confirms newly added advacned Config on the Configuration tab tableview
426-
Object.keys(allConfig).forEach((key) => {
427-
cy.findByText(`${engineType}.${key}`).should('be.visible');
428-
});
535+
if (isInvalidClusterSize) {
536+
// Verify error message is displayed for invalid synchronous replication
537+
cy.findByText(
538+
/synchronous_replication is only supported for clusters with 3 nodes/i
539+
).should('be.visible');
540+
} else {
541+
// Confirms newly added advanced Config on the Configuration tab tableview
542+
Object.keys(nestedConfig).forEach((key) => {
543+
cy.findByText(`${engineType}.${key}`).should('be.visible');
544+
});
545+
Object.keys(topLevelConfig).forEach((key) => {
546+
cy.findByText(`${key}`).should('be.visible');
547+
});
548+
}
429549
});
430550

431551
/*
@@ -469,6 +589,7 @@ describe('Update database clusters', () => {
469589
cy.wait(['@getDatabase', '@getDatabaseTypes']);
470590

471591
// Expand configure drawer to add configs
592+
ui.cdsButton.findButtonByTitle('Configure').scrollIntoView();
472593
ui.cdsButton
473594
.findButtonByTitle('Configure')
474595
.should('be.visible')

packages/manager/cypress/support/constants/databases.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,16 @@ export const databaseConfigurationsAdvConfig: DatabaseClusterConfiguration[] = [
564564
version: '8',
565565
ip: randomIp(),
566566
},
567+
{
568+
clusterSize: 2,
569+
dbType: 'postgresql',
570+
engine: 'PostgreSQL',
571+
label: randomLabel(),
572+
linodeType: 'g6-nanode-1',
573+
region: chooseRegion({ capabilities: ['Managed Databases'] }),
574+
version: '13',
575+
ip: randomIp(),
576+
},
567577
{
568578
clusterSize: 3,
569579
dbType: 'postgresql',

0 commit comments

Comments
 (0)