Support thumbnail export & import during backup of visual attachments

This commit is contained in:
trevor-signal 2024-07-16 16:39:56 -04:00 committed by GitHub
parent 451ee56c92
commit 61548061b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1326 additions and 327 deletions

View file

@ -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),

View file

@ -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,

View file

@ -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]
);
});
});
});

View file

@ -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,
});
});
});
});

View file

@ -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',
},
]);
});
});