@protontech/drive-sdk 0.12.1 → 0.13.1
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 -0
- package/dist/crypto/driveCrypto.js +1 -0
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/interface/events.d.ts +1 -1
- package/dist/interface/featureFlags.d.ts +2 -1
- package/dist/interface/featureFlags.js +1 -0
- package/dist/interface/featureFlags.js.map +1 -1
- package/dist/interface/httpClient.d.ts +1 -0
- package/dist/interface/nodes.d.ts +7 -0
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/telemetry.d.ts +4 -0
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/internal/apiService/apiService.d.ts +1 -0
- package/dist/internal/apiService/apiService.js +16 -7
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +24 -0
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +162 -101
- package/dist/internal/download/telemetry.js +4 -0
- package/dist/internal/download/telemetry.js.map +1 -1
- package/dist/internal/download/telemetry.test.js +5 -0
- package/dist/internal/download/telemetry.test.js.map +1 -1
- package/dist/internal/events/index.js +2 -2
- package/dist/internal/events/index.js.map +1 -1
- package/dist/internal/events/interface.d.ts +1 -1
- package/dist/internal/nodes/apiService.d.ts +4 -0
- package/dist/internal/nodes/apiService.js +4 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +8 -0
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.js +3 -0
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.d.ts +5 -5
- package/dist/internal/nodes/extendedAttributes.js +5 -14
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +16 -22
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +5 -0
- package/dist/internal/nodes/nodesManagement.d.ts +3 -3
- package/dist/internal/nodes/nodesManagement.js +7 -5
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/albumsManager.js +1 -0
- package/dist/internal/photos/albumsManager.js.map +1 -1
- package/dist/internal/photos/nodes.d.ts +1 -1
- package/dist/internal/photos/nodes.js +2 -2
- package/dist/internal/photos/nodes.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +5 -5
- package/dist/internal/photos/upload.js +8 -2
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharing/apiService.js +1 -1
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +1 -0
- package/dist/internal/upload/apiService.d.ts +45 -1
- package/dist/internal/upload/apiService.js +69 -1
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/blockVerifier.d.ts +4 -1
- package/dist/internal/upload/blockVerifier.js +5 -0
- package/dist/internal/upload/blockVerifier.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +2 -2
- package/dist/internal/upload/cryptoService.js +1 -3
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +4 -3
- package/dist/internal/upload/fileUploader.js +17 -7
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/index.d.ts +3 -3
- package/dist/internal/upload/index.js +17 -1
- package/dist/internal/upload/index.js.map +1 -1
- package/dist/internal/upload/index.test.d.ts +1 -0
- package/dist/internal/upload/index.test.js +71 -0
- package/dist/internal/upload/index.test.js.map +1 -0
- package/dist/internal/upload/interface.d.ts +2 -0
- package/dist/internal/upload/manager.d.ts +41 -2
- package/dist/internal/upload/manager.js +126 -44
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +268 -1
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/smallFileUploader.d.ts +83 -0
- package/dist/internal/upload/smallFileUploader.js +197 -0
- package/dist/internal/upload/smallFileUploader.js.map +1 -0
- package/dist/internal/upload/smallFileUploader.test.d.ts +1 -0
- package/dist/internal/upload/smallFileUploader.test.js +358 -0
- package/dist/internal/upload/smallFileUploader.test.js.map +1 -0
- package/dist/internal/upload/streamReader.d.ts +4 -0
- package/dist/internal/upload/streamReader.js +37 -0
- package/dist/internal/upload/streamReader.js.map +1 -0
- package/dist/internal/upload/streamReader.test.d.ts +1 -0
- package/dist/internal/upload/streamReader.test.js +90 -0
- package/dist/internal/upload/streamReader.test.js.map +1 -0
- package/dist/internal/upload/streamUploader.d.ts +6 -0
- package/dist/internal/upload/streamUploader.js +3 -3
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/telemetry.d.ts +3 -2
- package/dist/internal/upload/telemetry.js +5 -0
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/internal/upload/telemetry.test.js +6 -0
- package/dist/internal/upload/telemetry.test.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +1 -1
- package/dist/protonDrivePublicLinkClient.js +3 -1
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.js +21 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.d.ts +1 -0
- package/dist/telemetry.test.js +37 -0
- package/dist/telemetry.test.js.map +1 -0
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +1 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +2 -0
- package/src/interface/events.ts +1 -1
- package/src/interface/featureFlags.ts +1 -0
- package/src/interface/httpClient.ts +1 -0
- package/src/interface/nodes.ts +7 -0
- package/src/interface/telemetry.ts +4 -0
- package/src/internal/apiService/apiService.test.ts +30 -0
- package/src/internal/apiService/apiService.ts +23 -7
- package/src/internal/apiService/driveTypes.ts +162 -101
- package/src/internal/download/telemetry.test.ts +5 -0
- package/src/internal/download/telemetry.ts +5 -1
- package/src/internal/events/index.ts +2 -2
- package/src/internal/events/interface.ts +1 -1
- package/src/internal/nodes/apiService.test.ts +9 -0
- package/src/internal/nodes/apiService.ts +4 -0
- package/src/internal/nodes/cryptoService.ts +11 -1
- package/src/internal/nodes/extendedAttributes.test.ts +25 -25
- package/src/internal/nodes/extendedAttributes.ts +10 -19
- package/src/internal/nodes/interface.ts +5 -0
- package/src/internal/nodes/nodesManagement.ts +8 -6
- package/src/internal/photos/albumsManager.ts +1 -0
- package/src/internal/photos/nodes.ts +2 -2
- package/src/internal/photos/upload.ts +23 -10
- package/src/internal/sharing/apiService.ts +5 -5
- package/src/internal/upload/apiService.ts +167 -2
- package/src/internal/upload/blockVerifier.ts +12 -0
- package/src/internal/upload/cryptoService.ts +10 -10
- package/src/internal/upload/fileUploader.ts +20 -7
- package/src/internal/upload/index.test.ts +99 -0
- package/src/internal/upload/index.ts +45 -4
- package/src/internal/upload/interface.ts +2 -0
- package/src/internal/upload/manager.test.ts +368 -2
- package/src/internal/upload/manager.ts +229 -78
- package/src/internal/upload/smallFileUploader.test.ts +491 -0
- package/src/internal/upload/smallFileUploader.ts +353 -0
- package/src/internal/upload/streamReader.test.ts +109 -0
- package/src/internal/upload/streamReader.ts +38 -0
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/internal/upload/telemetry.test.ts +6 -0
- package/src/internal/upload/telemetry.ts +8 -2
- package/src/protonDrivePhotosClient.ts +1 -1
- package/src/protonDrivePublicLinkClient.ts +2 -0
- package/src/telemetry.test.ts +40 -0
- package/src/telemetry.ts +22 -0
- package/src/transformers.ts +2 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { IntegrityError } from '../../errors';
|
|
2
|
+
import { Thumbnail, ThumbnailType, UploadMetadata } from '../../interface';
|
|
3
|
+
import { SmallFileUploader, SmallFileRevisionUploader } from './smallFileUploader';
|
|
4
|
+
import { UploadTelemetry } from './telemetry';
|
|
5
|
+
import { UploadAPIService } from './apiService';
|
|
6
|
+
import { UploadCryptoService } from './cryptoService';
|
|
7
|
+
import { UploadManager } from './manager';
|
|
8
|
+
import { NodeCrypto } from './interface';
|
|
9
|
+
|
|
10
|
+
const MOCK_BLOCK_HASH = new Uint8Array(32).fill(1);
|
|
11
|
+
const MOCK_VERIFICATION_TOKEN = new Uint8Array(16).fill(2);
|
|
12
|
+
|
|
13
|
+
function createStream(bytes: number[]): ReadableStream<Uint8Array> {
|
|
14
|
+
return new ReadableStream({
|
|
15
|
+
start(controller) {
|
|
16
|
+
controller.enqueue(new Uint8Array(bytes));
|
|
17
|
+
controller.close();
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mockEncryptBlock(
|
|
23
|
+
verifyBlock: (block: Uint8Array) => Promise<{ verificationToken: Uint8Array }>,
|
|
24
|
+
_nodeKeys: unknown,
|
|
25
|
+
block: Uint8Array,
|
|
26
|
+
_index: number,
|
|
27
|
+
) {
|
|
28
|
+
const encryptedData = new Uint8Array(block);
|
|
29
|
+
return (async () => {
|
|
30
|
+
await verifyBlock(encryptedData);
|
|
31
|
+
return {
|
|
32
|
+
index: 0,
|
|
33
|
+
encryptedData,
|
|
34
|
+
armoredSignature: 'mockBlockSignature',
|
|
35
|
+
verificationToken: MOCK_VERIFICATION_TOKEN,
|
|
36
|
+
originalSize: block.length,
|
|
37
|
+
encryptedSize: block.length + 100,
|
|
38
|
+
hash: 'blockHash',
|
|
39
|
+
hashPromise: Promise.resolve(MOCK_BLOCK_HASH),
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('SmallFileUploader', () => {
|
|
45
|
+
let telemetry: UploadTelemetry;
|
|
46
|
+
let apiService: jest.Mocked<UploadAPIService>;
|
|
47
|
+
let cryptoService: jest.Mocked<UploadCryptoService>;
|
|
48
|
+
let uploadManager: jest.Mocked<UploadManager>;
|
|
49
|
+
let metadata: UploadMetadata;
|
|
50
|
+
let onFinish: jest.Mock;
|
|
51
|
+
let abortController: AbortController;
|
|
52
|
+
|
|
53
|
+
const parentFolderUid = 'parentFolderUid';
|
|
54
|
+
const name = 'test-file.txt';
|
|
55
|
+
|
|
56
|
+
const mockNodeCrypto = {
|
|
57
|
+
nodeKeys: {
|
|
58
|
+
decrypted: { key: {} as any },
|
|
59
|
+
encrypted: {
|
|
60
|
+
armoredKey: 'armoredKey',
|
|
61
|
+
armoredPassphrase: 'armoredPassphrase',
|
|
62
|
+
armoredPassphraseSignature: 'armoredPassphraseSignature',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
contentKey: {
|
|
66
|
+
encrypted: {
|
|
67
|
+
contentKeyPacket: new Uint8Array(10),
|
|
68
|
+
base64ContentKeyPacket: 'base64ContentKeyPacket',
|
|
69
|
+
armoredContentKeyPacketSignature: 'armoredContentKeyPacketSignature',
|
|
70
|
+
},
|
|
71
|
+
decrypted: { contentKeyPacketSessionKey: {} as any },
|
|
72
|
+
},
|
|
73
|
+
encryptedNode: {
|
|
74
|
+
encryptedName: 'encryptedName',
|
|
75
|
+
hash: 'hash',
|
|
76
|
+
},
|
|
77
|
+
signingKeys: { email: 'test@test.com', addressId: 'addr', nameAndPassphraseSigningKey: {} as any, contentSigningKey: {} as any },
|
|
78
|
+
} as NodeCrypto & { parentHashKey?: Uint8Array };
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
82
|
+
telemetry = {
|
|
83
|
+
getLoggerForRevision: jest.fn().mockReturnValue({
|
|
84
|
+
debug: jest.fn(),
|
|
85
|
+
info: jest.fn(),
|
|
86
|
+
warn: jest.fn(),
|
|
87
|
+
error: jest.fn(),
|
|
88
|
+
}),
|
|
89
|
+
getLoggerForSmallUpload: jest.fn().mockReturnValue({
|
|
90
|
+
debug: jest.fn(),
|
|
91
|
+
info: jest.fn(),
|
|
92
|
+
warn: jest.fn(),
|
|
93
|
+
error: jest.fn(),
|
|
94
|
+
}),
|
|
95
|
+
logBlockVerificationError: jest.fn(),
|
|
96
|
+
uploadFailed: jest.fn(),
|
|
97
|
+
uploadFinished: jest.fn(),
|
|
98
|
+
uploadInitFailed: jest.fn(),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
102
|
+
apiService = {};
|
|
103
|
+
|
|
104
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
105
|
+
cryptoService = {
|
|
106
|
+
encryptThumbnail: jest.fn().mockImplementation(async (_nodeKeys, thumbnail: Thumbnail) => ({
|
|
107
|
+
type: thumbnail.type,
|
|
108
|
+
encryptedData: new Uint8Array(thumbnail.thumbnail),
|
|
109
|
+
originalSize: thumbnail.thumbnail.length,
|
|
110
|
+
encryptedSize: thumbnail.thumbnail.length + 100,
|
|
111
|
+
hash: 'thumbnailHash',
|
|
112
|
+
})),
|
|
113
|
+
encryptBlock: jest.fn().mockImplementation(mockEncryptBlock),
|
|
114
|
+
verifyBlock: jest.fn().mockResolvedValue({ verificationToken: MOCK_VERIFICATION_TOKEN }),
|
|
115
|
+
commitFile: jest.fn().mockResolvedValue({
|
|
116
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
117
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
uploadManager = {
|
|
122
|
+
generateNewFileCrypto: jest.fn().mockResolvedValue(mockNodeCrypto),
|
|
123
|
+
uploadFile: jest.fn().mockResolvedValue({
|
|
124
|
+
nodeUid: 'nodeUid',
|
|
125
|
+
nodeRevisionUid: 'nodeRevisionUid',
|
|
126
|
+
}),
|
|
127
|
+
} as unknown as jest.Mocked<UploadManager>;
|
|
128
|
+
|
|
129
|
+
metadata = {
|
|
130
|
+
expectedSize: 3,
|
|
131
|
+
mediaType: 'application/octet-stream',
|
|
132
|
+
} as UploadMetadata;
|
|
133
|
+
|
|
134
|
+
onFinish = jest.fn();
|
|
135
|
+
abortController = new AbortController();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function createUploader() {
|
|
139
|
+
return new SmallFileUploader(
|
|
140
|
+
telemetry,
|
|
141
|
+
apiService,
|
|
142
|
+
cryptoService,
|
|
143
|
+
uploadManager,
|
|
144
|
+
metadata,
|
|
145
|
+
onFinish,
|
|
146
|
+
abortController.signal,
|
|
147
|
+
parentFolderUid,
|
|
148
|
+
name,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
describe('uploadFromStream', () => {
|
|
153
|
+
const thumbnails: Thumbnail[] = [];
|
|
154
|
+
const onProgress = jest.fn();
|
|
155
|
+
|
|
156
|
+
it('should start upload and call manager.generateNewFileCrypto and manager.uploadFile', async () => {
|
|
157
|
+
const uploader = createUploader();
|
|
158
|
+
const stream = createStream([1, 2, 3]);
|
|
159
|
+
|
|
160
|
+
const controller = await uploader.uploadFromStream(stream, thumbnails, onProgress);
|
|
161
|
+
const result = await controller.completion();
|
|
162
|
+
|
|
163
|
+
expect(uploadManager.generateNewFileCrypto).toHaveBeenCalledWith(parentFolderUid, name);
|
|
164
|
+
expect(uploadManager.uploadFile).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(result).toEqual({ nodeUid: 'nodeUid', nodeRevisionUid: 'nodeRevisionUid' });
|
|
166
|
+
expect(onProgress).toHaveBeenCalledWith(metadata.expectedSize);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should throw if upload already started', async () => {
|
|
170
|
+
const uploader = createUploader();
|
|
171
|
+
const stream = createStream([1, 2, 3]);
|
|
172
|
+
|
|
173
|
+
await uploader.uploadFromStream(stream, thumbnails, onProgress);
|
|
174
|
+
await expect(uploader.uploadFromStream(stream, thumbnails, onProgress)).rejects.toThrow(
|
|
175
|
+
'Upload already started',
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('buildPayloads (via upload flow)', () => {
|
|
181
|
+
it('should build commitPayload, encryptedBlock, and encryptedThumbnails from stream and pass to manager.uploadFile', async () => {
|
|
182
|
+
const uploader = createUploader();
|
|
183
|
+
const stream = createStream([1, 2, 3]);
|
|
184
|
+
const thumbnails: Thumbnail[] = [
|
|
185
|
+
{ type: ThumbnailType.Type1, thumbnail: new Uint8Array([10, 20]) },
|
|
186
|
+
{ type: ThumbnailType.Type2, thumbnail: new Uint8Array([30, 40, 50]) },
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
await uploader.uploadFromStream(stream, thumbnails, undefined);
|
|
190
|
+
await (uploader as any).controller.completion();
|
|
191
|
+
|
|
192
|
+
expect(uploadManager.uploadFile).toHaveBeenCalledWith(
|
|
193
|
+
parentFolderUid,
|
|
194
|
+
mockNodeCrypto,
|
|
195
|
+
metadata,
|
|
196
|
+
expect.objectContaining({
|
|
197
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
198
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
199
|
+
}),
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
encryptedData: expect.any(Uint8Array),
|
|
202
|
+
armoredSignature: 'mockBlockSignature',
|
|
203
|
+
verificationToken: MOCK_VERIFICATION_TOKEN,
|
|
204
|
+
}),
|
|
205
|
+
[
|
|
206
|
+
{ type: ThumbnailType.Type1, encryptedData: expect.any(Uint8Array) },
|
|
207
|
+
{ type: ThumbnailType.Type2, encryptedData: expect.any(Uint8Array) },
|
|
208
|
+
],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(cryptoService.encryptBlock).toHaveBeenCalledTimes(1);
|
|
212
|
+
expect(cryptoService.encryptThumbnail).toHaveBeenCalledTimes(2);
|
|
213
|
+
expect(cryptoService.commitFile).toHaveBeenCalledWith(
|
|
214
|
+
expect.anything(),
|
|
215
|
+
MOCK_BLOCK_HASH,
|
|
216
|
+
expect.any(String),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should pass encrypted block data matching stream content to crypto.encryptBlock', async () => {
|
|
221
|
+
const uploader = createUploader();
|
|
222
|
+
const content = [5, 6, 7, 8, 9];
|
|
223
|
+
metadata.expectedSize = content.length;
|
|
224
|
+
const stream = createStream(content);
|
|
225
|
+
|
|
226
|
+
await uploader.uploadFromStream(stream, [], undefined);
|
|
227
|
+
await (uploader as any).controller.completion();
|
|
228
|
+
|
|
229
|
+
expect(cryptoService.encryptBlock).toHaveBeenCalledWith(
|
|
230
|
+
expect.any(Function),
|
|
231
|
+
expect.anything(),
|
|
232
|
+
new Uint8Array(content),
|
|
233
|
+
0,
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should pass each thumbnail to crypto.encryptThumbnail with nodeKeys', async () => {
|
|
238
|
+
const uploader = createUploader();
|
|
239
|
+
const thumbnails: Thumbnail[] = [
|
|
240
|
+
{ type: ThumbnailType.Type1, thumbnail: new Uint8Array([1]) },
|
|
241
|
+
];
|
|
242
|
+
const stream = createStream([1, 2, 3]);
|
|
243
|
+
|
|
244
|
+
await uploader.uploadFromStream(stream, thumbnails, undefined);
|
|
245
|
+
await (uploader as any).controller.completion();
|
|
246
|
+
|
|
247
|
+
expect(cryptoService.encryptThumbnail).toHaveBeenCalledWith(
|
|
248
|
+
expect.objectContaining({
|
|
249
|
+
key: mockNodeCrypto.nodeKeys.decrypted.key,
|
|
250
|
+
contentKeyPacket: mockNodeCrypto.contentKey.encrypted.contentKeyPacket,
|
|
251
|
+
contentKeyPacketSessionKey: mockNodeCrypto.contentKey.decrypted.contentKeyPacketSessionKey,
|
|
252
|
+
signingKeys: mockNodeCrypto.signingKeys,
|
|
253
|
+
}),
|
|
254
|
+
{ type: ThumbnailType.Type1, thumbnail: new Uint8Array([1]) },
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should call commitFile with manifest and extended attributes', async () => {
|
|
259
|
+
const uploader = createUploader();
|
|
260
|
+
const stream = createStream([1, 2, 3]);
|
|
261
|
+
|
|
262
|
+
await uploader.uploadFromStream(stream, [], undefined);
|
|
263
|
+
await (uploader as any).controller.completion();
|
|
264
|
+
|
|
265
|
+
const [nodeKeys, manifest, extendedAttributes] = (cryptoService.commitFile as jest.Mock).mock.calls[0];
|
|
266
|
+
expect(manifest).toEqual(MOCK_BLOCK_HASH);
|
|
267
|
+
expect(extendedAttributes).toBeDefined();
|
|
268
|
+
expect(nodeKeys).toBeDefined();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('stream integrity', () => {
|
|
273
|
+
it('should throw IntegrityError when stream size does not match expectedSize', async () => {
|
|
274
|
+
const uploader = createUploader();
|
|
275
|
+
metadata.expectedSize = 5;
|
|
276
|
+
const stream = createStream([1, 2, 3]); // only 3 bytes
|
|
277
|
+
|
|
278
|
+
const controller = await uploader.uploadFromStream(stream, [], undefined);
|
|
279
|
+
|
|
280
|
+
await expect(controller.completion()).rejects.toThrow(IntegrityError);
|
|
281
|
+
await expect(controller.completion()).rejects.toMatchObject({
|
|
282
|
+
debug: { actual: 3, expected: 5 },
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should throw IntegrityError when stream sha1 does not match expectedSha1', async () => {
|
|
287
|
+
const uploader = createUploader();
|
|
288
|
+
metadata.expectedSha1 = 'a'.repeat(40); // wrong sha1
|
|
289
|
+
const stream = createStream([1, 2, 3]);
|
|
290
|
+
|
|
291
|
+
const controller = await uploader.uploadFromStream(stream, [], undefined);
|
|
292
|
+
|
|
293
|
+
await expect(controller.completion()).rejects.toThrow(IntegrityError);
|
|
294
|
+
await expect(controller.completion()).rejects.toMatchObject({
|
|
295
|
+
debug: expect.objectContaining({
|
|
296
|
+
expectedSha1: 'a'.repeat(40),
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('zero-byte file', () => {
|
|
303
|
+
it('should upload zero-byte file without calling encryptBlock and pass undefined block to manager.uploadFile', async () => {
|
|
304
|
+
metadata.expectedSize = 0;
|
|
305
|
+
const uploader = createUploader();
|
|
306
|
+
const stream = createStream([]);
|
|
307
|
+
const onProgress = jest.fn();
|
|
308
|
+
|
|
309
|
+
const controller = await uploader.uploadFromStream(stream, [], onProgress);
|
|
310
|
+
const result = await controller.completion();
|
|
311
|
+
|
|
312
|
+
expect(result).toEqual({ nodeUid: 'nodeUid', nodeRevisionUid: 'nodeRevisionUid' });
|
|
313
|
+
expect(cryptoService.encryptBlock).not.toHaveBeenCalled();
|
|
314
|
+
expect(uploadManager.uploadFile).toHaveBeenCalledWith(
|
|
315
|
+
parentFolderUid,
|
|
316
|
+
mockNodeCrypto,
|
|
317
|
+
metadata,
|
|
318
|
+
expect.objectContaining({
|
|
319
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
320
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
321
|
+
}),
|
|
322
|
+
undefined,
|
|
323
|
+
[],
|
|
324
|
+
);
|
|
325
|
+
expect(cryptoService.commitFile).toHaveBeenCalledWith(
|
|
326
|
+
expect.anything(),
|
|
327
|
+
new Uint8Array(0),
|
|
328
|
+
expect.any(String),
|
|
329
|
+
);
|
|
330
|
+
expect(onFinish).toHaveBeenCalled();
|
|
331
|
+
expect(onProgress).toHaveBeenCalledWith(0);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('SmallFileRevisionUploader', () => {
|
|
337
|
+
let telemetry: UploadTelemetry;
|
|
338
|
+
let apiService: jest.Mocked<UploadAPIService>;
|
|
339
|
+
let cryptoService: jest.Mocked<UploadCryptoService>;
|
|
340
|
+
let uploadManager: jest.Mocked<UploadManager>;
|
|
341
|
+
let metadata: UploadMetadata;
|
|
342
|
+
let onFinish: jest.Mock;
|
|
343
|
+
let abortController: AbortController;
|
|
344
|
+
|
|
345
|
+
const nodeUid = 'nodeUid';
|
|
346
|
+
|
|
347
|
+
const mockNodeKeys = {
|
|
348
|
+
key: {} as any,
|
|
349
|
+
contentKeyPacket: new Uint8Array(10),
|
|
350
|
+
contentKeyPacketSessionKey: {} as any,
|
|
351
|
+
signingKeys: { email: 'test@test.com', addressId: 'addr', nameAndPassphraseSigningKey: {} as any, contentSigningKey: {} as any },
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
beforeEach(() => {
|
|
355
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
356
|
+
telemetry = {
|
|
357
|
+
getLoggerForRevision: jest.fn().mockReturnValue({
|
|
358
|
+
debug: jest.fn(),
|
|
359
|
+
info: jest.fn(),
|
|
360
|
+
warn: jest.fn(),
|
|
361
|
+
error: jest.fn(),
|
|
362
|
+
}),
|
|
363
|
+
getLoggerForSmallUpload: jest.fn().mockReturnValue({
|
|
364
|
+
debug: jest.fn(),
|
|
365
|
+
info: jest.fn(),
|
|
366
|
+
warn: jest.fn(),
|
|
367
|
+
error: jest.fn(),
|
|
368
|
+
}),
|
|
369
|
+
logBlockVerificationError: jest.fn(),
|
|
370
|
+
uploadFailed: jest.fn(),
|
|
371
|
+
uploadFinished: jest.fn(),
|
|
372
|
+
uploadInitFailed: jest.fn(),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
376
|
+
apiService = {};
|
|
377
|
+
|
|
378
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
379
|
+
cryptoService = {
|
|
380
|
+
encryptThumbnail: jest.fn().mockImplementation(async (_nodeKeys, thumbnail: Thumbnail) => ({
|
|
381
|
+
type: thumbnail.type,
|
|
382
|
+
encryptedData: new Uint8Array(thumbnail.thumbnail),
|
|
383
|
+
originalSize: thumbnail.thumbnail.length,
|
|
384
|
+
encryptedSize: thumbnail.thumbnail.length + 100,
|
|
385
|
+
hash: 'thumbnailHash',
|
|
386
|
+
})),
|
|
387
|
+
encryptBlock: jest.fn().mockImplementation(
|
|
388
|
+
async (
|
|
389
|
+
verifyBlock: (b: Uint8Array) => Promise<{ verificationToken: Uint8Array }>,
|
|
390
|
+
_: unknown,
|
|
391
|
+
block: Uint8Array,
|
|
392
|
+
) => {
|
|
393
|
+
await verifyBlock(block);
|
|
394
|
+
return {
|
|
395
|
+
index: 0,
|
|
396
|
+
encryptedData: block,
|
|
397
|
+
armoredSignature: 'mockBlockSignature',
|
|
398
|
+
verificationToken: MOCK_VERIFICATION_TOKEN,
|
|
399
|
+
originalSize: block.length,
|
|
400
|
+
encryptedSize: block.length + 100,
|
|
401
|
+
hash: 'blockHash',
|
|
402
|
+
hashPromise: Promise.resolve(MOCK_BLOCK_HASH),
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
),
|
|
406
|
+
verifyBlock: jest.fn().mockResolvedValue({ verificationToken: MOCK_VERIFICATION_TOKEN }),
|
|
407
|
+
commitFile: jest.fn().mockResolvedValue({
|
|
408
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
409
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
410
|
+
}),
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
uploadManager = {
|
|
414
|
+
getExistingFileNodeCrypto: jest.fn().mockResolvedValue(mockNodeKeys),
|
|
415
|
+
uploadSmallRevision: jest.fn().mockResolvedValue({
|
|
416
|
+
nodeUid: 'nodeUid',
|
|
417
|
+
nodeRevisionUid: 'nodeRevisionUid',
|
|
418
|
+
}),
|
|
419
|
+
} as unknown as jest.Mocked<UploadManager>;
|
|
420
|
+
|
|
421
|
+
metadata = {
|
|
422
|
+
expectedSize: 3,
|
|
423
|
+
mediaType: 'application/octet-stream',
|
|
424
|
+
} as UploadMetadata;
|
|
425
|
+
|
|
426
|
+
onFinish = jest.fn();
|
|
427
|
+
abortController = new AbortController();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
function createUploader() {
|
|
431
|
+
return new SmallFileRevisionUploader(
|
|
432
|
+
telemetry,
|
|
433
|
+
apiService,
|
|
434
|
+
cryptoService,
|
|
435
|
+
uploadManager,
|
|
436
|
+
metadata,
|
|
437
|
+
onFinish,
|
|
438
|
+
abortController.signal,
|
|
439
|
+
nodeUid,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
it('should get node crypto, build payloads, and call uploadSmallRevision', async () => {
|
|
444
|
+
const uploader = createUploader();
|
|
445
|
+
const stream = createStream([1, 2, 3]);
|
|
446
|
+
|
|
447
|
+
const controller = await uploader.uploadFromStream(stream, [], undefined);
|
|
448
|
+
const result = await controller.completion();
|
|
449
|
+
|
|
450
|
+
expect(result).toEqual({ nodeUid: 'nodeUid', nodeRevisionUid: 'nodeRevisionUid' });
|
|
451
|
+
expect(cryptoService.encryptBlock).toHaveBeenCalledWith(expect.any(Function), expect.anything(), Uint8Array.from([1, 2, 3]), 0);
|
|
452
|
+
expect(uploadManager.getExistingFileNodeCrypto).toHaveBeenCalledWith(nodeUid);
|
|
453
|
+
expect(uploadManager.uploadSmallRevision).toHaveBeenCalledWith(
|
|
454
|
+
nodeUid,
|
|
455
|
+
mockNodeKeys,
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
458
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
459
|
+
}),
|
|
460
|
+
expect.objectContaining({
|
|
461
|
+
encryptedData: expect.any(Uint8Array),
|
|
462
|
+
armoredSignature: 'mockBlockSignature',
|
|
463
|
+
verificationToken: MOCK_VERIFICATION_TOKEN,
|
|
464
|
+
}),
|
|
465
|
+
[],
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should upload zero-byte revision without calling encryptBlock and pass undefined block to uploadSmallRevision', async () => {
|
|
470
|
+
metadata.expectedSize = 0;
|
|
471
|
+
const uploader = createUploader();
|
|
472
|
+
const stream = createStream([]);
|
|
473
|
+
|
|
474
|
+
const controller = await uploader.uploadFromStream(stream, [], undefined);
|
|
475
|
+
const result = await controller.completion();
|
|
476
|
+
|
|
477
|
+
expect(result).toEqual({ nodeUid: 'nodeUid', nodeRevisionUid: 'nodeRevisionUid' });
|
|
478
|
+
expect(cryptoService.encryptBlock).not.toHaveBeenCalled();
|
|
479
|
+
expect(uploadManager.uploadSmallRevision).toHaveBeenCalledWith(
|
|
480
|
+
nodeUid,
|
|
481
|
+
mockNodeKeys,
|
|
482
|
+
expect.objectContaining({
|
|
483
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
484
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
485
|
+
}),
|
|
486
|
+
undefined,
|
|
487
|
+
[],
|
|
488
|
+
);
|
|
489
|
+
expect(cryptoService.commitFile).toHaveBeenCalledWith(expect.anything(), new Uint8Array(0), expect.any(String));
|
|
490
|
+
});
|
|
491
|
+
});
|