index.spec.tsx 9.1 KB

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