Backup support for sticker messages
This commit is contained in:
parent
307e477035
commit
03406b15fa
5 changed files with 381 additions and 39 deletions
|
@ -37,6 +37,7 @@ import {
|
|||
} from './mediaId';
|
||||
import { redactGenericText } from '../../../util/privacy';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
import { toLogFormat } from '../../../types/errors';
|
||||
|
||||
export function convertFilePointerToAttachment(
|
||||
filePointer: Backups.FilePointer
|
||||
|
@ -225,6 +226,16 @@ export async function getFilePointerForAttachment({
|
|||
attachment.digest ?? ''
|
||||
)})`;
|
||||
|
||||
if (attachment.size == null) {
|
||||
log.warn(`${logId}: attachment had nullish size, dropping`);
|
||||
return {
|
||||
filePointer: new Backups.FilePointer({
|
||||
...filePointerRootProps,
|
||||
invalidAttachmentLocator: getInvalidAttachmentLocator(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (!isAttachmentLocallySaved(attachment)) {
|
||||
// 1. If the attachment is undownloaded, we cannot trust its digest / mediaName. Thus,
|
||||
// we only include a BackupLocator if this attachment already had one (e.g. we
|
||||
|
@ -290,7 +301,7 @@ export async function getFilePointerForAttachment({
|
|||
}
|
||||
|
||||
// Some attachments (e.g. those quoted ones copied from the original message) may not
|
||||
// have any encryption info, including a digest!
|
||||
// have any encryption info, including a digest.
|
||||
if (attachment.digest) {
|
||||
// From here on, this attachment is headed to (or already on) the backup tier!
|
||||
const mediaNameForCurrentVersionOfAttachment =
|
||||
|
@ -326,9 +337,25 @@ export async function getFilePointerForAttachment({
|
|||
}
|
||||
}
|
||||
|
||||
let attachmentWithNewEncryptionInfo: AttachmentReadyForBackup | undefined;
|
||||
try {
|
||||
log.info(`${logId}: Generating new encryption info for attachment`);
|
||||
const attachmentWithNewEncryptionInfo =
|
||||
attachmentWithNewEncryptionInfo =
|
||||
await generateNewEncryptionInfoForAttachment(attachment);
|
||||
} catch (e) {
|
||||
log.error(
|
||||
`${logId}: Error when generating new encryption info for attachment`,
|
||||
toLogFormat(e)
|
||||
);
|
||||
|
||||
return {
|
||||
filePointer: new Backups.FilePointer({
|
||||
...filePointerRootProps,
|
||||
invalidAttachmentLocator: getInvalidAttachmentLocator(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
filePointer: new Backups.FilePointer({
|
||||
...filePointerRootProps,
|
||||
|
|
|
@ -59,7 +59,11 @@ import type { LinkPreviewType } from '../../types/message/LinkPreviews';
|
|||
import { getMentionsRegex } from '../../types/Message';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import { isVoiceMessage, canBeDownloaded } from '../../types/Attachment';
|
||||
import {
|
||||
isVoiceMessage,
|
||||
canBeDownloaded,
|
||||
defaultBlurHash,
|
||||
} from '../../types/Attachment';
|
||||
import { type DefaultConversationColorType } from '../../types/Colors';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
|
||||
|
@ -301,16 +305,14 @@ export const getAttachmentsForMessage = ({
|
|||
if (sticker && sticker.data) {
|
||||
const { data } = sticker;
|
||||
|
||||
// We don't show anything if we don't have the sticker or the blurhash...
|
||||
if (!data.blurHash && (data.pending || !data.path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
...data,
|
||||
// We want to show the blurhash for stickers, not the spinner
|
||||
pending: false,
|
||||
// Stickers are not guaranteed to have a blurhash (e.g. if imported but
|
||||
// undownloaded from backup), so we want to make sure we have something to show
|
||||
blurHash: data.blurHash ?? defaultBlurHash(),
|
||||
url: data.path
|
||||
? window.Signal.Migrations.getAbsoluteAttachmentPath(data.path)
|
||||
: undefined,
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
import { v4 as generateGuid } from 'uuid';
|
||||
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
||||
import { omit } from 'lodash';
|
||||
import * as sinon from 'sinon';
|
||||
import { join } from 'path';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import type { ConversationModel } from '../../models/conversations';
|
||||
import * as Bytes from '../../Bytes';
|
||||
|
@ -13,7 +16,13 @@ import { ReadStatus } from '../../messages/MessageReadStatus';
|
|||
import { SeenStatus } from '../../MessageSeenStatus';
|
||||
import { loadCallsHistory } from '../../services/callHistoryLoader';
|
||||
import { setupBasics, asymmetricRoundtripHarness } from './helpers';
|
||||
import { AUDIO_MP3, IMAGE_JPEG, IMAGE_PNG, VIDEO_MP4 } from '../../types/MIME';
|
||||
import {
|
||||
AUDIO_MP3,
|
||||
IMAGE_JPEG,
|
||||
IMAGE_PNG,
|
||||
IMAGE_WEBP,
|
||||
VIDEO_MP4,
|
||||
} from '../../types/MIME';
|
||||
import type {
|
||||
MessageAttributesType,
|
||||
QuotedMessageType,
|
||||
|
@ -21,10 +30,12 @@ import type {
|
|||
import { isVoiceMessage, type AttachmentType } from '../../types/Attachment';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { SignalService } from '../../protobuf';
|
||||
import { getRandomBytes } from '../../Crypto';
|
||||
|
||||
const CONTACT_A = generateAci();
|
||||
|
||||
describe('backup/attachments', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let contactA: ConversationModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -41,6 +52,25 @@ describe('backup/attachments', () => {
|
|||
);
|
||||
|
||||
await loadCallsHistory();
|
||||
|
||||
sandbox = sinon.createSandbox();
|
||||
const getAbsoluteAttachmentPath = sandbox.stub(
|
||||
window.Signal.Migrations,
|
||||
'getAbsoluteAttachmentPath'
|
||||
);
|
||||
getAbsoluteAttachmentPath.callsFake(path => {
|
||||
if (path === 'path/to/sticker') {
|
||||
return join(__dirname, '../../../fixtures/kitten-3-64-64.jpg');
|
||||
}
|
||||
if (path === 'path/to/thumbnail') {
|
||||
return join(__dirname, '../../../fixtures/kitten-3-64-64.jpg');
|
||||
}
|
||||
return getAbsoluteAttachmentPath.wrappedMethod(path);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
function getBase64(str: string): string {
|
||||
|
@ -66,7 +96,7 @@ describe('backup/attachments', () => {
|
|||
width: 150,
|
||||
height: 150,
|
||||
contentType: IMAGE_PNG,
|
||||
path: '/path/to/thumbnail.png',
|
||||
path: 'path/to/thumbnail',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
@ -112,7 +142,7 @@ describe('backup/attachments', () => {
|
|||
],
|
||||
}),
|
||||
],
|
||||
BackupLevel.Messages
|
||||
{ backupLevel: BackupLevel.Messages }
|
||||
);
|
||||
});
|
||||
it('BackupLevel.Media, roundtrips normal attachments', async () => {
|
||||
|
@ -142,7 +172,7 @@ describe('backup/attachments', () => {
|
|||
],
|
||||
}),
|
||||
],
|
||||
BackupLevel.Media
|
||||
{ backupLevel: BackupLevel.Media }
|
||||
);
|
||||
});
|
||||
it('roundtrips voice message attachments', async () => {
|
||||
|
@ -174,7 +204,7 @@ describe('backup/attachments', () => {
|
|||
],
|
||||
}),
|
||||
],
|
||||
BackupLevel.Media
|
||||
{ backupLevel: BackupLevel.Media }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -201,7 +231,7 @@ describe('backup/attachments', () => {
|
|||
],
|
||||
}),
|
||||
],
|
||||
BackupLevel.Messages
|
||||
{ backupLevel: BackupLevel.Messages }
|
||||
);
|
||||
});
|
||||
it('BackupLevel.Media, roundtrips preview attachments', async () => {
|
||||
|
@ -245,7 +275,7 @@ describe('backup/attachments', () => {
|
|||
],
|
||||
}),
|
||||
],
|
||||
BackupLevel.Media
|
||||
{ backupLevel: BackupLevel.Media }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -273,7 +303,7 @@ describe('backup/attachments', () => {
|
|||
],
|
||||
}),
|
||||
],
|
||||
BackupLevel.Messages
|
||||
{ backupLevel: BackupLevel.Messages }
|
||||
);
|
||||
});
|
||||
it('BackupLevel.Media, roundtrips contact attachments', async () => {
|
||||
|
@ -308,7 +338,7 @@ describe('backup/attachments', () => {
|
|||
],
|
||||
}),
|
||||
],
|
||||
BackupLevel.Media
|
||||
{ backupLevel: BackupLevel.Media }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -348,7 +378,7 @@ describe('backup/attachments', () => {
|
|||
},
|
||||
}),
|
||||
],
|
||||
BackupLevel.Messages
|
||||
{ backupLevel: BackupLevel.Messages }
|
||||
);
|
||||
});
|
||||
it('BackupLevel.Media, roundtrips quote attachments', async () => {
|
||||
|
@ -393,7 +423,7 @@ describe('backup/attachments', () => {
|
|||
},
|
||||
}),
|
||||
],
|
||||
BackupLevel.Media
|
||||
{ backupLevel: BackupLevel.Media }
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -459,8 +489,241 @@ describe('backup/attachments', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
BackupLevel.Media
|
||||
{ backupLevel: BackupLevel.Media }
|
||||
);
|
||||
});
|
||||
|
||||
it('handles quotes which have been copied over from the original (and lack all encryption info)', async () => {
|
||||
const originalMessage = composeMessage(1);
|
||||
const quotedMessage: QuotedMessageType = {
|
||||
authorAci: originalMessage.sourceServiceId as AciString,
|
||||
isViewOnce: false,
|
||||
id: originalMessage.timestamp,
|
||||
referencedMessageNotFound: false,
|
||||
messageId: '',
|
||||
isGiftBadge: false,
|
||||
attachments: [
|
||||
{
|
||||
thumbnail: {
|
||||
contentType: IMAGE_PNG,
|
||||
size: 100,
|
||||
path: 'path/to/thumbnail',
|
||||
},
|
||||
contentType: VIDEO_MP4,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const quoteMessage = composeMessage(originalMessage.timestamp + 1, {
|
||||
quote: quotedMessage,
|
||||
});
|
||||
|
||||
await asymmetricRoundtripHarness(
|
||||
[originalMessage, quoteMessage],
|
||||
[
|
||||
originalMessage,
|
||||
{
|
||||
...quoteMessage,
|
||||
quote: {
|
||||
...quotedMessage,
|
||||
referencedMessageNotFound: false,
|
||||
attachments: [
|
||||
{
|
||||
// will do custom comparison for thumbnail below
|
||||
contentType: VIDEO_MP4,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
backupLevel: BackupLevel.Media,
|
||||
comparator: (msgBefore, msgAfter) => {
|
||||
if (msgBefore.timestamp === originalMessage.timestamp) {
|
||||
return assert.deepStrictEqual(msgBefore, msgAfter);
|
||||
}
|
||||
|
||||
const thumbnail = msgAfter.quote?.attachments[0]?.thumbnail;
|
||||
strictAssert(thumbnail, 'quote thumbnail exists');
|
||||
|
||||
assert.deepStrictEqual(
|
||||
omit(msgBefore, 'quote.attachments[0].thumbnail'),
|
||||
omit(msgAfter, 'quote.attachments[0].thumbnail')
|
||||
);
|
||||
|
||||
const { key, digest } = thumbnail;
|
||||
strictAssert(digest, 'quote digest was created');
|
||||
strictAssert(key, 'quote digest was created');
|
||||
|
||||
assert.deepStrictEqual(thumbnail, {
|
||||
contentType: IMAGE_PNG,
|
||||
size: 100,
|
||||
key,
|
||||
digest,
|
||||
backupLocator: {
|
||||
mediaName: digest,
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sticker attachments', () => {
|
||||
const packId = Bytes.toHex(getRandomBytes(16));
|
||||
const packKey = Bytes.toBase64(getRandomBytes(32));
|
||||
|
||||
describe('when copied over from sticker pack (i.e. missing encryption info)', () => {
|
||||
it('BackupLevel.Media, generates new encryption info', async () => {
|
||||
await asymmetricRoundtripHarness(
|
||||
[
|
||||
composeMessage(1, {
|
||||
sticker: {
|
||||
emoji: '🐒',
|
||||
packId,
|
||||
packKey,
|
||||
stickerId: 0,
|
||||
data: {
|
||||
contentType: IMAGE_WEBP,
|
||||
path: 'path/to/sticker',
|
||||
size: 5322,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
composeMessage(1, {
|
||||
sticker: {
|
||||
emoji: '🐒',
|
||||
packId,
|
||||
packKey,
|
||||
stickerId: 0,
|
||||
data: {
|
||||
contentType: IMAGE_WEBP,
|
||||
size: 5322,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
{
|
||||
backupLevel: BackupLevel.Media,
|
||||
comparator: (msgBefore, msgAfter) => {
|
||||
assert.deepStrictEqual(
|
||||
omit(msgBefore, 'sticker.data'),
|
||||
omit(msgAfter, 'sticker.data')
|
||||
);
|
||||
strictAssert(msgAfter.sticker?.data, 'sticker data exists');
|
||||
|
||||
const { key, digest } = msgAfter.sticker.data;
|
||||
strictAssert(digest, 'sticker digest was created');
|
||||
|
||||
assert.equal(Bytes.fromBase64(digest ?? '').byteLength, 32);
|
||||
assert.equal(Bytes.fromBase64(key ?? '').byteLength, 64);
|
||||
|
||||
assert.deepStrictEqual(msgAfter.sticker.data, {
|
||||
contentType: IMAGE_WEBP,
|
||||
size: 5322,
|
||||
width: 512,
|
||||
height: 512,
|
||||
key,
|
||||
digest,
|
||||
backupLocator: {
|
||||
mediaName: digest,
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
it('BackupLevel.Messages, generates invalid attachment locator', async () => {
|
||||
// since we aren't re-uploading with new encryption info, we can't include this
|
||||
// attachment in the backup proto
|
||||
await asymmetricRoundtripHarness(
|
||||
[
|
||||
composeMessage(1, {
|
||||
sticker: {
|
||||
emoji: '🐒',
|
||||
packId,
|
||||
packKey,
|
||||
stickerId: 0,
|
||||
data: {
|
||||
contentType: IMAGE_WEBP,
|
||||
path: 'path/to/sticker',
|
||||
size: 5322,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
composeMessage(1, {
|
||||
sticker: {
|
||||
emoji: '🐒',
|
||||
packId,
|
||||
packKey,
|
||||
stickerId: 0,
|
||||
data: {
|
||||
contentType: IMAGE_WEBP,
|
||||
size: 0,
|
||||
error: true,
|
||||
height: 512,
|
||||
width: 512,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
{
|
||||
backupLevel: BackupLevel.Messages,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('when this device sent sticker (i.e. encryption info exists on message)', () => {
|
||||
it('roundtrips sticker', async () => {
|
||||
const attachment = composeAttachment(1);
|
||||
strictAssert(attachment.digest, 'digest exists');
|
||||
await asymmetricRoundtripHarness(
|
||||
[
|
||||
composeMessage(1, {
|
||||
sticker: {
|
||||
emoji: '🐒',
|
||||
packId,
|
||||
packKey,
|
||||
stickerId: 0,
|
||||
data: attachment,
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
composeMessage(1, {
|
||||
sticker: {
|
||||
emoji: '🐒',
|
||||
packId,
|
||||
packKey,
|
||||
stickerId: 0,
|
||||
data: {
|
||||
...omit(attachment, [
|
||||
'iv',
|
||||
'path',
|
||||
'thumbnail',
|
||||
'uploadTimestamp',
|
||||
]),
|
||||
backupLocator: { mediaName: attachment.digest },
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
{
|
||||
backupLevel: BackupLevel.Media,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import * as Bytes from '../../Bytes';
|
|||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import type { GetBackupCdnInfoType } from '../../services/backups/util/mediaId';
|
||||
import { MASTER_KEY } from './helpers';
|
||||
|
||||
describe('convertFilePointerToAttachment', () => {
|
||||
it('processes filepointer with attachmentLocator', () => {
|
||||
|
@ -270,6 +271,22 @@ const notInBackupCdn: GetBackupCdnInfoType = async () => {
|
|||
};
|
||||
|
||||
describe('getFilePointerForAttachment', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox();
|
||||
sandbox.stub(window.storage, 'get').callsFake(key => {
|
||||
if (key === 'masterKey') {
|
||||
return MASTER_KEY;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('not downloaded locally', () => {
|
||||
const undownloadedAttachment = composeAttachment({ path: undefined });
|
||||
it('returns invalidAttachmentLocator if missing critical decryption info', async () => {
|
||||
|
@ -402,11 +419,7 @@ describe('getFilePointerForAttachment', () => {
|
|||
describe('BackupLevel.Media', () => {
|
||||
describe('if missing critical decryption / encryption info', () => {
|
||||
const FILE_PATH = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox();
|
||||
sandbox
|
||||
.stub(window.Signal.Migrations, 'getAbsoluteAttachmentPath')
|
||||
.callsFake(relPath => {
|
||||
|
@ -417,10 +430,6 @@ describe('getFilePointerForAttachment', () => {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('if missing key, generates new key & digest and removes existing CDN info', async () => {
|
||||
const { filePointer: result } = await getFilePointerForAttachment({
|
||||
attachment: {
|
||||
|
@ -450,6 +459,18 @@ describe('getFilePointerForAttachment', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('if file does not exist at local path, returns invalid attachment locator', async () => {
|
||||
await testAttachmentToFilePointer(
|
||||
{
|
||||
...downloadedAttachment,
|
||||
path: 'no/file/here.png',
|
||||
key: undefined,
|
||||
},
|
||||
filePointerWithInvalidLocator,
|
||||
{ backupLevel: BackupLevel.Media }
|
||||
);
|
||||
});
|
||||
|
||||
it('if not on backup tier, and missing iv, regenerates encryption info', async () => {
|
||||
const { filePointer: result } = await getFilePointerForAttachment({
|
||||
attachment: {
|
||||
|
|
|
@ -10,7 +10,11 @@ import { mkdtemp, rm } from 'fs/promises';
|
|||
import * as sinon from 'sinon';
|
||||
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
||||
|
||||
import type { MessageAttributesType } from '../../model-types';
|
||||
import type {
|
||||
EditHistoryType,
|
||||
MessageAttributesType,
|
||||
MessageReactionType,
|
||||
} from '../../model-types';
|
||||
import type {
|
||||
SendStateByConversationId,
|
||||
SendState,
|
||||
|
@ -31,18 +35,27 @@ export const PROFILE_KEY = getRandomBytes(32);
|
|||
// This is preserved across data erasure
|
||||
const CONVO_ID_TO_STABLE_ID = new Map<string, string>();
|
||||
|
||||
function mapConvoId(id?: string | null): string | undefined | null {
|
||||
function mapConvoId(id?: string | null): string | undefined {
|
||||
if (id == null) {
|
||||
return id;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return CONVO_ID_TO_STABLE_ID.get(id) ?? id;
|
||||
}
|
||||
|
||||
type MessageAttributesForComparisonType = Omit<
|
||||
MessageAttributesType,
|
||||
'id' | 'received_at' | 'editHistory' | 'reactions' | 'conversationId'
|
||||
> & {
|
||||
conversationId: string | undefined;
|
||||
editHistory?: Array<Omit<EditHistoryType, 'received_at'>>;
|
||||
reactions?: Array<Omit<MessageReactionType, 'fromId'>>;
|
||||
};
|
||||
|
||||
// We need to eliminate fields that won't stay stable through import/export
|
||||
function sortAndNormalize(
|
||||
messages: Array<MessageAttributesType>
|
||||
): Array<unknown> {
|
||||
): Array<MessageAttributesForComparisonType> {
|
||||
return sortBy(messages, 'sent_at').map(message => {
|
||||
const {
|
||||
changedId,
|
||||
|
@ -113,11 +126,19 @@ function sortAndNormalize(
|
|||
});
|
||||
}
|
||||
|
||||
type HarnessOptionsType = {
|
||||
backupLevel: BackupLevel;
|
||||
comparator?: (
|
||||
msgBefore: MessageAttributesForComparisonType,
|
||||
msgAfter: MessageAttributesForComparisonType
|
||||
) => void;
|
||||
};
|
||||
|
||||
export async function symmetricRoundtripHarness(
|
||||
messages: Array<MessageAttributesType>,
|
||||
backupLevel: BackupLevel = BackupLevel.Messages
|
||||
options: HarnessOptionsType = { backupLevel: BackupLevel.Messages }
|
||||
): Promise<void> {
|
||||
return asymmetricRoundtripHarness(messages, messages, backupLevel);
|
||||
return asymmetricRoundtripHarness(messages, messages, options);
|
||||
}
|
||||
|
||||
async function updateConvoIdToTitle() {
|
||||
|
@ -133,7 +154,7 @@ async function updateConvoIdToTitle() {
|
|||
export async function asymmetricRoundtripHarness(
|
||||
before: Array<MessageAttributesType>,
|
||||
after: Array<MessageAttributesType>,
|
||||
backupLevel: BackupLevel = BackupLevel.Messages
|
||||
options: HarnessOptionsType = { backupLevel: BackupLevel.Messages }
|
||||
): Promise<void> {
|
||||
const outDir = await mkdtemp(path.join(tmpdir(), 'signal-temp-'));
|
||||
const fetchAndSaveBackupCdnObjectMetadata = sinon.stub(
|
||||
|
@ -145,7 +166,7 @@ export async function asymmetricRoundtripHarness(
|
|||
|
||||
await Data.saveMessages(before, { forceSave: true, ourAci: OUR_ACI });
|
||||
|
||||
await backupsService.exportToDisk(targetOutputFile, backupLevel);
|
||||
await backupsService.exportToDisk(targetOutputFile, options.backupLevel);
|
||||
|
||||
await updateConvoIdToTitle();
|
||||
|
||||
|
@ -159,7 +180,15 @@ export async function asymmetricRoundtripHarness(
|
|||
|
||||
const expected = sortAndNormalize(after);
|
||||
const actual = sortAndNormalize(messagesFromDatabase);
|
||||
|
||||
if (options.comparator) {
|
||||
assert.strictEqual(actual.length, expected.length);
|
||||
for (let i = 0; i < actual.length; i += 1) {
|
||||
options.comparator(expected[i], actual[i]);
|
||||
}
|
||||
} else {
|
||||
assert.deepEqual(actual, expected);
|
||||
}
|
||||
} finally {
|
||||
fetchAndSaveBackupCdnObjectMetadata.restore();
|
||||
await rm(outDir, { recursive: true });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue