cozy-sharing 28.4.4 → 28.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/SharingProvider.js +296 -66
  3. package/dist/SharingProvider.spec.js +332 -2
  4. package/dist/components/FederatedFolder/DumbFederatedFolderModal.js +7 -4
  5. package/dist/components/FederatedFolder/FederatedFolderModal.js +79 -23
  6. package/dist/components/Recipient/LinkRecipient.js +6 -3
  7. package/dist/components/Recipient/LinkRecipient.stories.js +0 -1
  8. package/dist/components/Recipient/MemberRecipient.spec.js +13 -0
  9. package/dist/components/Recipient/MemberRecipientPermissions.js +80 -20
  10. package/dist/components/Recipient/MemberRecipientPermissions.spec.js +214 -0
  11. package/dist/components/Recipient/actions/setReadOnlySharedPermission.js +0 -1
  12. package/dist/components/Recipient/actions/setReadOnlySharedPermission.spec.js +72 -0
  13. package/dist/components/Recipient/actions/setReadWriteSharedPermission.js +0 -1
  14. package/dist/components/Recipient/actions/setReadWriteSharedPermission.spec.js +72 -0
  15. package/dist/components/ShareByLink.js +5 -1
  16. package/dist/components/ShareDialogCozyToCozy.spec.js +9 -0
  17. package/dist/components/ShareRestrictionModal/helpers.js +66 -29
  18. package/dist/components/SharedDrive/DumbSharedDriveModal.js +1 -1
  19. package/dist/components/SharedFolder/DumbBatchSharedFolderModal.js +17 -4
  20. package/dist/components/WhoHasAccess.js +0 -1
  21. package/dist/helpers/shortcodes.js +24 -0
  22. package/dist/state.js +3 -10
  23. package/locales/en.json +4 -1
  24. package/locales/fr.json +4 -1
  25. package/locales/ru.json +4 -1
  26. package/locales/vi.json +4 -1
  27. package/package.json +4 -4
  28. package/src/SharingProvider.jsx +128 -9
  29. package/src/SharingProvider.spec.jsx +224 -1
  30. package/src/components/FederatedFolder/DumbFederatedFolderModal.jsx +6 -3
  31. package/src/components/FederatedFolder/FederatedFolderModal.jsx +39 -6
  32. package/src/components/Recipient/LinkRecipient.jsx +10 -4
  33. package/src/components/Recipient/LinkRecipient.stories.jsx +0 -1
  34. package/src/components/Recipient/MemberRecipient.spec.jsx +11 -0
  35. package/src/components/Recipient/MemberRecipientPermissions.jsx +69 -16
  36. package/src/components/Recipient/MemberRecipientPermissions.spec.jsx +165 -0
  37. package/src/components/Recipient/actions/setReadOnlySharedPermission.js +1 -1
  38. package/src/components/Recipient/actions/setReadOnlySharedPermission.spec.jsx +95 -0
  39. package/src/components/Recipient/actions/setReadWriteSharedPermission.js +1 -1
  40. package/src/components/Recipient/actions/setReadWriteSharedPermission.spec.jsx +95 -0
  41. package/src/components/ShareByLink.jsx +3 -1
  42. package/src/components/ShareDialogCozyToCozy.spec.jsx +6 -0
  43. package/src/components/ShareRestrictionModal/helpers.js +42 -4
  44. package/src/components/SharedDrive/DumbSharedDriveModal.jsx +1 -1
  45. package/src/components/SharedFolder/DumbBatchSharedFolderModal.jsx +32 -16
  46. package/src/components/WhoHasAccess.jsx +0 -1
  47. package/src/helpers/shortcodes.js +28 -0
  48. package/src/state.js +3 -14
@@ -5,7 +5,7 @@ import { createMockClient } from 'cozy-client'
5
5
 
6
6
  import { SharingProvider } from './SharingProvider'
7
7
  import SharingContext from './context'
