import {browserHistory} from 'react-router';
import selectEvent from 'react-select-event';
import {GroupingConfigs} from 'sentry-fixture/groupingConfigs';
import LocationFixture from 'sentry-fixture/locationFixture';
import {Organization} from 'sentry-fixture/organization';
import RouterContextFixture from 'sentry-fixture/routerContextFixture';
import {
act,
fireEvent,
render,
renderGlobalModal,
screen,
userEvent,
waitFor,
} from 'sentry-test/reactTestingLibrary';
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
import {removePageFiltersStorage} from 'sentry/components/organizations/pageFilters/persistence';
import ProjectsStore from 'sentry/stores/projectsStore';
import ProjectContext 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 org = Organization();
const project = TestStubs.Project({
subjectPrefix: '[my-org]',
resolveAge: 48,
allowedDomains: ['example.com', 'https://example.com'],
scrapeJavaScript: true,
securityToken: 'security-token',
securityTokenHeader: 'x-security-header',
verifySSL: true,
});
const groupingConfigs = GroupingConfigs();
let routerContext;
let putMock;
const router = TestStubs.router();
const routerProps = {
location: LocationFixture(),
routes: router.routes,
route: router.routes[0],
router,
routeParams: router.params,
};
beforeEach(function () {
jest.spyOn(window.location, 'assign');
routerContext = RouterContextFixture([
{
router: TestStubs.router({
params: {
projectId: project.slug,
},
}),
},
]);
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/grouping-configs/`,
method: 'GET',
body: groupingConfigs,
});
MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/`,
method: 'GET',
body: project,
});
MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/environments/`,
method: 'GET',
body: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${org.slug}/users/`,
method: 'GET',
body: [],
});
});
afterEach(function () {
MockApiClient.clearMockResponses();
jest.clearAllMocks();
jest.restoreAllMocks();
});
it('renders form fields', function () {
render(
);
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 () {
routerContext.context.organization.scrapeJavaScript = false;
render(
,
{
context: routerContext,
}
);
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/${org.slug}/${project.slug}/`,
method: 'DELETE',
});
render(
);
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/${org.slug}/${project.slug}/transfer/`,
method: 'POST',
});
render(
);
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/${org.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/${org.slug}/${project.slug}/transfer/`,
method: 'POST',
statusCode: 400,
body: {detail: 'An organization owner could not be found'},
});
render(
);
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 () {
routerContext.context.organization.access = ['org:read'];
const {container} = render(
,
{context: routerContext}
);
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 = Organization({access: ['org:read']});
routerContext.context.organization = readOnlyOrg;
render(
,
{
context: routerContext,
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/${org.slug}/${project.slug}/`,
method: 'PUT',
body: {
...project,
platform: 'javascript',
},
});
render(
,
{context: routerContext}
);
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/${org.slug}/${project.slug}/`,
method: 'PUT',
body: {
...project,
slug: 'new-project',
},
});
render(
,
{context: routerContext}
);
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 () {
const params = {projectId: project.slug};
ProjectsStore.loadInitialData([project]);
putMock = MockApiClient.addMockResponse({
url: `/projects/${org.slug}/${project.slug}/`,
method: 'PUT',
body: {
...project,
slug: 'new-project',
},
});
render(
,
{context: routerContext}
);
});
it('can cancel unsaved changes for a field', async function () {
expect(screen.queryByRole('button', {name: 'Cancel'})).not.toBeInTheDocument();
const autoResolveSlider = getField('slider', '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 () {
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
const autoResolveSlider = getField('slider', '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'}));
await act(tick);
// API endpoint should have been called
expect(putMock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
data: {
resolveAge: 12,
},
})
);
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
});
});
});