personal-setting-token-access.spec.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { within } from '@testing-library/vue'
  3. import {
  4. checkSimpleTableContent,
  5. checkSimpleTableHeader,
  6. } from '#tests/support/components/checkSimpleTableContent.ts'
  7. import { visitView } from '#tests/support/components/visitView.ts'
  8. import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
  9. import { mockPermissions } from '#tests/support/mock-permissions.ts'
  10. import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
  11. import { waitForNextTick } from '#tests/support/utils.ts'
  12. import { mockFormUpdaterQuery } from '#shared/components/Form/graphql/queries/formUpdater.mocks.ts'
  13. import { mockUserCurrentAccessTokenAddMutation } from '#shared/entities/user/current/graphql/mutations/userCurrentAccessTokenAdd.mocks.ts'
  14. import { mockUserCurrentAccessTokenDeleteMutation } from '#shared/entities/user/current/graphql/mutations/userCurrentAccessTokenDelete.mocks.ts'
  15. import { mockUserCurrentAccessTokenListQuery } from '#shared/entities/user/current/graphql/queries/userCurrentAcessTokenList.mocks.ts'
  16. import { convertToGraphQLId } from '#shared/graphql/utils.ts'
  17. import { getUserCurrentAccessTokenUpdatesSubscriptionHandler } from '../graphql/subscriptions/userCurrentAccessTokenUpdates.mocks.ts'
  18. vi.hoisted(() => {
  19. vi.setSystemTime(new Date('2024-04-25T10:00:00Z'))
  20. })
  21. const userCurrentAccessTokenList = [
  22. {
  23. id: convertToGraphQLId('Token', 1),
  24. name: 'Example Token',
  25. preferences: {
  26. permission: ['user_preferences.access_token'],
  27. },
  28. createdAt: '2020-12-18T17:26:00Z',
  29. expiresAt: null,
  30. lastUsedAt: '2024-02-01T17:00:00Z',
  31. },
  32. {
  33. id: convertToGraphQLId('Token', 2),
  34. name: 'Ticket Handling',
  35. preferences: {
  36. permission: ['admin.user', 'admin.organization'],
  37. },
  38. createdAt: '2022-01-31T12:00:00Z',
  39. expiresAt: '2024-06-01T10:00:00Z',
  40. lastUsedAt: null,
  41. },
  42. ]
  43. const rowContents = [
  44. [
  45. 'Example Token',
  46. 'user_preferences.access_token',
  47. ['2020-12-18 17:26', '3 years ago'],
  48. '-',
  49. ['2024-02-01 17:00', '2 months ago'],
  50. ],
  51. [
  52. 'Ticket Handling',
  53. 'admin.user, admin.organization',
  54. ['2022-01-31 12:00', '2 years ago'],
  55. ['2024-06-01 10:00', 'in 1 month'],
  56. '-',
  57. ],
  58. ]
  59. describe('personal settings for token access', () => {
  60. beforeEach(() => {
  61. mockUserCurrent({
  62. firstname: 'John',
  63. lastname: 'Doe',
  64. })
  65. mockPermissions(['user_preferences.access_token'])
  66. mockApplicationConfig({
  67. api_token_access: true,
  68. })
  69. })
  70. afterAll(() => {
  71. vi.useRealTimers()
  72. })
  73. it('show initial message when no token exists yet', async () => {
  74. mockUserCurrentAccessTokenListQuery({ userCurrentAccessTokenList: [] })
  75. const view = await visitView('/personal-setting/token-access')
  76. expect(
  77. view.getByText(
  78. 'You can generate a personal access token for each application you use that needs access to the Zammad API.',
  79. ),
  80. ).toBeInTheDocument()
  81. expect(
  82. view.getByText(
  83. "Pick a name for the application, and we'll give you a unique token.",
  84. ),
  85. ).toBeInTheDocument()
  86. })
  87. it('redirects to the error page when api token access is disabled', async () => {
  88. mockApplicationConfig({
  89. api_token_access: false,
  90. })
  91. const view = await visitView('/personal-setting/token-access')
  92. await vi.waitFor(() => {
  93. expect(view, 'correctly redirects to error page').toHaveCurrentUrl(
  94. '/error-tab',
  95. )
  96. })
  97. })
  98. it('show existing personal access token', async () => {
  99. mockUserCurrentAccessTokenListQuery({ userCurrentAccessTokenList })
  100. const view = await visitView('/personal-setting/token-access')
  101. const tableLabel = 'Personal Access Tokens'
  102. const tableHeaders = [
  103. 'Name',
  104. 'Permissions',
  105. 'Created',
  106. 'Expires',
  107. 'Last Used',
  108. 'Actions',
  109. ]
  110. checkSimpleTableHeader(view, tableHeaders, tableLabel)
  111. checkSimpleTableContent(view, rowContents, tableLabel)
  112. const table = within(view.getByRole('table', { name: tableLabel }))
  113. expect(
  114. table.getAllByRole('button', { name: 'Delete this access token' }),
  115. ).toHaveLength(2)
  116. })
  117. it('can delete an personal access token', async () => {
  118. mockUserCurrentAccessTokenListQuery({ userCurrentAccessTokenList })
  119. const view = await visitView('/personal-setting/token-access')
  120. const table = within(view.getByRole('table'))
  121. const deleteButton = within(table.getAllByRole('row')[0]).getByRole(
  122. 'button',
  123. {
  124. name: 'Delete this access token',
  125. },
  126. )
  127. mockUserCurrentAccessTokenDeleteMutation({
  128. userCurrentAccessTokenDelete: {
  129. success: true,
  130. },
  131. })
  132. await view.events.click(deleteButton)
  133. await waitForNextTick()
  134. expect(
  135. await view.findByRole('dialog', { name: 'Delete Object' }),
  136. ).toBeInTheDocument()
  137. await view.events.click(view.getByRole('button', { name: 'Delete Object' }))
  138. checkSimpleTableContent(view, [rowContents[1]])
  139. })
  140. it('updates the personal access token list when a new access token is added', async () => {
  141. mockUserCurrentAccessTokenListQuery({ userCurrentAccessTokenList })
  142. const view = await visitView('/personal-setting/token-access')
  143. const accessTokenUpdateSubscription =
  144. getUserCurrentAccessTokenUpdatesSubscriptionHandler()
  145. accessTokenUpdateSubscription.trigger({
  146. userCurrentAccessTokenUpdates: {
  147. tokens: [
  148. ...userCurrentAccessTokenList,
  149. {
  150. id: convertToGraphQLId('Token', 3),
  151. name: 'New Token',
  152. preferences: {
  153. permission: ['ticket.agent'],
  154. },
  155. createdAt: '2024-04-25T09:59:59Z',
  156. expiresAt: null,
  157. lastUsedAt: null,
  158. },
  159. ],
  160. },
  161. })
  162. await waitForNextTick()
  163. const newAccessTokenRowContents = [
  164. 'New Token',
  165. 'ticket.agent',
  166. ['2024-04-25 09:59', 'just now'],
  167. ]
  168. checkSimpleTableContent(view, [...rowContents, newAccessTokenRowContents])
  169. })
  170. it('create new personal access token', async () => {
  171. mockFormUpdaterQuery({
  172. formUpdater: {
  173. fields: {
  174. permissions: {
  175. options: [
  176. {
  177. value: 'report',
  178. label: 'Report (%s)',
  179. description: 'To access the report interface.',
  180. },
  181. {
  182. value: 'ticket',
  183. label: 'Ticket (%s)',
  184. description: 'To access the ticket interface.',
  185. disabled: true,
  186. children: [
  187. {
  188. value: 'ticket.agent',
  189. label: 'Agent Tickets (%s)',
  190. description:
  191. 'To access the agent tickets based on group access.',
  192. },
  193. ],
  194. },
  195. ],
  196. },
  197. },
  198. },
  199. })
  200. mockUserCurrentAccessTokenListQuery({ userCurrentAccessTokenList })
  201. const view = await visitView('/personal-setting/token-access')
  202. const newAccessTokenButton = view.getByRole('button', {
  203. name: 'New Personal Access Token',
  204. })
  205. await view.events.click(newAccessTokenButton)
  206. const flyout = await view.findByRole('complementary', {
  207. name: 'New Personal Access Token',
  208. })
  209. expect(flyout).toBeInTheDocument()
  210. const name = await view.findByLabelText('Name')
  211. await view.events.type(name, 'A new token')
  212. const input = view.getByLabelText('Expiration date')
  213. await view.events.type(input, '2024-12-31{Enter}')
  214. const permissionsField = within(view.getByLabelText('Permissions'))
  215. const permissions = permissionsField.getAllByRole('treeitem')
  216. const toggleSwitch = within(permissions[0]).getByRole('switch')
  217. await view.events.click(toggleSwitch)
  218. mockUserCurrentAccessTokenAddMutation({
  219. userCurrentAccessTokenAdd: {
  220. tokenValue: 'new-token-1234',
  221. token: {
  222. id: convertToGraphQLId('Token', 3),
  223. name: 'A new token',
  224. preferences: {
  225. permission: ['report'],
  226. },
  227. createdAt: '2024-04-25T09:59:59Z',
  228. expiresAt: '2024-12-31T00:00:00Z',
  229. lastUsedAt: null,
  230. user: {
  231. id: '123',
  232. },
  233. },
  234. errors: null,
  235. },
  236. })
  237. await view.events.click(view.getByRole('button', { name: 'Create' }))
  238. expect(
  239. await view.findByLabelText('Your Personal Access Token'),
  240. ).toHaveValue('new-token-1234')
  241. await view.events.click(
  242. view.getByRole('button', {
  243. name: 'OK, I have copied my token',
  244. }),
  245. )
  246. expect(flyout).not.toBeInTheDocument()
  247. checkSimpleTableContent(view, [
  248. [
  249. 'A new token',
  250. 'report',
  251. ['2024-04-25 09:59', 'just now'],
  252. ['2024-12-31 00:00', 'in 8 months'],
  253. '-',
  254. ],
  255. ...rowContents,
  256. ])
  257. })
  258. })