Skip to content
Open
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-13383-tests-1770646447321.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Add spec for create nofitication channel ([#13383](https://github.com/linode/manager/pull/13383))
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/**
* @file Integration Tests for CloudPulse Alerting β€” Notification Channel Creation Validation
*/
import { profileFactory } from '@linode/utilities';
import { mockGetAccount, mockGetUsers } from 'support/intercepts/account';
import {
mockCreateAlertChannelError,
mockCreateAlertChannelSuccess,
mockGetAlertChannels,
} 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 { CREATE_CHANNEL_SUCCESS_MESSAGE } from 'src/features/CloudPulse/Alerts/constants';

// Define mock data for the test.

const mockAccount = accountFactory.build();
const mockProfile = profileFactory.build({
restricted: false,
});
const notificationChannels = notificationChannelFactory.buildList(5);
const createNotificationChannel = notificationChannelFactory.build({
label: 'Test Channel Name',

Check warning on line 31 in packages/manager/cypress/e2e/core/cloudpulse/alert-notification-channel-create.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":31,"column":10,"nodeType":"Literal","endLine":31,"endColumn":29}
channel_type: 'email',
content: {
email: {
email_addresses: ['user1', 'user2'],
message: 'You have a new Alert',
subject: 'Sample Alert',
},
},
});

describe('CloudPulse Alerting - Notification Channel Creation Validation', () => {
/**
* Verify successful creation of a new email notification channel with success snackbar
* Verifies the payload sent to the API and the UI listing of the newly created channel.
* Verifies server error handling during channel creation.
*/
beforeEach(() => {
mockGetAccount(mockAccount);
mockGetProfile(mockProfile);
const mockflags = flagsFactory.build({
aclpAlerting: {
notificationChannels: true,
},
});
mockAppendFeatureFlags(mockflags);
mockGetAlertChannels(notificationChannels).as(
'getAlertNotificationChannels'
);
mockCreateAlertChannelSuccess(createNotificationChannel).as(
'createAlertChannelNew'
);

// Mock 2 users for recipient selection
const users = [
accountUserFactory.build({ username: 'user1' }),
accountUserFactory.build({ username: 'user2' }),
];

mockGetUsers(users).as('getAccountUsers');

// Visit Notification Channels page
cy.visitWithLogin('/alerts/notification-channels');

Check warning on line 73 in packages/manager/cypress/e2e/core/cloudpulse/alert-notification-channel-create.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":73,"column":23,"nodeType":"Literal","endLine":73,"endColumn":54}
});
it('should create email notification channel, verify payload and UI listing', () => {
// Open Create Channel page
ui.button
.findByTitle('Create Channel')

Check warning on line 78 in packages/manager/cypress/e2e/core/cloudpulse/alert-notification-channel-create.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":78,"column":20,"nodeType":"Literal","endLine":78,"endColumn":36}
.should('be.visible')

Check warning on line 79 in packages/manager/cypress/e2e/core/cloudpulse/alert-notification-channel-create.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 16 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 16 times.","line":79,"column":15,"nodeType":"Literal","endLine":79,"endColumn":27}
.and('be.enabled')

Check warning on line 80 in packages/manager/cypress/e2e/core/cloudpulse/alert-notification-channel-create.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 4 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 4 times.","line":80,"column":12,"nodeType":"Literal","endLine":80,"endColumn":24}
.click();

// Verify breadcrumb heading
ui.breadcrumb.find().within(() => {
cy.contains('Notification Channels').should('be.visible');
cy.contains('Create Channel').should('be.visible');
});

// Verify Channel Settings heading
ui.heading.findByText('Channel Settings').should('be.visible');

// Select notification type (Email)
cy.get('[data-qa-textfield-label="Type"]').should('be.visible');
ui.autocomplete
.findByLabel('channel-type-select')
.should('be.visible')
.click();
ui.autocompletePopper.findByTitle('Email').click();

// Enter channel name
cy.get('[data-qa-textfield-label="Name"]').should('be.visible');
cy.findByPlaceholderText('Enter a name for the channel')
.should('be.visible')
.type('Test Channel Name');

cy.get('[data-qa-textfield-helper-text="true"]')
.should('be.visible')
.and('have.text', 'Select up to 10 Recipients');

// Open recipients autocomplete and select users
cy.get('[data-qa-textfield-label="Recipients"]').should('be.visible');
ui.autocomplete
.findByLabel('recipients-select')
.should('be.visible')
.click();
ui.autocompletePopper.findByTitle('user1').click();
ui.autocompletePopper.findByTitle('user2').click();

// Verify selected chips
cy.get('[data-tag-index]')
.should('have.length', 2)
.each(($chip, index) => {
const expectedUsers = ['user1', 'user2'];
cy.wrap($chip)
.find('.MuiChip-label')
.should('contain.text', expectedUsers[index]);
});

// Verify Cancel button is enabled
ui.buttonGroup
.findButtonByTitle('Cancel')
.should('be.visible')
.and('be.enabled');

// Verify Submit button is enabled and click
ui.buttonGroup
.findButtonByTitle('Submit')
.should('be.visible')
.and('be.enabled')
.click();

// Validate API request payload for notification channel creation
cy.wait('@createAlertChannelNew').then((interception) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this block of code doesn't test anything in the UI, it's just testing the test code which mocks the response. the test below where you filter for the label to ensure that the new item is visible is sufficient and does this better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed but since we are also carrying out integration testing, the payload validation is covered as part of that flow. A similar implementation has been consistently followed across other test cases as well to ensure the right values are passed to the appropriate labels maintaining end-to-end confidence.
cc : @venkymano-akamai , @santoshp210-akamai

expect(interception)
.to.have.property('response')
.with.property('statusCode', 200);

const payload = interception.request.body;

// Top-level fields
expect(payload.label).to.equal('Test Channel Name');
expect(payload.channel_type).to.equal('email');

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

const expectedRecipients = ['user1', 'user2'];

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

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

cy.wait('@getAlertNotificationChannels');

// 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'
);
});
// Verify the newly created channel appears in the listing
const expected = createNotificationChannel;

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

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(expected.label).should('be.visible');
cy.findByText('Email').should('be.visible');
});
});
});
it('should display server related message when API returns an error during channel creation', () => {
mockCreateAlertChannelError('Internal Server Error', 500).as(
'createAlertChannelServerError'
);

cy.visitWithLogin('/alerts/notification-channels');

// Open Create Channel drawer
ui.button
.findByTitle('Create Channel')
.should('be.visible')
.and('be.enabled')
.click();

// Select notification type
ui.autocomplete.findByLabel('channel-type-select').click();
ui.autocompletePopper.findByTitle('Email').click();

// Enter channel name
cy.findByPlaceholderText('Enter a name for the channel').type(
'Error Channel'
);

// Select recipients
ui.autocomplete.findByLabel('recipients-select').click();
ui.autocompletePopper.findByTitle('user1').click();

// Submit form
ui.buttonGroup.findButtonByTitle('Submit').click();

// Wait for the intercepted API call
cy.wait('@createAlertChannelServerError')
.its('response.statusCode')
.should('eq', 500);

// Verify toast message
ui.toast.assertMessage('Internal Server Error');
});
});
45 changes: 45 additions & 0 deletions packages/manager/cypress/support/intercepts/cloudpulse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,48 @@ export const mockDeleteChannelError = (
}
);
};

