import {AvailableNotificationActionsFixture} from 'sentry-fixture/availableNotificationActions'; import {ProjectFixture} from 'getsentry-test/fixtures/project'; import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription'; import {initializeOrg} from 'sentry-test/initializeOrg'; import { cleanup, render, renderGlobalModal, screen, userEvent, } from 'sentry-test/reactTestingLibrary'; import type {Project} from 'sentry/types/project'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; import {SPIKE_PROTECTION_OPTION_DISABLED} from 'getsentry/views/spikeProtection/constants'; import SpikeProtectionProjects from 'getsentry/views/spikeProtection/spikeProtectionProjects'; describe('project renders and toggles', () => { const projects = [ ProjectFixture({ id: '1', slug: 'project1', // If the project option is True, the feature is disabled options: {[SPIKE_PROTECTION_OPTION_DISABLED]: true}, access: ['project:read'], }), ProjectFixture({ id: '2', slug: 'project2', // If the project option is False, the feature is Enabled options: {[SPIKE_PROTECTION_OPTION_DISABLED]: false}, access: ['project:read', 'project:write', 'project:admin'], }), ]; let organization: any, router: any, mockGet: any, mockPost: any, mockDelete: any, mockDeleteAll: any, mockPostAll: any, mockGetProjectNotificationActions: any; beforeEach(() => { MockApiClient.clearMockResponses(); const newData = initializeOrg({ projects, organization: { features: ['global-views'], openMembership: true, access: ['org:write'], }, }); organization = newData.organization; router = newData.router; SubscriptionStore.set(organization.slug, SubscriptionFixture({organization})); mockGet = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/projects/`, method: 'GET', body: projects, statusCode: 200, }); mockPost = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/spike-protections/`, method: 'POST', body: [], statusCode: 200, }); mockPostAll = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/spike-protections/?projectSlug=$all`, method: 'POST', body: [], statusCode: 200, }); mockDelete = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/spike-protections/`, method: 'DELETE', body: [], statusCode: 200, }); mockDeleteAll = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/spike-protections/?projectSlug=$all`, method: 'DELETE', body: [], statusCode: 200, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/notifications/available-actions/`, method: 'GET', body: AvailableNotificationActionsFixture(), statusCode: 200, }); mockGetProjectNotificationActions = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/notifications/actions/`, match: [ MockApiClient.matchQuery({ triggerType: 'spike-protection', project: projects[0]!.id, }), ], method: 'GET', body: [ { id: 2, organizationId: parseInt(organization.id, 10), integrationId: null, sentryAppId: null, projects: [parseInt(projects[0]!.id, 10)], serviceType: 'sentry_notification', triggerType: 'spike-protection', targetType: 'specific', targetIdentifier: 'default', targetDisplay: 'default', }, ], statusCode: 200, }); }); afterEach(() => { cleanup(); }); async function validateComponents(project: Project, isEnabled: boolean) { const toggle = await screen.findByTestId(`${project.slug}-spike-protection-toggle`); if (isEnabled) { expect(toggle).toBeChecked(); } else { expect(toggle).not.toBeChecked(); } return {toggle}; } it('renders projects table even with no projects', async () => { const mockGetNoProjects = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/projects/`, method: 'GET', body: [], statusCode: 200, }); render(, {router}); expect(mockGetNoProjects).toHaveBeenCalled(); expect(await screen.findByText('There are no items to display')).toBeInTheDocument(); }); it('renders projects accessible to user under closed membership', async () => { organization.openMembership = false; render(, {organization}); expect(await screen.findByText('project1')).toBeInTheDocument(); const requestOptions = mockGet.mock.calls[0][1]; expect(requestOptions.query).toEqual( expect.objectContaining({query: ' is_member:1'}) ); }); it('renders all projects for superuser', async () => { // even under closed membership organization.access = ['org:superuser']; render(, {organization}); expect(await screen.findByText('project1')).toBeInTheDocument(); const requestOptions = mockGet.mock.calls[0][1]; expect(requestOptions.query).toEqual( expect.not.objectContaining({query: ' is_member:1'}) ); }); it('renders all projects for owner', async () => { // even under closed membership organization.access = ['org:admin']; render(, {organization}); expect(await screen.findByText('project1')).toBeInTheDocument(); const requestOptions = mockGet.mock.calls[0][1]; expect(requestOptions.query).toEqual( expect.not.objectContaining({query: ' is_member:1'}) ); }); it('renders toggles', async () => { const project = projects[0]!; render(, {router}); expect(mockGet).toHaveBeenCalled(); const {toggle} = await validateComponents(project, false); await userEvent.click(toggle); await validateComponents(project, true); expect(mockPost).toHaveBeenCalled(); }); it('renders enabled/disabled toggles from team-roles', async () => { const projectDisabled = projects[0]!; const projectEnabled = projects[1]!; organization.access = []; render(, {organization}); const toggleDisabled = await screen.findByTestId( `${projectDisabled.slug}-spike-protection-toggle` ); expect(toggleDisabled).toBeDisabled(); const toggleEnabled = await screen.findByTestId( `${projectEnabled.slug}-spike-protection-toggle` ); expect(toggleEnabled).toBeEnabled(); }); it('renders default value for toggles', async () => { const newProjects = [ ProjectFixture({ id: '1', slug: 'project1', options: {[SPIKE_PROTECTION_OPTION_DISABLED]: true}, }), ProjectFixture({ id: '2', slug: 'project2', options: {[SPIKE_PROTECTION_OPTION_DISABLED]: false}, }), ]; MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/projects/`, method: 'GET', body: [...newProjects], statusCode: 200, }); MockApiClient.addMockResponse({ url: `/organizations/org-slug/notifications/available-actions/`, method: 'GET', body: AvailableNotificationActionsFixture(), statusCode: 200, }); MockApiClient.addMockResponse({ url: `/organizations/org-slug/notifications/actions/`, match: [MockApiClient.matchQuery({triggerType: 'spike-protection'})], method: 'GET', body: [], statusCode: 200, }); render(, {router}); await validateComponents(newProjects[0]!, false); await validateComponents(newProjects[1]!, true); }); it('responds to successful toggles', async () => { const project = projects[0]!; render(, {router}); expect(mockGet).toHaveBeenCalled(); const {toggle} = await validateComponents(project, false); await userEvent.click(toggle); expect(mockPost).toHaveBeenCalled(); await validateComponents(project, true); // toggle off spike protection await userEvent.click(toggle); expect(mockDelete).toHaveBeenCalled(); await validateComponents(project, false); }); it('responds to unsuccessful toggle', async () => { const mockPostFailed = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/spike-protections/`, method: 'POST', statusCode: 403, }); const project = projects[0]!; render(, {router}); expect(mockGet).toHaveBeenCalled(); const {toggle} = await validateComponents(project, false); await userEvent.click(toggle); expect(mockPostFailed).toHaveBeenCalled(); await validateComponents(project, false); }); it('searches successfully', async () => { const projectSlug = projects[0]!.slug; render(, {organization}); const projectSearch = await screen.findByPlaceholderText('Search projects'); await userEvent.click(projectSearch); await userEvent.paste(projectSlug); expect(projectSearch).toHaveValue(projectSlug); expect(mockGet).toHaveBeenCalledTimes(2); const requestOptions = mockGet.mock.calls[1][1]; expect(requestOptions.query).toEqual(expect.objectContaining({query: projectSlug})); }); it('response to successful toggle for all projects', async () => { renderGlobalModal(); render(, {router}); await validateComponents(projects[0]!, false); await validateComponents(projects[1]!, true); const enableAll = await screen.findByTestId(`sp-enable-all`); await userEvent.click(enableAll); await userEvent.click(await screen.findByTestId('confirm-button')); expect(mockPostAll).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({data: {projects: []}}) ); for (const project of projects) { await validateComponents(project, true); } }); it('response to successful disable toggle for all projects', async () => { renderGlobalModal(); render(, {router}); await validateComponents(projects[0]!, false); await validateComponents(projects[1]!, true); const disableAll = await screen.findByTestId(`sp-disable-all`); await userEvent.click(disableAll); await userEvent.click(await screen.findByTestId('confirm-button')); expect(mockDeleteAll).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({data: {projects: []}}) ); for (const project of projects) { await validateComponents(project, false); } }); it('fetches notification actions upon accordion opening', async () => { const project = projects[0]!; mockGet = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/projects/`, method: 'GET', body: [project], statusCode: 200, }); render(, {organization}); await userEvent.click( await screen.findByTestId(`${project.slug}-spike-protection-toggle`) ); await userEvent.click(screen.getByTestId('accordion-title')); expect(mockGetProjectNotificationActions).toHaveBeenCalled(); expect(await screen.findByTestId('sentry_notification-action')).toBeInTheDocument(); }); it('closes the accordion upon disable', async () => { const project = projects[0]!; render(, {organization}); await userEvent.click( await screen.findByTestId(`${project.slug}-spike-protection-toggle`) ); await userEvent.click(screen.getByTestId(`${project.slug}-spike-protection-toggle`)); expect( screen.getByTestId(`${project.slug}-accordion-row-disabled`) ).toBeInTheDocument(); }); });