8
- // import { receiveSharings } from './state'
8
+ import reducer, { addSharingLink, getDocumentPermissions } from './state'
9
9
  import AppLike from '../test/AppLike'
10
10
 
11
11
  const AppWrapper = ({ children, client, isPublic }) => {
@@ -105,6 +105,229 @@ describe('SharingProvider', () => {
105
105
  })
106
106
  })
107
107
 
108
+ describe('shareByLink', () => {
109
+ const PERM_DRIVE_FILE = {
110
+ type: 'io.cozy.permissions',
111
+ id: 'perm_drive_file',
112
+ attributes: {
113
+ type: 'share',
114
+ permissions: {
115
+ rule0: {
116
+ type: 'io.cozy.files',
117
+ verbs: ['GET'],
118
+ values: ['file_in_drive']
119
+ }
120
+ },
121
+ shortcodes: { code: 'shortcode123' }
122
+ }
123
+ }
124
+
125
+ const driveFile = {
126
+ _id: 'file_in_drive',
127
+ id: 'file_in_drive',
128
+ driveId: 'drive_123'
129
+ }
130
+
131
+ it('dispatches addSharingLink exactly once for a shared drive file', async () => {
132
+ const mockCreateSharingLink = jest
133
+ .fn()
134
+ .mockResolvedValue({ data: PERM_DRIVE_FILE })
135
+ const mockClient = createMockClient({})
136
+ mockClient.collection = jest.fn().mockReturnValue({
137
+ createSharingLink: mockCreateSharingLink
138
+ })
139
+
140
+ const provider = new SharingProvider({ client: mockClient })
141
+ provider.state = reducer()
142
+ provider.dispatch = jest.fn(action => {
143
+ provider.state = reducer(provider.state, action)
144
+ })
145
+
146
+ await provider.shareByLink(driveFile, { verbs: ['GET'] })
147
+
148
+ expect(provider.dispatch).toHaveBeenCalledTimes(1)
149
+ const permissions = getDocumentPermissions(provider.state, driveFile._id)
150
+ expect(permissions).toHaveLength(1)
151
+ expect(permissions[0].id).toBe(PERM_DRIVE_FILE.id)
152
+ })
153
+
154
+ it('dispatches addSharingLink once for a regular file (non shared drive)', async () => {
155
+ const PERM_REGULAR_FILE = {
156
+ type: 'io.cozy.permissions',
157
+ id: 'perm_regular_file',
158
+ attributes: {
159
+ type: 'share',
160
+ permissions: {
161
+ rule0: {
162
+ type: 'io.cozy.files',
163
+ verbs: ['GET'],
164
+ values: ['regular_file_id']
165
+ }
166
+ },
167
+ shortcodes: { code: 'shortcode456' }
168
+ }
169
+ }
170
+ const regularFile = { _id: 'regular_file_id', id: 'regular_file_id' }
171
+ const mockCreateSharingLink = jest
172
+ .fn()
173
+ .mockResolvedValue({ data: PERM_REGULAR_FILE })
174
+ const mockClient = createMockClient({})
175
+ mockClient.collection = jest.fn().mockReturnValue({
176
+ createSharingLink: mockCreateSharingLink
177
+ })
178
+
179
+ const provider = new SharingProvider({ client: mockClient })
180
+ provider.state = reducer()
181
+ provider.permissionCol = { createSharingLink: mockCreateSharingLink }
182
+ provider.dispatch = jest.fn(action => {
183
+ provider.state = reducer(provider.state, action)
184
+ })
185
+
186
+ await provider.shareByLink(regularFile, { verbs: ['GET'] })
187
+
188
+ expect(provider.dispatch).toHaveBeenCalledTimes(1)
189
+ const permissions = getDocumentPermissions(provider.state, regularFile._id)
190
+ expect(permissions).toHaveLength(1)
191
+ expect(permissions[0].id).toBe(PERM_REGULAR_FILE.id)
192
+ })
193
+ })
194
+
195
+ describe('updateDocumentPermissions', () => {
196
+ const PERM_DRIVE_FILE = {
197
+ type: 'io.cozy.permissions',
198
+ id: 'perm_drive_file',
199
+ attributes: {
200
+ type: 'share',
201
+ permissions: {
202
+ rule0: {
203
+ type: 'io.cozy.files',
204
+ verbs: ['GET'],
205
+ values: ['file_in_drive']
206
+ }
207
+ },
208
+ shortcodes: { code: 'shortcode123' }
209
+ }
210
+ }
211
+
212
+ it('sends only one PATCH request for a shared drive file', async () => {
213
+ const driveFile = {
214
+ _id: 'file_in_drive',
215
+ id: 'file_in_drive',
216
+ driveId: 'drive_123'
217
+ }
218
+ const mockAdd = jest.fn().mockResolvedValue({ data: PERM_DRIVE_FILE })
219
+ const mockClient = createMockClient({})
220
+ mockClient.collection = jest.fn().mockReturnValue({ add: mockAdd })
221
+
222
+ const provider = new SharingProvider({ client: mockClient })
223
+ // Seed the state with exactly one permission (as it would be after a correct shareByLink)
224
+ provider.state = reducer(undefined, addSharingLink(PERM_DRIVE_FILE))
225
+ provider.dispatch = jest.fn()
226
+
227
+ await provider.updateDocumentPermissions(driveFile, {
228
+ verbs: ['GET'],
229
+ expiresAt: '',
230
+ password: ''
231
+ })
232
+
233
+ expect(mockAdd).toHaveBeenCalledTimes(1)
234
+ })
235
+ })
236
+
237
+ describe('updateSharingMemberType', () => {
238
+ const mockContact = { _id: 'contact-1', email: [{ address: 'bob@bob.cozy' }] }
239
+ const mockSharing = {
240
+ id: 'sharing-123',
241
+ type: 'io.cozy.sharings',
242
+ attributes: {
243
+ members: [
244
+ { status: 'owner', email: 'owner@cozy.local' },
245
+ { status: 'ready', email: 'bob@bob.cozy' }
246
+ ]
247
+ }
248
+ }
249
+
250
+ let instance
251
+ let mockContactsCollection
252
+
253
+ beforeEach(() => {
254
+ mockContactsCollection = {
255
+ find: jest.fn().mockResolvedValue({ data: [mockContact] })
256
+ }
257
+
258
+ const mockClient = {
259
+ getStackClient: () => ({ uri: 'http://cozy.local' }),
260
+ collection: jest.fn().mockReturnValue(mockContactsCollection)
261
+ }
262
+
263
+ instance = new SharingProvider({ client: mockClient })
264
+ instance.state = {
265
+ ...instance.state,
266
+ sharings: [mockSharing]
267
+ }
268
+ instance.sharingCol = {
269
+ revokeRecipient: jest.fn().mockResolvedValue({})
270
+ }
271
+ jest.spyOn(instance, 'addRecipients').mockResolvedValue({})
272
+ })
273
+
274
+ it('should throw when sharing is not found', async () => {
275
+ await expect(
276
+ instance.updateSharingMemberType('unknown-id', 1, 'one-way')
277
+ ).rejects.toThrow('Sharing not found')
278
+ })
279
+
280
+ it('should throw when member is not found', async () => {
281
+ await expect(
282
+ instance.updateSharingMemberType('sharing-123', 99, 'one-way')
283
+ ).rejects.toThrow('Member not found')
284
+ })
285
+
286
+ it('should throw when contact is not found', async () => {
287
+ mockContactsCollection.find.mockResolvedValue({ data: [] })
288
+
289
+ await expect(
290
+ instance.updateSharingMemberType('sharing-123', 1, 'one-way')
291
+ ).rejects.toThrow('Contact not found')
292
+ })
293
+
294
+ it('should revoke and re-add with readOnly when switching to one-way', async () => {
295
+ await instance.updateSharingMemberType('sharing-123', 1, 'one-way')
296
+
297
+ expect(instance.sharingCol.revokeRecipient).toHaveBeenCalledWith(
298
+ mockSharing,
299
+ 1
300
+ )
301
+ expect(instance.addRecipients).toHaveBeenCalledWith({
302
+ document: mockSharing,
303
+ recipients: [],
304
+ readOnlyRecipients: [mockContact]
305
+ })
306
+ })
307
+
308
+ it('should revoke and re-add as read-write when switching to two-way', async () => {
309
+ await instance.updateSharingMemberType('sharing-123', 1, 'two-way')
310
+
311
+ expect(instance.sharingCol.revokeRecipient).toHaveBeenCalledWith(
312
+ mockSharing,
313
+ 1
314
+ )
315
+ expect(instance.addRecipients).toHaveBeenCalledWith({
316
+ document: mockSharing,
317
+ recipients: [mockContact],
318
+ readOnlyRecipients: []
319
+ })
320
+ })
321
+
322
+ it('should rethrow error if addRecipients fails after revocation', async () => {
323
+ instance.addRecipients.mockRejectedValue(new Error('Network error'))
324
+
325
+ await expect(
326
+ instance.updateSharingMemberType('sharing-123', 1, 'one-way')
327
+ ).rejects.toThrow('Network error')
328
+ })
329
+ })
330
+
108
331
  // TODO Convert with react-testing-library
