@protontech/drive-sdk 0.3.1 → 0.3.2
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.
- package/dist/crypto/driveCrypto.d.ts +1 -1
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/interface.d.ts +1 -1
- package/dist/crypto/openPGPCrypto.d.ts +1 -1
- package/dist/crypto/openPGPCrypto.js +4 -1
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/internal/download/cryptoService.js +2 -2
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.js +2 -2
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +3 -1
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/nodes/cache.js +3 -1
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cryptoReporter.d.ts +20 -0
- package/dist/internal/nodes/cryptoReporter.js +96 -0
- package/dist/internal/nodes/cryptoReporter.js.map +1 -0
- package/dist/internal/nodes/cryptoService.d.ts +17 -12
- package/dist/internal/nodes/cryptoService.js +17 -97
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +3 -1
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/index.js +3 -1
- package/dist/internal/nodes/index.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +1 -1
- package/dist/internal/nodes/nodesAccess.d.ts +2 -2
- package/dist/internal/nodes/nodesAccess.js +52 -54
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/sharing/cache.d.ts +3 -0
- package/dist/internal/sharing/cache.js +17 -2
- package/dist/internal/sharing/cache.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +1 -1
- package/dist/internal/sharing/interface.js +1 -1
- package/dist/internal/sharing/sharingAccess.js +6 -0
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +242 -33
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/internal/sharingPublic/apiService.d.ts +1 -1
- package/dist/internal/sharingPublic/apiService.js +9 -2
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoService.d.ts +6 -20
- package/dist/internal/sharingPublic/cryptoService.js +40 -103
- package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +2 -2
- package/dist/internal/sharingPublic/index.js +2 -2
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/interface.d.ts +1 -43
- package/dist/internal/sharingPublic/manager.d.ts +1 -1
- package/dist/internal/sharingPublic/manager.js +9 -7
- package/dist/internal/sharingPublic/manager.js.map +1 -1
- package/dist/internal/upload/streamUploader.js +1 -1
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +3 -1
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.js +1 -0
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +13 -4
- package/dist/protonDrivePublicLinkClient.js +13 -11
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +1 -1
- package/src/crypto/interface.ts +1 -1
- package/src/crypto/openPGPCrypto.ts +5 -2
- package/src/internal/download/cryptoService.ts +2 -2
- package/src/internal/download/fileDownloader.test.ts +3 -1
- package/src/internal/download/fileDownloader.ts +2 -2
- package/src/internal/nodes/cache.ts +3 -1
- package/src/internal/nodes/cryptoReporter.ts +145 -0
- package/src/internal/nodes/cryptoService.test.ts +3 -1
- package/src/internal/nodes/cryptoService.ts +44 -137
- package/src/internal/nodes/index.ts +3 -1
- package/src/internal/nodes/interface.ts +1 -1
- package/src/internal/nodes/nodesAccess.ts +59 -61
- package/src/internal/sharing/cache.ts +19 -2
- package/src/internal/sharing/interface.ts +1 -1
- package/src/internal/sharing/sharingAccess.test.ts +282 -34
- package/src/internal/sharing/sharingAccess.ts +6 -0
- package/src/internal/sharingPublic/apiService.ts +11 -2
- package/src/internal/sharingPublic/cryptoService.ts +71 -135
- package/src/internal/sharingPublic/index.ts +3 -2
- package/src/internal/sharingPublic/interface.ts +8 -53
- package/src/internal/sharingPublic/manager.ts +9 -8
- package/src/internal/upload/streamUploader.test.ts +3 -1
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/protonDriveClient.ts +1 -0
- package/src/protonDrivePublicLinkClient.ts +26 -12
|
@@ -10,6 +10,7 @@ import { NodeAPIService } from './apiService';
|
|
|
10
10
|
import { NodesCache } from './cache';
|
|
11
11
|
import { NodesCryptoCache } from './cryptoCache';
|
|
12
12
|
import { NodesCryptoService } from './cryptoService';
|
|
13
|
+
import { NodesCryptoReporter } from './cryptoReporter';
|
|
13
14
|
import { SharesService } from './interface';
|
|
14
15
|
import { NodesAccess } from './nodesAccess';
|
|
15
16
|
import { NodesManagement } from './nodesManagement';
|
|
@@ -40,7 +41,8 @@ export function initNodesModule(
|
|
|
40
41
|
const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService);
|
|
41
42
|
const cache = new NodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
|
|
42
43
|
const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
|
|
43
|
-
const
|
|
44
|
+
const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
|
|
45
|
+
const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
|
|
44
46
|
const nodesAccess = new NodesAccess(
|
|
45
47
|
telemetry.getLogger('nodes'),
|
|
46
48
|
api,
|
|
@@ -56,7 +56,7 @@ export interface EncryptedNodeCrypto {
|
|
|
56
56
|
nameSignatureEmail?: string;
|
|
57
57
|
armoredKey: string;
|
|
58
58
|
armoredNodePassphrase: string;
|
|
59
|
-
armoredNodePassphraseSignature
|
|
59
|
+
armoredNodePassphraseSignature?: string;
|
|
60
60
|
membership?: {
|
|
61
61
|
inviterEmail: string;
|
|
62
62
|
base64MemberSharePassphraseKeyPacket: string;
|
|
@@ -289,7 +289,7 @@ export class NodesAccess {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
const { node: unparsedNode, keys } = await this.cryptoService.decryptNode(encryptedNode, parentKey);
|
|
292
|
-
const node = await this.
|
|
292
|
+
const node = await parseNode(this.logger, unparsedNode);
|
|
293
293
|
try {
|
|
294
294
|
await this.cache.setNode(node);
|
|
295
295
|
} catch (error: unknown) {
|
|
@@ -305,65 +305,6 @@ export class NodesAccess {
|
|
|
305
305
|
return { node, keys };
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
private async parseNode(unparsedNode: DecryptedUnparsedNode): Promise<DecryptedNode> {
|
|
309
|
-
let nodeName: Result<string, Error | InvalidNameError> = unparsedNode.name;
|
|
310
|
-
if (unparsedNode.name.ok) {
|
|
311
|
-
try {
|
|
312
|
-
validateNodeName(unparsedNode.name.value);
|
|
313
|
-
} catch (error: unknown) {
|
|
314
|
-
this.logger.warn(`Node name validation failed: ${error instanceof Error ? error.message : error}`);
|
|
315
|
-
nodeName = resultError({
|
|
316
|
-
name: unparsedNode.name.value,
|
|
317
|
-
error: error instanceof Error ? error.message : c('Error').t`Unknown error`,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (unparsedNode.type === NodeType.File) {
|
|
323
|
-
const extendedAttributes = unparsedNode.activeRevision?.ok
|
|
324
|
-
? parseFileExtendedAttributes(
|
|
325
|
-
this.logger,
|
|
326
|
-
unparsedNode.activeRevision.value.creationTime,
|
|
327
|
-
unparsedNode.activeRevision.value.extendedAttributes,
|
|
328
|
-
)
|
|
329
|
-
: undefined;
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
...unparsedNode,
|
|
333
|
-
isStale: false,
|
|
334
|
-
activeRevision: !unparsedNode.activeRevision?.ok
|
|
335
|
-
? unparsedNode.activeRevision
|
|
336
|
-
: resultOk({
|
|
337
|
-
uid: unparsedNode.activeRevision.value.uid,
|
|
338
|
-
state: unparsedNode.activeRevision.value.state,
|
|
339
|
-
creationTime: unparsedNode.activeRevision.value.creationTime,
|
|
340
|
-
storageSize: unparsedNode.activeRevision.value.storageSize,
|
|
341
|
-
contentAuthor: unparsedNode.activeRevision.value.contentAuthor,
|
|
342
|
-
thumbnails: unparsedNode.activeRevision.value.thumbnails,
|
|
343
|
-
...extendedAttributes,
|
|
344
|
-
}),
|
|
345
|
-
folder: undefined,
|
|
346
|
-
treeEventScopeId: splitNodeUid(unparsedNode.uid).volumeId,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const extendedAttributes = unparsedNode.folder?.extendedAttributes
|
|
351
|
-
? parseFolderExtendedAttributes(this.logger, unparsedNode.folder.extendedAttributes)
|
|
352
|
-
: undefined;
|
|
353
|
-
return {
|
|
354
|
-
...unparsedNode,
|
|
355
|
-
name: nodeName,
|
|
356
|
-
isStale: false,
|
|
357
|
-
activeRevision: undefined,
|
|
358
|
-
folder: extendedAttributes
|
|
359
|
-
? {
|
|
360
|
-
...extendedAttributes,
|
|
361
|
-
}
|
|
362
|
-
: undefined,
|
|
363
|
-
treeEventScopeId: splitNodeUid(unparsedNode.uid).volumeId,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
308
|
async getParentKeys(
|
|
368
309
|
node: Pick<DecryptedNode, 'parentUid' | 'shareId'>,
|
|
369
310
|
): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
|
|
@@ -375,7 +316,7 @@ export class NodesAccess {
|
|
|
375
316
|
// Change the error message to be more specific.
|
|
376
317
|
// Original error message is referring to node, while here
|
|
377
318
|
// it referes to as parent to follow the method context.
|
|
378
|
-
throw new DecryptionError(c('Error').t`Parent cannot be decrypted
|
|
319
|
+
throw new DecryptionError(c('Error').t`Parent cannot be decrypted`, { cause: error });
|
|
379
320
|
}
|
|
380
321
|
throw error;
|
|
381
322
|
}
|
|
@@ -458,3 +399,60 @@ export class NodesAccess {
|
|
|
458
399
|
return node.parentUid ? this.getRootNode(node.parentUid) : node;
|
|
459
400
|
}
|
|
460
401
|
}
|
|
402
|
+
|
|
403
|
+
export async function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedNode): Promise<DecryptedNode> {
|
|
404
|
+
let nodeName: Result<string, Error | InvalidNameError> = unparsedNode.name;
|
|
405
|
+
if (unparsedNode.name.ok) {
|
|
406
|
+
try {
|
|
407
|
+
validateNodeName(unparsedNode.name.value);
|
|
408
|
+
} catch (error: unknown) {
|
|
409
|
+
logger.warn(`Node name validation failed: ${error instanceof Error ? error.message : error}`);
|
|
410
|
+
nodeName = resultError({
|
|
411
|
+
name: unparsedNode.name.value,
|
|
412
|
+
error: error instanceof Error ? error.message : c('Error').t`Unknown error`,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const treeEventScopeId = splitNodeUid(unparsedNode.uid).volumeId;
|
|
418
|
+
|
|
419
|
+
if (unparsedNode.type === NodeType.File) {
|
|
420
|
+
const extendedAttributes = unparsedNode.activeRevision?.ok
|
|
421
|
+
? parseFileExtendedAttributes(
|
|
422
|
+
logger,
|
|
423
|
+
unparsedNode.activeRevision.value.creationTime,
|
|
424
|
+
unparsedNode.activeRevision.value.extendedAttributes,
|
|
425
|
+
)
|
|
426
|
+
: undefined;
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
...unparsedNode,
|
|
430
|
+
isStale: false,
|
|
431
|
+
activeRevision: !unparsedNode.activeRevision?.ok
|
|
432
|
+
? unparsedNode.activeRevision
|
|
433
|
+
: resultOk({
|
|
434
|
+
uid: unparsedNode.activeRevision.value.uid,
|
|
435
|
+
state: unparsedNode.activeRevision.value.state,
|
|
436
|
+
creationTime: unparsedNode.activeRevision.value.creationTime,
|
|
437
|
+
storageSize: unparsedNode.activeRevision.value.storageSize,
|
|
438
|
+
contentAuthor: unparsedNode.activeRevision.value.contentAuthor,
|
|
439
|
+
thumbnails: unparsedNode.activeRevision.value.thumbnails,
|
|
440
|
+
...extendedAttributes,
|
|
441
|
+
}),
|
|
442
|
+
folder: undefined,
|
|
443
|
+
treeEventScopeId,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const extendedAttributes = unparsedNode.folder?.extendedAttributes
|
|
448
|
+
? parseFolderExtendedAttributes(logger, unparsedNode.folder.extendedAttributes)
|
|
449
|
+
: undefined;
|
|
450
|
+
return {
|
|
451
|
+
...unparsedNode,
|
|
452
|
+
name: nodeName,
|
|
453
|
+
isStale: false,
|
|
454
|
+
activeRevision: undefined,
|
|
455
|
+
folder: extendedAttributes,
|
|
456
|
+
treeEventScopeId,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
@@ -44,11 +44,28 @@ export class SharingCache {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async getSharedWithMeNodeUids(): Promise<string[]> {
|
|
47
|
-
return this.getNodeUids(SharingType.
|
|
47
|
+
return this.getNodeUids(SharingType.SharedWithMe);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async hasSharedWithMeNodeUidsLoaded(): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
await this.getNodeUids(SharingType.SharedWithMe);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async addSharedWithMeNodeUid(nodeUid: string): Promise<void> {
|
|
60
|
+
return this.addNodeUid(SharingType.SharedWithMe, nodeUid);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async removeSharedWithMeNodeUid(nodeUid: string): Promise<void> {
|
|
64
|
+
return this.removeNodeUid(SharingType.SharedWithMe, nodeUid);
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
async setSharedWithMeNodeUids(nodeUids: string[] | undefined): Promise<void> {
|
|
51
|
-
return this.setNodeUids(SharingType.
|
|
68
|
+
return this.setNodeUids(SharingType.SharedWithMe, nodeUids);
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
/**
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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) => ({
|
|
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: '
|
|
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
96
|
getMyFilesIDs: 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('
|
|
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);
|
|
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('
|
|
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);
|
|
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('
|
|
119
|
-
customPassword: resultOk(
|
|
120
|
-
nodeName: resultOk('
|
|
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: '
|
|
130
|
-
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: '
|
|
346
|
+
name: resultOk('Document.pdf'),
|
|
133
347
|
type: NodeType.File,
|
|
134
|
-
mediaType: '
|
|
348
|
+
mediaType: 'image/jpeg',
|
|
135
349
|
},
|
|
136
350
|
}),
|
|
137
351
|
]);
|
|
138
352
|
});
|
|
139
353
|
|
|
140
|
-
it('should return degraded bookmark
|
|
354
|
+
it('should return degraded bookmark when custom password cannot be decrypted', async () => {
|
|
141
355
|
cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
|
|
142
|
-
url:
|
|
143
|
-
customPassword:
|
|
144
|
-
nodeName:
|
|
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:
|
|
154
|
-
customPassword:
|
|
367
|
+
url: resultOk('https://example.com/file.pdf'),
|
|
368
|
+
customPassword: resultError('Password decryption failed'),
|
|
155
369
|
node: {
|
|
156
|
-
name:
|
|
370
|
+
name: resultOk('Document.pdf'),
|
|
157
371
|
type: NodeType.File,
|
|
158
|
-
mediaType: '
|
|
372
|
+
mediaType: 'image/jpeg',
|
|
159
373
|
},
|
|
160
374
|
}),
|
|
161
375
|
]);
|
|
162
376
|
});
|
|
163
377
|
|
|
164
|
-
it('should return degraded bookmark
|
|
378
|
+
it('should return degraded bookmark when node name cannot be decrypted', async () => {
|
|
165
379
|
cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
|
|
166
|
-
url: resultOk('
|
|
380
|
+
url: resultOk('https://example.com/file.pdf'),
|
|
167
381
|
customPassword: resultOk(undefined),
|
|
168
|
-
nodeName: resultError('
|
|
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('
|
|
391
|
+
url: resultOk('https://example.com/file.pdf'),
|
|
178
392
|
customPassword: resultOk(undefined),
|
|
179
393
|
node: {
|
|
180
|
-
name: resultError('
|
|
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: '
|
|
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
|
});
|
|
@@ -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> {
|