@protontech/drive-sdk 0.3.1 → 0.4.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 (181) hide show
  1. package/dist/crypto/driveCrypto.d.ts +1 -1
  2. package/dist/crypto/driveCrypto.js.map +1 -1
  3. package/dist/crypto/interface.d.ts +1 -1
  4. package/dist/crypto/openPGPCrypto.d.ts +1 -1
  5. package/dist/crypto/openPGPCrypto.js +4 -1
  6. package/dist/crypto/openPGPCrypto.js.map +1 -1
  7. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  8. package/dist/internal/apiService/errors.d.ts +3 -0
  9. package/dist/internal/apiService/errors.js +7 -1
  10. package/dist/internal/apiService/errors.js.map +1 -1
  11. package/dist/internal/devices/interface.d.ts +1 -1
  12. package/dist/internal/devices/manager.js +1 -1
  13. package/dist/internal/devices/manager.js.map +1 -1
  14. package/dist/internal/devices/manager.test.js +3 -3
  15. package/dist/internal/devices/manager.test.js.map +1 -1
  16. package/dist/internal/download/cryptoService.js +2 -2
  17. package/dist/internal/download/cryptoService.js.map +1 -1
  18. package/dist/internal/download/fileDownloader.js +2 -2
  19. package/dist/internal/download/fileDownloader.js.map +1 -1
  20. package/dist/internal/download/fileDownloader.test.js +3 -1
  21. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  22. package/dist/internal/events/apiService.js +1 -1
  23. package/dist/internal/events/apiService.js.map +1 -1
  24. package/dist/internal/events/coreEventManager.js +1 -1
  25. package/dist/internal/events/coreEventManager.js.map +1 -1
  26. package/dist/internal/events/coreEventManager.test.js +18 -24
  27. package/dist/internal/events/coreEventManager.test.js.map +1 -1
  28. package/dist/internal/events/index.d.ts +3 -4
  29. package/dist/internal/events/index.js +4 -4
  30. package/dist/internal/events/index.js.map +1 -1
  31. package/dist/internal/events/interface.d.ts +3 -0
  32. package/dist/internal/nodes/apiService.d.ts +12 -3
  33. package/dist/internal/nodes/apiService.js +53 -13
  34. package/dist/internal/nodes/apiService.js.map +1 -1
  35. package/dist/internal/nodes/apiService.test.js +19 -2
  36. package/dist/internal/nodes/apiService.test.js.map +1 -1
  37. package/dist/internal/nodes/cache.js +3 -1
  38. package/dist/internal/nodes/cache.js.map +1 -1
  39. package/dist/internal/nodes/cryptoReporter.d.ts +20 -0
  40. package/dist/internal/nodes/cryptoReporter.js +96 -0
  41. package/dist/internal/nodes/cryptoReporter.js.map +1 -0
  42. package/dist/internal/nodes/cryptoService.d.ts +18 -13
  43. package/dist/internal/nodes/cryptoService.js +18 -98
  44. package/dist/internal/nodes/cryptoService.js.map +1 -1
  45. package/dist/internal/nodes/cryptoService.test.js +7 -5
  46. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  47. package/dist/internal/nodes/errors.d.ts +4 -0
  48. package/dist/internal/nodes/errors.js +9 -0
  49. package/dist/internal/nodes/errors.js.map +1 -0
  50. package/dist/internal/nodes/index.js +3 -1
  51. package/dist/internal/nodes/index.js.map +1 -1
  52. package/dist/internal/nodes/index.test.js +1 -1
  53. package/dist/internal/nodes/index.test.js.map +1 -1
  54. package/dist/internal/nodes/interface.d.ts +5 -2
  55. package/dist/internal/nodes/nodesAccess.d.ts +4 -4
  56. package/dist/internal/nodes/nodesAccess.js +77 -69
  57. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  58. package/dist/internal/nodes/nodesAccess.test.js +48 -8
  59. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  60. package/dist/internal/nodes/nodesManagement.d.ts +2 -0
  61. package/dist/internal/nodes/nodesManagement.js +86 -9
  62. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  63. package/dist/internal/nodes/nodesManagement.test.js +81 -5
  64. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  65. package/dist/internal/photos/albums.d.ts +9 -7
  66. package/dist/internal/photos/albums.js +26 -13
  67. package/dist/internal/photos/albums.js.map +1 -1
  68. package/dist/internal/photos/apiService.d.ts +34 -3
  69. package/dist/internal/photos/apiService.js +96 -3
  70. package/dist/internal/photos/apiService.js.map +1 -1
  71. package/dist/internal/photos/index.d.ts +20 -4
  72. package/dist/internal/photos/index.js +30 -7
  73. package/dist/internal/photos/index.js.map +1 -1
  74. package/dist/internal/photos/interface.d.ts +25 -1
  75. package/dist/internal/photos/shares.d.ts +43 -0
  76. package/dist/internal/photos/shares.js +112 -0
  77. package/dist/internal/photos/shares.js.map +1 -0
  78. package/dist/internal/photos/timeline.d.ts +15 -0
  79. package/dist/internal/photos/timeline.js +22 -0
  80. package/dist/internal/photos/timeline.js.map +1 -0
  81. package/dist/internal/shares/manager.d.ts +1 -1
  82. package/dist/internal/shares/manager.js +4 -4
  83. package/dist/internal/shares/manager.js.map +1 -1
  84. package/dist/internal/shares/manager.test.js +7 -7
  85. package/dist/internal/shares/manager.test.js.map +1 -1
  86. package/dist/internal/sharing/cache.d.ts +3 -0
  87. package/dist/internal/sharing/cache.js +17 -2
  88. package/dist/internal/sharing/cache.js.map +1 -1
  89. package/dist/internal/sharing/interface.d.ts +2 -2
  90. package/dist/internal/sharing/interface.js +1 -1
  91. package/dist/internal/sharing/sharingAccess.js +7 -1
  92. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  93. package/dist/internal/sharing/sharingAccess.test.js +243 -34
  94. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  95. package/dist/internal/sharingPublic/apiService.d.ts +1 -1
  96. package/dist/internal/sharingPublic/apiService.js +9 -2
  97. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  98. package/dist/internal/sharingPublic/cryptoService.d.ts +6 -20
  99. package/dist/internal/sharingPublic/cryptoService.js +40 -103
  100. package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
  101. package/dist/internal/sharingPublic/index.d.ts +2 -2
  102. package/dist/internal/sharingPublic/index.js +2 -2
  103. package/dist/internal/sharingPublic/index.js.map +1 -1
  104. package/dist/internal/sharingPublic/interface.d.ts +1 -43
  105. package/dist/internal/sharingPublic/manager.d.ts +1 -1
  106. package/dist/internal/sharingPublic/manager.js +9 -7
  107. package/dist/internal/sharingPublic/manager.js.map +1 -1
  108. package/dist/internal/upload/streamUploader.js +1 -1
  109. package/dist/internal/upload/streamUploader.js.map +1 -1
  110. package/dist/internal/upload/streamUploader.test.js +3 -1
  111. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  112. package/dist/protonDriveClient.d.ts +20 -3
  113. package/dist/protonDriveClient.js +24 -4
  114. package/dist/protonDriveClient.js.map +1 -1
  115. package/dist/protonDrivePhotosClient.d.ts +86 -12
  116. package/dist/protonDrivePhotosClient.js +132 -29
  117. package/dist/protonDrivePhotosClient.js.map +1 -1
  118. package/dist/protonDrivePublicLinkClient.d.ts +13 -4
  119. package/dist/protonDrivePublicLinkClient.js +13 -11
  120. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  121. package/package.json +1 -1
  122. package/src/crypto/driveCrypto.ts +1 -1
  123. package/src/crypto/interface.ts +1 -1
  124. package/src/crypto/openPGPCrypto.ts +5 -2
  125. package/src/internal/apiService/errorCodes.ts +1 -0
  126. package/src/internal/apiService/errors.ts +6 -0
  127. package/src/internal/devices/interface.ts +1 -1
  128. package/src/internal/devices/manager.test.ts +3 -3
  129. package/src/internal/devices/manager.ts +1 -1
  130. package/src/internal/download/cryptoService.ts +2 -2
  131. package/src/internal/download/fileDownloader.test.ts +3 -1
  132. package/src/internal/download/fileDownloader.ts +2 -2
  133. package/src/internal/events/apiService.ts +1 -1
  134. package/src/internal/events/coreEventManager.test.ts +21 -27
  135. package/src/internal/events/coreEventManager.ts +1 -1
  136. package/src/internal/events/index.ts +3 -4
  137. package/src/internal/events/interface.ts +4 -0
  138. package/src/internal/nodes/apiService.test.ts +35 -1
  139. package/src/internal/nodes/apiService.ts +103 -17
  140. package/src/internal/nodes/cache.ts +3 -1
  141. package/src/internal/nodes/cryptoReporter.ts +145 -0
  142. package/src/internal/nodes/cryptoService.test.ts +11 -9
  143. package/src/internal/nodes/cryptoService.ts +45 -138
  144. package/src/internal/nodes/errors.ts +5 -0
  145. package/src/internal/nodes/index.test.ts +1 -1
  146. package/src/internal/nodes/index.ts +3 -1
  147. package/src/internal/nodes/interface.ts +6 -2
  148. package/src/internal/nodes/nodesAccess.test.ts +68 -8
  149. package/src/internal/nodes/nodesAccess.ts +101 -76
  150. package/src/internal/nodes/nodesManagement.test.ts +100 -5
  151. package/src/internal/nodes/nodesManagement.ts +100 -13
  152. package/src/internal/photos/albums.ts +31 -12
  153. package/src/internal/photos/apiService.ts +159 -4
  154. package/src/internal/photos/index.ts +54 -9
  155. package/src/internal/photos/interface.ts +23 -1
  156. package/src/internal/photos/shares.ts +134 -0
  157. package/src/internal/photos/timeline.ts +24 -0
  158. package/src/internal/shares/manager.test.ts +7 -7
  159. package/src/internal/shares/manager.ts +4 -4
  160. package/src/internal/sharing/cache.ts +19 -2
  161. package/src/internal/sharing/interface.ts +2 -2
  162. package/src/internal/sharing/sharingAccess.test.ts +283 -35
  163. package/src/internal/sharing/sharingAccess.ts +7 -1
  164. package/src/internal/sharingPublic/apiService.ts +11 -2
  165. package/src/internal/sharingPublic/cryptoService.ts +71 -135
  166. package/src/internal/sharingPublic/index.ts +3 -2
  167. package/src/internal/sharingPublic/interface.ts +8 -53
  168. package/src/internal/sharingPublic/manager.ts +9 -8
  169. package/src/internal/upload/streamUploader.test.ts +3 -1
  170. package/src/internal/upload/streamUploader.ts +1 -1
  171. package/src/protonDriveClient.ts +34 -4
  172. package/src/protonDrivePhotosClient.ts +211 -32
  173. package/src/protonDrivePublicLinkClient.ts +26 -12
  174. package/dist/internal/photos/cache.d.ts +0 -6
  175. package/dist/internal/photos/cache.js +0 -15
  176. package/dist/internal/photos/cache.js.map +0 -1
  177. package/dist/internal/photos/photosTimeline.d.ts +0 -10
  178. package/dist/internal/photos/photosTimeline.js +0 -19
  179. package/dist/internal/photos/photosTimeline.js.map +0 -1
  180. package/src/internal/photos/cache.ts +0 -11
  181. package/src/internal/photos/photosTimeline.ts +0 -17
