index.spec.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {
  3. render,
  4. renderGlobalModal,
  5. screen,
  6. userEvent,
  7. waitForElementToBeRemoved,
  8. } from 'sentry-test/reactTestingLibrary';
  9. import {textWithMarkupMatcher} from 'sentry-test/utils';
  10. import * as indicators from 'sentry/actionCreators/indicator';
  11. import OrganizationsStore from 'sentry/stores/organizationsStore';
  12. import {OrgAuthToken} from 'sentry/types';
  13. import {OrganizationAuthTokensIndex} from 'sentry/views/settings/organizationAuthTokens';
  14. describe('OrganizationAuthTokensIndex', function () {
  15. const ENDPOINT = '/organizations/org-slug/org-auth-tokens/';
  16. const PROJECTS_ENDPOINT = '/organizations/org-slug/projects/';
  17. const {organization, project, router} = initializeOrg();
  18. const defaultProps = {
  19. organization,
  20. router,
  21. location: router.location,
  22. params: {orgId: organization.slug},
  23. routes: router.routes,
  24. route: {},
  25. routeParams: router.params,
  26. };
  27. let projectsMock: jest.Mock<any>;
  28. beforeEach(function () {
  29. OrganizationsStore.addOrReplace(organization);
  30. projectsMock = MockApiClient.addMockResponse({
  31. url: PROJECTS_ENDPOINT,
  32. method: 'GET',
  33. body: [project],
  34. });
  35. });
  36. afterEach(function () {
  37. MockApiClient.clearMockResponses();
  38. });
  39. it('shows tokens', async function () {
  40. const tokens: OrgAuthToken[] = [
  41. {
  42. id: '1',
  43. name: 'My Token 1',
  44. tokenLastCharacters: '1234',
  45. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  46. scopes: ['org:read'],
  47. },
  48. {
  49. id: '2',
  50. name: 'My Token 2',
  51. tokenLastCharacters: 'ABCD',
  52. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  53. scopes: ['org:read'],
  54. dateLastUsed: new Date(),
  55. projectLastUsedId: project.id,
  56. },
  57. ];
  58. const mock = MockApiClient.addMockResponse({
  59. url: ENDPOINT,
  60. method: 'GET',
  61. body: tokens,
  62. });
  63. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  64. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  65. // Then list
  66. expect(screen.getByText('My Token 1')).toBeInTheDocument();
  67. expect(screen.getByText('My Token 2')).toBeInTheDocument();
  68. expect(screen.getByText('never used')).toBeInTheDocument();
  69. expect(
  70. await screen.findByText(
  71. textWithMarkupMatcher('a few seconds ago in project Project Name')
  72. )
  73. ).toBeInTheDocument();
  74. expect(screen.queryByTestId('loading-error')).not.toBeInTheDocument();
  75. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  76. expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument();
  77. expect(mock).toHaveBeenCalledTimes(1);
  78. expect(mock).toHaveBeenCalledWith(ENDPOINT, expect.objectContaining({method: 'GET'}));
  79. expect(projectsMock).toHaveBeenCalledTimes(1);
  80. expect(projectsMock).toHaveBeenCalledWith(
  81. PROJECTS_ENDPOINT,
  82. expect.objectContaining({method: 'GET', query: {query: `id:${project.id}`}})
  83. );
  84. });
  85. it('handle error when loading tokens', async function () {
  86. const mock = MockApiClient.addMockResponse({
  87. url: ENDPOINT,
  88. method: 'GET',
  89. statusCode: 400,
  90. });
  91. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  92. expect(await screen.findByTestId('loading-error')).toHaveTextContent(
  93. 'Failed to load auth tokens for the organization.'
  94. );
  95. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  96. expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument();
  97. expect(mock).toHaveBeenCalledTimes(1);
  98. });
  99. it('shows empty state', async function () {
  100. const tokens: OrgAuthToken[] = [];
  101. MockApiClient.addMockResponse({
  102. url: ENDPOINT,
  103. method: 'GET',
  104. body: tokens,
  105. });
  106. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  107. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  108. expect(screen.getByTestId('empty-state')).toBeInTheDocument();
  109. expect(screen.queryByTestId('loading-error')).not.toBeInTheDocument();
  110. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  111. });
  112. describe('revoking', function () {
  113. it('allows to revoke tokens', async function () {
  114. jest.spyOn(indicators, 'addSuccessMessage');
  115. const tokens: OrgAuthToken[] = [
  116. {
  117. id: '1',
  118. name: 'My Token 1',
  119. tokenLastCharacters: '1234',
  120. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  121. scopes: ['org:read'],
  122. },
  123. {
  124. id: '2',
  125. name: 'My Token 2',
  126. tokenLastCharacters: 'ABCD',
  127. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  128. scopes: ['org:read'],
  129. },
  130. {
  131. id: '3',
  132. name: 'My Token 3',
  133. tokenLastCharacters: 'ABCD',
  134. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  135. scopes: ['org:read'],
  136. },
  137. ];
  138. MockApiClient.addMockResponse({
  139. url: ENDPOINT,
  140. method: 'GET',
  141. body: tokens,
  142. });
  143. const deleteMock = MockApiClient.addMockResponse({
  144. url: `${ENDPOINT}2/`,
  145. method: 'DELETE',
  146. });
  147. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  148. renderGlobalModal();
  149. expect(await screen.findByText('My Token 1')).toBeInTheDocument();
  150. expect(screen.getByText('My Token 2')).toBeInTheDocument();
  151. expect(screen.getByText('My Token 3')).toBeInTheDocument();
  152. expect(screen.getByLabelText('Revoke My Token 2')).toBeEnabled();
  153. await userEvent.click(screen.getByLabelText('Revoke My Token 2'));
  154. // Confirm modal
  155. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  156. expect(screen.getByText('My Token 1')).toBeInTheDocument();
  157. expect(screen.queryByText('My Token 2')).not.toBeInTheDocument();
  158. expect(screen.getByText('My Token 3')).toBeInTheDocument();
  159. expect(indicators.addSuccessMessage).toHaveBeenCalledWith(
  160. 'Revoked auth token for the organization.'
  161. );
  162. expect(deleteMock).toHaveBeenCalledTimes(1);
  163. });
  164. it('handles API error when revoking token', async function () {
  165. jest.spyOn(indicators, 'addErrorMessage');
  166. const tokens: OrgAuthToken[] = [
  167. {
  168. id: '1',
  169. name: 'My Token 1',
  170. tokenLastCharacters: '1234',
  171. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  172. scopes: ['org:read'],
  173. },
  174. ];
  175. MockApiClient.addMockResponse({
  176. url: ENDPOINT,
  177. method: 'GET',
  178. body: tokens,
  179. });
  180. const deleteMock = MockApiClient.addMockResponse({
  181. url: `${ENDPOINT}1/`,
  182. method: 'DELETE',
  183. statusCode: 400,
  184. });
  185. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  186. renderGlobalModal();
  187. expect(await screen.findByText('My Token 1')).toBeInTheDocument();
  188. expect(screen.getByLabelText('Revoke My Token 1')).toBeEnabled();
  189. await userEvent.click(screen.getByLabelText('Revoke My Token 1'));
  190. // Confirm modal
  191. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  192. expect(screen.getByText('My Token 1')).toBeInTheDocument();
  193. expect(indicators.addErrorMessage).toHaveBeenCalledWith(
  194. 'Failed to revoke the auth token for the organization.'
  195. );
  196. expect(deleteMock).toHaveBeenCalledTimes(1);
  197. });
  198. it('does not allow to revoke without permission', async function () {
  199. const org = TestStubs.Organization({
  200. access: ['org:read'],
  201. });
  202. const tokens: OrgAuthToken[] = [
  203. {
  204. id: '1',
  205. name: 'My Token 1',
  206. tokenLastCharacters: '1234',
  207. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  208. scopes: ['org:read'],
  209. },
  210. ];
  211. const props = {
  212. ...defaultProps,
  213. organization: org,
  214. };
  215. MockApiClient.addMockResponse({
  216. url: ENDPOINT,
  217. method: 'GET',
  218. body: tokens,
  219. });
  220. render(<OrganizationAuthTokensIndex {...props} />, {organization: org});
  221. expect(await screen.findByText('My Token 1')).toBeInTheDocument();
  222. expect(screen.getByLabelText('Revoke My Token 1')).toBeDisabled();
  223. });
  224. });
  225. });