Signal-Desktop/test/modules/types/message_test.js

725 lines
19 KiB
JavaScript
Raw Normal View History

2020-10-30 20:34:04 +00:00
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const { assert } = require('chai');
const sinon = require('sinon');
const Message = require('../../../js/modules/types/message');
const { SignalService } = require('../../../ts/protobuf');
2021-09-24 00:49:05 +00:00
const Bytes = require('../../../ts/Bytes');
describe('Message', () => {
const logger = {
warn: () => null,
error: () => null,
};
describe('createAttachmentDataWriter', () => {
2018-04-04 01:10:34 +00:00
it('should ignore messages that didnt go through attachment migration', async () => {
const input = {
body: 'Imagine there is no heaven…',
schemaVersion: 2,
};
const expected = {
body: 'Imagine there is no heaven…',
schemaVersion: 2,
};
const writeExistingAttachmentData = () => {};
const actual = await Message.createAttachmentDataWriter({
writeExistingAttachmentData,
logger,
})(input);
2018-04-04 01:10:34 +00:00
assert.deepEqual(actual, expected);
});
it('should ignore messages without attachments', async () => {
const input = {
body: 'Imagine there is no heaven…',
schemaVersion: 4,
attachments: [],
};
const expected = {
body: 'Imagine there is no heaven…',
schemaVersion: 4,
attachments: [],
};
const writeExistingAttachmentData = () => {};
const actual = await Message.createAttachmentDataWriter({
writeExistingAttachmentData,
logger,
})(input);
2018-04-04 01:10:34 +00:00
assert.deepEqual(actual, expected);
});
it('should write attachments to file system on original path', async () => {
const input = {
body: 'Imagine there is no heaven…',
schemaVersion: 4,
2018-04-27 21:25:04 +00:00
attachments: [
{
path: 'ab/abcdefghi',
2021-09-24 00:49:05 +00:00
data: Bytes.fromString('Its easy if you try'),
2018-04-27 21:25:04 +00:00
},
],
2018-04-04 01:10:34 +00:00
};
const expected = {
body: 'Imagine there is no heaven…',
schemaVersion: 4,
2018-04-27 21:25:04 +00:00
attachments: [
{
path: 'ab/abcdefghi',
},
],
contact: [],
2019-01-16 03:03:56 +00:00
preview: [],
2018-04-04 01:10:34 +00:00
};
2018-04-27 21:25:04 +00:00
const writeExistingAttachmentData = attachment => {
2018-04-04 01:10:34 +00:00
assert.equal(attachment.path, 'ab/abcdefghi');
2021-09-24 00:49:05 +00:00
assert.strictEqual(
Bytes.toString(attachment.data),
'Its easy if you try'
2018-04-27 21:25:04 +00:00
);
2018-04-04 01:10:34 +00:00
};
const actual = await Message.createAttachmentDataWriter({
writeExistingAttachmentData,
logger,
})(input);
2018-04-04 01:10:34 +00:00
assert.deepEqual(actual, expected);
});
2018-04-20 21:55:33 +00:00
it('should process quote attachment thumbnails', async () => {
const input = {
body: 'Imagine there is no heaven…',
schemaVersion: 4,
attachments: [],
quote: {
2018-04-27 21:25:04 +00:00
attachments: [
{
thumbnail: {
path: 'ab/abcdefghi',
2021-09-24 00:49:05 +00:00
data: Bytes.fromString('Its easy if you try'),
2018-04-27 21:25:04 +00:00
},
2018-04-20 21:55:33 +00:00
},
2018-04-27 21:25:04 +00:00
],
2018-04-20 21:55:33 +00:00
},
};
const expected = {
body: 'Imagine there is no heaven…',
schemaVersion: 4,
attachments: [],
quote: {
2018-04-27 21:25:04 +00:00
attachments: [
{
thumbnail: {
path: 'ab/abcdefghi',
},
2018-04-20 21:55:33 +00:00
},
2018-04-27 21:25:04 +00:00
],
2018-04-20 21:55:33 +00:00
},
contact: [],
2019-01-16 03:03:56 +00:00
preview: [],
};
const writeExistingAttachmentData = attachment => {
assert.equal(attachment.path, 'ab/abcdefghi');
2021-09-24 00:49:05 +00:00
assert.strictEqual(
Bytes.toString(attachment.data),
'Its easy if you try'
);
};
const actual = await Message.createAttachmentDataWriter({
writeExistingAttachmentData,
logger,
})(input);
assert.deepEqual(actual, expected);
});
it('should process contact avatars', async () => {
const input = {
body: 'Imagine there is no heaven…',
schemaVersion: 4,
attachments: [],
contact: [
{
name: 'john',
avatar: {
isProfile: false,
avatar: {
path: 'ab/abcdefghi',
2021-09-24 00:49:05 +00:00
data: Bytes.fromString('Its easy if you try'),
},
},
},
],
};
const expected = {
body: 'Imagine there is no heaven…',
schemaVersion: 4,
attachments: [],
contact: [
{
name: 'john',
avatar: {
isProfile: false,
avatar: {
path: 'ab/abcdefghi',
},
},
},
],
2019-01-16 03:03:56 +00:00
preview: [],
2018-04-20 21:55:33 +00:00
};
2018-04-27 21:25:04 +00:00
const writeExistingAttachmentData = attachment => {
2018-04-20 21:55:33 +00:00
assert.equal(attachment.path, 'ab/abcdefghi');
2021-09-24 00:49:05 +00:00
assert.strictEqual(
Bytes.toString(attachment.data),
'Its easy if you try'
2018-04-27 21:25:04 +00:00
);
2018-04-20 21:55:33 +00:00
};
const actual = await Message.createAttachmentDataWriter({
writeExistingAttachmentData,
logger,
})(input);
2018-04-20 21:55:33 +00:00
assert.deepEqual(actual, expected);
});
2018-04-04 01:10:34 +00:00
});
describe('initializeSchemaVersion', () => {
it('should ignore messages with previously inherited schema', () => {
const input = {
body: 'Imagine there is no heaven…',
schemaVersion: 2,
};
const expected = {
body: 'Imagine there is no heaven…',
schemaVersion: 2,
};
const actual = Message.initializeSchemaVersion({
message: input,
logger,
});
assert.deepEqual(actual, expected);
});
context('for message without attachments', () => {
it('should initialize schema version to zero', () => {
const input = {
body: 'Imagine there is no heaven…',
attachments: [],
};
const expected = {
body: 'Imagine there is no heaven…',
attachments: [],
schemaVersion: 0,
};
const actual = Message.initializeSchemaVersion({
message: input,
logger,
});
assert.deepEqual(actual, expected);
});
});
context('for message with attachments', () => {
it('should inherit existing attachment schema version', () => {
const input = {
body: 'Imagine there is no heaven…',
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'image/jpeg',
fileName: 'lennon.jpg',
schemaVersion: 7,
},
],
};
const expected = {
body: 'Imagine there is no heaven…',
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'image/jpeg',
fileName: 'lennon.jpg',
},
],
schemaVersion: 7,
};
const actual = Message.initializeSchemaVersion({
message: input,
logger,
});
assert.deepEqual(actual, expected);
});
});
});
describe('upgradeSchema', () => {
it('should upgrade an unversioned message to the latest version', async () => {
const input = {
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'audio/aac',
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
2021-09-24 00:49:05 +00:00
data: Bytes.fromString('Its easy if you try'),
2018-04-27 21:25:04 +00:00
fileName: 'test\u202Dfig.exe',
size: 1111,
},
],
schemaVersion: 0,
};
const expected = {
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'audio/aac',
flags: 1,
2018-04-27 21:25:04 +00:00
path: 'abc/abcdefg',
fileName: 'test\uFFFDfig.exe',
size: 1111,
},
],
hasAttachments: 1,
hasVisualMediaAttachments: undefined,
hasFileAttachments: undefined,
schemaVersion: Message.CURRENT_SCHEMA_VERSION,
contact: [],
};
2021-09-24 00:49:05 +00:00
const expectedAttachmentData = 'Its easy if you try';
2018-03-15 00:45:33 +00:00
const context = {
2018-04-27 21:25:04 +00:00
writeNewAttachmentData: async attachmentData => {
2021-09-24 00:49:05 +00:00
assert.strictEqual(
Bytes.toString(attachmentData),
expectedAttachmentData
);
2018-03-15 00:45:33 +00:00
return 'abc/abcdefg';
},
getRegionCode: () => 'US',
getAbsoluteAttachmentPath: () => 'some/path/on/disk',
makeObjectUrl: () => 'blob://FAKE',
revokeObjectUrl: () => null,
getImageDimensions: () => ({ height: 10, width: 15 }),
makeImageThumbnail: () => new Blob(),
makeVideoScreenshot: () => new Blob(),
logger: {
warn: () => null,
error: () => null,
},
2018-03-15 00:45:33 +00:00
};
const actual = await Message.upgradeSchema(input, context);
assert.deepEqual(actual, expected);
});
context('with multiple upgrade steps', () => {
it('should return last valid message when any upgrade step fails', async () => {
const input = {
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
},
],
schemaVersion: 0,
};
const expected = {
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
},
],
hasUpgradedToVersion1: true,
schemaVersion: 1,
};
2020-09-09 00:46:29 +00:00
const v1 = async message => ({
...message,
hasUpgradedToVersion1: true,
});
const v2 = async () => {
throw new Error('boom');
};
2020-09-09 00:46:29 +00:00
const v3 = async message => ({
...message,
hasUpgradedToVersion3: true,
});
const toVersion1 = Message._withSchemaVersion({
schemaVersion: 1,
upgrade: v1,
});
const toVersion2 = Message._withSchemaVersion({
schemaVersion: 2,
upgrade: v2,
});
const toVersion3 = Message._withSchemaVersion({
schemaVersion: 3,
upgrade: v3,
});
const context = { logger };
const upgradeSchema = async message =>
toVersion3(
await toVersion2(await toVersion1(message, context), context),
context
);
const actual = await upgradeSchema(input);
assert.deepEqual(actual, expected);
});
it('should skip out-of-order upgrade steps', async () => {
const input = {
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
},
],
schemaVersion: 0,
};
const expected = {
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'application/json',
data: null,
fileName: 'test\u202Dfig.exe',
size: 1111,
},
],
schemaVersion: 2,
hasUpgradedToVersion1: true,
hasUpgradedToVersion2: true,
};
2020-09-09 00:46:29 +00:00
const v1 = async attachment => ({
...attachment,
hasUpgradedToVersion1: true,
});
const v2 = async attachment => ({
...attachment,
hasUpgradedToVersion2: true,
});
const v3 = async attachment => ({
...attachment,
hasUpgradedToVersion3: true,
});
const toVersion1 = Message._withSchemaVersion({
schemaVersion: 1,
upgrade: v1,
});
const toVersion2 = Message._withSchemaVersion({
schemaVersion: 2,
upgrade: v2,
});
const toVersion3 = Message._withSchemaVersion({
schemaVersion: 3,
upgrade: v3,
});
const context = { logger };
// NOTE: We upgrade to 3 before 2, i.e. the pipeline should abort:
const upgradeSchema = async attachment =>
toVersion2(
await toVersion3(await toVersion1(attachment, context), context),
context
);
const actual = await upgradeSchema(input);
assert.deepEqual(actual, expected);
});
});
});
describe('_withSchemaVersion', () => {
it('should require a version number', () => {
const toVersionX = () => {};
assert.throws(
() =>
Message._withSchemaVersion({ schemaVersion: toVersionX, upgrade: 2 }),
'_withSchemaVersion: schemaVersion is invalid'
);
});
it('should require an upgrade function', () => {
assert.throws(
() => Message._withSchemaVersion({ schemaVersion: 2, upgrade: 3 }),
'_withSchemaVersion: upgrade must be a function'
);
});
it('should skip upgrading if message has already been upgraded', async () => {
2020-09-09 00:46:29 +00:00
const upgrade = async message => ({ ...message, foo: true });
const upgradeWithVersion = Message._withSchemaVersion({
schemaVersion: 3,
upgrade,
});
const input = {
id: 'guid-guid-guid-guid',
schemaVersion: 4,
};
const expected = {
id: 'guid-guid-guid-guid',
schemaVersion: 4,
};
const actual = await upgradeWithVersion(input, { logger });
assert.deepEqual(actual, expected);
});
it('should return original message if upgrade function throws', async () => {
const upgrade = async () => {
throw new Error('boom!');
};
const upgradeWithVersion = Message._withSchemaVersion({
schemaVersion: 3,
upgrade,
});
const input = {
id: 'guid-guid-guid-guid',
schemaVersion: 0,
};
const expected = {
id: 'guid-guid-guid-guid',
schemaVersion: 0,
};
const actual = await upgradeWithVersion(input, { logger });
assert.deepEqual(actual, expected);
});
it('should return original message if upgrade function returns null', async () => {
const upgrade = async () => null;
const upgradeWithVersion = Message._withSchemaVersion({
schemaVersion: 3,
upgrade,
});
const input = {
id: 'guid-guid-guid-guid',
schemaVersion: 0,
};
const expected = {
id: 'guid-guid-guid-guid',
schemaVersion: 0,
};
const actual = await upgradeWithVersion(input, { logger });
assert.deepEqual(actual, expected);
});
});
describe('_mapQuotedAttachments', () => {
it('handles message with no quote', async () => {
2018-04-27 21:25:04 +00:00
const upgradeAttachment = sinon
.stub()
.throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = {
body: 'hey there!',
};
const result = await upgradeVersion(message);
assert.deepEqual(result, message);
});
it('handles quote with no attachments', async () => {
2018-04-27 21:25:04 +00:00
const upgradeAttachment = sinon
.stub()
.throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = {
body: 'hey there!',
quote: {
text: 'hey!',
},
};
const expected = {
body: 'hey there!',
quote: {
text: 'hey!',
attachments: [],
},
};
const result = await upgradeVersion(message, { logger });
assert.deepEqual(result, expected);
});
it('handles zero attachments', async () => {
2018-04-27 21:25:04 +00:00
const upgradeAttachment = sinon
.stub()
.throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = {
body: 'hey there!',
quote: {
text: 'hey!',
attachments: [],
},
};
const result = await upgradeVersion(message, { logger });
assert.deepEqual(result, message);
});
it('handles attachments with no thumbnail', async () => {
2018-04-27 21:25:04 +00:00
const upgradeAttachment = sinon
.stub()
.throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = {
body: 'hey there!',
quote: {
text: 'hey!',
attachments: [
{
fileName: 'manifesto.txt',
contentType: 'text/plain',
},
],
},
};
const result = await upgradeVersion(message, { logger });
assert.deepEqual(result, message);
});
it('does not eliminate thumbnails with missing data field', async () => {
2018-04-27 21:25:04 +00:00
const upgradeAttachment = sinon
.stub()
.returns({ fileName: 'processed!' });
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = {
body: 'hey there!',
quote: {
text: 'hey!',
2018-04-27 21:25:04 +00:00
attachments: [
{
fileName: 'cat.gif',
contentType: 'image/gif',
thumbnail: {
fileName: 'not yet downloaded!',
2018-04-27 21:25:04 +00:00
},
},
2018-04-27 21:25:04 +00:00
],
},
};
const expected = {
body: 'hey there!',
quote: {
text: 'hey!',
2018-04-27 21:25:04 +00:00
attachments: [
{
contentType: 'image/gif',
fileName: 'cat.gif',
thumbnail: {
fileName: 'processed!',
},
2018-04-27 21:25:04 +00:00
},
],
},
};
const result = await upgradeVersion(message, { logger });
assert.deepEqual(result, expected);
});
it('calls provided async function for each quoted attachment', async () => {
const upgradeAttachment = sinon.stub().resolves({
path: '/new/path/on/disk',
});
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
const message = {
body: 'hey there!',
quote: {
text: 'hey!',
2018-04-27 21:25:04 +00:00
attachments: [
{
thumbnail: {
data: 'data is here',
},
},
2018-04-27 21:25:04 +00:00
],
},
};
const expected = {
body: 'hey there!',
quote: {
text: 'hey!',
2018-04-27 21:25:04 +00:00
attachments: [
{
thumbnail: {
path: '/new/path/on/disk',
},
},
2018-04-27 21:25:04 +00:00
],
},
};
const result = await upgradeVersion(message, { logger });
assert.deepEqual(result, expected);
});
});
describe('_mapContact', () => {
it('handles message with no contact field', async () => {
const upgradeContact = sinon
.stub()
.throws(new Error("Shouldn't be called"));
const upgradeVersion = Message._mapContact(upgradeContact);
const message = {
body: 'hey there!',
};
const expected = {
body: 'hey there!',
contact: [],
};
const result = await upgradeVersion(message);
assert.deepEqual(result, expected);
});
it('handles one contact', async () => {
const upgradeContact = contact => Promise.resolve(contact);
const upgradeVersion = Message._mapContact(upgradeContact);
const message = {
body: 'hey there!',
contact: [
{
name: {
displayName: 'Someone somewhere',
},
},
],
};
const expected = {
body: 'hey there!',
contact: [
{
name: {
displayName: 'Someone somewhere',
},
},
],
};
const result = await upgradeVersion(message);
assert.deepEqual(result, expected);
});
});
});