123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-public/'})}
- routes={router.routes}
- routeParams={{}}
- route={{}}
- params={{}}
- />
- );
- }
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-internal/'})}
- routes={router.routes}
- routeParams={{}}
- route={{}}
- params={{}}
- />
- );
- }
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-public/'})}
- routes={router.routes}
- routeParams={{}}
- route={{}}
- params={{appSlug: sentryApp.slug}}
- />
- );
- }
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-public/'})}
- routes={router.routes}
- routeParams={{}}
- route={{}}
- params={{appSlug: sentryApp.slug}}
- />
- );
- }
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-public/'})}
- routes={router.routes}
- routeParams={{}}
- route={{}}
- params={{appSlug: sentryApp.slug}}
- />
- );
- }
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-public/'})}
- routes={router.routes}
- routeParams={{}}
- route={{}}
- params={{appSlug: sentryApp.slug}}
- />
- );
- }
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-public/'})}
- routes={router.routes}
- routeParams={{}}
- route={{}}
- params={{appSlug: sentryApp.slug}}
- />
- );
- }
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-public/'})}
- routes={router.routes}
- routeParams={{}}
- route={{}}
- params={{appSlug: sentryApp.slug}}
- />
- );
- }
- 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(
- <SentryApplicationDetails
- router={router}
- location={LocationFixture({pathname: 'new-public/'})}
- routes={router.routes}
- route={{}}
- routeParams={{}}
- params={{appSlug: sentryApp.slug}}
- />
- );
- 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<HTMLInputElement>('new-client-secret')).toHaveValue(
- 'newSecret!'
- );
- expect(rotateSecretApiCall).toHaveBeenCalledTimes(1);
- });
- });
- });
|