index.spec.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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(
  67. await screen.findByText(
  68. textWithMarkupMatcher('a few seconds ago in project Project Name')
  69. )
  70. ).toBeInTheDocument();
  71. expect(screen.getByText('My Token 1')).toBeInTheDocument();
  72. expect(screen.getByText('My Token 2')).toBeInTheDocument();
  73. expect(screen.getByText('never used')).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('shows unused tokens', async function () {
  86. const tokens: OrgAuthToken[] = [
  87. {
  88. id: '1',
  89. name: 'My Token 1',
  90. tokenLastCharacters: '1234',
  91. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  92. scopes: ['org:read'],
  93. },
  94. {
  95. id: '2',
  96. name: 'My Token 2',
  97. tokenLastCharacters: 'ABCD',
  98. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  99. scopes: ['org:read'],
  100. },
  101. ];
  102. MockApiClient.addMockResponse({
  103. url: ENDPOINT,
  104. method: 'GET',
  105. body: tokens,
  106. });
  107. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  108. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  109. // Then list
  110. expect(screen.getByText('My Token 1')).toBeInTheDocument();
  111. expect(screen.getByText('My Token 2')).toBeInTheDocument();
  112. expect(screen.getAllByText('never used')).toHaveLength(2);
  113. });
  114. it('handle error when loading tokens', async function () {
  115. const mock = MockApiClient.addMockResponse({
  116. url: ENDPOINT,
  117. method: 'GET',
  118. statusCode: 400,
  119. });
  120. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  121. expect(await screen.findByTestId('loading-error')).toHaveTextContent(
  122. 'Failed to load auth tokens for the organization.'
  123. );
  124. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  125. expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument();
  126. expect(mock).toHaveBeenCalledTimes(1);
  127. });
  128. it('shows empty state', async function () {
  129. const tokens: OrgAuthToken[] = [];
  130. MockApiClient.addMockResponse({
  131. url: ENDPOINT,
  132. method: 'GET',
  133. body: tokens,
  134. });
  135. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  136. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  137. expect(screen.getByTestId('empty-state')).toBeInTheDocument();
  138. expect(screen.queryByTestId('loading-error')).not.toBeInTheDocument();
  139. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  140. });
  141. describe('revoking', function () {
  142. it('allows to revoke tokens', async function () {
  143. jest.spyOn(indicators, 'addSuccessMessage');
  144. const tokens: OrgAuthToken[] = [
  145. {
  146. id: '1',
  147. name: 'My Token 1',
  148. tokenLastCharacters: '1234',
  149. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  150. scopes: ['org:read'],
  151. },
  152. {
  153. id: '2',
  154. name: 'My Token 2',
  155. tokenLastCharacters: 'ABCD',
  156. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  157. scopes: ['org:read'],
  158. },
  159. {
  160. id: '3',
  161. name: 'My Token 3',
  162. tokenLastCharacters: 'ABCD',
  163. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  164. scopes: ['org:read'],
  165. },
  166. ];
  167. MockApiClient.addMockResponse({
  168. url: ENDPOINT,
  169. method: 'GET',
  170. body: tokens,
  171. });
  172. const deleteMock = MockApiClient.addMockResponse({
  173. url: `${ENDPOINT}2/`,
  174. method: 'DELETE',
  175. });
  176. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  177. renderGlobalModal();
  178. expect(await screen.findByText('My Token 1')).toBeInTheDocument();
  179. expect(screen.getByText('My Token 2')).toBeInTheDocument();
  180. expect(screen.getByText('My Token 3')).toBeInTheDocument();
  181. expect(screen.getByLabelText('Revoke My Token 2')).toBeEnabled();
  182. await userEvent.click(screen.getByLabelText('Revoke My Token 2'));
  183. // Confirm modal
  184. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  185. expect(screen.getByText('My Token 1')).toBeInTheDocument();
  186. expect(screen.queryByText('My Token 2')).not.toBeInTheDocument();
  187. expect(screen.getByText('My Token 3')).toBeInTheDocument();
  188. expect(indicators.addSuccessMessage).toHaveBeenCalledWith(
  189. 'Revoked auth token for the organization.'
  190. );
  191. expect(deleteMock).toHaveBeenCalledTimes(1);
  192. });
  193. it('handles API error when revoking token', async function () {
  194. jest.spyOn(indicators, 'addErrorMessage');
  195. const tokens: OrgAuthToken[] = [
  196. {
  197. id: '1',
  198. name: 'My Token 1',
  199. tokenLastCharacters: '1234',
  200. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  201. scopes: ['org:read'],
  202. },
  203. ];
  204. MockApiClient.addMockResponse({
  205. url: ENDPOINT,
  206. method: 'GET',
  207. body: tokens,
  208. });
  209. const deleteMock = MockApiClient.addMockResponse({
  210. url: `${ENDPOINT}1/`,
  211. method: 'DELETE',
  212. statusCode: 400,
  213. });
  214. render(<OrganizationAuthTokensIndex {...defaultProps} />);
  215. renderGlobalModal();
  216. expect(await screen.findByText('My Token 1')).toBeInTheDocument();
  217. expect(screen.getByLabelText('Revoke My Token 1')).toBeEnabled();
  218. await userEvent.click(screen.getByLabelText('Revoke My Token 1'));
  219. // Confirm modal
  220. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  221. expect(screen.getByText('My Token 1')).toBeInTheDocument();
  222. expect(indicators.addErrorMessage).toHaveBeenCalledWith(
  223. 'Failed to revoke the auth token for the organization.'
  224. );
  225. expect(deleteMock).toHaveBeenCalledTimes(1);
  226. });
  227. it('does not allow to revoke without permission', async function () {
  228. const org = TestStubs.Organization({
  229. access: ['org:read'],
  230. });
  231. const tokens: OrgAuthToken[] = [
  232. {
  233. id: '1',
  234. name: 'My Token 1',
  235. tokenLastCharacters: '1234',
  236. dateCreated: new Date('2023-01-01T00:00:00.000Z'),
  237. scopes: ['org:read'],
  238. },
  239. ];
  240. const props = {
  241. ...defaultProps,
  242. organization: org,
  243. };
  244. MockApiClient.addMockResponse({
  245. url: ENDPOINT,
  246. method: 'GET',
  247. body: tokens,
  248. });
  249. render(<OrganizationAuthTokensIndex {...props} />, {organization: org});
  250. expect(await screen.findByText('My Token 1')).toBeInTheDocument();
  251. expect(screen.getByLabelText('Revoke My Token 1')).toBeDisabled();
  252. });
  253. });
  254. });