Skip to content
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13440-tests-1772556190302.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Cypress tests for Postgresql Synchronous Replication Advanced Configuration ([#13440](https://github.com/linode/manager/pull/13440))
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
mockGetDatabaseEngineConfigs,
mockGetDatabaseTypes,
mockUpdateDatabase,
mockUpdateDatabaseError,
} from 'support/intercepts/databases';
import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags';
import { ui } from 'support/ui';
Expand Down Expand Up @@ -40,15 +41,45 @@
*/
const getFlattenDefaultConfigs = (
engineConfig: Record<string, any>,
prefix = ''
prefix = '',
includePrefix = true
): string[] =>
Object.entries(engineConfig).flatMap(([key, value]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
return typeof value === 'object' && value !== null && !Array.isArray(value)
? getFlattenDefaultConfigs(value, fullKey)
: [fullKey];
? getFlattenDefaultConfigs(
value,
includePrefix ? fullKey : '',
includePrefix
)
: [includePrefix ? fullKey : key];
});

const flattenConfigsEngineLevel = (
configs: Record<string, any>

Check warning on line 59 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Unexpected any. Specify a different type. Raw Output: {"ruleId":"@typescript-eslint/no-explicit-any","severity":1,"message":"Unexpected any. Specify a different type.","line":59,"column":27,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":59,"endColumn":30,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1681,1684],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1681,1684],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}
): Record<string, any> => {

Check warning on line 60 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Unexpected any. Specify a different type. Raw Output: {"ruleId":"@typescript-eslint/no-explicit-any","severity":1,"message":"Unexpected any. Specify a different type.","line":60,"column":19,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":60,"endColumn":22,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1704,1707],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1704,1707],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}
const result: Record<string, any> = {};

Check warning on line 61 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Unexpected any. Specify a different type. Raw Output: {"ruleId":"@typescript-eslint/no-explicit-any","severity":1,"message":"Unexpected any. Specify a different type.","line":61,"column":32,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":61,"endColumn":35,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1745,1748],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1745,1748],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}
Object.entries(configs).forEach(([key, value]) => {
if (
typeof value === 'object' &&
value !== null &&
// Only flatten if value is a config group (not a config leaf)
Object.values(value).every(
(v) => typeof v === 'object' && v !== null && !Array.isArray(v)
)
) {
// Nested group (e.g., pg, mysql)
Object.entries(value).forEach(([subKey, subValue]) => {
result[subKey] = subValue;
});
} else {
// Top-level config
result[key] = value;
}
});
return result;
};

/**
* Get list of advanced Configurations available for users to add/modify
*
Expand Down Expand Up @@ -100,9 +131,14 @@
? false
: value.example;

// Get all existing config keys from engine_config (handles nested structures)
const existingConfigKeys = new Set(
getFlattenDefaultConfigs(database.engine_config, '', false)
);

// Process new configs to be added
const newEntries = Object.entries(configsList)
.filter(([key]) => !database.engine_config[engineType][key])
.filter(([key]) => !existingConfigKeys.has(key))
.slice(0, addSingle ? 1 : undefined); // Limit to 1 if addSingle, otherwise all

if (newEntries.length > 0) {
Expand All @@ -121,8 +157,20 @@
.within(() => {
// Confirms configure drawer already renders default configs
Object.keys(database.engine_config[engineType]).forEach((key) => {
cy.findByText(`${engineType}.${key}`).scrollIntoView();
cy.findByText(`${engineType}.${key}`).should('be.visible');
});
Object.keys(database.engine_config)
.filter(
(key) =>
key !== 'pg' &&
key !== 'mysql' &&
typeof database.engine_config[key] !== 'object'
)
.forEach((key) => {
cy.findByText(key).scrollIntoView();
cy.findByText(key).should('be.visible');
});

// Adding configs one at a time from the dropdown
cy.get(
Expand All @@ -140,9 +188,24 @@

// Type value for non-boolean configs
if (value.type !== 'boolean') {
cy.get(`[name="${flatKey}"]`).scrollIntoView();
cy.get(`[name="${flatKey}"]`).should('be.visible').clear();
cy.get(`[name="${flatKey}"]`).type(additionalConfigs[flatKey]);
if (value.enum) {
cy.findByText(flatKey).scrollIntoView();
cy.findByText(flatKey)
.parent()
.within(() => {
ui.autocomplete.find().click();
ui.autocomplete.find().clear();
ui.autocomplete.find().type(`${additionalConfigs[flatKey]}`);
});
ui.autocompletePopper
.findByTitle(`${additionalConfigs[flatKey]}`)
.click();
} else {
cy.get(`[name="${flatKey}"]`).scrollIntoView();
cy.get(`[name="${flatKey}"]`).should('be.visible');
cy.get(`[name="${flatKey}"]`).clear();
cy.get(`[name="${flatKey}"]`).type(additionalConfigs[flatKey]);
}
}
});
});
Expand Down Expand Up @@ -195,7 +258,7 @@
);

mockGetAccount(accountFactory.build()).as('getAccount');
mockGetDatabase(database).as('getDatabase').debug();
mockGetDatabase(database).as('getDatabase');
mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes');
mockGetDatabaseEngineConfigs(database.engine, mockConfigs);

Expand All @@ -215,6 +278,7 @@
});

// Confirms all the buttons are in the initial state - enabled/disabled
ui.cdsButton.findButtonByTitle('Configure').scrollIntoView();
ui.cdsButton
.findButtonByTitle('Configure')
.should('be.visible')
Expand All @@ -226,18 +290,15 @@
.findButtonByTitle('Add')
.should('exist')
.should('be.disabled');
ui.button
.findByTitle('Save')
.scrollIntoView()
.should('be.visible')
.should('be.disabled');
ui.button.findByTitle('Save').should('exist').should('be.disabled');

ui.button
.findByTitle('Cancel')
.scrollIntoView()
.should('be.visible')
.should('exist')
.should('be.enabled')
.click();
.then((btn) => {

Check warning on line 299 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Refactor this code to not nest functions more than 4 levels deep. Raw Output: {"ruleId":"sonarjs/no-nested-functions","severity":1,"message":"Refactor this code to not nest functions more than 4 levels deep.","line":299,"column":25,"nodeType":null,"endLine":299,"endColumn":27}
btn[0].click();
});

ui.cdsButton
.findButtonByTitle('Configure')
Expand All @@ -247,9 +308,11 @@

ui.drawer.findByTitle('Advanced Configuration').should('be.visible');
cy.get('[aria-label="Close drawer"]')
.should('be.visible')
.should('exist')
.should('be.enabled')
.click();
.then((btn) => {

Check warning on line 313 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Refactor this code to not nest functions more than 4 levels deep. Raw Output: {"ruleId":"sonarjs/no-nested-functions","severity":1,"message":"Refactor this code to not nest functions more than 4 levels deep.","line":313,"column":25,"nodeType":null,"endLine":313,"endColumn":27}
btn[0].click();
});
});

/*
Expand Down Expand Up @@ -296,6 +359,7 @@
cy.wait(['@getDatabase', '@getDatabaseTypes']);

// Expand configure drawer to add configs
ui.cdsButton.findButtonByTitle('Configure').scrollIntoView();
ui.cdsButton
.findButtonByTitle('Configure')
.should('be.visible')
Expand All @@ -313,31 +377,52 @@
true
);

const isSyncReplicationQuorum =
singleConfig['synchronous_replication'] === 'quorum';
const isInvaliClusterSize =
database.cluster_size < 3 && isSyncReplicationQuorum;

// Update advanced configurations with the newly added config
mockUpdateDatabase(database.id, database.engine, {
...database,
engine_config: {
...(database.engine_config as ConfigCategoryValues),
[engineType]: {
...(existingConfig as ConfigCategoryValues),
...singleConfig,
if (isInvaliClusterSize) {
mockUpdateDatabaseError(
database.id,
database.engine,
'engine_config.synchronous_replication',
'synchronous_replication is only supported for clusters with 3 nodes'
).as('updateAdvancedConfiguration');
} else {
mockUpdateDatabase(database.id, database.engine, {
...database,
engine_config: {
...(database.engine_config as ConfigCategoryValues),
[engineType]: {
...(existingConfig as ConfigCategoryValues),
...singleConfig,
},
},
},
}).as('updateAdvancedConfiguration');

}).as('updateAdvancedConfiguration');
}
// Save or Save and Restart Services as per the config added
ui.button
.findByTitle(saveRestartButton)
.scrollIntoView()
.should('be.visible')
.should('exist')
.should('be.enabled')
.click();
.then((btn) => {

Check warning on line 410 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Refactor this code to not nest functions more than 4 levels deep. Raw Output: {"ruleId":"sonarjs/no-nested-functions","severity":1,"message":"Refactor this code to not nest functions more than 4 levels deep.","line":410,"column":25,"nodeType":null,"endLine":410,"endColumn":27}
btn[0].click();
});
cy.wait('@updateAdvancedConfiguration');

// Confirms newly added advacned Config on the Configuration tab tableview
cy.findByText(`${engineType}.${Object.keys(singleConfig)[0]}`).should(
'be.visible'
);
if (isInvaliClusterSize) {
// Verify error message is displayed for invalid synchronous replication
cy.findByText(
/synchronous_replication is only supported for clusters with 3 nodes/i
).should('be.visible');
} else {
// Confirms newly added advanced Config on the Configuration tab tableview
cy.findByText(
`${engineType}.${Object.keys(singleConfig)[0]}`
).should('be.visible');
}
});

/*
Expand Down Expand Up @@ -384,48 +469,83 @@
cy.wait(['@getDatabase', '@getDatabaseTypes']);

// Expand configure drawer to add configs
ui.cdsButton.findButtonByTitle('Configure').scrollIntoView();
ui.cdsButton
.findButtonByTitle('Configure')
.should('be.visible')
.should('be.enabled')
.click();

const flatMockConfigs = flattenConfigsEngineLevel(mockConfigs);

// Add configs from the configList to the existing database cluster
const {
additionalConfigs: allConfig,
saveButton: saveRestartButton,
} = addConfigsToUI(
mockConfigs[engineType],
database,
engineType,
false
);
} = addConfigsToUI(flatMockConfigs, database, engineType, false);

const nestedConfig: Record<string, any> = {};

Check warning on line 487 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Unexpected any. Specify a different type. Raw Output: {"ruleId":"@typescript-eslint/no-explicit-any","severity":1,"message":"Unexpected any. Specify a different type.","line":487,"column":46,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":487,"endColumn":49,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[17683,17686],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[17683,17686],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}
const topLevelConfig: Record<string, any> = {};

Check warning on line 488 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Unexpected any. Specify a different type. Raw Output: {"ruleId":"@typescript-eslint/no-explicit-any","severity":1,"message":"Unexpected any. Specify a different type.","line":488,"column":48,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":488,"endColumn":51,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[17741,17744],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[17741,17744],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}
// Separate nested engine configs and top-level configs
Object.entries(allConfig).forEach(([key, value]) => {

Check warning on line 490 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Refactor this code to not nest functions more than 4 levels deep. Raw Output: {"ruleId":"sonarjs/no-nested-functions","severity":1,"message":"Refactor this code to not nest functions more than 4 levels deep.","line":490,"column":60,"nodeType":null,"endLine":490,"endColumn":62}
if (key in mockConfigs[engineType]) {
nestedConfig[key] = value;
} else {
topLevelConfig[key] = value;
}
});

const isSyncReplicationQuorum =
allConfig['synchronous_replication'] === 'quorum';
const isInvalidClusterSize =
database.cluster_size < 3 && isSyncReplicationQuorum;

// Update advanced configurations with the newly added config
mockUpdateDatabase(database.id, database.engine, {
...database,
engine_config: {
...(database.engine_config as ConfigCategoryValues),
[engineType]: {
...(existingConfig as ConfigCategoryValues),
...allConfig,
if (isInvalidClusterSize) {
mockUpdateDatabaseError(
database.id,
database.engine,
'engine_config.synchronous_replication',
'synchronous_replication is only supported for clusters with 3 nodes'
).as('updateAdvancedConfiguration');
} else {
mockUpdateDatabase(database.id, database.engine, {
...database,
engine_config: {
...(database.engine_config as ConfigCategoryValues),
[engineType]: {
...(existingConfig as ConfigCategoryValues),
...nestedConfig,
},
...topLevelConfig,
},
},
}).as('updateAdvancedConfiguration');
}).as('updateAdvancedConfiguration');
}

// Save or Save and Restart Services as per the config added
ui.button
.findByTitle(saveRestartButton)
.scrollIntoView()
.should('be.visible')
.should('exist')
.should('be.enabled')
.click();
.then((btn) => {

Check warning on line 530 in packages/manager/cypress/e2e/core/databases/advanced-configuration.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Refactor this code to not nest functions more than 4 levels deep. Raw Output: {"ruleId":"sonarjs/no-nested-functions","severity":1,"message":"Refactor this code to not nest functions more than 4 levels deep.","line":530,"column":25,"nodeType":null,"endLine":530,"endColumn":27}
btn[0].click();
});
cy.wait('@updateAdvancedConfiguration');

// Confirms newly added advacned Config on the Configuration tab tableview
Object.keys(allConfig).forEach((key) => {
cy.findByText(`${engineType}.${key}`).should('be.visible');
});
if (isInvalidClusterSize) {
// Verify error message is displayed for invalid synchronous replication
cy.findByText(
/synchronous_replication is only supported for clusters with 3 nodes/i
).should('be.visible');
} else {
// Confirms newly added advanced Config on the Configuration tab tableview
Object.keys(nestedConfig).forEach((key) => {
cy.findByText(`${engineType}.${key}`).should('be.visible');
});
Object.keys(topLevelConfig).forEach((key) => {
cy.findByText(`${key}`).should('be.visible');
});
}
});

/*
Expand Down Expand Up @@ -469,6 +589,7 @@
cy.wait(['@getDatabase', '@getDatabaseTypes']);

// Expand configure drawer to add configs
ui.cdsButton.findButtonByTitle('Configure').scrollIntoView();
ui.cdsButton
.findButtonByTitle('Configure')
.should('be.visible')
Expand Down
10 changes: 10 additions & 0 deletions packages/manager/cypress/support/constants/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,16 @@ export const databaseConfigurationsAdvConfig: DatabaseClusterConfiguration[] = [
version: '8',
ip: randomIp(),
},
{
clusterSize: 2,
dbType: 'postgresql',
engine: 'PostgreSQL',
label: randomLabel(),
linodeType: 'g6-nanode-1',
region: chooseRegion({ capabilities: ['Managed Databases'] }),
version: '13',
ip: randomIp(),
},
{
clusterSize: 3,
dbType: 'postgresql',
Expand Down
Loading
Loading