243 lines
6.6 KiB
TypeScript
243 lines
6.6 KiB
TypeScript
|
// Copyright 2024 Signal Messenger, LLC
|
||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
|
||
|
import { assert } from 'chai';
|
||
|
import * as sinon from 'sinon';
|
||
|
import { IMAGE_PNG } from '../../types/MIME';
|
||
|
import {
|
||
|
AttachmentPermanentlyUndownloadableError,
|
||
|
downloadAttachment,
|
||
|
} from '../../util/downloadAttachment';
|
||
|
import { MediaTier } from '../../types/AttachmentDownload';
|
||
|
import { HTTPError } from '../../textsecure/Errors';
|
||
|
import { getCdnNumberForBackupTier } from '../../textsecure/downloadAttachment';
|
||
|
import { MASTER_KEY } from '../backup/helpers';
|
||
|
import { getMediaIdFromMediaName } from '../../services/backups/util/mediaId';
|
||
|
|
||
|
describe('utils/downloadAttachment', () => {
|
||
|
const baseAttachment = {
|
||
|
size: 100,
|
||
|
contentType: IMAGE_PNG,
|
||
|
};
|
||
|
|
||
|
let sandbox: sinon.SinonSandbox;
|
||
|
const fakeServer = {};
|
||
|
beforeEach(() => {
|
||
|
sandbox = sinon.createSandbox();
|
||
|
sandbox.stub(window, 'textsecure').value({ server: fakeServer });
|
||
|
});
|
||
|
afterEach(() => {
|
||
|
sandbox.restore();
|
||
|
});
|
||
|
|
||
|
it('downloads from transit tier first if no backup information', async () => {
|
||
|
const stubDownload = sinon.stub();
|
||
|
const attachment = {
|
||
|
...baseAttachment,
|
||
|
cdnKey: 'cdnKey',
|
||
|
cdnNumber: 2,
|
||
|
};
|
||
|
await downloadAttachment(attachment, {
|
||
|
downloadAttachmentFromServer: stubDownload,
|
||
|
});
|
||
|
assert.equal(stubDownload.callCount, 1);
|
||
|
assert.deepEqual(stubDownload.getCall(0).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.STANDARD },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('throw permanently missing error if attachment fails with 404 and no backup information', async () => {
|
||
|
const stubDownload = sinon
|
||
|
.stub()
|
||
|
.onFirstCall()
|
||
|
.throws(new HTTPError('not found', { code: 404, headers: {} }));
|
||
|
|
||
|
const attachment = {
|
||
|
...baseAttachment,
|
||
|
cdnKey: 'cdnKey',
|
||
|
cdnNumber: 2,
|
||
|
};
|
||
|
await assert.isRejected(
|
||
|
downloadAttachment(attachment, {
|
||
|
downloadAttachmentFromServer: stubDownload,
|
||
|
}),
|
||
|
AttachmentPermanentlyUndownloadableError
|
||
|
);
|
||
|
|
||
|
assert.equal(stubDownload.callCount, 1);
|
||
|
assert.deepEqual(stubDownload.getCall(0).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.STANDARD },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('downloads from backup tier first if there is backup information', async () => {
|
||
|
const stubDownload = sinon.stub();
|
||
|
const attachment = {
|
||
|
...baseAttachment,
|
||
|
cdnKey: 'cdnKey',
|
||
|
cdnNumber: 2,
|
||
|
backupLocator: {
|
||
|
mediaName: 'medianame',
|
||
|
},
|
||
|
};
|
||
|
await downloadAttachment(attachment, {
|
||
|
downloadAttachmentFromServer: stubDownload,
|
||
|
});
|
||
|
assert.equal(stubDownload.callCount, 1);
|
||
|
assert.deepEqual(stubDownload.getCall(0).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.BACKUP },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('falls back to transit tier if backup download fails with 404', async () => {
|
||
|
const stubDownload = sinon
|
||
|
.stub()
|
||
|
.onFirstCall()
|
||
|
.throws(new HTTPError('not found', { code: 404, headers: {} }));
|
||
|
|
||
|
const attachment = {
|
||
|
...baseAttachment,
|
||
|
cdnKey: 'cdnKey',
|
||
|
cdnNumber: 2,
|
||
|
backupLocator: {
|
||
|
mediaName: 'medianame',
|
||
|
},
|
||
|
};
|
||
|
await downloadAttachment(attachment, {
|
||
|
downloadAttachmentFromServer: stubDownload,
|
||
|
});
|
||
|
assert.equal(stubDownload.callCount, 2);
|
||
|
assert.deepEqual(stubDownload.getCall(0).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.BACKUP },
|
||
|
]);
|
||
|
assert.deepEqual(stubDownload.getCall(1).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.STANDARD },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('falls back to transit tier if backup download fails with any other error', async () => {
|
||
|
const stubDownload = sinon
|
||
|
.stub()
|
||
|
.onFirstCall()
|
||
|
.throws(new Error('could not decrypt!'));
|
||
|
|
||
|
const attachment = {
|
||
|
...baseAttachment,
|
||
|
cdnKey: 'cdnKey',
|
||
|
cdnNumber: 2,
|
||
|
backupLocator: {
|
||
|
mediaName: 'medianame',
|
||
|
},
|
||
|
};
|
||
|
await downloadAttachment(attachment, {
|
||
|
downloadAttachmentFromServer: stubDownload,
|
||
|
});
|
||
|
assert.equal(stubDownload.callCount, 2);
|
||
|
assert.deepEqual(stubDownload.getCall(0).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.BACKUP },
|
||
|
]);
|
||
|
assert.deepEqual(stubDownload.getCall(1).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.STANDARD },
|
||
|
]);
|
||
|
});
|
||
|
|
||
|
it('does not throw permanently missing error if not found on transit tier but there is backuplocator', async () => {
|
||
|
const stubDownload = sinon
|
||
|
.stub()
|
||
|
.throws(new HTTPError('not found', { code: 404, headers: {} }));
|
||
|
|
||
|
const attachment = {
|
||
|
...baseAttachment,
|
||
|
cdnKey: 'cdnKey',
|
||
|
cdnNumber: 2,
|
||
|
backupLocator: {
|
||
|
mediaName: 'medianame',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
await assert.isRejected(
|
||
|
downloadAttachment(attachment, {
|
||
|
downloadAttachmentFromServer: stubDownload,
|
||
|
}),
|
||
|
HTTPError
|
||
|
);
|
||
|
assert.equal(stubDownload.callCount, 2);
|
||
|
assert.deepEqual(stubDownload.getCall(0).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.BACKUP },
|
||
|
]);
|
||
|
assert.deepEqual(stubDownload.getCall(1).args, [
|
||
|
fakeServer,
|
||
|
attachment,
|
||
|
{ mediaTier: MediaTier.STANDARD },
|
||
|
]);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('getCdnNumberForBackupTier', () => {
|
||
|
let sandbox: sinon.SinonSandbox;
|
||
|
|
||
|
beforeEach(() => {
|
||
|
sandbox = sinon.createSandbox();
|
||
|
sandbox.stub(window.storage, 'get').callsFake(key => {
|
||
|
if (key === 'masterKey') {
|
||
|
return MASTER_KEY;
|
||
|
}
|
||
|
return undefined;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
afterEach(async () => {
|
||
|
await window.Signal.Data.clearAllBackupCdnObjectMetadata();
|
||
|
sandbox.restore();
|
||
|
});
|
||
|
|
||
|
const baseAttachment = {
|
||
|
size: 100,
|
||
|
contentType: IMAGE_PNG,
|
||
|
};
|
||
|
it('uses cdnNumber on attachment', async () => {
|
||
|
const result = await getCdnNumberForBackupTier({
|
||
|
...baseAttachment,
|
||
|
backupLocator: { mediaName: 'mediaName', cdnNumber: 4 },
|
||
|
});
|
||
|
assert.equal(result, 4);
|
||
|
});
|
||
|
it('uses default cdn number if none on attachment', async () => {
|
||
|
const result = await getCdnNumberForBackupTier({
|
||
|
...baseAttachment,
|
||
|
backupLocator: { mediaName: 'mediaName' },
|
||
|
});
|
||
|
assert.equal(result, 3);
|
||
|
});
|
||
|
it('uses cdn number in DB if none on attachment', async () => {
|
||
|
await window.Signal.Data.saveBackupCdnObjectMetadata([
|
||
|
{
|
||
|
mediaId: getMediaIdFromMediaName('mediaName').string,
|
||
|
cdnNumber: 42,
|
||
|
sizeOnBackupCdn: 128,
|
||
|
},
|
||
|
]);
|
||
|
const result = await getCdnNumberForBackupTier({
|
||
|
...baseAttachment,
|
||
|
backupLocator: { mediaName: 'mediaName' },
|
||
|
});
|
||
|
assert.equal(result, 42);
|
||
|
});
|
||
|
});
|