123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522 |
- import type {PlainRoute} from 'react-router';
- import {browserHistory} from 'react-router';
- import moment from 'moment';
- import {EnvironmentsFixture} from 'sentry-fixture/environments';
- import {ProjectFixture} from 'sentry-fixture/project';
- 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 {
- act,
- render,
- renderGlobalModal,
- screen,
- userEvent,
- waitFor,
- within,
- } from 'sentry-test/reactTestingLibrary';
- import selectEvent from 'sentry-test/selectEvent';
- import {
- addErrorMessage,
- addLoadingMessage,
- addSuccessMessage,
- } from 'sentry/actionCreators/indicator';
- import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks';
- import ProjectsStore from 'sentry/stores/projectsStore';
- import {metric} from 'sentry/utils/analytics';
- import IssueRuleEditor from 'sentry/views/alerts/rules/issue';
- import {permissionAlertText} from 'sentry/views/settings/project/permissionAlert';
- import ProjectAlerts from 'sentry/views/settings/projectAlerts';
- jest.unmock('sentry/utils/recreateRoute');
- jest.mock('sentry/actionCreators/onboardingTasks');
- jest.mock('sentry/actionCreators/indicator', () => ({
- addSuccessMessage: jest.fn(),
- addErrorMessage: jest.fn(),
- addLoadingMessage: jest.fn(),
- }));
- jest.mock('sentry/utils/analytics', () => ({
- metric: {
- startSpan: jest.fn(() => ({
- setTag: jest.fn(),
- setData: jest.fn(),
- })),
- endSpan: jest.fn(),
- mark: jest.fn(),
- measure: jest.fn(),
- },
- trackAnalytics: jest.fn(),
- }));
- const projectAlertRuleDetailsRoutes: PlainRoute<any>[] = [
- {
- path: '/',
- },
- {
- path: '/settings/',
- indexRoute: {},
- },
- {
- path: ':orgId/',
- },
- {
- path: 'projects/:projectId/',
- },
- {},
- {
- indexRoute: {},
- },
- {
- path: 'alerts/',
- indexRoute: {},
- },
- {
- path: 'rules/',
- indexRoute: {},
- childRoutes: [{path: 'new/'}, {path: ':ruleId/'}],
- },
- {path: ':ruleId/'},
- ];
- const createWrapper = (props = {}) => {
- const {organization, project, routerContext, router} = initializeOrg(props);
- const params = {
- projectId: project.slug,
- organizationId: organization.slug,
- ruleId: router.location.query.createFromDuplicate ? undefined : '1',
- };
- const onChangeTitleMock = jest.fn();
- const wrapper = render(
- <ProjectAlerts
- {...RouteComponentPropsFixture()}
- organization={organization}
- project={project}
- params={params}
- >
- <IssueRuleEditor
- route={RouteComponentPropsFixture().route}
- routeParams={RouteComponentPropsFixture().routeParams}
- params={params}
- location={router.location}
- routes={projectAlertRuleDetailsRoutes}
- router={router}
- members={[]}
- onChangeTitle={onChangeTitleMock}
- project={project}
- userTeamIds={[]}
- />
- </ProjectAlerts>,
- {context: routerContext, organization}
- );
- return {
- wrapper,
- organization,
- project,
- onChangeTitleMock,
- router,
- };
- };
- describe('IssueRuleEditor', function () {
- beforeEach(function () {
- MockApiClient.clearMockResponses();
- browserHistory.replace = jest.fn();
- 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/rules/preview/',
- method: 'POST',
- body: [],
- });
- ProjectsStore.loadInitialData([ProjectFixture()]);
- });
- afterEach(function () {
- jest.clearAllMocks();
- ProjectsStore.reset();
- });
- describe('Viewing the rule', () => {
- it('is visible without org-level alerts:write', async () => {
- createWrapper({
- organization: {access: []},
- project: {access: []},
- });
- expect(await screen.findByText(permissionAlertText)).toBeInTheDocument();
- expect(screen.queryByLabelText('Save Rule')).toBeDisabled();
- });
- it('is enabled with org-level alerts:write', async () => {
- createWrapper({
- organization: {access: ['alerts:write']},
- project: {access: []},
- });
- expect(await screen.findByLabelText('Save Rule')).toBeEnabled();
- expect(screen.queryByText(permissionAlertText)).not.toBeInTheDocument();
- });
- it('is enabled with project-level alerts:write', async () => {
- createWrapper({
- organization: {access: []},
- project: {access: ['alerts:write']},
- });
- expect(await screen.findByLabelText('Save Rule')).toBeEnabled();
- expect(screen.queryByText(permissionAlertText)).not.toBeInTheDocument();
- });
- });
- describe('Edit Rule', function () {
- let mock;
- const endpoint = '/projects/org-slug/project-slug/rules/1/';
- beforeEach(function () {
- mock = MockApiClient.addMockResponse({
- url: endpoint,
- method: 'PUT',
- body: ProjectAlertRuleFixture(),
- });
- });
- it('gets correct rule name', async function () {
- const rule = ProjectAlertRuleFixture();
- mock = MockApiClient.addMockResponse({
- url: endpoint,
- method: 'GET',
- body: rule,
- });
- const {onChangeTitleMock} = createWrapper();
- await waitFor(() => expect(mock).toHaveBeenCalled());
- expect(onChangeTitleMock).toHaveBeenCalledWith(rule.name);
- });
- it('deletes rule', async function () {
- const deleteMock = MockApiClient.addMockResponse({
- url: endpoint,
- method: 'DELETE',
- body: {},
- });
- createWrapper();
- renderGlobalModal();
- await userEvent.click(screen.getByLabelText('Delete Rule'));
- expect(
- await screen.findByText(/Are you sure you want to delete "My alert rule"\?/)
- ).toBeInTheDocument();
- await userEvent.click(screen.getByTestId('confirm-button'));
- await waitFor(() => expect(deleteMock).toHaveBeenCalled());
- expect(browserHistory.replace).toHaveBeenCalledWith(
- '/settings/org-slug/projects/project-slug/alerts/'
- );
- });
- it('sends correct environment value', async function () {
- createWrapper();
- await selectEvent.select(screen.getByText('staging'), 'production');
- await userEvent.click(screen.getByText('Save Rule'));
- await waitFor(() =>
- expect(mock).toHaveBeenCalledWith(
- endpoint,
- expect.objectContaining({
- data: expect.objectContaining({environment: 'production'}),
- })
- )
- );
- expect(metric.startSpan).toHaveBeenCalledTimes(1);
- expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
- });
- it('strips environment value if "All environments" is selected', async function () {
- createWrapper();
- await selectEvent.select(screen.getByText('staging'), 'All Environments');
- await userEvent.click(screen.getByText('Save Rule'));
- await waitFor(() => expect(mock).toHaveBeenCalledTimes(1));
- expect(mock).not.toHaveBeenCalledWith(
- endpoint,
- expect.objectContaining({
- data: expect.objectContaining({environment: '__all_environments__'}),
- })
- );
- expect(metric.startSpan).toHaveBeenCalledTimes(1);
- expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
- });
- it('updates the alert onboarding task', async function () {
- createWrapper();
- await userEvent.click(screen.getByText('Save Rule'));
- await waitFor(() => expect(updateOnboardingTask).toHaveBeenCalledTimes(1));
- expect(metric.startSpan).toHaveBeenCalledTimes(1);
- expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
- });
- it('renders multiple sentry apps at the same time', async () => {
- const linearApp = {
- id: 'sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction',
- enabled: true,
- actionType: 'sentryapp',
- service: 'linear',
- sentryAppInstallationUuid: 'linear-d864bc2a8755',
- prompt: 'Linear',
- label: 'Create a Linear issue with these ',
- formFields: {
- type: 'alert-rule-settings',
- uri: '/hooks/sentry/alert-rule-action',
- description:
- 'When the alert fires automatically create a Linear issue with the following properties.',
- required_fields: [
- {
- name: 'teamId',
- label: 'Team',
- type: 'select',
- uri: '/hooks/sentry/issues/teams',
- choices: [['test-6f0b2b4d402b', 'Sentry']],
- },
- ],
- optional_fields: [
- // Optional fields removed
- ],
- },
- };
- const threadsApp = {
- id: 'sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction',
- enabled: true,
- actionType: 'sentryapp',
- service: 'threads',
- sentryAppInstallationUuid: 'threads-987c470e50cc',
- prompt: 'Threads',
- label: 'Post to a Threads channel with these ',
- formFields: {
- type: 'alert-rule-settings',
- uri: '/sentry/saveAlert',
- required_fields: [
- {
- type: 'select',
- label: 'Channel',
- name: 'channel',
- async: true,
- uri: '/sentry/channels',
- choices: [],
- },
- ],
- },
- };
- MockApiClient.addMockResponse({
- url: '/projects/org-slug/project-slug/rules/configuration/',
- body: {actions: [linearApp, threadsApp], conditions: [], filters: []},
- });
- createWrapper();
- await selectEvent.select(await screen.findByText('Add action...'), 'Threads');
- await selectEvent.select(screen.getByText('Add action...'), 'Linear');
- expect(screen.getByText('Create a Linear issue with these')).toBeInTheDocument();
- expect(
- screen.getByText('Post to a Threads channel with these')
- ).toBeInTheDocument();
- });
- it('opts out of the alert being disabled', async function () {
- MockApiClient.addMockResponse({
- url: '/projects/org-slug/project-slug/rules/1/',
- body: ProjectAlertRuleFixture({
- status: 'disabled',
- disableDate: moment().add(1, 'day').toISOString(),
- }),
- });
- createWrapper();
- await userEvent.click(await screen.findByRole('button', {name: 'Save Rule'}));
- await waitFor(() =>
- expect(mock).toHaveBeenCalledWith(
- endpoint,
- expect.objectContaining({
- data: expect.objectContaining({optOutEdit: true}),
- })
- )
- );
- });
- it('renders environment selector in adopted release filter', async function () {
- createWrapper({project: ProjectFixture({environments: ['production', 'staging']})});
- // Add the adopted release filter
- await selectEvent.select(
- screen.getByText('Add optional filter...'),
- /The {oldest_or_newest} release associated/
- );
- const filtersContainer = await screen.findByTestId('rule-filters');
- // Production environment is preselected because it's the first option.
- // staging should also be selectable.
- await selectEvent.select(
- within(filtersContainer).getAllByText('production')[0],
- 'staging'
- );
- });
- });
- describe('Edit Rule: Slack Channel Look Up', function () {
- const uuid = 'xxxx-xxxx-xxxx';
- beforeEach(function () {
- jest.useFakeTimers();
- });
- afterEach(function () {
- jest.clearAllTimers();
- });
- it('success status updates the rule', async function () {
- const mockSuccess = MockApiClient.addMockResponse({
- url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
- body: {status: 'success', rule: ProjectAlertRuleFixture({name: 'Slack Rule'})},
- });
- MockApiClient.addMockResponse({
- url: '/projects/org-slug/project-slug/rules/1/',
- method: 'PUT',
- statusCode: 202,
- body: {uuid},
- });
- const {router} = createWrapper();
- await userEvent.click(await screen.findByRole('button', {name: 'Save Rule'}), {
- delay: null,
- });
- act(() => jest.advanceTimersByTime(1000));
- await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
- await waitFor(() => expect(addSuccessMessage).toHaveBeenCalledTimes(1));
- await waitFor(() => expect(mockSuccess).toHaveBeenCalledTimes(1));
- expect(router.push).toHaveBeenCalledWith({
- pathname: '/organizations/org-slug/alerts/rules/project-slug/1/details/',
- });
- });
- it('pending status keeps loading true', async function () {
- const pollingMock = MockApiClient.addMockResponse({
- url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
- body: {status: 'pending'},
- });
- MockApiClient.addMockResponse({
- url: '/projects/org-slug/project-slug/rules/1/',
- method: 'PUT',
- statusCode: 202,
- body: {uuid},
- });
- createWrapper();
- await userEvent.click(await screen.findByRole('button', {name: 'Save Rule'}), {
- delay: null,
- });
- act(() => jest.advanceTimersByTime(1000));
- expect(addLoadingMessage).toHaveBeenCalledTimes(2);
- expect(pollingMock).toHaveBeenCalledTimes(1);
- expect(await screen.findByTestId('loading-mask')).toBeInTheDocument();
- });
- it('failed status renders error message', async function () {
- const mockFailed = MockApiClient.addMockResponse({
- url: `/projects/org-slug/project-slug/rule-task/${uuid}/`,
- body: {status: 'failed'},
- });
- MockApiClient.addMockResponse({
- url: '/projects/org-slug/project-slug/rules/1/',
- method: 'PUT',
- statusCode: 202,
- body: {uuid},
- });
- createWrapper();
- await userEvent.click(await screen.findByRole('button', {name: 'Save Rule'}), {
- delay: null,
- });
- act(() => jest.advanceTimersByTime(1000));
- await waitFor(() => expect(addLoadingMessage).toHaveBeenCalledTimes(2));
- await waitFor(() => expect(mockFailed).toHaveBeenCalledTimes(1));
- expect(addErrorMessage).toHaveBeenCalledTimes(1);
- expect(addErrorMessage).toHaveBeenCalledWith('An error occurred');
- });
- });
- describe('Duplicate Rule', function () {
- let mock;
- const rule = ProjectAlertRuleFixture();
- const endpoint = `/projects/org-slug/project-slug/rules/${rule.id}/`;
- beforeEach(function () {
- mock = MockApiClient.addMockResponse({
- url: endpoint,
- method: 'GET',
- body: rule,
- });
- });
- it('gets correct rule to duplicate and renders fields correctly', async function () {
- createWrapper({
- organization: {
- access: ['alerts:write'],
- },
- router: {
- location: {
- query: {
- createFromDuplicate: 'true',
- duplicateRuleId: `${rule.id}`,
- },
- },
- },
- });
- expect(await screen.findByTestId('alert-name')).toHaveValue(`${rule.name} copy`);
- expect(screen.queryByText('A new issue is created')).toBeInTheDocument();
- expect(mock).toHaveBeenCalled();
- });
- it('does not add FirstSeenEventCondition to a duplicate rule', async function () {
- MockApiClient.addMockResponse({
- url: endpoint,
- method: 'GET',
- body: {...rule, conditions: []},
- });
- createWrapper({
- organization: {
- access: ['alerts:write'],
- },
- router: {
- location: {
- query: {
- createFromDuplicate: 'true',
- duplicateRuleId: `${rule.id}`,
- },
- },
- },
- });
- expect(await screen.findByTestId('alert-name')).toHaveValue(`${rule.name} copy`);
- expect(screen.queryByText('A new issue is created')).not.toBeInTheDocument();
- });
- });
- });
|