123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- import {GroupingConfigsFixture} from 'sentry-fixture/groupingConfigs';
- import {LocationFixture} from 'sentry-fixture/locationFixture';
- import {OrganizationFixture} from 'sentry-fixture/organization';
- import {ProjectFixture} from 'sentry-fixture/project';
- import {RouterFixture} from 'sentry-fixture/routerFixture';
- import {
- fireEvent,
- render,
- renderGlobalModal,
- screen,
- userEvent,
- waitFor,
- } from 'sentry-test/reactTestingLibrary';
- import selectEvent from 'sentry-test/selectEvent';
- import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
- import {removePageFiltersStorage} from 'sentry/components/organizations/pageFilters/persistence';
- import ProjectsStore from 'sentry/stores/projectsStore';
- import {browserHistory} from 'sentry/utils/browserHistory';
- import ProjectContextProvider from 'sentry/views/projects/projectContext';
- import ProjectGeneralSettings from 'sentry/views/settings/projectGeneralSettings';
- jest.mock('sentry/actionCreators/indicator');
- jest.mock('sentry/components/organizations/pageFilters/persistence');
- function getField(role, name) {
- return screen.getByRole(role, {name});
- }
- describe('projectGeneralSettings', function () {
- const organization = OrganizationFixture();
- const project = ProjectFixture({
- subjectPrefix: '[my-org]',
- resolveAge: 48,
- allowedDomains: ['example.com', 'https://example.com'],
- scrapeJavaScript: true,
- securityToken: 'security-token',
- securityTokenHeader: 'x-security-header',
- verifySSL: true,
- });
- const groupingConfigs = GroupingConfigsFixture();
- let putMock;
- const router = RouterFixture();
- const routerProps = {
- location: LocationFixture(),
- routes: router.routes,
- route: router.routes[0],
- router,
- routeParams: router.params,
- };
- beforeEach(function () {
- jest.spyOn(window.location, 'assign');
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/grouping-configs/`,
- method: 'GET',
- body: groupingConfigs,
- });
- MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/`,
- method: 'GET',
- body: project,
- });
- MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/environments/`,
- method: 'GET',
- body: [],
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/users/`,
- method: 'GET',
- body: [],
- });
- });
- afterEach(function () {
- MockApiClient.clearMockResponses();
- jest.clearAllMocks();
- jest.restoreAllMocks();
- });
- it('renders form fields', function () {
- render(
- <ProjectGeneralSettings {...routerProps} params={{projectId: project.slug}} />,
- {organization}
- );
- expect(getField('textbox', 'Name')).toHaveValue('Project Name');
- expect(getField('textbox', 'Subject Prefix')).toHaveValue('[my-org]');
- // Step 19 of the auto resolve slider equates to 48 hours. This is
- // different from thee actual field value (which will be 48)
- expect(getField('slider', 'Auto Resolve')).toHaveValue('19');
- expect(getField('textbox', 'Allowed Domains')).toHaveValue(
- 'example.com\nhttps://example.com'
- );
- expect(getField('checkbox', 'Enable JavaScript source fetching')).toBeChecked();
- expect(getField('textbox', 'Security Token')).toHaveValue('security-token');
- expect(getField('textbox', 'Security Token Header')).toHaveValue('x-security-header');
- expect(getField('checkbox', 'Verify TLS/SSL')).toBeChecked();
- });
- it('disables scrapeJavaScript when equivalent org setting is false', function () {
- const orgWithoutScrapeJavaScript = OrganizationFixture({
- scrapeJavaScript: false,
- });
- render(
- <ProjectGeneralSettings {...routerProps} params={{projectId: project.slug}} />,
- {
- organization: orgWithoutScrapeJavaScript,
- }
- );
- expect(getField('checkbox', 'Enable JavaScript source fetching')).toBeDisabled();
- expect(getField('checkbox', 'Enable JavaScript source fetching')).not.toBeChecked();
- });
- it('project admins can remove project', async function () {
- const deleteMock = MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/`,
- method: 'DELETE',
- });
- render(
- <ProjectGeneralSettings {...routerProps} params={{projectId: project.slug}} />,
- {organization}
- );
- await userEvent.click(screen.getByRole('button', {name: 'Remove Project'}));
- // Click confirmation button
- renderGlobalModal();
- await userEvent.click(screen.getByTestId('confirm-button'));
- expect(deleteMock).toHaveBeenCalled();
- expect(removePageFiltersStorage).toHaveBeenCalledWith('org-slug');
- });
- it('project admins can transfer project', async function () {
- const deleteMock = MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/transfer/`,
- method: 'POST',
- });
- render(
- <ProjectGeneralSettings {...routerProps} params={{projectId: project.slug}} />,
- {organization}
- );
- await userEvent.click(screen.getByRole('button', {name: 'Transfer Project'}));
- // Click confirmation button
- renderGlobalModal();
- await userEvent.type(getField('textbox', 'Organization Owner'), 'billy@sentry.io');
- await userEvent.click(screen.getByTestId('confirm-button'));
- await waitFor(() =>
- expect(deleteMock).toHaveBeenCalledWith(
- `/projects/${organization.slug}/${project.slug}/transfer/`,
- expect.objectContaining({
- method: 'POST',
- data: {
- email: 'billy@sentry.io',
- },
- })
- )
- );
- expect(addSuccessMessage).toHaveBeenCalled();
- });
- it('handles errors on transfer project', async function () {
- const deleteMock = MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/transfer/`,
- method: 'POST',
- statusCode: 400,
- body: {detail: 'An organization owner could not be found'},
- });
- render(
- <ProjectGeneralSettings {...routerProps} params={{projectId: project.slug}} />,
- {organization}
- );
- await userEvent.click(screen.getByRole('button', {name: 'Transfer Project'}));
- // Click confirmation button
- renderGlobalModal();
- await userEvent.type(getField('textbox', 'Organization Owner'), 'billy@sentry.io');
- await userEvent.click(screen.getByTestId('confirm-button'));
- await waitFor(() => expect(deleteMock).toHaveBeenCalled());
- expect(addSuccessMessage).not.toHaveBeenCalled();
- expect(addErrorMessage).toHaveBeenCalled();
- // Check the error message
- const {container} = render((addErrorMessage as jest.Mock).mock.calls[0][0]);
- expect(container).toHaveTextContent(
- 'Error transferring project-slug. An organization owner could not be found'
- );
- });
- it('displays transfer/remove message for non-admins', function () {
- const nonAdminOrg = OrganizationFixture({
- access: ['org:read'],
- });
- const {container} = render(
- <ProjectGeneralSettings {...routerProps} params={{projectId: project.slug}} />,
- {organization: nonAdminOrg}
- );
- expect(container).toHaveTextContent(
- 'You do not have the required permission to remove this project.'
- );
- expect(container).toHaveTextContent(
- 'You do not have the required permission to transfer this project.'
- );
- });
- it('disables the form for users without write permissions', function () {
- const readOnlyOrg = OrganizationFixture({access: ['org:read']});
- render(
- <ProjectGeneralSettings {...routerProps} params={{projectId: project.slug}} />,
- {
- organization: readOnlyOrg,
- }
- );
- // no textboxes are enabled
- screen.queryAllByRole('textbox').forEach(textbox => expect(textbox).toBeDisabled());
- expect(screen.getByTestId('project-permission-alert')).toBeInTheDocument();
- });
- it('changing project platform updates ProjectsStore', async function () {
- const params = {projectId: project.slug};
- ProjectsStore.loadInitialData([project]);
- putMock = MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/`,
- method: 'PUT',
- body: {
- ...project,
- platform: 'javascript',
- },
- });
- render(
- <ProjectContextProvider projectSlug={project.slug}>
- <ProjectGeneralSettings
- {...routerProps}
- routes={[]}
- location={LocationFixture()}
- params={params}
- />
- </ProjectContextProvider>,
- {organization}
- );
- const platformSelect = await screen.findByRole('textbox', {name: 'Platform'});
- await selectEvent.select(platformSelect, ['React']);
- expect(putMock).toHaveBeenCalled();
- // updates ProjectsStore
- expect(ProjectsStore.getById('2')!.platform).toBe('javascript');
- });
- it('changing name updates ProjectsStore', async function () {
- const params = {projectId: project.slug};
- ProjectsStore.loadInitialData([project]);
- putMock = MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/`,
- method: 'PUT',
- body: {
- ...project,
- slug: 'new-project',
- },
- });
- render(
- <ProjectContextProvider projectSlug={project.slug}>
- <ProjectGeneralSettings
- {...routerProps}
- routes={[]}
- location={LocationFixture()}
- params={params}
- />
- </ProjectContextProvider>,
- {organization}
- );
- await userEvent.type(
- await screen.findByRole('textbox', {name: 'Name'}),
- 'New Project'
- );
- // Slug does not save on blur
- expect(putMock).not.toHaveBeenCalled();
- // Saves when clicking save
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- // Redirects the user
- await waitFor(() => expect(browserHistory.replace).toHaveBeenCalled());
- expect(ProjectsStore.getById('2')!.slug).toBe('new-project');
- });
- describe('Non-"save on blur" Field', function () {
- beforeEach(function () {
- ProjectsStore.loadInitialData([project]);
- putMock = MockApiClient.addMockResponse({
- url: `/projects/${organization.slug}/${project.slug}/`,
- method: 'PUT',
- body: {
- ...project,
- slug: 'new-project',
- },
- });
- });
- function renderProjectGeneralSettings() {
- const params = {projectId: project.slug};
- render(
- <ProjectContextProvider projectSlug={project.slug}>
- <ProjectGeneralSettings
- {...routerProps}
- routes={[]}
- location={LocationFixture()}
- params={params}
- />
- </ProjectContextProvider>,
- {organization}
- );
- }
- it('can cancel unsaved changes for a field', async function () {
- renderProjectGeneralSettings();
- expect(screen.queryByRole('button', {name: 'Cancel'})).not.toBeInTheDocument();
- const autoResolveSlider = await screen.findByRole('slider', {name: 'Auto Resolve'});
- expect(autoResolveSlider).toHaveValue('19');
- // Change value
- fireEvent.change(autoResolveSlider, {target: {value: '12'}});
- expect(autoResolveSlider).toHaveValue('12');
- // Click cancel
- await userEvent.click(screen.getByRole('button', {name: 'Cancel'}));
- // Cancel row should disappear
- expect(screen.queryByRole('button', {name: 'Cancel'})).not.toBeInTheDocument();
- // Value should be reverted
- expect(autoResolveSlider).toHaveValue('19');
- // PUT should not be called
- expect(putMock).not.toHaveBeenCalled();
- });
- it('saves when value is changed and "Save" clicked', async function () {
- renderProjectGeneralSettings();
- expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
- const autoResolveSlider = await screen.findByRole('slider', {name: 'Auto Resolve'});
- expect(autoResolveSlider).toHaveValue('19');
- // Change value
- fireEvent.change(autoResolveSlider, {target: {value: '12'}});
- expect(autoResolveSlider).toHaveValue('12');
- // Should not have put mock called yet
- expect(putMock).not.toHaveBeenCalled();
- // Click "Save"
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- // API endpoint should have been called
- await waitFor(() => {
- expect(putMock).toHaveBeenCalledWith(
- expect.anything(),
- expect.objectContaining({
- data: {
- resolveAge: 12,
- },
- })
- );
- });
- expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
- });
- });
- });
|