@@ -1,4 +1,6 @@
1
- import { NodeType, resultError, resultOk } from '../../interface';
1
+ import { getMockLogger } from '../../tests/logger';
2
+ import { NodeType, resultError, resultOk, MemberRole } from '../../interface';
3
+ import { ValidationError } from '../../errors';
2
4
  import { SharingAPIService } from './apiService';
3
5
  import { SharingCache } from './cache';
4
6
  import { SharingCryptoService } from './cryptoService';
@@ -14,8 +16,12 @@ describe('SharingAccess', () => {
14
16
 
15
17
  let sharingAccess: SharingAccess;
16
18
 
17
- const nodeUids = Array.from({ length: BATCH_LOADING_SIZE + 5 }, (_, i) => `nodeUid${i}`);
18
- const nodes = nodeUids.map((nodeUid) => ({ nodeUid }));
19
+ const nodeUids = Array.from({ length: BATCH_LOADING_SIZE + 5 }, (_, i) => `volumeId~nodeUid${i}`);
20
+ const nodes = nodeUids.map((nodeUid) => ({
21
+ nodeUid,
22
+ shareId: 'shareId',
23
+ name: { ok: true, value: `name${nodeUid.split('~')[1]}` }
24
+ }));
19
25
  const nodeUidsIterator = async function* () {
20
26
  for (const nodeUid of nodeUids) {
21
27
  yield nodeUid;
@@ -33,25 +39,67 @@ describe('SharingAccess', () => {
33
39
  creationTime: new Date('2025-01-01'),
34
40
  node: {
35
41
  type: NodeType.File,
36
- mediaType: 'mediaType',
42
+ mediaType: 'image/jpeg',
37
43
  },
38
44
  };
39
45
  }),
46
+ removeMember: jest.fn(),
47
+ iterateInvitationUids: jest.fn().mockImplementation(async function* () {
48
+ yield 'invitationUid';
49
+ }),
50
+ getInvitation: jest.fn().mockResolvedValue({
51
+ uid: 'invitationUid',
52
+ node: { uid: 'volumeId~nodeUid' },
53
+ inviteeEmail: 'invitee-email',
54
+ role: MemberRole.Viewer,
55
+ }),
56
+ acceptInvitation: jest.fn(),
57
+ rejectInvitation: jest.fn(),
58
+ deleteBookmark: jest.fn(),
40
59
  };
60
+
41
61
  // @ts-expect-error No need to implement all methods for mocking
42
62
  cache = {
43
63
  setSharedByMeNodeUids: jest.fn(),
44
64
  setSharedWithMeNodeUids: jest.fn(),
65
+ getSharedByMeNodeUids: jest.fn(),
66
+ getSharedWithMeNodeUids: jest.fn(),
67
+ hasSharedByMeNodeUidsLoaded: jest.fn().mockResolvedValue(true),
68
+ hasSharedWithMeNodeUidsLoaded: jest.fn().mockResolvedValue(true),
69
+ addSharedByMeNodeUid: jest.fn(),
70
+ removeSharedByMeNodeUid: jest.fn(),
71
+ addSharedWithMeNodeUid: jest.fn(),
72
+ removeSharedWithMeNodeUid: jest.fn(),
45
73
  };
74
+
46
75
  // @ts-expect-error No need to implement all methods for mocking
47
76
  cryptoService = {
48
77
  decryptInvitation: jest.fn(),
49
78
  decryptBookmark: jest.fn(),
79
+ decryptInvitationWithNode: jest.fn().mockResolvedValue({
80
+ uid: 'invitationUid',
81
+ inviteeEmail: 'invitee-email',
82
+ role: MemberRole.Viewer,
83
+ node: {
84
+ uid: 'volumeId~nodeUid',
85
+ name: { ok: true, value: 'SharedFile.txt' },
86
+ type: NodeType.File,
87
+ },
88
+ }),
89
+ acceptInvitation: jest.fn().mockResolvedValue({
90
+ base64SessionKeySignature: 'mockSignature',
91
+ }),
50
92
  };
93
+
51
94
  // @ts-expect-error No need to implement all methods for mocking
52
95
  sharesService = {
53
- getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
96
+ getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
97
+ loadEncryptedShare: jest.fn().mockResolvedValue({
98
+ id: 'shareId',
99
+ membership: { memberUid: 'memberUid' },
100
+ }),
54
101
  };
102
+
55
103
  // @ts-expect-error No need to implement all methods for mocking
56
104
  nodesService = {
57
105
  iterateNodes: jest.fn().mockImplementation(async function* (nodeUids) {
@@ -61,13 +109,18 @@ describe('SharingAccess', () => {
61
109
  }
62
110
  }
63
111
  }),
112
+ getNode: jest.fn().mockResolvedValue({
113
+ nodeUid: 'volumeId~nodeUid',
114
+ shareId: 'shareId',
115
+ name: { ok: true, value: 'TestFile.txt' },
116
+ }),
64
117
  };
65
118
 
66
119
  sharingAccess = new SharingAccess(apiService, cache, cryptoService, sharesService, nodesService);
67
120
  });
