Support thumbnail export & import during backup of visual attachments
This commit is contained in:
parent
451ee56c92
commit
61548061b8
30 changed files with 1326 additions and 327 deletions
|
@ -577,6 +577,7 @@ describe('Crypto', () => {
|
|||
writeFileSync(ciphertextPath, encryptedAttachment.ciphertext);
|
||||
|
||||
const decryptedAttachment = await decryptAttachmentV2({
|
||||
type: 'standard',
|
||||
ciphertextPath,
|
||||
idForLogging: 'test',
|
||||
...splitKeys(keys),
|
||||
|
@ -634,6 +635,7 @@ describe('Crypto', () => {
|
|||
);
|
||||
|
||||
const decryptedAttachment = await decryptAttachmentV2({
|
||||
type: 'standard',
|
||||
ciphertextPath,
|
||||
idForLogging: 'test',
|
||||
...splitKeys(keys),
|
||||
|
@ -931,6 +933,7 @@ describe('Crypto', () => {
|
|||
outerCiphertextPath = encryptResult.outerCiphertextPath;
|
||||
|
||||
const decryptedAttachment = await decryptAttachmentV2({
|
||||
type: 'standard',
|
||||
ciphertextPath: outerCiphertextPath,
|
||||
idForLogging: 'test',
|
||||
...splitKeys(innerKeys),
|
||||
|
@ -986,6 +989,7 @@ describe('Crypto', () => {
|
|||
outerCiphertextPath = encryptResult.outerCiphertextPath;
|
||||
|
||||
const decryptedAttachment = await decryptAttachmentV2({
|
||||
type: 'standard',
|
||||
ciphertextPath: outerCiphertextPath,
|
||||
idForLogging: 'test',
|
||||
...splitKeys(innerKeys),
|
||||
|
@ -1035,6 +1039,7 @@ describe('Crypto', () => {
|
|||
|
||||
await assert.isRejected(
|
||||
decryptAttachmentV2({
|
||||
type: 'standard',
|
||||
ciphertextPath: outerCiphertextPath,
|
||||
idForLogging: 'test',
|
||||
...splitKeys(innerKeys),
|
||||
|
|
|
@ -17,6 +17,7 @@ import type { AttachmentType } from '../../types/Attachment';
|
|||
import { strictAssert } from '../../util/assert';
|
||||
import type { GetBackupCdnInfoType } from '../../services/backups/util/mediaId';
|
||||
import { MASTER_KEY } from './helpers';
|
||||
import { getRandomBytes } from '../../Crypto';
|
||||
|
||||
describe('convertFilePointerToAttachment', () => {
|
||||
it('processes filepointer with attachmentLocator', () => {
|
||||
|
@ -179,6 +180,8 @@ function composeAttachment(
|
|||
incrementalMac: 'incrementalMac',
|
||||
incrementalMacChunkSize: 1000,
|
||||
uploadTimestamp: 1234,
|
||||
localKey: Bytes.toBase64(getRandomBytes(32)),
|
||||
version: 2,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
@ -545,6 +548,12 @@ describe('getFilePointerForAttachment', () => {
|
|||
});
|
||||
|
||||
describe('getBackupJobForAttachmentAndFilePointer', async () => {
|
||||
beforeEach(async () => {
|
||||
await window.storage.put('masterKey', Bytes.toBase64(getRandomBytes(32)));
|
||||
});
|
||||
afterEach(async () => {
|
||||
await window.Signal.Data.removeAll();
|
||||
});
|
||||
const attachment = composeAttachment();
|
||||
|
||||
it('returns null if filePointer does not have backupLocator', async () => {
|
||||
|
@ -590,6 +599,8 @@ describe('getBackupJobForAttachmentAndFilePointer', async () => {
|
|||
digest: 'digest',
|
||||
iv: 'iv',
|
||||
size: 100,
|
||||
localKey: attachment.localKey,
|
||||
version: attachment.version,
|
||||
transitCdnInfo: {
|
||||
cdnKey: 'cdnKey',
|
||||
cdnNumber: 2,
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
import * as sinon from 'sinon';
|
||||
import { assert } from 'chai';
|
||||
import { join } from 'path';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { ensureFile } from 'fs-extra';
|
||||
|
||||
import * as Bytes from '../../Bytes';
|
||||
import {
|
||||
|
@ -14,28 +16,40 @@ import {
|
|||
import type {
|
||||
AttachmentBackupJobType,
|
||||
CoreAttachmentBackupJobType,
|
||||
StandardAttachmentBackupJobType,
|
||||
ThumbnailAttachmentBackupJobType,
|
||||
} from '../../types/AttachmentBackup';
|
||||
import dataInterface from '../../sql/Client';
|
||||
import { getRandomBytes } from '../../Crypto';
|
||||
import { VIDEO_MP4 } from '../../types/MIME';
|
||||
import { APPLICATION_OCTET_STREAM, VIDEO_MP4 } from '../../types/MIME';
|
||||
import { createName, getRelativePath } from '../../util/attachmentPath';
|
||||
import {
|
||||
encryptAttachmentV2,
|
||||
generateKeys,
|
||||
safeUnlinkSync,
|
||||
} from '../../AttachmentCrypto';
|
||||
|
||||
const TRANSIT_CDN = 2;
|
||||
const TRANSIT_CDN_FOR_NEW_UPLOAD = 42;
|
||||
const BACKUP_CDN = 3;
|
||||
|
||||
const RELATIVE_ATTACHMENT_PATH = getRelativePath(createName());
|
||||
const LOCAL_ENCRYPTION_KEYS = Bytes.toBase64(generateKeys());
|
||||
const ATTACHMENT_SIZE = 3577986;
|
||||
|
||||
describe('AttachmentBackupManager/JobManager', () => {
|
||||
let backupManager: AttachmentBackupManager | undefined;
|
||||
let runJob: sinon.SinonSpy;
|
||||
let backupMediaBatch: sinon.SinonStub;
|
||||
let backupsService = {};
|
||||
let encryptAndUploadAttachment: sinon.SinonStub;
|
||||
let getAbsoluteAttachmentPath: sinon.SinonStub;
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let isInCall: sinon.SinonStub;
|
||||
|
||||
function composeJob(
|
||||
index: number,
|
||||
overrides: Partial<CoreAttachmentBackupJobType['data']> = {}
|
||||
): CoreAttachmentBackupJobType {
|
||||
): StandardAttachmentBackupJobType {
|
||||
const mediaName = `mediaName${index}`;
|
||||
|
||||
return {
|
||||
|
@ -43,17 +57,41 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
type: 'standard',
|
||||
receivedAt: index,
|
||||
data: {
|
||||
path: 'ghost-kitty.mp4',
|
||||
path: RELATIVE_ATTACHMENT_PATH,
|
||||
contentType: VIDEO_MP4,
|
||||
keys: 'keys=',
|
||||
iv: 'iv==',
|
||||
digest: 'digest=',
|
||||
version: 2,
|
||||
localKey: LOCAL_ENCRYPTION_KEYS,
|
||||
transitCdnInfo: {
|
||||
cdnKey: 'transitCdnKey',
|
||||
cdnNumber: TRANSIT_CDN,
|
||||
uploadTimestamp: Date.now(),
|
||||
},
|
||||
size: 128,
|
||||
size: ATTACHMENT_SIZE,
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function composeThumbnailJob(
|
||||
index: number,
|
||||
overrides: Partial<ThumbnailAttachmentBackupJobType['data']> = {}
|
||||
): ThumbnailAttachmentBackupJobType {
|
||||
const mediaName = `thumbnail${index}`;
|
||||
|
||||
return {
|
||||
mediaName,
|
||||
type: 'thumbnail',
|
||||
receivedAt: index,
|
||||
data: {
|
||||
fullsizePath: RELATIVE_ATTACHMENT_PATH,
|
||||
fullsizeSize: ATTACHMENT_SIZE,
|
||||
contentType: VIDEO_MP4,
|
||||
version: 2,
|
||||
localKey: LOCAL_ENCRYPTION_KEYS,
|
||||
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
|
@ -83,14 +121,9 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
cdnKey: 'newKeyOnTransitTier',
|
||||
cdnNumber: TRANSIT_CDN_FOR_NEW_UPLOAD,
|
||||
});
|
||||
const decryptAttachmentV2ToSink = sinon.stub();
|
||||
|
||||
getAbsoluteAttachmentPath = sandbox.stub().callsFake(path => {
|
||||
if (path === 'ghost-kitty.mp4') {
|
||||
return join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
}
|
||||
return getAbsoluteAttachmentPath.wrappedMethod(path);
|
||||
});
|
||||
|
||||
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
|
||||
runJob = sandbox.stub().callsFake((job: AttachmentBackupJobType) => {
|
||||
return runAttachmentBackupJob(job, false, {
|
||||
// @ts-expect-error incomplete stubbing
|
||||
|
@ -98,6 +131,7 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
backupMediaBatch,
|
||||
getAbsoluteAttachmentPath,
|
||||
encryptAndUploadAttachment,
|
||||
decryptAttachmentV2ToSink,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -106,18 +140,34 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
shouldHoldOffOnStartingQueuedJobs: isInCall,
|
||||
runJob,
|
||||
});
|
||||
|
||||
const absolutePath = getAbsoluteAttachmentPath(RELATIVE_ATTACHMENT_PATH);
|
||||
await ensureFile(absolutePath);
|
||||
await encryptAttachmentV2({
|
||||
plaintext: {
|
||||
absolutePath: join(__dirname, '../../../fixtures/ghost-kitty.mp4'),
|
||||
},
|
||||
keys: Bytes.fromBase64(LOCAL_ENCRYPTION_KEYS),
|
||||
sink: createWriteStream(absolutePath),
|
||||
getAbsoluteAttachmentPath,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore();
|
||||
delete window.textsecure.server;
|
||||
safeUnlinkSync(
|
||||
window.Signal.Migrations.getAbsoluteAttachmentPath(
|
||||
RELATIVE_ATTACHMENT_PATH
|
||||
)
|
||||
);
|
||||
await backupManager?.stop();
|
||||
});
|
||||
|
||||
async function addJobs(
|
||||
num: number,
|
||||
overrides: Partial<CoreAttachmentBackupJobType['data']> = {}
|
||||
): Promise<Array<CoreAttachmentBackupJobType>> {
|
||||
overrides: Partial<StandardAttachmentBackupJobType['data']> = {}
|
||||
): Promise<Array<StandardAttachmentBackupJobType>> {
|
||||
const jobs = new Array(num)
|
||||
.fill(null)
|
||||
.map((_, idx) => composeJob(idx, overrides));
|
||||
|
@ -128,6 +178,20 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
return jobs;
|
||||
}
|
||||
|
||||
async function addThumbnailJobs(
|
||||
num: number,
|
||||
overrides: Partial<ThumbnailAttachmentBackupJobType['data']> = {}
|
||||
): Promise<Array<ThumbnailAttachmentBackupJobType>> {
|
||||
const jobs = new Array(num)
|
||||
.fill(null)
|
||||
.map((_, idx) => composeThumbnailJob(idx, overrides));
|
||||
for (const job of jobs) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await backupManager?.addJob(job);
|
||||
}
|
||||
return jobs;
|
||||
}
|
||||
|
||||
function waitForJobToBeStarted(
|
||||
job: CoreAttachmentBackupJobType,
|
||||
attempts: number = 0
|
||||
|
@ -156,22 +220,13 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
});
|
||||
}
|
||||
|
||||
it('saves jobs, removes jobs, and runs 3 jobs at a time in descending receivedAt order', async () => {
|
||||
it('runs 3 jobs at a time in descending receivedAt order, fullsize first', async () => {
|
||||
const jobs = await addJobs(5);
|
||||
const thumbnailJobs = await addThumbnailJobs(5);
|
||||
|
||||
// Confirm they are saved to DB
|
||||
const allJobs = await getAllSavedJobs();
|
||||
assert.strictEqual(allJobs.length, 5);
|
||||
assert.strictEqual(
|
||||
JSON.stringify(allJobs.map(job => job.mediaName)),
|
||||
JSON.stringify([
|
||||
'mediaName4',
|
||||
'mediaName3',
|
||||
'mediaName2',
|
||||
'mediaName1',
|
||||
'mediaName0',
|
||||
])
|
||||
);
|
||||
assert.strictEqual(allJobs.length, 10);
|
||||
|
||||
await backupManager?.start();
|
||||
await waitForJobToBeStarted(jobs[2]);
|
||||
|
@ -183,7 +238,21 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
assert.strictEqual(runJob.callCount, 5);
|
||||
assertRunJobCalledWith([jobs[4], jobs[3], jobs[2], jobs[1], jobs[0]]);
|
||||
|
||||
await waitForJobToBeCompleted(jobs[0]);
|
||||
await waitForJobToBeCompleted(thumbnailJobs[0]);
|
||||
assert.strictEqual(runJob.callCount, 10);
|
||||
|
||||
assertRunJobCalledWith([
|
||||
jobs[4],
|
||||
jobs[3],
|
||||
jobs[2],
|
||||
jobs[1],
|
||||
jobs[0],
|
||||
thumbnailJobs[4],
|
||||
thumbnailJobs[3],
|
||||
thumbnailJobs[2],
|
||||
thumbnailJobs[1],
|
||||
thumbnailJobs[0],
|
||||
]);
|
||||
assert.strictEqual((await getAllSavedJobs()).length, 0);
|
||||
});
|
||||
|
||||
|
@ -250,7 +319,11 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
|
||||
it('without transitCdnInfo, will permanently remove job if file not found at path', async () => {
|
||||
const [job] = await addJobs(1, { transitCdnInfo: undefined });
|
||||
getAbsoluteAttachmentPath.returns('no/file/here');
|
||||
safeUnlinkSync(
|
||||
window.Signal.Migrations.getAbsoluteAttachmentPath(
|
||||
RELATIVE_ATTACHMENT_PATH
|
||||
)
|
||||
);
|
||||
await backupManager?.start();
|
||||
await waitForJobToBeCompleted(job);
|
||||
|
||||
|
@ -261,4 +334,28 @@ describe('AttachmentBackupManager/JobManager', () => {
|
|||
const allRemainingJobs = await getAllSavedJobs();
|
||||
assert.strictEqual(allRemainingJobs.length, 0);
|
||||
});
|
||||
|
||||
describe('thumbnail backups', () => {
|
||||
it('addJobAndMaybeThumbnailJob conditionally adds thumbnail job', async () => {
|
||||
const jobForVisualAttachment = composeJob(0);
|
||||
const jobForNonVisualAttachment = composeJob(1, {
|
||||
contentType: APPLICATION_OCTET_STREAM,
|
||||
});
|
||||
|
||||
await backupManager?.addJobAndMaybeThumbnailJob(jobForVisualAttachment);
|
||||
await backupManager?.addJobAndMaybeThumbnailJob(
|
||||
jobForNonVisualAttachment
|
||||
);
|
||||
|
||||
const thumbnailMediaName = Bytes.toBase64(
|
||||
Bytes.fromString(`${jobForVisualAttachment.mediaName}_thumbnail`)
|
||||
);
|
||||
const allJobs = await getAllSavedJobs();
|
||||
assert.strictEqual(allJobs.length, 3);
|
||||
assert.sameMembers(
|
||||
allJobs.map(job => job.mediaName),
|
||||
['mediaName1', 'mediaName0', thumbnailMediaName]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,17 +5,52 @@
|
|||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import * as sinon from 'sinon';
|
||||
import { assert } from 'chai';
|
||||
import * as MIME from '../../types/MIME';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import * as MIME from '../../types/MIME';
|
||||
import {
|
||||
AttachmentDownloadManager,
|
||||
AttachmentDownloadUrgency,
|
||||
runDownloadAttachmentJobInner,
|
||||
type NewAttachmentDownloadJobType,
|
||||
} from '../../jobs/AttachmentDownloadManager';
|
||||
import type { AttachmentDownloadJobType } from '../../types/AttachmentDownload';
|
||||
import dataInterface from '../../sql/Client';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
import { type AciString } from '../../types/ServiceId';
|
||||
import { type AttachmentType, AttachmentVariant } from '../../types/Attachment';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
||||
function composeJob({
|
||||
messageId,
|
||||
receivedAt,
|
||||
attachmentOverrides,
|
||||
}: Pick<NewAttachmentDownloadJobType, 'messageId' | 'receivedAt'> & {
|
||||
attachmentOverrides?: Partial<AttachmentType>;
|
||||
}): AttachmentDownloadJobType {
|
||||
const digest = `digestFor${messageId}`;
|
||||
const size = 128;
|
||||
const contentType = MIME.IMAGE_PNG;
|
||||
return {
|
||||
messageId,
|
||||
receivedAt,
|
||||
sentAt: receivedAt,
|
||||
attachmentType: 'attachment',
|
||||
digest,
|
||||
size,
|
||||
contentType,
|
||||
active: false,
|
||||
attempts: 0,
|
||||
retryAfter: null,
|
||||
lastAttemptTimestamp: null,
|
||||
attachment: {
|
||||
contentType,
|
||||
size,
|
||||
digest: `digestFor${messageId}`,
|
||||
...attachmentOverrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('AttachmentDownloadManager/JobManager', () => {
|
||||
let downloadManager: AttachmentDownloadManager | undefined;
|
||||
|
@ -24,36 +59,6 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
let clock: sinon.SinonFakeTimers;
|
||||
let isInCall: sinon.SinonStub;
|
||||
|
||||
function composeJob({
|
||||
messageId,
|
||||
receivedAt,
|
||||
}: Pick<
|
||||
NewAttachmentDownloadJobType,
|
||||
'messageId' | 'receivedAt'
|
||||
>): AttachmentDownloadJobType {
|
||||
const digest = `digestFor${messageId}`;
|
||||
const size = 128;
|
||||
const contentType = MIME.IMAGE_PNG;
|
||||
return {
|
||||
messageId,
|
||||
receivedAt,
|
||||
sentAt: receivedAt,
|
||||
attachmentType: 'attachment',
|
||||
digest,
|
||||
size,
|
||||
contentType,
|
||||
active: false,
|
||||
attempts: 0,
|
||||
retryAfter: null,
|
||||
lastAttemptTimestamp: null,
|
||||
attachment: {
|
||||
contentType,
|
||||
size,
|
||||
digest: `digestFor${messageId}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await dataInterface.removeAll();
|
||||
|
||||
|
@ -72,13 +77,13 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
downloadManager = new AttachmentDownloadManager({
|
||||
...AttachmentDownloadManager.defaultParams,
|
||||
shouldHoldOffOnStartingQueuedJobs: isInCall,
|
||||
runJob,
|
||||
runDownloadAttachmentJob: runJob,
|
||||
getRetryConfig: () => ({
|
||||
maxAttempts: 5,
|
||||
backoffConfig: {
|
||||
multiplier: 5,
|
||||
multiplier: 2,
|
||||
firstBackoffs: [MINUTE],
|
||||
maxBackoffTime: 30 * MINUTE,
|
||||
maxBackoffTime: 10 * MINUTE,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
@ -143,7 +148,7 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
.getCalls()
|
||||
.map(
|
||||
call =>
|
||||
`${call.args[0].messageId}${call.args[0].attachmentType}.${call.args[0].digest}`
|
||||
`${call.args[0].job.messageId}${call.args[0].job.attachmentType}.${call.args[0].job.digest}`
|
||||
)
|
||||
),
|
||||
JSON.stringify(
|
||||
|
@ -158,8 +163,13 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
// prior (unfinished) invocations can prevent subsequent calls after the clock is
|
||||
// ticked forward and make tests unreliable
|
||||
await dataInterface.getAllItems();
|
||||
await clock.tickAsync(ms);
|
||||
await dataInterface.getAllItems();
|
||||
const now = Date.now();
|
||||
while (Date.now() < now + ms) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await clock.tickAsync(downloadManager?.tickInterval ?? 1000);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await dataInterface.getAllItems();
|
||||
}
|
||||
}
|
||||
|
||||
function getPromisesForAttempts(
|
||||
|
@ -270,7 +280,7 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
const job0Attempts = getPromisesForAttempts(jobs[0], 1);
|
||||
const job1Attempts = getPromisesForAttempts(jobs[1], 5);
|
||||
|
||||
runJob.callsFake(async (job: AttachmentDownloadJobType) => {
|
||||
runJob.callsFake(async ({ job }: { job: AttachmentDownloadJobType }) => {
|
||||
return new Promise<{ status: 'finished' | 'retry' }>(resolve => {
|
||||
Promise.resolve().then(() => {
|
||||
if (job.messageId === jobs[0].messageId) {
|
||||
|
@ -299,16 +309,16 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
|
||||
await job1Attempts[1].completed;
|
||||
assert.strictEqual(runJob.callCount, 3);
|
||||
await advanceTime(5 * MINUTE);
|
||||
await advanceTime(2 * MINUTE);
|
||||
|
||||
await job1Attempts[2].completed;
|
||||
assert.strictEqual(runJob.callCount, 4);
|
||||
|
||||
await advanceTime(25 * MINUTE);
|
||||
await advanceTime(4 * MINUTE);
|
||||
await job1Attempts[3].completed;
|
||||
assert.strictEqual(runJob.callCount, 5);
|
||||
|
||||
await advanceTime(30 * MINUTE);
|
||||
await advanceTime(8 * MINUTE);
|
||||
await job1Attempts[4].completed;
|
||||
|
||||
assert.strictEqual(runJob.callCount, 6);
|
||||
|
@ -359,15 +369,15 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
await attempts[1].completed;
|
||||
assert.strictEqual(runJob.callCount, 5);
|
||||
|
||||
await advanceTime(5 * MINUTE);
|
||||
await advanceTime(2 * MINUTE);
|
||||
await attempts[2].completed;
|
||||
assert.strictEqual(runJob.callCount, 6);
|
||||
|
||||
await advanceTime(25 * MINUTE);
|
||||
await advanceTime(4 * MINUTE);
|
||||
await attempts[3].completed;
|
||||
assert.strictEqual(runJob.callCount, 7);
|
||||
|
||||
await advanceTime(30 * MINUTE);
|
||||
await advanceTime(8 * MINUTE);
|
||||
await attempts[4].completed;
|
||||
assert.strictEqual(runJob.callCount, 8);
|
||||
|
||||
|
@ -375,3 +385,237 @@ describe('AttachmentDownloadManager/JobManager', () => {
|
|||
assert.isUndefined(await dataInterface.getAttachmentDownloadJob(jobs[0]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('AttachmentDownloadManager/runDownloadAttachmentJob', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let downloadAttachment: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox();
|
||||
downloadAttachment = sandbox.stub().returns({
|
||||
path: '/path/to/file',
|
||||
iv: Buffer.alloc(16),
|
||||
plaintextHash: 'plaintextHash',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
describe('visible message', () => {
|
||||
it('will only download full-size if attachment not from backup', async () => {
|
||||
const job = composeJob({
|
||||
messageId: '1',
|
||||
receivedAt: 1,
|
||||
});
|
||||
|
||||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
dependencies: { downloadAttachment },
|
||||
});
|
||||
|
||||
assert.strictEqual(result.onlyAttemptedBackupThumbnail, false);
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
});
|
||||
it('will download thumbnail if attachment is from backup', async () => {
|
||||
const job = composeJob({
|
||||
messageId: '1',
|
||||
receivedAt: 1,
|
||||
attachmentOverrides: {
|
||||
backupLocator: {
|
||||
mediaName: 'medianame',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
dependencies: { downloadAttachment },
|
||||
});
|
||||
|
||||
strictAssert(
|
||||
result.onlyAttemptedBackupThumbnail === true,
|
||||
'only attempted backup thumbnail'
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
omit(result.attachmentWithThumbnail, 'thumbnailFromBackup'),
|
||||
{
|
||||
contentType: MIME.IMAGE_PNG,
|
||||
size: 128,
|
||||
digest: 'digestFor1',
|
||||
backupLocator: { mediaName: 'medianame' },
|
||||
}
|
||||
);
|
||||
assert.equal(
|
||||
result.attachmentWithThumbnail.thumbnailFromBackup?.path,
|
||||
'/path/to/file'
|
||||
);
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.ThumbnailFromBackup,
|
||||
});
|
||||
});
|
||||
it('will download full size if thumbnail already backed up', async () => {
|
||||
const job = composeJob({
|
||||
messageId: '1',
|
||||
receivedAt: 1,
|
||||
attachmentOverrides: {
|
||||
backupLocator: {
|
||||
mediaName: 'medianame',
|
||||
},
|
||||
thumbnailFromBackup: {
|
||||
path: '/path/to/thumbnail',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
dependencies: { downloadAttachment },
|
||||
});
|
||||
assert.strictEqual(result.onlyAttemptedBackupThumbnail, false);
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
});
|
||||
|
||||
it('will attempt to download full size if thumbnail fails', async () => {
|
||||
downloadAttachment = sandbox.stub().callsFake(() => {
|
||||
throw new Error('error while downloading');
|
||||
});
|
||||
|
||||
const job = composeJob({
|
||||
messageId: '1',
|
||||
receivedAt: 1,
|
||||
attachmentOverrides: {
|
||||
backupLocator: {
|
||||
mediaName: 'medianame',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await assert.isRejected(
|
||||
runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: true,
|
||||
dependencies: { downloadAttachment },
|
||||
})
|
||||
);
|
||||
|
||||
assert.strictEqual(downloadAttachment.callCount, 2);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.ThumbnailFromBackup,
|
||||
});
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(1).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('message not visible', () => {
|
||||
it('will only download full-size if message not visible', async () => {
|
||||
const job = composeJob({
|
||||
messageId: '1',
|
||||
receivedAt: 1,
|
||||
attachmentOverrides: {
|
||||
backupLocator: {
|
||||
mediaName: 'medianame',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
dependencies: { downloadAttachment },
|
||||
});
|
||||
assert.strictEqual(result.onlyAttemptedBackupThumbnail, false);
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
});
|
||||
it('will fallback to thumbnail if main download fails and backuplocator exists', async () => {
|
||||
downloadAttachment = sandbox.stub().callsFake(({ variant }) => {
|
||||
if (variant === AttachmentVariant.Default) {
|
||||
throw new Error('error while downloading');
|
||||
}
|
||||
return {
|
||||
path: '/path/to/thumbnail',
|
||||
iv: Buffer.alloc(16),
|
||||
plaintextHash: 'plaintextHash',
|
||||
};
|
||||
});
|
||||
|
||||
const job = composeJob({
|
||||
messageId: '1',
|
||||
receivedAt: 1,
|
||||
attachmentOverrides: {
|
||||
backupLocator: {
|
||||
mediaName: 'medianame',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
dependencies: { downloadAttachment },
|
||||
});
|
||||
assert.strictEqual(result.onlyAttemptedBackupThumbnail, false);
|
||||
assert.strictEqual(downloadAttachment.callCount, 2);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(1).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.ThumbnailFromBackup,
|
||||
});
|
||||
});
|
||||
|
||||
it("won't fallback to thumbnail if main download fails and no backup locator", async () => {
|
||||
downloadAttachment = sandbox.stub().callsFake(({ variant }) => {
|
||||
if (variant === AttachmentVariant.Default) {
|
||||
throw new Error('error while downloading');
|
||||
}
|
||||
return {
|
||||
path: '/path/to/thumbnail',
|
||||
iv: Buffer.alloc(16),
|
||||
plaintextHash: 'plaintextHash',
|
||||
};
|
||||
});
|
||||
|
||||
const job = composeJob({
|
||||
messageId: '1',
|
||||
receivedAt: 1,
|
||||
});
|
||||
|
||||
await assert.isRejected(
|
||||
runDownloadAttachmentJobInner({
|
||||
job,
|
||||
isForCurrentlyVisibleMessage: false,
|
||||
dependencies: { downloadAttachment },
|
||||
})
|
||||
);
|
||||
|
||||
assert.strictEqual(downloadAttachment.callCount, 1);
|
||||
assert.deepStrictEqual(downloadAttachment.getCall(0).args[0], {
|
||||
attachment: job.attachment,
|
||||
variant: AttachmentVariant.Default,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,11 +13,13 @@ import { HTTPError } from '../../textsecure/Errors';
|
|||
import { getCdnNumberForBackupTier } from '../../textsecure/downloadAttachment';
|
||||
import { MASTER_KEY } from '../backup/helpers';
|
||||
import { getMediaIdFromMediaName } from '../../services/backups/util/mediaId';
|
||||
import { AttachmentVariant } from '../../types/Attachment';
|
||||
|
||||
describe('utils/downloadAttachment', () => {
|
||||
const baseAttachment = {
|
||||
size: 100,
|
||||
contentType: IMAGE_PNG,
|
||||
digest: 'digest',
|
||||
};
|
||||
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
|
@ -37,14 +39,21 @@ describe('utils/downloadAttachment', () => {
|
|||
cdnKey: 'cdnKey',
|
||||
cdnNumber: 2,
|
||||
};
|
||||
await downloadAttachment(attachment, {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
await downloadAttachment({
|
||||
attachment,
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
});
|
||||
assert.equal(stubDownload.callCount, 1);
|
||||
assert.deepEqual(stubDownload.getCall(0).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.STANDARD },
|
||||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -60,8 +69,11 @@ describe('utils/downloadAttachment', () => {
|
|||
cdnNumber: 2,
|
||||
};
|
||||
await assert.isRejected(
|
||||
downloadAttachment(attachment, {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
downloadAttachment({
|
||||
attachment,
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
}),
|
||||
AttachmentPermanentlyUndownloadableError
|
||||
);
|
||||
|
@ -70,7 +82,11 @@ describe('utils/downloadAttachment', () => {
|
|||
assert.deepEqual(stubDownload.getCall(0).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.STANDARD },
|
||||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -84,14 +100,21 @@ describe('utils/downloadAttachment', () => {
|
|||
mediaName: 'medianame',
|
||||
},
|
||||
};
|
||||
await downloadAttachment(attachment, {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
await downloadAttachment({
|
||||
attachment,
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
});
|
||||
assert.equal(stubDownload.callCount, 1);
|
||||
assert.deepEqual(stubDownload.getCall(0).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.BACKUP },
|
||||
{
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -109,19 +132,30 @@ describe('utils/downloadAttachment', () => {
|
|||
mediaName: 'medianame',
|
||||
},
|
||||
};
|
||||
await downloadAttachment(attachment, {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
await downloadAttachment({
|
||||
attachment,
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
});
|
||||
assert.equal(stubDownload.callCount, 2);
|
||||
assert.deepEqual(stubDownload.getCall(0).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.BACKUP },
|
||||
{
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(stubDownload.getCall(1).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.STANDARD },
|
||||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -139,19 +173,30 @@ describe('utils/downloadAttachment', () => {
|
|||
mediaName: 'medianame',
|
||||
},
|
||||
};
|
||||
await downloadAttachment(attachment, {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
await downloadAttachment({
|
||||
attachment,
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
});
|
||||
assert.equal(stubDownload.callCount, 2);
|
||||
assert.deepEqual(stubDownload.getCall(0).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.BACKUP },
|
||||
{
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(stubDownload.getCall(1).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.STANDARD },
|
||||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -170,8 +215,11 @@ describe('utils/downloadAttachment', () => {
|
|||
};
|
||||
|
||||
await assert.isRejected(
|
||||
downloadAttachment(attachment, {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
downloadAttachment({
|
||||
attachment,
|
||||
dependencies: {
|
||||
downloadAttachmentFromServer: stubDownload,
|
||||
},
|
||||
}),
|
||||
HTTPError
|
||||
);
|
||||
|
@ -179,12 +227,20 @@ describe('utils/downloadAttachment', () => {
|
|||
assert.deepEqual(stubDownload.getCall(0).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.BACKUP },
|
||||
{
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(stubDownload.getCall(1).args, [
|
||||
fakeServer,
|
||||
attachment,
|
||||
{ mediaTier: MediaTier.STANDARD },
|
||||
{
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
variant: AttachmentVariant.Default,
|
||||
logPrefix: '[REDACTED]est',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue