Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13431-tests-1771917197854.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Added automation spec for edit notifiation channel ([#13431](https://github.com/linode/manager/pull/13431))
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* @file Integration Tests for CloudPulse Alerting β€” Notification Channel Edit Validation
*/
import { profileFactory } from '@linode/utilities';
import { mockGetAccount, mockGetUsers } from 'support/intercepts/account';
import {
mockGetAlertChannelById,
mockGetAlertChannels,
mockUpdateAlertChannelById,
mockUpdateAlertChannelByIdError,
} from 'support/intercepts/cloudpulse';
import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags';
import { mockGetProfile } from 'support/intercepts/profile';
import { ui } from 'support/ui';

import {
accountFactory,
accountUserFactory,
flagsFactory,
notificationChannelFactory,
} from 'src/factories';
import { UPDATE_CHANNEL_SUCCESS_MESSAGE } from 'src/features/CloudPulse/Alerts/constants';

// Define mock data for the test.

const mockAccount = accountFactory.build();
const mockUsers = [
accountUserFactory.build({ username: 'user1' }),
accountUserFactory.build({ username: 'user2' }),
...accountUserFactory.buildList(6),
];
const mockProfile = profileFactory.build({
restricted: false,
});
const notificationChannels = notificationChannelFactory.buildList(5);
const editNotificationChannel = notificationChannelFactory.build({
label: 'Test Channel Name',
channel_type: 'email',
details: {
email: {
usernames: ['user1', 'user2'],
},
},
});
const { id, label } = editNotificationChannel;

describe('CloudPulse Alerting - Notification Channel Edit Validation', () => {
/**
* Verifies successful editing of an existing email notification channel with a success snackbar.
* Verifies the update payload sent to the API and the UI listing after the channel is edited.
* Verifies server error handling during channel update failures.
*/
beforeEach(() => {
mockAppendFeatureFlags(flagsFactory.build());
mockGetAccount(mockAccount);
mockGetProfile(mockProfile);
mockGetAlertChannels([...notificationChannels, editNotificationChannel]).as(
'getAlertNotificationChannels'
);
mockGetAlertChannelById(id, editNotificationChannel).as(
'getAlertChannelById'
);
mockGetUsers(mockUsers).as('getAccountUsers');
mockUpdateAlertChannelById(id, {
...editNotificationChannel,
label: 'Updated Channel Name',

Check warning on line 66 in packages/manager/cypress/e2e/core/cloudpulse/alert-notification-channel-edit.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 3 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":66,"column":14,"nodeType":"Literal","endLine":66,"endColumn":36}
}).as('updateAlertChannelById');

// Visit Notification Channels page
cy.visitWithLogin('/alerts/notification-channels');
});
it('should verify edit action menu allows edit of the notification channel, verify payload and UI listing', () => {
// Wait for initial data load
cy.wait('@getAlertNotificationChannels');

// Select the first notification channel to edit
ui.actionMenu
.findByTitle('Action menu for Notification Channel ' + label)
.click();
ui.actionMenuItem.findByTitle('Edit').click();

cy.wait('@getAlertChannelById');

cy.url().should('include', '/alerts/notification-channels/edit/' + id);

// Modify the channel label
const newChannelLabel = 'Updated Channel Name';

cy.findByLabelText('Type').should('be.disabled').and('have.value', 'Email');
cy.findByLabelText('Name').clear();
cy.findByLabelText('Name').type(newChannelLabel);

cy.findByLabelText('Recipients').click();

// Remove one recipient
cy.contains('.MuiChip-label', 'user1')
.should('be.visible')

Check warning on line 97 in packages/manager/cypress/e2e/core/cloudpulse/alert-notification-channel-edit.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 5 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 5 times.","line":97,"column":15,"nodeType":"Literal","endLine":97,"endColumn":27}
.parents('.MuiChip-root')
.find('.MuiChip-deleteIcon')
.click();
// Add a different recipient
ui.autocompletePopper.findByTitle('user-3').click();

// Click the Save button
ui.buttonGroup
.findButtonByTitle('Save')
.should('be.visible')
.should('be.enabled')
.click();

// Verify success toast
ui.toast.assertMessage(UPDATE_CHANNEL_SUCCESS_MESSAGE);

// Validate the request payload data
cy.wait('@updateAlertChannelById').then((interception) => {
expect(interception)
.to.have.property('response')
.with.property('statusCode', 200);

const payload = interception.request.body;

// Top-level fields
expect(payload.label).to.equal(newChannelLabel);

// Email details validation
expect(payload.details).to.have.property('email');
expect(payload.details.email.usernames).to.have.length(2);

const expectedRecipients = ['user2', 'user-3'];

expectedRecipients.forEach((username, index) => {
expect(payload.details.email.usernames[index]).to.equal(username);
});
});

// Verify navigation back to Notification Channels listing page
cy.url().should('include', '/alerts/notification-channels');
ui.tabList.find().within(() => {
cy.get('[data-testid="Notification Channels"]').should(
'have.text',
'Notification Channels'
);
});

cy.findByPlaceholderText('Search for Notification Channels').as(
'searchInput'
);
cy.get('@searchInput').clear();
cy.get('@searchInput').type(newChannelLabel);

cy.get('[data-qa="notification-channels-table"]')
.find('tbody:visible')
.within(() => {
cy.get('tr').should('have.length', 1);
cy.get('tr')
.first()
.within(() => {
cy.findByText(newChannelLabel).should('be.visible');
cy.findByText('Email').should('be.visible');
});
});
});
it('should display server error when editing a notification channel fails', () => {
// Simulate server error on update
mockUpdateAlertChannelByIdError(id, 'Internal server error').as(
'updateAlertChannelByIdError'
);
// Wait for initial data load
cy.wait('@getAlertNotificationChannels');

// Select the first notification channel to edit
ui.actionMenu
.findByTitle('Action menu for Notification Channel ' + label)
.click();
ui.actionMenuItem.findByTitle('Edit').click();

cy.wait('@getAlertChannelById');

// Modify the channel label
const newChannelLabel = 'Updated Channel Name';
cy.findByLabelText('Name').clear();
cy.findByLabelText('Name').type(newChannelLabel);

// Click the Save button
ui.buttonGroup
.findButtonByTitle('Save')
.should('be.visible')
.should('be.enabled')
.click();

ui.toast.assertMessage('Internal server error');
cy.url().should('include', '/alerts/notification-channels/edit/' + id);
});
});
66 changes: 66 additions & 0 deletions packages/manager/cypress/support/intercepts/cloudpulse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,69 @@ export const mockCreateAlertChannelError = (
makeErrorResponse(errorPayload, statusCode)
);
};
/**
* Mocks get call for a specific alert channel by ID.
*
* @param {number} id - The ID of the alert channel to retrieve.
* @param {NotificationChannel} channel - The notification channel object to return in the response.
* @returns {Cypress.Chainable<null>} - A Cypress chainable used to continue the test flow.
*/
export const mockGetAlertChannelById = (
id: number,
channel: NotificationChannel
): Cypress.Chainable<null> => {
return cy.intercept(
'GET',
apiMatcher(`/monitor/alert-channels/${id}`),
makeResponse(channel)
);
};

/**
* Mocks put call to update a specific alert channel by ID.
* Intercepts PUT requests to update alert channels and returns the provided channel object.
*
* @param {number} id - The ID of the alert channel to update.
* @param {NotificationChannel} channel - The notification channel object to return in the response.
* @returns {Cypress.Chainable<null>} - A Cypress chainable used to continue the test flow.
*/
export const mockUpdateAlertChannelById = (
id: number,
channel: NotificationChannel
): Cypress.Chainable<null> => {
return cy.intercept(
'PUT',
apiMatcher(`/monitor/alert-channels/${id}`),
makeResponse(channel)
);
};

/**
* Mocks error responses when updating a specific alert channel by ID.
* Intercepts PUT requests to update alert channels and returns an error response.
*
* @param {number} id - The ID of the alert channel to update.
* @param {Object | string} errorPayload - Either an object with field and reason properties for validation errors,
* or a string error message for server errors.
* @param {number} statusCode - The HTTP status code for the error response (default is 400).
* @returns {Cypress.Chainable<null>} - A Cypress chainable used to continue the test flow.
*
* @example
* // Mock a validation error (400)
* mockUpdateAlertChannelByIdError(123, { field: 'name', reason: 'Required' }, 400);
*
* @example
* // Mock a server error (500)
* mockUpdateAlertChannelByIdError(123, 'Internal server error', 500);
*/
export const mockUpdateAlertChannelByIdError = (
id: number,
errorPayload: string | { field: string; reason: string },
statusCode: number = 400
): Cypress.Chainable<null> => {
return cy.intercept(
'PUT',
apiMatcher(`/monitor/alert-channels/${id}`),
makeErrorResponse(errorPayload, statusCode)
);
};
Loading