import {LocationFixture} from 'sentry-fixture/locationFixture'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {RouterFixture} from 'sentry-fixture/routerFixture'; import {SentryAppFixture} from 'sentry-fixture/sentryApp'; import {SentryAppTokenFixture} from 'sentry-fixture/sentryAppToken'; import { render, renderGlobalModal, screen, userEvent, waitFor, } from 'sentry-test/reactTestingLibrary'; import selectEvent from 'sentry-test/selectEvent'; import SentryApplicationDetails from 'sentry/views/settings/organizationDeveloperSettings/sentryApplicationDetails'; describe('Sentry Application Details', function () { let sentryApp; let token; let createAppRequest; let editAppRequest; const maskedValue = '************oken'; const router = RouterFixture(); beforeEach(() => { MockApiClient.clearMockResponses(); }); describe('Creating a new public Sentry App', () => { function renderComponent() { return render( ); } beforeEach(() => { createAppRequest = MockApiClient.addMockResponse({ url: '/sentry-apps/', method: 'POST', body: [], }); }); it('has inputs for redirectUrl and verifyInstall', () => { renderComponent(); expect( screen.getByRole('checkbox', {name: 'Verify Installation'}) ).toBeInTheDocument(); expect(screen.getByRole('textbox', {name: 'Redirect URL'})).toBeInTheDocument(); }); it('shows empty scopes and no credentials', function () { renderComponent(); expect(screen.getByText('Permissions')).toBeInTheDocument(); // new app starts off with no scopes selected expect(screen.getByRole('checkbox', {name: 'issue'})).not.toBeChecked(); expect(screen.getByRole('checkbox', {name: 'error'})).not.toBeChecked(); expect(screen.getByRole('checkbox', {name: 'comment'})).not.toBeChecked(); }); it('does not show logo upload fields', function () { renderComponent(); expect(screen.queryByText('Logo')).not.toBeInTheDocument(); expect(screen.queryByText('Small Icon')).not.toBeInTheDocument(); }); it('saves', async function () { renderComponent(); await userEvent.type(screen.getByRole('textbox', {name: 'Name'}), 'Test App'); await userEvent.type(screen.getByRole('textbox', {name: 'Author'}), 'Sentry'); await userEvent.type( screen.getByRole('textbox', {name: 'Webhook URL'}), 'https://webhook.com' ); await userEvent.type( screen.getByRole('textbox', {name: 'Redirect URL'}), 'https://webhook.com/setup' ); await userEvent.click(screen.getByRole('textbox', {name: 'Schema'})); await userEvent.paste('{}'); await userEvent.click(screen.getByRole('checkbox', {name: 'Alert Rule Action'})); await selectEvent.select(screen.getByRole('textbox', {name: 'Member'}), 'Admin'); await selectEvent.select( screen.getByRole('textbox', {name: 'Issue & Event'}), 'Admin' ); await userEvent.click(screen.getByRole('checkbox', {name: 'issue'})); await userEvent.click(screen.getByRole('button', {name: 'Save Changes'})); const data = { name: 'Test App', author: 'Sentry', organization: OrganizationFixture().slug, redirectUrl: 'https://webhook.com/setup', webhookUrl: 'https://webhook.com', scopes: expect.arrayContaining([ 'member:read', 'member:admin', 'event:read', 'event:admin', ]), events: ['issue'], isInternal: false, verifyInstall: true, isAlertable: true, allowedOrigins: [], schema: {}, }; expect(createAppRequest).toHaveBeenCalledWith( '/sentry-apps/', expect.objectContaining({ data, method: 'POST', }) ); }); }); describe('Creating a new internal Sentry App', () => { function renderComponent() { return render( ); } it('does not show logo upload fields', function () { renderComponent(); expect(screen.queryByText('Logo')).not.toBeInTheDocument(); expect(screen.queryByText('Small Icon')).not.toBeInTheDocument(); }); it('no inputs for redirectUrl and verifyInstall', () => { renderComponent(); expect( screen.queryByRole('checkbox', {name: 'Verify Installation'}) ).not.toBeInTheDocument(); expect( screen.queryByRole('textbox', {name: 'Redirect URL'}) ).not.toBeInTheDocument(); }); }); describe('Renders public app', function () { function renderComponent() { return render( ); } beforeEach(() => { sentryApp = SentryAppFixture(); sentryApp.events = ['issue']; MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, body: sentryApp, }); MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/api-tokens/`, body: [], }); }); it('shows logo upload fields', function () { renderComponent(); expect(screen.getByText('Logo')).toBeInTheDocument(); expect(screen.getByText('Small Icon')).toBeInTheDocument(); }); it('has inputs for redirectUrl and verifyInstall', () => { renderComponent(); expect( screen.getByRole('checkbox', {name: 'Verify Installation'}) ).toBeInTheDocument(); expect(screen.getByRole('textbox', {name: 'Redirect URL'})).toBeInTheDocument(); }); it('shows application data', async function () { renderComponent(); await selectEvent.openMenu(screen.getByRole('textbox', {name: 'Project'})); expect(screen.getByRole('menuitemradio', {name: 'Read'})).toBeChecked(); }); it('renders clientId and clientSecret for public apps', function () { renderComponent(); expect(screen.getByRole('textbox', {name: 'Client ID'})).toBeInTheDocument(); expect(screen.getByRole('textbox', {name: 'Client Secret'})).toBeInTheDocument(); }); }); describe('Renders for internal apps', () => { function renderComponent() { return render( ); } beforeEach(() => { sentryApp = SentryAppFixture({ status: 'internal', }); token = SentryAppTokenFixture(); sentryApp.events = ['issue']; MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, body: sentryApp, }); MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/api-tokens/`, body: [token], }); }); it('no inputs for redirectUrl and verifyInstall', () => { renderComponent(); expect( screen.queryByRole('checkbox', {name: 'Verify Installation'}) ).not.toBeInTheDocument(); expect( screen.queryByRole('textbox', {name: 'Redirect URL'}) ).not.toBeInTheDocument(); }); it('shows logo upload fields', function () { renderComponent(); expect(screen.getByText('Logo')).toBeInTheDocument(); expect(screen.getByText('Small Icon')).toBeInTheDocument(); }); it('has tokens', function () { renderComponent(); expect(screen.getByText('Tokens')).toBeInTheDocument(); expect(screen.getByLabelText('Token preview')).toHaveTextContent('oken'); }); it('shows just clientSecret', function () { renderComponent(); expect(screen.queryByRole('textbox', {name: 'Client ID'})).not.toBeInTheDocument(); expect(screen.getByRole('textbox', {name: 'Client Secret'})).toBeInTheDocument(); }); }); describe('Renders masked values', () => { function renderComponent() { return render( ); } beforeEach(() => { sentryApp = SentryAppFixture({ status: 'internal', clientSecret: maskedValue, }); token = SentryAppTokenFixture({token: maskedValue, refreshToken: maskedValue}); sentryApp.events = ['issue']; MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, body: sentryApp, }); MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/api-tokens/`, body: [token], }); }); it('shows masked tokens', function () { renderComponent(); expect(screen.getByLabelText('Token preview')).toHaveTextContent(maskedValue); }); it('shows masked clientSecret', function () { renderComponent(); expect(screen.getByRole('textbox', {name: 'Client Secret'})).toHaveValue( maskedValue ); }); }); describe('Editing internal app tokens', () => { function renderComponent() { return render( ); } beforeEach(() => { sentryApp = SentryAppFixture({ status: 'internal', isAlertable: true, }); token = SentryAppTokenFixture(); sentryApp.events = ['issue']; MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, body: sentryApp, }); MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/api-tokens/`, body: [token], }); }); it('adding token to list', async function () { MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/api-tokens/`, method: 'POST', body: [ SentryAppTokenFixture({ token: '392847329', dateCreated: '2018-03-02T18:30:26Z', id: '234', }), ], }); renderComponent(); expect(screen.queryByLabelText('Generated token')).not.toBeInTheDocument(); expect(screen.getAllByLabelText('Token preview')).toHaveLength(1); await userEvent.click(screen.getByRole('button', {name: 'New Token'})); await waitFor(() => { expect(screen.getAllByLabelText('Token preview')).toHaveLength(1); }); await waitFor(() => { expect(screen.getAllByLabelText('Generated token')).toHaveLength(1); }); }); it('removing token from list', async function () { MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/api-tokens/${token.id}/`, method: 'DELETE', body: {}, }); renderComponent(); renderGlobalModal(); await userEvent.click(screen.getByRole('button', {name: 'Remove'})); // Confirm modal await userEvent.click(screen.getByRole('button', {name: 'Confirm'})); expect(await screen.findByText('No tokens created yet.')).toBeInTheDocument(); }); it('removing webhookURL unsets isAlertable and changes webhookDisabled to true', async () => { renderComponent(); expect(screen.getByRole('checkbox', {name: 'Alert Rule Action'})).toBeChecked(); await userEvent.clear(screen.getByRole('textbox', {name: 'Webhook URL'})); expect(screen.getByRole('checkbox', {name: 'Alert Rule Action'})).not.toBeChecked(); }); }); describe('Editing an existing public Sentry App', () => { function renderComponent() { return render( ); } beforeEach(() => { sentryApp = SentryAppFixture(); sentryApp.events = ['issue']; sentryApp.scopes = ['project:read', 'event:read']; editAppRequest = MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, method: 'PUT', body: [], }); MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, body: sentryApp, }); MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/api-tokens/`, body: [], }); }); it('updates app with correct data', async function () { renderComponent(); await userEvent.clear(screen.getByRole('textbox', {name: 'Redirect URL'})); await userEvent.type( screen.getByRole('textbox', {name: 'Redirect URL'}), 'https://hello.com/' ); await userEvent.click(screen.getByRole('textbox', {name: 'Schema'})); await userEvent.paste('{}'); await userEvent.click(screen.getByRole('checkbox', {name: 'issue'})); await userEvent.click(screen.getByRole('button', {name: 'Save Changes'})); expect(editAppRequest).toHaveBeenCalledWith( `/sentry-apps/${sentryApp.slug}/`, expect.objectContaining({ data: expect.objectContaining({ redirectUrl: 'https://hello.com/', events: [], }), method: 'PUT', }) ); }); it('submits with no-access for event subscription when permission is revoked', async () => { renderComponent(); await userEvent.click(screen.getByRole('checkbox', {name: 'issue'})); await userEvent.click(screen.getByRole('textbox', {name: 'Schema'})); await userEvent.paste('{}'); await selectEvent.select( screen.getByRole('textbox', {name: 'Issue & Event'}), 'No Access' ); await userEvent.click(screen.getByRole('button', {name: 'Save Changes'})); expect(editAppRequest).toHaveBeenCalledWith( `/sentry-apps/${sentryApp.slug}/`, expect.objectContaining({ data: expect.objectContaining({ events: [], }), method: 'PUT', }) ); }); }); describe('Editing an existing public Sentry App with a scope error', () => { function renderComponent() { render( ); } beforeEach(() => { sentryApp = SentryAppFixture(); editAppRequest = MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, method: 'PUT', statusCode: 400, body: { scopes: [ "Requested permission of member:write exceeds requester's permission. Please contact an administrator to make the requested change.", "Requested permission of member:admin exceeds requester's permission. Please contact an administrator to make the requested change.", ], }, }); MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, body: sentryApp, }); MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/api-tokens/`, body: [], }); }); it('renders the error', async () => { renderComponent(); await userEvent.click(screen.getByRole('button', {name: 'Save Changes'})); expect( await screen.findByText( "Requested permission of member:admin exceeds requester's permission. Please contact an administrator to make the requested change." ) ).toBeInTheDocument(); }); it('handles client secret rotation', async function () { sentryApp = SentryAppFixture(); sentryApp.clientSecret = null; MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, body: sentryApp, }); const rotateSecretApiCall = MockApiClient.addMockResponse({ method: 'POST', url: `/sentry-apps/${sentryApp.slug}/rotate-secret/`, body: { clientSecret: 'newSecret!', }, }); render( ); renderGlobalModal(); expect(screen.getByText('hidden')).toBeInTheDocument(); expect( screen.getByRole('button', {name: 'Rotate client secret'}) ).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: 'Rotate client secret'})); // Confirm modal await userEvent.click(screen.getByRole('button', {name: 'Confirm'})); expect( screen.getByText('This will be the only time your client secret is visible!') ).toBeInTheDocument(); expect(screen.getByText('Your new Client Secret')).toBeInTheDocument(); expect(screen.getByLabelText('new-client-secret')).toHaveValue( 'newSecret!' ); expect(rotateSecretApiCall).toHaveBeenCalledTimes(1); }); }); });