/**
* Mocks successful creation of an alert channel (200).
* Intercepts POST requests to create alert channels and returns the provided channel object.
*
* @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 mockCreateAlertChannelSuccess = (
channel: NotificationChannel
): Cypress.Chainable<null> => {
return cy.intercept(
'POST',
apiMatcher('/monitor/alert-channels'),
makeResponse(channel) // defaults to 200
);
};

/**
* Mocks error responses when creating alert channels.
* Intercepts POST requests to create alert channels and returns an error response.
*
* @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)
* mockCreateAlertChannelError({ field: 'name', reason: 'Required' }, 400);
*
* @example
* // Mock a server error (500)
* mockCreateAlertChannelError('Internal server error', 500);
*/
export const mockCreateAlertChannelError = (
errorPayload: string | { field: string; reason: string },
statusCode: number = 400
): Cypress.Chainable<null> => {
return cy.intercept(
'POST',
apiMatcher('/monitor/alert-channels'),
makeErrorResponse(errorPayload, statusCode)
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,19 @@ export const CreateNotificationChannel = () => {
return (
<Paper sx={{ paddingLeft: 1, paddingRight: 1, paddingTop: 2 }}>
<Breadcrumb
breadcrumbDataAttrs={{
'data-qa-breadcrumb': true,
}}
crumbOverrides={overrides}
pathname="/NotificationChannels/Create Channel"
/>
<FormProvider {...formMethods}>
<form onSubmit={onSubmit}>
<Typography marginTop={2} variant="h2">
<Typography
data-qa-header="Channel Settings"
marginTop={2}
variant="h2"
>
Channel Settings
</Typography>
<Controller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const NotificationChannelTypeSelect = React.memo(

return (
<Autocomplete
data-qa-autocomplete="channel-type-select"
data-testid="channel-type-select"
disableClearable={disabled}
disabled={disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const NotificationRecipients = React.memo(

return (
<Autocomplete
data-qa-autocomplete="recipients-select"
data-testid="recipients-select"
disableSelectAll={recipientsLimitReached}
errorText={
Expand Down