68
121
 
69
122
  describe('iterateSharedNodes', () => {
70
- it('should iterate from cache', async () => {
123
+ it('should iterate from cache when available', async () => {
71
124
  cache.getSharedByMeNodeUids = jest.fn().mockResolvedValue(nodeUids);
72
125
 
73
126
  const result = await Array.fromAsync(sharingAccess.iterateSharedNodes());
@@ -77,20 +130,32 @@ describe('SharingAccess', () => {
77
130
  expect(cache.setSharedByMeNodeUids).not.toHaveBeenCalled();
78
131
  });
79
132
 
80
- it('should iterate from API', async () => {
81
- cache.getSharedByMeNodeUids = jest.fn().mockRejectedValue(new Error('Not cached'));
133
+ it('should iterate from API when cache is empty', async () => {
134
+ cache.getSharedByMeNodeUids = jest.fn().mockRejectedValue(new Error('Cache miss'));
82
135
 
83
136
  const result = await Array.fromAsync(sharingAccess.iterateSharedNodes());
84
137
 
85
138
  expect(result).toEqual(nodes);
86
139
  expect(apiService.iterateSharedNodeUids).toHaveBeenCalledWith('volumeId', undefined);
87
- expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
140
+ expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2);
88
141
  expect(cache.setSharedByMeNodeUids).toHaveBeenCalledWith(nodeUids);
89
142
  });
143
+
144
+ it('should ignore missing nodes during iteration', async () => {
145
+ cache.getSharedByMeNodeUids = jest.fn().mockResolvedValue(['volumeId~nodeUid1', 'volumeId~missingNode']);
146
+ nodesService.iterateNodes = jest.fn().mockImplementation(async function* () {
147
+ yield { nodeUid: 'volumeId~nodeUid1', name: { ok: true, value: 'file1.txt' } };
148
+ yield { missingUid: 'volumeId~missingNode' };
149
+ });
150
+
151
+ const result = await Array.fromAsync(sharingAccess.iterateSharedNodes());
152
+
153
+ expect(result).toEqual([{ nodeUid: 'volumeId~nodeUid1', name: { ok: true, value: 'file1.txt' } }]);
154
+ });
90
155
  });
91
156
 
92
157
  describe('iterateSharedNodesWithMe', () => {
93
- it('should iterate from cache', async () => {
158
+ it('should iterate from cache when available', async () => {
94
159
  cache.getSharedWithMeNodeUids = jest.fn().mockResolvedValue(nodeUids);
95
160
 
96
161
  const result = await Array.fromAsync(sharingAccess.iterateSharedNodesWithMe());
@@ -100,24 +165,149 @@ describe('SharingAccess', () => {
100
165
  expect(cache.setSharedWithMeNodeUids).not.toHaveBeenCalled();
101
166
  });
102
167
 
103
- it('should iterate from API', async () => {
104
- cache.getSharedWithMeNodeUids = jest.fn().mockRejectedValue(new Error('Not cached'));
168
+ it('should iterate from API when cache is empty', async () => {
169
+ cache.getSharedWithMeNodeUids = jest.fn().mockRejectedValue(new Error('Cache miss'));
105
170
 
106
171
  const result = await Array.fromAsync(sharingAccess.iterateSharedNodesWithMe());
107
172
 
108
173
  expect(result).toEqual(nodes);
109
174
  expect(apiService.iterateSharedWithMeNodeUids).toHaveBeenCalledWith(undefined);
110
- expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
175
+ expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2);
111
176
  expect(cache.setSharedWithMeNodeUids).toHaveBeenCalledWith(nodeUids);
112
177
  });
113
178
  });
114
179
 
180
+ describe('removeSharedNodeWithMe', () => {
181
+ const nodeUid = 'volumeId~nodeUid';
182
+
183
+ it('should remove member and update cache', async () => {
184
+ await sharingAccess.removeSharedNodeWithMe(nodeUid);
185
+
186
+ expect(nodesService.getNode).toHaveBeenCalledWith(nodeUid);
187
+ expect(sharesService.loadEncryptedShare).toHaveBeenCalledWith('shareId');
188
+ expect(apiService.removeMember).toHaveBeenCalledWith('memberUid');
189
+ expect(cache.removeSharedWithMeNodeUid).toHaveBeenCalledWith(nodeUid);
190
+ });
191
+
192
+ it('should return early if node is not shared', async () => {
193
+ nodesService.getNode = jest.fn().mockResolvedValue({
194
+ nodeUid,
195
+ shareId: undefined,
196
+ name: { ok: true, value: 'UnsharedFile.txt' }
197
+ });
198
+
199
+ await sharingAccess.removeSharedNodeWithMe(nodeUid);
200
+
201
+ expect(sharesService.loadEncryptedShare).not.toHaveBeenCalled();
202
+ expect(apiService.removeMember).not.toHaveBeenCalled();
203
+ expect(cache.removeSharedWithMeNodeUid).not.toHaveBeenCalled();
204
+ });
205
+
206
+ it('should throw ValidationError if no membership found', async () => {
207
+ sharesService.loadEncryptedShare = jest.fn().mockResolvedValue({
208
+ id: 'shareId',
209
+ membership: undefined,
210
+ });
211
+
212
+ await expect(sharingAccess.removeSharedNodeWithMe(nodeUid)).rejects.toThrow(ValidationError);
213
+ expect(apiService.removeMember).not.toHaveBeenCalled();
214
+ expect(cache.removeSharedWithMeNodeUid).not.toHaveBeenCalled();
215
+ });
216
+ });
217
+
218
+ describe('iterateInvitations', () => {
219
+ it('should iterate and decrypt invitations', async () => {
220
+ const result = await Array.fromAsync(sharingAccess.iterateInvitations());
221
+
222
+ expect(result).toEqual([{
223
+ uid: 'invitationUid',
224
+ inviteeEmail: 'invitee-email',
225
+ role: MemberRole.Viewer,
226
+ node: {
227
+ uid: 'volumeId~nodeUid',
228
+ name: { ok: true, value: 'SharedFile.txt' },
229
+ type: NodeType.File,
230
+ },
231
+ }]);
232
+ expect(apiService.iterateInvitationUids).toHaveBeenCalledWith(undefined);
233
+ expect(apiService.getInvitation).toHaveBeenCalledWith('invitationUid');
234
+ expect(cryptoService.decryptInvitationWithNode).toHaveBeenCalledWith({
235
+ uid: 'invitationUid',
236
+ node: { uid: 'volumeId~nodeUid' },
237
+ inviteeEmail: 'invitee-email',
238
+ role: MemberRole.Viewer,
239
+ });
240
+ });
241
+ });
242
+
243
+ describe('acceptInvitation', () => {
244
+ it('should accept invitation and update cache', async () => {
245
+ const invitationUid = 'invitationUid';
246
+
247
+ await sharingAccess.acceptInvitation(invitationUid);
248
+
249
+ expect(apiService.getInvitation).toHaveBeenCalledWith(invitationUid);
250
+ expect(cryptoService.acceptInvitation).toHaveBeenCalledWith({
251
+ uid: 'invitationUid',
252
+ node: { uid: 'volumeId~nodeUid' },
253
+ inviteeEmail: 'invitee-email',
254
+ role: MemberRole.Viewer,
255
+ });
256
+ expect(apiService.acceptInvitation).toHaveBeenCalledWith(invitationUid, 'mockSignature');
257
+ expect(cache.addSharedWithMeNodeUid).toHaveBeenCalledWith('volumeId~nodeUid');
258
+ });
259
+
260
+ it('should not update cache when not loaded', async () => {
261
+ const invitationUid = 'invitationUid';
262
+ cache.hasSharedWithMeNodeUidsLoaded = jest.fn().mockResolvedValue(false);
263
+
264
+ await sharingAccess.acceptInvitation(invitationUid);
265
+
266
+ expect(apiService.acceptInvitation).toHaveBeenCalledWith(invitationUid, 'mockSignature');
267
+ expect(cache.addSharedWithMeNodeUid).not.toHaveBeenCalled();
268
+ });
269
+ });
270
+
271
+ describe('rejectInvitation', () => {
272
+ it('should reject invitation', async () => {
273
+ const invitationUid = 'invitationUid';
274
+
275
+ await sharingAccess.rejectInvitation(invitationUid);
276
+
277
+ expect(apiService.rejectInvitation).toHaveBeenCalledWith(invitationUid);
278
+ });
279
+ });
280
+
115
281
  describe('iterateBookmarks', () => {
116
- it('should return decrypted bookmark', async () => {
282
+ it('should return successfully decrypted bookmark', async () => {
283
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
284
+ url: resultOk('https://example.com/file.pdf'),
285
+ customPassword: resultOk('password123'),
286
+ nodeName: resultOk('ImportantDocument.pdf'),
287
+ });
288
+
289
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
290
+
291
+ expect(result).toEqual([
292
+ resultOk({
293
+ uid: 'tokenId',
294
+ creationTime: new Date('2025-01-01'),
295
+ url: 'https://example.com/file.pdf',
296
+ customPassword: 'password123',
297
+ node: {
298
+ name: 'ImportantDocument.pdf',
299
+ type: NodeType.File,
300
+ mediaType: 'image/jpeg',
301
+ },
302
+ }),
303
+ ]);
304
+ });
305
+
306
+ it('should return successfully decrypted bookmark with undefined password', async () => {
117
307
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
118
- url: resultOk('url'),
119
- customPassword: resultOk('customPassword'),
120
- nodeName: resultOk('nodeName'),
308
+ url: resultOk('https://example.com/file.pdf'),
309
+ customPassword: resultOk(undefined),
310
+ nodeName: resultOk('PublicDocument.pdf'),
121
311
  });
122
312
 
123
313
  const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
@@ -126,22 +316,46 @@ describe('SharingAccess', () => {
126
316
  resultOk({
127
317
  uid: 'tokenId',
128
318
  creationTime: new Date('2025-01-01'),
129
- url: 'url',
130
- customPassword: 'customPassword',
319
+ url: 'https://example.com/file.pdf',
320
+ customPassword: undefined,
321
+ node: {
322
+ name: 'PublicDocument.pdf',
323
+ type: NodeType.File,
324
+ mediaType: 'image/jpeg',
325
+ },
326
+ }),
327
+ ]);
328
+ });
329
+
330
+ it('should return degraded bookmark when URL cannot be decrypted', async () => {
331
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
332
+ url: resultError('URL decryption failed'),
333
+ customPassword: resultOk('password123'),
334
+ nodeName: resultOk('Document.pdf'),
335
+ });
336
+
337
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
338
+
339
+ expect(result).toEqual([
340
+ resultError({
341
+ uid: 'tokenId',
342
+ creationTime: new Date('2025-01-01'),
343
+ url: resultError('URL decryption failed'),
344
+ customPassword: resultOk('password123'),
131
345
  node: {
132
- name: 'nodeName',
346
+ name: resultOk('Document.pdf'),
133
347
  type: NodeType.File,
134
- mediaType: 'mediaType',
348
+ mediaType: 'image/jpeg',
135
349
  },
136
350
  }),
137
351
  ]);
138
352
  });
139
353
 
140
- it('should return degraded bookmark if URL password cannot be decrypted', async () => {
354
+ it('should return degraded bookmark when custom password cannot be decrypted', async () => {
141
355
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
142
- url: resultError('url cannot be decrypted'),
143
- customPassword: resultOk('url cannot be decrypted'),
144
- nodeName: resultError('url cannot be decrypted'),
356
+ url: resultOk('https://example.com/file.pdf'),
357
+ customPassword: resultError('Password decryption failed'),
358
+ nodeName: resultOk('Document.pdf'),
145
359
  });
146
360
 
147
361
  const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
@@ -150,22 +364,22 @@ describe('SharingAccess', () => {
150
364
  resultError({
151
365
  uid: 'tokenId',
152
366
  creationTime: new Date('2025-01-01'),
153
- url: resultError('url cannot be decrypted'),
154
- customPassword: resultOk('url cannot be decrypted'),
367
+ url: resultOk('https://example.com/file.pdf'),
368
+ customPassword: resultError('Password decryption failed'),
155
369
  node: {
156
- name: resultError('url cannot be decrypted'),
370
+ name: resultOk('Document.pdf'),
157
371
  type: NodeType.File,
158
- mediaType: 'mediaType',
372
+ mediaType: 'image/jpeg',
159
373
  },
160
374
  }),
161
375
  ]);
162
376
  });
163
377
 
164
- it('should return degraded bookmark if node name cannot be decrypted', async () => {
378
+ it('should return degraded bookmark when node name cannot be decrypted', async () => {
165
379
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
166
- url: resultOk('url'),
380
+ url: resultOk('https://example.com/file.pdf'),
167
381
  customPassword: resultOk(undefined),
168
- nodeName: resultError('node name cannot be decrypted'),
382
+ nodeName: resultError('Node name decryption failed'),
169
383
  });
170
384
 
171
385
  const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
@@ -174,15 +388,49 @@ describe('SharingAccess', () => {
174
388
  resultError({
175
389
  uid: 'tokenId',
176
390
  creationTime: new Date('2025-01-01'),
177
- url: resultOk('url'),
391
+ url: resultOk('https://example.com/file.pdf'),
178
392
  customPassword: resultOk(undefined),
179
393
  node: {
180
- name: resultError('node name cannot be decrypted'),
394
+ name: resultError('Node name decryption failed'),
395
+ type: NodeType.File,
396
+ mediaType: 'image/jpeg',
397
+ },
398
+ }),
399
+ ]);
400
+ });
401
+
402
+ it('should return degraded bookmark when all decryption fails', async () => {
403
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
404
+ url: resultError('URL decryption failed'),
405
+ customPassword: resultError('Password decryption failed'),
406
+ nodeName: resultError('Node name decryption failed'),
407
+ });
408
+
409
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
410
+
411
+ expect(result).toEqual([
412
+ resultError({
413
+ uid: 'tokenId',
414
+ creationTime: new Date('2025-01-01'),
415
+ url: resultError('URL decryption failed'),
416
+ customPassword: resultError('Password decryption failed'),
417
+ node: {
418
+ name: resultError('Node name decryption failed'),
181
419
  type: NodeType.File,
182
- mediaType: 'mediaType',
420
+ mediaType: 'image/jpeg',
183
421
  },
184
422
  }),
185
423
  ]);
186
424
  });
187
425
  });
426
+
427
+ describe('deleteBookmark', () => {
428
+ it('should delete bookmark using tokenId', async () => {
429
+ const bookmarkUid = 'tokenId123';
430
+
431
+ await sharingAccess.deleteBookmark(bookmarkUid);
432
+
433
+ expect(apiService.deleteBookmark).toHaveBeenCalledWith(bookmarkUid);
434
+ });
435
+ });
188
436
  });
@@ -40,7 +40,7 @@ export class SharingAccess {
40
40
  const nodeUids = await this.cache.getSharedByMeNodeUids();
41
41
  yield* this.iterateSharedNodesFromCache(nodeUids, signal);
42
42
  } catch {
43
- const { volumeId } = await this.sharesService.getMyFilesIDs();
43
+ const { volumeId } = await this.sharesService.getOwnVolumeIDs();
44
44
  const nodeUidsIterator = this.apiService.iterateSharedNodeUids(volumeId, signal);
45
45
  yield* this.iterateSharedNodesFromAPI(
46
46
  nodeUidsIterator,
@@ -124,6 +124,9 @@ export class SharingAccess {
124
124
  }
125
125
 
126
126
  await this.apiService.removeMember(memberUid);
127
+ if (await this.cache.hasSharedWithMeNodeUidsLoaded()) {
128
+ await this.cache.removeSharedWithMeNodeUid(nodeUid);
129
+ }
127
130
  }
128
131
 
129
132
  async *iterateInvitations(signal?: AbortSignal): AsyncGenerator<ProtonInvitationWithNode> {
@@ -138,6 +141,9 @@ export class SharingAccess {
138
141
  const encryptedInvitation = await this.apiService.getInvitation(invitationUid);
139
142
  const { base64SessionKeySignature } = await this.cryptoService.acceptInvitation(encryptedInvitation);
140
143
  await this.apiService.acceptInvitation(invitationUid, base64SessionKeySignature);
144
+ if (await this.cache.hasSharedWithMeNodeUidsLoaded()) {
145
+ await this.cache.addSharedWithMeNodeUid(encryptedInvitation.node.uid);
146
+ }
141
147
  }
142
148
 
143
149
  async rejectInvitation(invitationUid: string): Promise<void> {
@@ -1,5 +1,5 @@
1
1
  import { DriveAPIService, drivePaths, nodeTypeNumberToNodeType } from '../apiService';
2
- import { Logger } from '../../interface';
2
+ import { Logger, MemberRole } from '../../interface';
3
3
  import { makeNodeUid, splitNodeUid } from '../uids';
4
4
  import { EncryptedShareCrypto, EncryptedNode } from './interface';
5
5
 
@@ -42,13 +42,14 @@ export class SharingPublicAPIService {
42
42
  };
43
43
  }
44
44
 
45
- async *iterateChildren(parentUid: string): AsyncGenerator<EncryptedNode> {
45
+ async *iterateFolderChildren(parentUid: string, signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
46
46
  const { volumeId: token, nodeId } = splitNodeUid(parentUid);
47
47
 
48
48
  let page = 0;
49
49
  while (true) {
50
50
  const response = await this.apiService.get<GetTokenFolderChildrenResponse>(
51
51
  `drive/urls/${token}/folders/${nodeId}/children?Page=${page}&PageSize=${PAGE_SIZE}`,
52
+ signal,
52
53
  );
53
54
 
54
55
  for (const link of response.Links) {
@@ -72,6 +73,10 @@ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token
72
73
  uid: makeNodeUid(token.Token, token.LinkID),
73
74
  parentUid: undefined,
74
75
  type: nodeTypeNumberToNodeType(logger, token.LinkType),
76
+ creationTime: new Date(), // TODO
77
+
78
+ isShared: false,
79
+ directRole: MemberRole.Viewer, // TODO
75
80
  };
76
81
 
77
82
  const baseCryptoNodeMetadata = {
@@ -124,7 +129,11 @@ function linkToEncryptedNode(
124
129
  uid: makeNodeUid(token, link.LinkID),
125
130
  parentUid: link.ParentLinkID ? makeNodeUid(token, link.ParentLinkID) : undefined,
126
131
  type: nodeTypeNumberToNodeType(logger, link.Type),
132
+ creationTime: new Date(), // TODO
127
133
  totalStorageSize: link.TotalSize,
134
+
135
+ isShared: false,
136
+ directRole: MemberRole.Viewer, // TODO
128
137
  };
129
138
 
130
139
  const baseCryptoNodeMetadata = {