@protontech/drive-sdk 0.0.11 → 0.0.13
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/cache/index.d.ts +2 -0
- package/dist/cache/index.js +6 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/interface.d.ts +105 -0
- package/dist/cache/interface.js +3 -0
- package/dist/cache/interface.js.map +1 -0
- package/dist/cache/memoryCache.d.ts +18 -0
- package/dist/cache/memoryCache.js +78 -0
- package/dist/cache/memoryCache.js.map +1 -0
- package/dist/cache/memoryCache.test.d.ts +1 -0
- package/dist/cache/memoryCache.test.js +121 -0
- package/dist/cache/memoryCache.test.js.map +1 -0
- package/dist/crypto/hmac.d.ts +22 -0
- package/dist/crypto/hmac.js +44 -0
- package/dist/crypto/hmac.js.map +1 -0
- package/dist/crypto/utils.d.ts +2 -0
- package/dist/crypto/utils.js +35 -0
- package/dist/crypto/utils.js.map +1 -0
- package/dist/errors.d.ts +142 -0
- package/dist/errors.js +168 -0
- package/dist/errors.js.map +1 -0
- package/dist/interface/account.js +3 -0
- package/dist/interface/account.js.map +1 -0
- package/dist/interface/author.d.ts +26 -0
- package/dist/interface/author.js +3 -0
- package/dist/interface/author.js.map +1 -0
- package/dist/interface/download.d.ts +29 -0
- package/dist/interface/download.js +3 -0
- package/dist/interface/download.js.map +1 -0
- package/dist/interface/httpClient.d.ts +38 -0
- package/dist/interface/httpClient.js +3 -0
- package/dist/interface/httpClient.js.map +1 -0
- package/dist/interface/index.d.ts +1 -1
- package/dist/interface/nodes.d.ts +12 -1
- package/dist/interface/nodes.js +11 -0
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/result.d.ts +9 -0
- package/dist/interface/result.js +11 -0
- package/dist/interface/result.js.map +1 -0
- package/dist/interface/thumbnail.d.ts +17 -0
- package/dist/interface/thumbnail.js +9 -0
- package/dist/interface/thumbnail.js.map +1 -0
- package/dist/interface/upload.d.ts +64 -0
- package/dist/interface/upload.js +3 -0
- package/dist/interface/upload.js.map +1 -0
- package/dist/internal/apiService/driveTypes.d.ts +1341 -465
- package/dist/internal/apiService/errorCodes.d.ts +30 -0
- package/dist/internal/apiService/errorCodes.js +11 -0
- package/dist/internal/apiService/errorCodes.js.map +1 -0
- package/dist/internal/apiService/errors.js +2 -2
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/apiService/observerStream.d.ts +3 -0
- package/dist/internal/apiService/observerStream.js +15 -0
- package/dist/internal/apiService/observerStream.js.map +1 -0
- package/dist/internal/apiService/transformers.js +2 -0
- package/dist/internal/apiService/transformers.js.map +1 -1
- package/dist/internal/asyncIteratorMap.d.ts +15 -0
- package/dist/internal/asyncIteratorMap.js +59 -0
- package/dist/internal/asyncIteratorMap.js.map +1 -0
- package/dist/internal/asyncIteratorMap.test.d.ts +1 -0
- package/dist/internal/asyncIteratorMap.test.js +120 -0
- package/dist/internal/asyncIteratorMap.test.js.map +1 -0
- package/dist/internal/batchLoading.d.ts +34 -0
- package/dist/internal/batchLoading.js +68 -0
- package/dist/internal/batchLoading.js.map +1 -0
- package/dist/internal/batchLoading.test.d.ts +1 -0
- package/dist/internal/batchLoading.test.js +50 -0
- package/dist/internal/batchLoading.test.js.map +1 -0
- package/dist/internal/download/controller.d.ts +8 -0
- package/dist/internal/download/controller.js +22 -0
- package/dist/internal/download/controller.js.map +1 -0
- package/dist/internal/download/queue.d.ts +5 -0
- package/dist/internal/download/queue.js +31 -0
- package/dist/internal/download/queue.js.map +1 -0
- package/dist/internal/errors.js +28 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/errors.test.js +22 -0
- package/dist/internal/errors.test.js.map +1 -0
- package/dist/internal/events/interface.d.ts +47 -0
- package/dist/internal/events/interface.js +12 -0
- package/dist/internal/events/interface.js.map +1 -0
- package/dist/internal/nodes/apiService.d.ts +2 -2
- package/dist/internal/nodes/apiService.js +16 -6
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +30 -8
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.js +1 -0
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +1 -0
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +34 -0
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/index.test.js +3 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +3 -1
- package/dist/internal/nodes/mediaTypes.d.ts +2 -0
- package/dist/internal/nodes/mediaTypes.js +13 -0
- package/dist/internal/nodes/mediaTypes.js.map +1 -0
- package/dist/internal/nodes/nodesAccess.js +28 -7
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +7 -6
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/nodes/validations.d.ts +4 -0
- package/dist/internal/nodes/validations.js +21 -0
- package/dist/internal/nodes/validations.js.map +1 -0
- package/dist/internal/sharing/apiService.js +19 -2
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/uids.d.ts +38 -0
- package/dist/internal/uids.js +85 -0
- package/dist/internal/uids.js.map +1 -0
- package/dist/internal/upload/chunkStreamReader.d.ts +13 -0
- package/dist/internal/upload/chunkStreamReader.js +46 -0
- package/dist/internal/upload/chunkStreamReader.js.map +1 -0
- package/dist/internal/upload/chunkStreamReader.test.d.ts +1 -0
- package/dist/internal/upload/chunkStreamReader.test.js +75 -0
- package/dist/internal/upload/chunkStreamReader.test.js.map +1 -0
- package/dist/internal/upload/controller.d.ts +8 -0
- package/dist/internal/upload/controller.js +25 -0
- package/dist/internal/upload/controller.js.map +1 -0
- package/dist/internal/upload/digests.d.ts +8 -0
- package/dist/internal/upload/digests.js +22 -0
- package/dist/internal/upload/digests.js.map +1 -0
- package/dist/internal/upload/fileUploader.d.ts +49 -53
- package/dist/internal/upload/fileUploader.js +91 -395
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/fileUploader.test.js +38 -292
- package/dist/internal/upload/fileUploader.test.js.map +1 -1
- package/dist/internal/upload/index.d.ts +3 -3
- package/dist/internal/upload/index.js +20 -41
- package/dist/internal/upload/index.js.map +1 -1
- package/dist/internal/upload/manager.d.ts +1 -1
- package/dist/internal/upload/manager.js +16 -19
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +42 -83
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/queue.d.ts +5 -0
- package/dist/internal/upload/queue.js +32 -0
- package/dist/internal/upload/queue.js.map +1 -0
- package/dist/internal/upload/streamUploader.d.ts +62 -0
- package/dist/internal/upload/streamUploader.js +441 -0
- package/dist/internal/upload/streamUploader.js.map +1 -0
- package/dist/internal/upload/streamUploader.test.d.ts +1 -0
- package/dist/internal/upload/streamUploader.test.js +358 -0
- package/dist/internal/upload/streamUploader.test.js.map +1 -0
- package/dist/internal/utils.d.ts +1 -0
- package/dist/internal/utils.js +13 -0
- package/dist/internal/utils.js.map +1 -0
- package/dist/internal/wait.d.ts +3 -0
- package/dist/internal/wait.js +28 -0
- package/dist/internal/wait.js.map +1 -0
- package/dist/internal/wait.test.d.ts +1 -0
- package/dist/internal/wait.test.js +21 -0
- package/dist/internal/wait.test.js.map +1 -0
- package/dist/protonDriveClient.d.ts +4 -4
- package/dist/protonDriveClient.js +1 -1
- package/dist/protonDriveClient.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +10 -4
- package/src/interface/index.ts +1 -1
- package/src/interface/nodes.ts +11 -0
- package/src/interface/upload.ts +53 -3
- package/src/internal/apiService/driveTypes.ts +1341 -465
- package/src/internal/apiService/errors.ts +3 -2
- package/src/internal/apiService/transformers.ts +2 -0
- package/src/internal/asyncIteratorMap.test.ts +150 -0
- package/src/internal/asyncIteratorMap.ts +64 -0
- package/src/internal/nodes/apiService.test.ts +36 -7
- package/src/internal/nodes/apiService.ts +19 -7
- package/src/internal/nodes/cache.test.ts +1 -0
- package/src/internal/nodes/cache.ts +1 -0
- package/src/internal/nodes/cryptoService.test.ts +38 -0
- package/src/internal/nodes/index.test.ts +3 -1
- package/src/internal/nodes/interface.ts +4 -1
- package/src/internal/nodes/nodesAccess.test.ts +7 -6
- package/src/internal/nodes/nodesAccess.ts +30 -7
- package/src/internal/sharing/apiService.ts +24 -2
- package/src/internal/upload/fileUploader.test.ts +46 -376
- package/src/internal/upload/fileUploader.ts +114 -494
- package/src/internal/upload/index.ts +26 -50
- package/src/internal/upload/manager.test.ts +45 -92
- package/src/internal/upload/manager.ts +30 -32
- package/src/internal/upload/streamUploader.test.ts +469 -0
- package/src/internal/upload/streamUploader.ts +552 -0
- package/src/protonDriveClient.ts +5 -4
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { Thumbnail, ThumbnailType, UploadMetadata } from '../../interface';
|
|
2
|
+
import { IntegrityError } from '../../errors';
|
|
3
|
+
import { APIHTTPError, HTTPErrorCode } from '../apiService';
|
|
4
|
+
import { FILE_CHUNK_SIZE, StreamUploader } from './streamUploader';
|
|
5
|
+
import { UploadTelemetry } from './telemetry';
|
|
6
|
+
import { UploadAPIService } from './apiService';
|
|
7
|
+
import { UploadCryptoService } from './cryptoService';
|
|
8
|
+
import { UploadController } from './controller';
|
|
9
|
+
import { BlockVerifier } from './blockVerifier';
|
|
10
|
+
import { NodeRevisionDraft } from './interface';
|
|
11
|
+
import { UploadManager } from './manager';
|
|
12
|
+
|
|
13
|
+
const BLOCK_ENCRYPTION_OVERHEAD = 10000;
|
|
14
|
+
|
|
15
|
+
async function mockEncryptBlock(verifyBlock: (block: Uint8Array) => Promise<void>, _: any, block: Uint8Array, index: number) {
|
|
16
|
+
await verifyBlock(block);
|
|
17
|
+
return {
|
|
18
|
+
index,
|
|
19
|
+
encryptedData: block,
|
|
20
|
+
armoredSignature: 'signature',
|
|
21
|
+
verificationToken: 'verificationToken',
|
|
22
|
+
originalSize: block.length,
|
|
23
|
+
encryptedSize: block.length + BLOCK_ENCRYPTION_OVERHEAD,
|
|
24
|
+
hash: 'blockHash',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mockUploadBlock(_: string, __: string, encryptedBlock: Uint8Array, onProgress: (uploadedBytes: number) => void) {
|
|
29
|
+
onProgress(encryptedBlock.length);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('StreamUploader', () => {
|
|
33
|
+
let telemetry: UploadTelemetry;
|
|
34
|
+
let apiService: jest.Mocked<UploadAPIService>;
|
|
35
|
+
let cryptoService: UploadCryptoService;
|
|
36
|
+
let uploadManager: UploadManager;
|
|
37
|
+
let blockVerifier: BlockVerifier;
|
|
38
|
+
let revisionDraft: NodeRevisionDraft;
|
|
39
|
+
let metadata: UploadMetadata;
|
|
40
|
+
let controller: UploadController;
|
|
41
|
+
let onFinish: () => Promise<void>;
|
|
42
|
+
let abortController: AbortController;
|
|
43
|
+
|
|
44
|
+
let uploader: StreamUploader;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
48
|
+
telemetry = {
|
|
49
|
+
getLoggerForRevision: jest.fn().mockReturnValue({
|
|
50
|
+
debug: jest.fn(),
|
|
51
|
+
info: jest.fn(),
|
|
52
|
+
warn: jest.fn(),
|
|
53
|
+
error: jest.fn(),
|
|
54
|
+
}),
|
|
55
|
+
logBlockVerificationError: jest.fn(),
|
|
56
|
+
uploadFailed: jest.fn(),
|
|
57
|
+
uploadFinished: jest.fn(),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
61
|
+
apiService = {
|
|
62
|
+
requestBlockUpload: jest.fn().mockImplementation((_, __, blocks) => ({
|
|
63
|
+
blockTokens: blocks.contentBlocks.map((block: { index: number }) => ({
|
|
64
|
+
index: block.index,
|
|
65
|
+
bareUrl: `bareUrl/block:${block.index}`,
|
|
66
|
+
token: `token/block:${block.index}`,
|
|
67
|
+
})),
|
|
68
|
+
thumbnailTokens: (blocks.thumbnails || []).map((thumbnail: { type: number }) => ({
|
|
69
|
+
type: thumbnail.type,
|
|
70
|
+
bareUrl: `bareUrl/thumbnail:${thumbnail.type}`,
|
|
71
|
+
token: `token/thumbnail:${thumbnail.type}`,
|
|
72
|
+
})),
|
|
73
|
+
})),
|
|
74
|
+
uploadBlock: jest.fn().mockImplementation(mockUploadBlock),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
78
|
+
cryptoService = {
|
|
79
|
+
encryptThumbnail: jest.fn().mockImplementation(async (_, thumbnail) => ({
|
|
80
|
+
type: thumbnail.type,
|
|
81
|
+
encryptedData: thumbnail.thumbnail,
|
|
82
|
+
originalSize: thumbnail.thumbnail.length,
|
|
83
|
+
encryptedSize: thumbnail.thumbnail + 1000,
|
|
84
|
+
hash: 'thumbnailHash',
|
|
85
|
+
})),
|
|
86
|
+
encryptBlock: jest.fn().mockImplementation(mockEncryptBlock),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
90
|
+
uploadManager = {
|
|
91
|
+
commitDraft: jest.fn().mockResolvedValue(undefined),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
95
|
+
blockVerifier = {
|
|
96
|
+
verifyBlock: jest.fn().mockResolvedValue(undefined),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
revisionDraft = {
|
|
100
|
+
nodeRevisionUid: 'revisionUid',
|
|
101
|
+
nodeKeys: {
|
|
102
|
+
signatureAddress: { addressId: 'addressId' },
|
|
103
|
+
},
|
|
104
|
+
} as NodeRevisionDraft;
|
|
105
|
+
|
|
106
|
+
metadata = {
|
|
107
|
+
// 3 blocks: 4 + 4 + 2 MB
|
|
108
|
+
expectedSize: 10 * 1024 * 1024,
|
|
109
|
+
} as UploadMetadata;
|
|
110
|
+
|
|
111
|
+
controller = new UploadController();
|
|
112
|
+
onFinish = jest.fn();
|
|
113
|
+
abortController = new AbortController();
|
|
114
|
+
|
|
115
|
+
uploader = new StreamUploader(
|
|
116
|
+
telemetry,
|
|
117
|
+
apiService,
|
|
118
|
+
cryptoService,
|
|
119
|
+
uploadManager,
|
|
120
|
+
blockVerifier,
|
|
121
|
+
revisionDraft,
|
|
122
|
+
metadata,
|
|
123
|
+
onFinish,
|
|
124
|
+
abortController.signal,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('start', () => {
|
|
129
|
+
let thumbnails: Thumbnail[];
|
|
130
|
+
let thumbnailSize: number;
|
|
131
|
+
|
|
132
|
+
let onProgress: (uploadedBytes: number) => void;
|
|
133
|
+
let stream: ReadableStream<Uint8Array>;
|
|
134
|
+
|
|
135
|
+
const verifySuccess = async () => {
|
|
136
|
+
await uploader.start(stream, thumbnails, onProgress);
|
|
137
|
+
|
|
138
|
+
const numberOfExpectedBlocks = Math.ceil(metadata.expectedSize / FILE_CHUNK_SIZE);
|
|
139
|
+
expect(uploadManager.commitDraft).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(uploadManager.commitDraft).toHaveBeenCalledWith(
|
|
141
|
+
revisionDraft,
|
|
142
|
+
expect.anything(),
|
|
143
|
+
metadata,
|
|
144
|
+
{
|
|
145
|
+
size: metadata.expectedSize,
|
|
146
|
+
blockSizes: metadata.expectedSize ? [
|
|
147
|
+
...Array(numberOfExpectedBlocks - 1).fill(FILE_CHUNK_SIZE),
|
|
148
|
+
metadata.expectedSize % FILE_CHUNK_SIZE
|
|
149
|
+
] : [],
|
|
150
|
+
modificationTime: undefined,
|
|
151
|
+
digests: {
|
|
152
|
+
sha1: expect.anything(),
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
metadata.expectedSize + numberOfExpectedBlocks * BLOCK_ENCRYPTION_OVERHEAD,
|
|
156
|
+
);
|
|
157
|
+
expect(telemetry.uploadFinished).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(telemetry.uploadFinished).toHaveBeenCalledWith('revisionUid', metadata.expectedSize + thumbnailSize);
|
|
159
|
+
expect(telemetry.uploadFailed).not.toHaveBeenCalled();
|
|
160
|
+
expect(onFinish).toHaveBeenCalledTimes(1);
|
|
161
|
+
expect(onFinish).toHaveBeenCalledWith(false);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const verifyFailure = async (error: string, uploadedBytes: number | undefined, expectedSize = metadata.expectedSize) => {
|
|
165
|
+
const promise = uploader.start(stream, thumbnails, onProgress);
|
|
166
|
+
await expect(promise).rejects.toThrow(error);
|
|
167
|
+
|
|
168
|
+
expect(telemetry.uploadFinished).not.toHaveBeenCalled();
|
|
169
|
+
expect(telemetry.uploadFailed).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(telemetry.uploadFailed).toHaveBeenCalledWith(
|
|
171
|
+
'revisionUid',
|
|
172
|
+
new Error(error),
|
|
173
|
+
uploadedBytes === undefined ? expect.anything() : uploadedBytes,
|
|
174
|
+
expectedSize,
|
|
175
|
+
);
|
|
176
|
+
expect(onFinish).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(onFinish).toHaveBeenCalledWith(true);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const verifyOnProgress = async (uploadedBytes: number[]) => {
|
|
181
|
+
expect(onProgress).toHaveBeenCalledTimes(uploadedBytes.length);
|
|
182
|
+
for (let i = 0; i < uploadedBytes.length; i++) {
|
|
183
|
+
expect(onProgress).toHaveBeenNthCalledWith(i + 1, uploadedBytes[i]);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
onProgress = jest.fn();
|
|
189
|
+
thumbnails = [
|
|
190
|
+
{
|
|
191
|
+
type: ThumbnailType.Type1,
|
|
192
|
+
thumbnail: new Uint8Array(1024),
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
thumbnailSize = thumbnails.reduce((acc, thumbnail) => acc + thumbnail.thumbnail.length, 0);
|
|
196
|
+
stream = new ReadableStream({
|
|
197
|
+
start(controller) {
|
|
198
|
+
const chunkSize = 1024;
|
|
199
|
+
const chunkCount = metadata.expectedSize / chunkSize;
|
|
200
|
+
for (let i = 1; i <= chunkCount; i++) {
|
|
201
|
+
controller.enqueue(new Uint8Array(chunkSize));
|
|
202
|
+
}
|
|
203
|
+
controller.close();
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should upload successfully", async () => {
|
|
209
|
+
await verifySuccess();
|
|
210
|
+
expect(apiService.requestBlockUpload).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(apiService.uploadBlock).toHaveBeenCalledTimes(4); // 3 blocks + 1 thumbnail
|
|
212
|
+
expect(blockVerifier.verifyBlock).toHaveBeenCalledTimes(3); // 3 blocks
|
|
213
|
+
expect(telemetry.logBlockVerificationError).not.toHaveBeenCalled();
|
|
214
|
+
await verifyOnProgress([thumbnailSize, 4 * 1024 * 1024, 4 * 1024 * 1024, 2 * 1024 * 1024]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should upload successfully empty file without thumbnail", async () => {
|
|
218
|
+
metadata = {
|
|
219
|
+
expectedSize: 0,
|
|
220
|
+
} as UploadMetadata;
|
|
221
|
+
stream = new ReadableStream({
|
|
222
|
+
start(controller) {
|
|
223
|
+
controller.close();
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
thumbnails = [];
|
|
227
|
+
thumbnailSize = 0;
|
|
228
|
+
uploader = new StreamUploader(
|
|
229
|
+
telemetry,
|
|
230
|
+
apiService,
|
|
231
|
+
cryptoService,
|
|
232
|
+
uploadManager,
|
|
233
|
+
blockVerifier,
|
|
234
|
+
revisionDraft,
|
|
235
|
+
metadata,
|
|
236
|
+
onFinish,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
await verifySuccess();
|
|
240
|
+
expect(apiService.requestBlockUpload).toHaveBeenCalledTimes(0);
|
|
241
|
+
expect(apiService.uploadBlock).toHaveBeenCalledTimes(0);
|
|
242
|
+
expect(blockVerifier.verifyBlock).toHaveBeenCalledTimes(0);
|
|
243
|
+
await verifyOnProgress([]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should upload successfully empty file with thumbnail", async () => {
|
|
247
|
+
metadata = {
|
|
248
|
+
expectedSize: 0,
|
|
249
|
+
} as UploadMetadata;
|
|
250
|
+
stream = new ReadableStream({
|
|
251
|
+
start(controller) {
|
|
252
|
+
controller.close();
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
uploader = new StreamUploader(
|
|
256
|
+
telemetry,
|
|
257
|
+
apiService,
|
|
258
|
+
cryptoService,
|
|
259
|
+
uploadManager,
|
|
260
|
+
blockVerifier,
|
|
261
|
+
revisionDraft,
|
|
262
|
+
metadata,
|
|
263
|
+
onFinish,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
await verifySuccess();
|
|
267
|
+
expect(apiService.requestBlockUpload).toHaveBeenCalledTimes(1);
|
|
268
|
+
expect(apiService.uploadBlock).toHaveBeenCalledTimes(1);
|
|
269
|
+
expect(blockVerifier.verifyBlock).toHaveBeenCalledTimes(0);
|
|
270
|
+
await verifyOnProgress([thumbnailSize]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should handle failure when encrypting thumbnails', async () => {
|
|
274
|
+
cryptoService.encryptThumbnail = jest.fn().mockImplementation(async function () {
|
|
275
|
+
throw new Error('Failed to encrypt thumbnail');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await verifyFailure('Failed to encrypt thumbnail', 0);
|
|
279
|
+
expect(cryptoService.encryptThumbnail).toHaveBeenCalledTimes(1);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle failure when encrypting block', async () => {
|
|
283
|
+
cryptoService.encryptBlock = jest.fn().mockImplementation(async function () {
|
|
284
|
+
throw new Error('Failed to encrypt block');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Encrypting thumbnails is before blocks, thus it can be uploaded before failure.
|
|
288
|
+
await verifyFailure('Failed to encrypt block', 1024);
|
|
289
|
+
// 1 block + 1 retry, others are skipped
|
|
290
|
+
expect(cryptoService.encryptBlock).toHaveBeenCalledTimes(2);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should handle one time-off failure when encrypting block', async () => {
|
|
294
|
+
let count = 0;
|
|
295
|
+
cryptoService.encryptBlock = jest.fn().mockImplementation(async function (verifyBlock, keys, block, index) {
|
|
296
|
+
if (count === 0) {
|
|
297
|
+
count++;
|
|
298
|
+
throw new Error('Failed to encrypt block');
|
|
299
|
+
}
|
|
300
|
+
return mockEncryptBlock(verifyBlock, keys, block, index);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await verifySuccess();
|
|
304
|
+
// 1 block + 1 retry + 2 other blocks without retry
|
|
305
|
+
expect(cryptoService.encryptBlock).toHaveBeenCalledTimes(4);
|
|
306
|
+
await verifyOnProgress([thumbnailSize, 4 * 1024 * 1024, 4 * 1024 * 1024, 2 * 1024 * 1024]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should handle failure when requesting tokens', async () => {
|
|
310
|
+
apiService.requestBlockUpload = jest.fn().mockImplementation(async function () {
|
|
311
|
+
throw new Error('Failed to request tokens');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await verifyFailure('Failed to request tokens', 0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should handle failure when uploading thumbnail', async () => {
|
|
318
|
+
apiService.uploadBlock = jest.fn().mockImplementation(async function (bareUrl, token, block, onProgress) {
|
|
319
|
+
if (token === 'token/thumbnail:1') {
|
|
320
|
+
throw new Error('Failed to upload thumbnail');
|
|
321
|
+
}
|
|
322
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// 10 MB uploaded as blocks still uploaded
|
|
326
|
+
await verifyFailure('Failed to upload thumbnail', 10 * 1024 * 1024);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should handle one time-off failure when uploading thubmnail', async () => {
|
|
330
|
+
let count = 0;
|
|
331
|
+
apiService.uploadBlock = jest.fn().mockImplementation(async function (bareUrl, token, block, onProgress) {
|
|
332
|
+
if (token === 'token/thumbnail:1' && count === 0) {
|
|
333
|
+
count++;
|
|
334
|
+
throw new Error('Failed to upload thumbnail');
|
|
335
|
+
}
|
|
336
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await verifySuccess();
|
|
340
|
+
expect(apiService.requestBlockUpload).toHaveBeenCalledTimes(1);
|
|
341
|
+
// 3 blocks + 1 retry + 1 thumbnail
|
|
342
|
+
expect(apiService.uploadBlock).toHaveBeenCalledTimes(5);
|
|
343
|
+
await verifyOnProgress([4 * 1024 * 1024, 4 * 1024 * 1024, 2 * 1024 * 1024, 1024]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should handle failure when uploading block', async () => {
|
|
347
|
+
apiService.uploadBlock = jest.fn().mockImplementation(async function (bareUrl, token, block, onProgress) {
|
|
348
|
+
if (token === 'token/block:3') {
|
|
349
|
+
throw new Error('Failed to upload block');
|
|
350
|
+
}
|
|
351
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ~8 MB uploaded as 2 first blocks + 1 thumbnail still uploaded
|
|
355
|
+
await verifyFailure('Failed to upload block', 8 * 1024 * 1024 + 1024);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should handle one time-off failure when uploading block', async () => {
|
|
359
|
+
let count = 0;
|
|
360
|
+
apiService.uploadBlock = jest.fn().mockImplementation(async function (bareUrl, token, block, onProgress) {
|
|
361
|
+
if (token === 'token/block:2' && count === 0) {
|
|
362
|
+
count++;
|
|
363
|
+
throw new Error('Failed to upload block');
|
|
364
|
+
}
|
|
365
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
await verifySuccess();
|
|
369
|
+
expect(apiService.requestBlockUpload).toHaveBeenCalledTimes(1);
|
|
370
|
+
// 3 blocks + 1 retry + 1 thumbnail
|
|
371
|
+
expect(apiService.uploadBlock).toHaveBeenCalledTimes(5);
|
|
372
|
+
await verifyOnProgress([1024, 4 * 1024 * 1024, 2 * 1024 * 1024, 4 * 1024 * 1024]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should handle expired token when uploading block', async () => {
|
|
376
|
+
let count = 0;
|
|
377
|
+
apiService.uploadBlock = jest.fn().mockImplementation(async function (bareUrl, token, block, onProgress) {
|
|
378
|
+
if (token === 'token/block:2' && count === 0) {
|
|
379
|
+
count++;
|
|
380
|
+
throw new APIHTTPError('Expired token', HTTPErrorCode.NOT_FOUND);
|
|
381
|
+
}
|
|
382
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await verifySuccess();
|
|
386
|
+
// 1 for first try + 1 for retry
|
|
387
|
+
expect(apiService.requestBlockUpload).toHaveBeenCalledTimes(2);
|
|
388
|
+
expect(apiService.requestBlockUpload).toHaveBeenCalledWith(
|
|
389
|
+
revisionDraft.nodeRevisionUid,
|
|
390
|
+
revisionDraft.nodeKeys.signatureAddress.addressId,
|
|
391
|
+
{
|
|
392
|
+
contentBlocks: [
|
|
393
|
+
{
|
|
394
|
+
index: 2,
|
|
395
|
+
encryptedSize: 4 * 1024 * 1024 + 10000,
|
|
396
|
+
hash: 'blockHash',
|
|
397
|
+
armoredSignature: 'signature',
|
|
398
|
+
verificationToken: 'verificationToken',
|
|
399
|
+
}
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
// 3 blocks + 1 retry + 1 thumbnail
|
|
404
|
+
expect(apiService.uploadBlock).toHaveBeenCalledTimes(5);
|
|
405
|
+
await verifyOnProgress([1024, 4 * 1024 * 1024, 2 * 1024 * 1024, 4 * 1024 * 1024]);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should handle abortion', async () => {
|
|
409
|
+
const error = new Error('Aborted');
|
|
410
|
+
const promise = uploader.start(stream, thumbnails, onProgress);
|
|
411
|
+
abortController.abort(error);
|
|
412
|
+
await promise;
|
|
413
|
+
expect(apiService.uploadBlock.mock.calls[0][4]?.aborted).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('verifyIntegrity', () => {
|
|
417
|
+
it('should report block verification error', async () => {
|
|
418
|
+
blockVerifier.verifyBlock = jest.fn().mockRejectedValue(new IntegrityError('Block verification error'));
|
|
419
|
+
await verifyFailure('Block verification error', 1024);
|
|
420
|
+
expect(telemetry.logBlockVerificationError).toHaveBeenCalledWith(false);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should report block verification error when retry helped', async () => {
|
|
424
|
+
blockVerifier.verifyBlock = jest.fn().mockRejectedValueOnce(new IntegrityError('Block verification error')).mockResolvedValue({
|
|
425
|
+
verificationToken: new Uint8Array(),
|
|
426
|
+
});
|
|
427
|
+
await verifySuccess();
|
|
428
|
+
expect(telemetry.logBlockVerificationError).toHaveBeenCalledWith(true);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should throw an error if block count does not match', async () => {
|
|
432
|
+
uploader = new StreamUploader(
|
|
433
|
+
telemetry,
|
|
434
|
+
apiService,
|
|
435
|
+
cryptoService,
|
|
436
|
+
uploadManager,
|
|
437
|
+
blockVerifier,
|
|
438
|
+
revisionDraft,
|
|
439
|
+
{
|
|
440
|
+
// Fake expected size to break verification
|
|
441
|
+
expectedSize: 1 * 1024 * 1024 + 1024,
|
|
442
|
+
mediaType: '',
|
|
443
|
+
},
|
|
444
|
+
onFinish,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
await verifyFailure(
|
|
448
|
+
'Some file parts failed to upload',
|
|
449
|
+
10 * 1024 * 1024 + 1024,
|
|
450
|
+
1 * 1024 * 1024 + 1024,
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should throw an error if file size does not match', async () => {
|
|
455
|
+
cryptoService.encryptBlock = jest.fn().mockImplementation(async (_, __, block, index) => ({
|
|
456
|
+
index,
|
|
457
|
+
encryptedData: block,
|
|
458
|
+
armoredSignature: 'signature',
|
|
459
|
+
verificationToken: 'verificationToken',
|
|
460
|
+
originalSize: 0, // Fake original size to break verification
|
|
461
|
+
encryptedSize: block.length + 10000,
|
|
462
|
+
hash: 'blockHash',
|
|
463
|
+
}));
|
|
464
|
+
|
|
465
|
+
await verifyFailure('Some file bytes failed to upload', 10 * 1024 * 1024 + 1024);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|