Files
2024-02-15 17:01:50 +00:00

741 lines
24 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import selectEvent from 'react-select-event';
import {EnvironmentsFixture} from 'sentry-fixture/environments';
import {GroupsFixture} from 'sentry-fixture/groups';
import {LocationFixture} from 'sentry-fixture/locationFixture';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectAlertRuleFixture} from 'sentry-fixture/projectAlertRule';
import {ProjectAlertRuleConfigurationFixture} from 'sentry-fixture/projectAlertRuleConfiguration';
import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import ProjectsStore from 'sentry/stores/projectsStore';
import TeamStore from 'sentry/stores/teamStore';
import {metric, trackAnalytics} from 'sentry/utils/analytics';
import AlertsContainer from 'sentry/views/alerts';
import AlertBuilderProjectProvider from 'sentry/views/alerts/builder/projectProvider';
import ProjectAlertsCreate from 'sentry/views/alerts/create';
jest.unmock('sentry/utils/recreateRoute');
// updateOnboardingTask triggers an out of band state update
jest.mock('sentry/actionCreators/onboardingTasks');
jest.mock('sentry/actionCreators/members', () => ({
fetchOrgMembers: jest.fn(() => Promise.resolve([])),
indexMembersByProject: jest.fn(() => {
return {};
}),
}));
jest.mock('react-router');
jest.mock('sentry/utils/analytics', () => ({
metric: {
startTransaction: jest.fn(() => ({
setTag: jest.fn(),
setData: jest.fn(),
})),
endTransaction: jest.fn(),
mark: jest.fn(),
measure: jest.fn(),
},
trackAnalytics: jest.fn(),
}));
describe('ProjectAlertsCreate', function () {
beforeEach(function () {
TeamStore.init();
TeamStore.loadInitialData([], false, null);
MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/configuration/',
body: ProjectAlertRuleConfigurationFixture(),
});
MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/1/',
body: ProjectAlertRuleFixture(),
});
MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/environments/',
body: EnvironmentsFixture(),
});
MockApiClient.addMockResponse({
url: `/projects/org-slug/project-slug/?expand=hasAlertIntegration`,
body: {},
});
MockApiClient.addMockResponse({
url: `/projects/org-slug/project-slug/ownership/`,
method: 'GET',
body: {
fallthrough: false,
autoAssignment: false,
},
});
MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/preview/',
method: 'POST',
body: [],
});
});
afterEach(function () {
MockApiClient.clearMockResponses();
jest.clearAllMocks();
});
const createWrapper = (props = {}, location = {}) => {
const {organization, project, router, routerContext} = initializeOrg(props);
ProjectsStore.loadInitialData([project]);
const params = {orgId: organization.slug, projectId: project.slug};
const wrapper = render(
<AlertsContainer>
<AlertBuilderProjectProvider
{...RouteComponentPropsFixture()}
params={params}
organization={organization}
hasMetricAlerts={false}
>
<ProjectAlertsCreate
{...RouteComponentPropsFixture()}
hasMetricAlerts={false}
members={[]}
params={params}
organization={organization}
project={project}
location={LocationFixture({
pathname: `/organizations/org-slug/alerts/rules/${project.slug}/new/`,
query: {createFromWizard: 'true'},
...location,
})}
router={router}
/>
</AlertBuilderProjectProvider>
</AlertsContainer>,
{organization, context: routerContext}
);
return {
wrapper,
organization,
project,
router,
};
};
it('adds default parameters if wizard was skipped', async function () {
const location = {query: {}};
const wrapper = createWrapper(undefined, location);
await waitFor(() => {
expect(wrapper.router.replace).toHaveBeenCalledWith(
expect.objectContaining({
pathname: '/organizations/org-slug/alerts/new/metric',
query: {
aggregate: 'count()',
dataset: 'events',
eventTypes: 'error',
project: 'project-slug',
},
})
);
});
});
describe('Issue Alert', function () {
it('loads default values', async function () {
createWrapper();
expect(await screen.findByText('All Environments')).toBeInTheDocument();
expect(await screen.findByText('any')).toBeInTheDocument();
expect(await screen.findByText('all')).toBeInTheDocument();
expect(await screen.findByText('24 hours')).toBeInTheDocument();
});
it('can remove filters', async function () {
createWrapper();
const mock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/',
method: 'POST',
body: ProjectAlertRuleFixture(),
});
// Change name of alert rule
await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
// Add a filter and remove it
await selectEvent.select(screen.getByText('Add optional filter...'), [
'The issue is older or newer than...',
]);
await userEvent.click(screen.getAllByLabelText('Delete Node')[1]);
await userEvent.click(screen.getByText('Save Rule'));
await waitFor(() => {
expect(mock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: {
actionMatch: 'any',
actions: [],
conditions: [
expect.objectContaining({
id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
}),
],
filterMatch: 'all',
filters: [],
frequency: 60 * 24,
name: 'myname',
owner: null,
},
})
);
});
});
it('can remove triggers', async function () {
const {organization} = createWrapper();
const mock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/',
method: 'POST',
body: ProjectAlertRuleFixture(),
});
// delete node
await userEvent.click(screen.getByLabelText('Delete Node'));
// Change name of alert rule
await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
// Add a trigger and remove it
await selectEvent.select(screen.getByText('Add optional trigger...'), [
'A new issue is created',
]);
await userEvent.click(screen.getByLabelText('Delete Node'));
await waitFor(() => {
expect(trackAnalytics).toHaveBeenCalledWith('edit_alert_rule.add_row', {
name: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
organization,
project_id: '2',
type: 'conditions',
});
});
await userEvent.click(screen.getByText('Save Rule'));
await waitFor(() => {
expect(mock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: {
actionMatch: 'any',
actions: [],
conditions: [],
filterMatch: 'all',
filters: [],
frequency: 60 * 24,
name: 'myname',
owner: null,
},
})
);
});
});
it('can remove actions', async function () {
createWrapper();
const mock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/',
method: 'POST',
body: ProjectAlertRuleFixture(),
});
// Change name of alert rule
await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
// Add an action and remove it
await selectEvent.select(screen.getByText('Add action...'), [
'Send a notification to all legacy integrations',
]);
await userEvent.click(screen.getAllByLabelText('Delete Node')[1]);
await userEvent.click(screen.getByText('Save Rule'));
await waitFor(() => {
expect(mock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: {
actionMatch: 'any',
actions: [],
conditions: [
expect.objectContaining({
id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
}),
],
filterMatch: 'all',
filters: [],
frequency: 60 * 24,
name: 'myname',
owner: null,
},
})
);
});
});
describe('updates and saves', function () {
let mock;
beforeEach(function () {
mock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/',
method: 'POST',
body: ProjectAlertRuleFixture(),
});
});
afterEach(function () {
jest.clearAllMocks();
});
it('environment, async action and filter match', async function () {
const wrapper = createWrapper();
// Change target environment
await selectEvent.select(screen.getByText('All Environments'), ['production']);
// Change actionMatch and filterMatch dropdown
const anyDropdown = screen.getByText('any');
expect(anyDropdown).toBeInTheDocument();
const allDropdown = screen.getByText('all');
expect(allDropdown).toBeInTheDocument();
await selectEvent.select(anyDropdown, ['all']);
await selectEvent.select(allDropdown, ['any']);
// Change name of alert rule
await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
await userEvent.click(screen.getByText('Save Rule'));
expect(mock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: {
actionMatch: 'all',
filterMatch: 'any',
conditions: [
expect.objectContaining({
id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
}),
],
actions: [],
filters: [],
environment: 'production',
frequency: 60 * 24,
name: 'myname',
owner: null,
},
})
);
expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
await waitFor(() => {
expect(wrapper.router.push).toHaveBeenCalledWith({
pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
});
});
});
it('new condition', async function () {
const wrapper = createWrapper();
// Change name of alert rule
await userEvent.click(screen.getByPlaceholderText('Enter Alert Name'));
await userEvent.paste('myname');
// Add another condition
await selectEvent.select(screen.getByText('Add optional filter...'), [
"The event's tags match {key} {match} {value}",
]);
// Edit new Condition
await userEvent.click(screen.getByPlaceholderText('key'));
await userEvent.paste('conditionKey');
await userEvent.click(screen.getByPlaceholderText('value'));
await userEvent.paste('conditionValue');
await selectEvent.select(screen.getByText('contains'), ['does not equal']);
await userEvent.click(screen.getByText('Save Rule'));
expect(mock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: {
actionMatch: 'any',
actions: [],
conditions: [
expect.objectContaining({
id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
}),
],
filterMatch: 'all',
filters: [
{
id: 'sentry.rules.filters.tagged_event.TaggedEventFilter',
key: 'conditionKey',
match: 'ne',
value: 'conditionValue',
},
],
frequency: 60 * 24,
name: 'myname',
owner: null,
},
})
);
expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
await waitFor(() => {
expect(wrapper.router.push).toHaveBeenCalledWith({
pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
});
});
});
it('new filter', async function () {
const wrapper = createWrapper();
// Change name of alert rule
await userEvent.click(screen.getByPlaceholderText('Enter Alert Name'));
await userEvent.paste('myname');
// delete one condition
await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
// Add a new filter
await selectEvent.select(screen.getByText('Add optional filter...'), [
'The issue is older or newer than...',
]);
await userEvent.click(screen.getByPlaceholderText('10'));
await userEvent.paste('12');
await userEvent.click(screen.getByText('Save Rule'));
expect(mock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: {
actionMatch: 'any',
filterMatch: 'all',
filters: [
{
id: 'sentry.rules.filters.age_comparison.AgeComparisonFilter',
comparison_type: 'older',
time: 'minute',
value: '12',
},
],
actions: [],
conditions: [],
frequency: 60 * 24,
name: 'myname',
owner: null,
},
})
);
expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
await waitFor(() => {
expect(wrapper.router.push).toHaveBeenCalledWith({
pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
});
});
});
it('new action', async function () {
const wrapper = createWrapper();
// Change name of alert rule
await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'myname');
// Add a new action
await selectEvent.select(screen.getByText('Add action...'), [
'Suggested Assignees, Team, or Member',
]);
// Update action interval
await selectEvent.select(screen.getByText('24 hours'), ['60 minutes']);
await userEvent.click(screen.getByText('Save Rule'));
expect(mock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: {
actionMatch: 'any',
actions: [
{id: 'sentry.mail.actions.NotifyEmailAction', targetType: 'IssueOwners'},
],
conditions: [
expect.objectContaining({
id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
}),
],
filterMatch: 'all',
filters: [],
frequency: '60',
name: 'myname',
owner: null,
},
})
);
expect(metric.startTransaction).toHaveBeenCalledWith({name: 'saveAlertRule'});
await waitFor(() => {
expect(wrapper.router.push).toHaveBeenCalledWith({
pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
});
});
});
});
});
describe('test preview chart', () => {
it('valid preview table', async () => {
const groups = GroupsFixture();
const date = new Date();
for (let i = 0; i < groups.length; i++) {
groups[i].lastTriggered = String(date);
}
const mock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/preview/',
method: 'POST',
body: groups,
headers: {
'X-Hits': String(groups.length),
Endpoint: 'endpoint',
},
});
createWrapper();
await waitFor(() => {
expect(mock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
data: {
actionMatch: 'any',
conditions: [
expect.objectContaining({
id: 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
}),
],
filterMatch: 'all',
filters: [],
frequency: 60 * 24,
endpoint: null,
},
})
);
});
expect(
screen.getByText('4 issues would have triggered this rule in the past 14 days', {
exact: false,
})
).toBeInTheDocument();
for (const group of groups) {
expect(screen.getByText(group.shortId)).toBeInTheDocument();
}
expect(screen.getAllByText('3mo ago')[0]).toBeInTheDocument();
});
it('invalid preview alert', async () => {
const mock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/preview/',
method: 'POST',
statusCode: 400,
});
createWrapper();
// delete existion condition
await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
await waitFor(() => {
expect(mock).toHaveBeenCalled();
});
expect(
screen.getByText('Select a condition to generate a preview')
).toBeInTheDocument();
await selectEvent.select(screen.getByText('Add optional trigger...'), [
'A new issue is created',
]);
expect(
screen.getByText('Preview is not supported for these conditions')
).toBeInTheDocument();
});
it('empty preview table', async () => {
const mock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/preview/',
method: 'POST',
body: [],
headers: {
'X-Hits': '0',
Endpoint: 'endpoint',
},
});
createWrapper();
await waitFor(() => {
expect(mock).toHaveBeenCalled();
});
expect(
screen.getByText("We couldn't find any issues that would've triggered your rule")
).toBeInTheDocument();
});
});
describe('test incompatible conditions', () => {
const errorText =
'The conditions highlighted in red are in conflict. They may prevent the alert from ever being triggered.';
it('shows error for incompatible conditions', async () => {
createWrapper();
const anyDropdown = screen.getByText('any');
expect(anyDropdown).toBeInTheDocument();
await selectEvent.select(anyDropdown, ['all']);
await selectEvent.select(screen.getByText('Add optional trigger...'), [
'The issue changes state from resolved to unresolved',
]);
expect(screen.getByText(errorText)).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Save Rule'})).toHaveAttribute(
'aria-disabled',
'true'
);
await userEvent.click(screen.getAllByLabelText('Delete Node')[0]);
expect(screen.queryByText(errorText)).not.toBeInTheDocument();
});
it('test any filterMatch', async () => {
createWrapper();
const allDropdown = screen.getByText('all');
await selectEvent.select(allDropdown, ['any']);
await selectEvent.select(screen.getByText('Add optional filter...'), [
'The issue is older or newer than...',
]);
await userEvent.type(screen.getByPlaceholderText('10'), '10');
await userEvent.click(document.body);
await selectEvent.select(screen.getByText('Add optional filter...'), [
'The issue has happened at least {x} times (Note: this is approximate)',
]);
expect(screen.getByText(errorText)).toBeInTheDocument();
await userEvent.click(screen.getAllByLabelText('Delete Node')[1]);
await userEvent.clear(screen.getByDisplayValue('10'));
await userEvent.click(document.body);
expect(screen.queryByText(errorText)).not.toBeInTheDocument();
});
});
it('shows archived to escalating instead of ignored to unresolved', async () => {
createWrapper({
organization: OrganizationFixture(),
});
await selectEvent.select(screen.getByText('Add optional trigger...'), [
'The issue changes state from archived to escalating',
]);
expect(
screen.getByText('The issue changes state from archived to escalating')
).toBeInTheDocument();
});
it('displays noisy alert checkbox for no conditions + filters', async function () {
const mock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/',
method: 'POST',
body: ProjectAlertRuleFixture(),
});
createWrapper({organization: {features: ['noisy-alert-warning']}});
await userEvent.click((await screen.findAllByLabelText('Delete Node'))[0]);
await selectEvent.select(screen.getByText('Add action...'), [
'Suggested Assignees, Team, or Member',
]);
expect(
screen.getByText(/Alerts without conditions can fire too frequently/)
).toBeInTheDocument();
expect(trackAnalytics).toHaveBeenCalledWith(
'alert_builder.noisy_warning_viewed',
expect.anything()
);
await userEvent.click(screen.getByText('Save Rule'));
expect(mock).not.toHaveBeenCalled();
await userEvent.click(
screen.getByRole('checkbox', {name: 'Yes, I don’t mind if this alert gets noisy'})
);
await userEvent.click(screen.getByText('Save Rule'));
expect(mock).toHaveBeenCalled();
expect(trackAnalytics).toHaveBeenCalledWith(
'alert_builder.noisy_warning_agreed',
expect.anything()
);
});
it('does not display noisy alert banner for legacy integrations', async function () {
createWrapper({organization: {features: ['noisy-alert-warning']}});
await userEvent.click((await screen.findAllByLabelText('Delete Node'))[0]);
await selectEvent.select(screen.getByText('Add action...'), [
'Send a notification to all legacy integrations',
]);
expect(
screen.queryByText(/Alerts without conditions can fire too frequently/)
).not.toBeInTheDocument();
await selectEvent.select(screen.getByText('Add action...'), [
'Suggested Assignees, Team, or Member',
]);
expect(
screen.getByText(/Alerts without conditions can fire too frequently/)
).toBeInTheDocument();
});
it('displays duplicate error banner with link', async function () {
MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/rules/',
method: 'POST',
statusCode: 400,
body: {
name: [
"This rule is an exact duplicate of 'test alert' in this project and may not be created.",
],
ruleId: [1337],
},
});
createWrapper();
await userEvent.click(screen.getByText('Save Rule'));
const bannerLink = await screen.findByRole('link', {
name: /rule fully duplicates "test alert"/,
});
expect(bannerLink).toBeInTheDocument();
expect(bannerLink).toHaveAttribute(
'href',
'/organizations/org-slug/alerts/rules/project-slug/1337/details/'
);
});
});