109
332
  // describe('hasWriteAccess', () => {
110
333
  // it('tells if a doc is writtable', () => {
@@ -3,7 +3,7 @@ import React from 'react'
3
3
  import { useI18n } from 'twake-i18n'
4
4
 
5
5
  import withLocales from '../../hoc/withLocales'
6
- import DumbBatchSharedFolderModal from '../SharedFolder/DumbBatchSharedFolderModal'
6
+ import { DumbBatchSharedFolderModal } from '../SharedFolder/DumbBatchSharedFolderModal'
7
7
 
8
8
  export const DumbFederatedFolderModal = withLocales(
9
9
  ({
@@ -18,7 +18,8 @@ export const DumbFederatedFolderModal = withLocales(
18
18
  onSend,
19
19
  onClose,
20
20
  onShare,
21
- sharingLink
21
+ sharingLink,
22
+ showShareByEmail
22
23
  }) => {
23
24
  const { t } = useI18n()
24
25
  return (
@@ -39,6 +40,7 @@ export const DumbFederatedFolderModal = withLocales(
39
40
  addPeopleLabel={t('FederatedFolder.addPeople')}
40
41
  addButtonLabel={t('FederatedFolder.add')}
41
42
  shareLabel={t('FederatedFolder.share')}
43
+ showShareByEmail={showShareByEmail}
42
44
  />
43
45
  )
44
46
  }
@@ -56,7 +58,8 @@ DumbFederatedFolderModal.propTypes = {
56
58
  onSend: PropTypes.func.isRequired,
57
59
  onClose: PropTypes.func.isRequired,
58
60
  onShare: PropTypes.func.isRequired,
59
- sharingLink: PropTypes.string
61
+ sharingLink: PropTypes.string,
62
+ showShareByEmail: PropTypes.bool
60
63
  }
61
64
 
62
65
  export default DumbFederatedFolderModal
@@ -1,5 +1,5 @@
1
1
  import PropTypes from 'prop-types'
2
- import React, { useState } from 'react'
2
+ import React, { useState, useEffect } from 'react'
3
3
  import { useI18n } from 'twake-i18n'
4
4
 
5
5
  import { useClient } from 'cozy-client'
@@ -19,13 +19,43 @@ export const FederatedFolderModal = withLocales(
19
19
  ({ onClose, document: existingDocument }) => {
20
20
  const client = useClient()
21
21
  const { t } = useI18n()
22
- const { share, getSharingLink } = useSharingContext()
22
+ const {
23
+ share,
24
+ getSharingLink,
25
+ getFederatedShareLink,
26
+ getDocumentPermissions,
27
+ getOwner,
28
+ getSharingById
29
+ } = useSharingContext()
23
30
  const { showAlert } = useAlert()
24
31
 
25
- // Get sharing link
26
- const sharingLink = existingDocument
27
- ? getSharingLink(existingDocument._id)
28
- : null
32
+ const [sharingLink, setSharingLink] = useState(null)
33
+
34
+ const documentPermissions = existingDocument
35
+ ? getDocumentPermissions(existingDocument._id)
36
+ : []
37
+
38
+ useEffect(() => {
39
+ const fetchSharingLink = async () => {
40
+ if (!existingDocument) return
41
+
42
+ if (existingDocument.driveId) {
43
+ const link = await getFederatedShareLink(existingDocument)
44
+ setSharingLink(link)
45
+ } else {
46
+ setSharingLink(getSharingLink(existingDocument._id))
47
+ }
48
+ }
49
+
50
+ fetchSharingLink()
51
+ }, [
52
+ existingDocument,
53
+ documentPermissions,
54
+ getFederatedShareLink,
55
+ getSharingLink,
56
+ getOwner,
57
+ getSharingById
58
+ ]) // eslint-disable-line react-hooks/exhaustive-deps
29
59
 
30
60
  const [federatedRecipients, setFederatedRecipients] = useState({
31
61
  recipients: [],
@@ -92,6 +122,8 @@ export const FederatedFolderModal = withLocales(
92
122
 
93
123
  const modalTitle = t('FederatedFolder.shareTitle', { name: folderName })
94
124
 
125
+ const isSharedDrive = Boolean(existingDocument?.driveId)
126
+
95
127
  return (
96
128
  <DumbFederatedFolderModal
97
129
  title={modalTitle}
@@ -106,6 +138,7 @@ export const FederatedFolderModal = withLocales(
106
138
  onClose={onClose}
107
139
  onShare={onShare}
108
140
  sharingLink={sharingLink}
141
+ showShareByEmail={!isSharedDrive}
109
142
  />
110
143
  )
111
144
  }
@@ -26,8 +26,7 @@ const LinkRecipient = props => {
26
26
  const { isMobile } = useBreakpoints()
27
27
  const { getDocumentPermissions } = useSharingContext()
28
28
 
29
- const { recipientConfirmationData, verifyRecipient, link, fadeIn, document } =
30
- props
29
+ const { recipientConfirmationData, verifyRecipient, fadeIn, document } = props
31
30
 
32
31
  const permissions = getDocumentPermissions(document?._id)
33
32
  const hasPassword = checkIsPermissionHasPassword(permissions)
@@ -44,7 +43,7 @@ const LinkRecipient = props => {
44
43
  ? t('Share.recipients.expires', {
45
44
  date: dateFormatted
46
45
  })
47
- : link
46
+ : ''
48
47
 
49
48
  const RightPart = recipientConfirmationData ? (
50
49
  <RecipientConfirm
@@ -74,7 +73,14 @@ const LinkRecipient = props => {
74
73
  }
75
74
 
76
75
  LinkRecipient.propTypes = {
77
- link: PropTypes.string.isRequired
76
+ recipientConfirmationData: PropTypes.oneOfType([
77
+ PropTypes.bool,
78
+ PropTypes.object
79
+ ]),
80
+ verifyRecipient: PropTypes.func,
81
+ fadeIn: PropTypes.bool,
82
+ document: PropTypes.object,
83
+ permissions: PropTypes.arrayOf(PropTypes.object)
78
84
  }
79
85
 
80
86
  export default LinkRecipient
@@ -5,7 +5,6 @@ const meta = {
5
5
  args: {
6
6
  recipientConfirmationData: false,
7
7
  verifyRecipient: () => true,
8
- link: 'test',
9
8
  fadeIn: false
10
9
  }
11
10
  }
@@ -6,6 +6,17 @@ import { createMockClient } from 'cozy-client'
6
6
  import MemberRecipient from './MemberRecipient'
7
7
  import AppLike from '../../../test/AppLike'
8
8
 
9
+ const mockUpdateDocumentPermissions = jest.fn()
10
+ const mockUpdateSharingMemberType = jest.fn()
11
+
12
+ jest.mock('../../hooks/useSharingContext', () => ({
13
+ // eslint-disable-next-line react/display-name
14
+ useSharingContext: () => ({
15
+ updateDocumentPermissions: mockUpdateDocumentPermissions,
16
+ updateSharingMemberType: mockUpdateSharingMemberType
17
+ })
18
+ }))
19
+
9
20
  describe('MemberRecipient component', () => {
10
21
  const client = createMockClient({})
11
22
  client.options = {
@@ -2,6 +2,7 @@ import React, { useState, useRef, useCallback } from 'react'
2
2
  import { useI18n } from 'twake-i18n'
3
3
 
4
4
  import { useClient } from 'cozy-client'
5
+ import minilog from 'cozy-minilog'
5
6
  import ActionsMenu from 'cozy-ui/transpiled/react/ActionsMenu'
6
7
  import {
7
8
  makeActions,
@@ -10,9 +11,14 @@ import {
10
11
  import DropdownButton from 'cozy-ui/transpiled/react/DropdownButton'
11
12
  import Spinner from 'cozy-ui/transpiled/react/Spinner'
12
13
  import Typography from 'cozy-ui/transpiled/react/Typography'
14
+ import { useAlert } from 'cozy-ui/transpiled/react/providers/Alert'
13
15
 
14
- import { permission } from './actions/permission'
15
16
  import { revokeMember } from './actions/revokeMember'
17
+ import { setReadOnlySharedPermission } from './actions/setReadOnlySharedPermission'
18
+ import { setReadWriteSharedPermission } from './actions/setReadWriteSharedPermission'
19
+ import { useSharingContext } from '../../hooks/useSharingContext'
20
+
21
+ const log = minilog('MemberRecipientPermissions')
16
22
 
17
23
  const MemberRecipientPermissions = ({
18
24
  isOwner,
@@ -30,6 +36,8 @@ const MemberRecipientPermissions = ({
30
36
  const { t } = useI18n()
31
37
  const buttonRef = useRef()
32
38
  const client = useClient()
39
+ const { updateSharingMemberType } = useSharingContext()
40
+ const { showAlert } = useAlert()
33
41
 
34
42
  const [revoking, setRevoking] = useState(false)
35
43
  const [isMenuDisplayed, setMenuDisplayed] = useState(false)
@@ -42,27 +50,72 @@ const MemberRecipientPermissions = ({
42
50
  !contactIsOwner &&
43
51
  ((instanceMatchesClient && !isOwner) || isOwner)
44
52
 
45
- const toggleMenu = () => setMenuDisplayed(!isMenuDisplayed)
46
- const hideMenu = () => setMenuDisplayed(false)
53
+ const toggleMenu = useCallback(() => {
54
+ setMenuDisplayed(displayed => !displayed)
55
+ }, [])
56
+ const hideMenu = useCallback(() => {
57
+ setMenuDisplayed(false)
58
+ }, [])
47
59
 
48
60
  const handleRevocation = useCallback(async () => {
49
61
  setRevoking(true)
50
- if (isOwner) {
51
- await onRevoke(document, sharingId, memberIndex)
52
- } else {
53
- await onRevokeSelf(document)
62
+ try {
63
+ if (isOwner) {
64
+ await onRevoke(document, sharingId, memberIndex)
65
+ } else {
66
+ await onRevokeSelf(document)
67
+ }
68
+ } finally {
69
+ setRevoking(false)
54
70
  }
55
- setRevoking(false)
56
71
  }, [isOwner, onRevoke, onRevokeSelf, document, sharingId, memberIndex])
57
72
 
58
- const actions = makeActions([permission, divider, revokeMember], {
59
- client,
60
- t,
61
- type: type ?? 'one-way',
62
- isOwner,
63
- isSharedDrive,
64
- handleRevocation
65
- })
73
+ const setType = useCallback(
74
+ async newType => {
75
+ if (newType === type) {
76
+ hideMenu()
77
+ return
78
+ }
79
+ try {
80
+ await updateSharingMemberType(sharingId, memberIndex, newType)
81
+ } catch (error) {
82
+ log.error('Failed to change member permission type', error)
83
+ showAlert({
84
+ message: t('Share.members.error.changePermission'),
85
+ severity: 'error',
86
+ variant: 'filled'
87
+ })
88
+ }
89
+ hideMenu()
90
+ },
91
+ [
92
+ hideMenu,
93
+ memberIndex,
94
+ sharingId,
95
+ showAlert,
96
+ t,
97
+ type,
98
+ updateSharingMemberType
99
+ ]
100
+ )
101
+
102
+ const actions = makeActions(
103
+ [
104
+ setReadOnlySharedPermission,
105
+ setReadWriteSharedPermission,
106
+ divider,
107
+ revokeMember
108
+ ],
109
+ {
110
+ client,
111
+ t,
112
+ type: type ?? 'one-way',
113
+ isOwner,
114
+ isSharedDrive,
115
+ setType,
116
+ handleRevocation
117
+ }
118
+ )
66
119
 
67
120
  return (
68
121
  <div className={className}>
@@ -0,0 +1,165 @@
1
+ import { render, fireEvent, screen, act, waitFor } from '@testing-library/react'
2
+ import React from 'react'
3
+
4
+ import { createMockClient } from 'cozy-client'
5
+
6
+ import MemberRecipientPermissions from './MemberRecipientPermissions'
7
+ import AppLike from '../../../test/AppLike'
8
+
9
+ const mockUpdateSharingMemberType = jest.fn()
10
+ const mockShowAlert = jest.fn()
11
+
12
+ jest.mock('../../hooks/useSharingContext', () => ({
13
+ useSharingContext: () => ({
14
+ updateSharingMemberType: mockUpdateSharingMemberType
15
+ })
16
+ }))
17
+
18
+ jest.mock('cozy-ui/transpiled/react/providers/Alert', () => ({
19
+ __esModule: true,
20
+ default: ({ children }) => children,
21
+ useAlert: () => ({
22
+ showAlert: mockShowAlert
23
+ })
24
+ }))
25
+
26
+ describe('MemberRecipientPermissions component', () => {
27
+ const client = createMockClient({})
28
+ client.options = {
29
+ uri: 'http://cozy.local:8080'
30
+ }
31
+
32
+ const defaultProps = {
33
+ isOwner: true,
34
+ isSharedDrive: false,
35
+ status: 'ready',
36
+ instance: 'http://cozy.local:8080',
37
+ type: 'two-way',
38
+ document: { id: 'doc-123', type: 'io.cozy.files', path: '/test' },
39
+ sharingId: 'sharing-123',
40
+ memberIndex: 1,
41
+ onRevoke: jest.fn(),
42
+ onRevokeSelf: jest.fn()
43
+ }
44
+
45
+ beforeEach(() => {
46
+ jest.clearAllMocks()
47
+ })
48
+
49
+ const setup = props => {
50
+ return render(
51
+ <AppLike client={client}>
52
+ <MemberRecipientPermissions {...defaultProps} {...props} />
53
+ </AppLike>
54
+ )
55
+ }
56
+
57
+ let createRangeBackup
58
+
59
+ beforeAll(() => {
60
+ createRangeBackup = global.document.createRange
61
+ global.document.createRange = jest.fn(() => ({
62
+ setStart: () => {},
63
+ setEnd: () => {},
64
+ commonAncestorContainer: {
65
+ nodeName: 'BODY',
66
+ ownerDocument: document
67
+ }
68
+ }))
69
+ })
70
+
71
+ afterAll(() => {
72
+ global.document.createRange = createRangeBackup
73
+ })
74
+
75
+ it('should render the current permission type', () => {
76
+ setup()
77
+ expect(screen.getByText('can change')).toBeInTheDocument()
78
+ })
79
+
80
+ it('should call updateSharingMemberType when changing to one-way', async () => {
81
+ mockUpdateSharingMemberType.mockResolvedValue(undefined)
82
+
83
+ const { getByText } = setup({ type: 'two-way' })
84
+
85
+ fireEvent.click(getByText('can change'))
86
+ fireEvent.click(getByText('Can view'))
87
+
88
+ expect(mockUpdateSharingMemberType).toHaveBeenCalledWith(
89
+ 'sharing-123',
90
+ 1,
91
+ 'one-way'
92
+ )
93
+ })
94
+
95
+ it('should call updateSharingMemberType when changing to two-way', async () => {
96
+ mockUpdateSharingMemberType.mockResolvedValue(undefined)
97
+
98
+ const { getByText } = setup({ type: 'one-way' })
99
+
100
+ fireEvent.click(getByText('can view'))
101
+ fireEvent.click(getByText('Can change'))
102
+
103
+ expect(mockUpdateSharingMemberType).toHaveBeenCalledWith(
104
+ 'sharing-123',
105
+ 1,
106
+ 'two-way'
107
+ )
108
+ })
109
+
110
+ it('should show error alert when updateSharingMemberType fails', async () => {
111
+ mockUpdateSharingMemberType.mockRejectedValue(new Error('Network error'))
112
+
113
+ const { getByText } = setup({ type: 'two-way' })
114
+
115
+ await act(async () => {
116
+ fireEvent.click(getByText('can change'))
117
+ fireEvent.click(getByText('Can view'))
118
+ })
119
+
120
+ await waitFor(() => {
121
+ expect(mockShowAlert).toHaveBeenCalledWith({
122
+ message:
123
+ 'An error occurred while changing the permissions. Please try again.',
124
+ severity: 'error',
125
+ variant: 'filled'
126
+ })
127
+ })
128
+ })
129
+
130
+ it('should not call updateSharingMemberType when selecting same type', async () => {
131
+ mockUpdateSharingMemberType.mockResolvedValue(undefined)
132
+
133
+ const { getByText } = setup({ type: 'two-way' })
134
+
135
+ fireEvent.click(getByText('can change'))
136
+ fireEvent.click(getByText('Can change'))
137
+
138
+ expect(mockUpdateSharingMemberType).not.toHaveBeenCalled()
139
+ })
140
+
141
+ it('should not render menu when user is not owner and not the shared drive contact', () => {
142
+ const { queryByText } = setup({
143
+ isOwner: false,
144
+ instance: 'http://other-cozy.local:8080'
145
+ })
146
+
147
+ expect(queryByText('can change')).not.toBeInTheDocument()
148
+ expect(queryByText('can view')).not.toBeInTheDocument()
149
+ })
150
+
151
+ it('should render menu for owner', () => {
152
+ const { getByText } = setup({ isOwner: true })
153
+
154
+ expect(getByText('can change')).toBeInTheDocument()
155
+ })
156
+
157
+ it('should render menu for the user themselves', () => {
158
+ const { getByText } = setup({
159
+ isOwner: false,
160
+ instance: 'http://cozy.local:8080'
161
+ })
162
+
163
+ expect(getByText('can change')).toBeInTheDocument()
164
+ })
165
+ })