654 lines
16 KiB
TypeScript
654 lines
16 KiB
TypeScript
// Copyright 2018-2020 Signal Messenger, LLC
|
||
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
||
import { assert } from 'chai';
|
||
import * as sinon from 'sinon';
|
||
|
||
import { IMAGE_GIF, IMAGE_PNG } from '../../types/MIME';
|
||
import { MessageAttributesType } from '../../model-types.d';
|
||
import {
|
||
Avatar,
|
||
Email,
|
||
Phone,
|
||
_validate,
|
||
embeddedContactSelector,
|
||
getName,
|
||
parseAndWriteAvatar,
|
||
} from '../../types/EmbeddedContact';
|
||
|
||
describe('Contact', () => {
|
||
const NUMBER = '+12025550099';
|
||
const logger = {
|
||
error: () => undefined,
|
||
};
|
||
|
||
const writeNewAttachmentData = sinon
|
||
.stub()
|
||
.throws(new Error("Shouldn't be called"));
|
||
|
||
const getDefaultMessageAttrs = (): Pick<
|
||
MessageAttributesType,
|
||
| 'id'
|
||
| 'conversationId'
|
||
| 'type'
|
||
| 'sent_at'
|
||
| 'received_at'
|
||
| 'timestamp'
|
||
| 'body'
|
||
> => {
|
||
return {
|
||
id: 'id',
|
||
conversationId: 'convo-id',
|
||
type: 'incoming',
|
||
sent_at: 1,
|
||
received_at: 2,
|
||
timestamp: 1,
|
||
|
||
body: 'hey there',
|
||
};
|
||
};
|
||
|
||
describe('getName', () => {
|
||
it('returns displayName if provided', () => {
|
||
const contact = {
|
||
name: {
|
||
displayName: 'displayName',
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
organization: 'Somewhere, Inc.',
|
||
};
|
||
const expected = 'displayName';
|
||
const actual = getName(contact);
|
||
assert.strictEqual(actual, expected);
|
||
});
|
||
|
||
it('returns organization if no displayName', () => {
|
||
const contact = {
|
||
name: {
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
organization: 'Somewhere, Inc.',
|
||
};
|
||
const expected = 'Somewhere, Inc.';
|
||
const actual = getName(contact);
|
||
assert.strictEqual(actual, expected);
|
||
});
|
||
|
||
it('returns givenName + familyName if no displayName or organization', () => {
|
||
const contact = {
|
||
name: {
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
};
|
||
const expected = 'givenName familyName';
|
||
const actual = getName(contact);
|
||
assert.strictEqual(actual, expected);
|
||
});
|
||
|
||
it('returns just givenName', () => {
|
||
const contact = {
|
||
name: {
|
||
givenName: 'givenName',
|
||
},
|
||
};
|
||
const expected = 'givenName';
|
||
const actual = getName(contact);
|
||
assert.strictEqual(actual, expected);
|
||
});
|
||
|
||
it('returns just familyName', () => {
|
||
const contact = {
|
||
name: {
|
||
familyName: 'familyName',
|
||
},
|
||
};
|
||
const expected = 'familyName';
|
||
const actual = getName(contact);
|
||
assert.strictEqual(actual, expected);
|
||
});
|
||
});
|
||
|
||
describe('embeddedContactSelector', () => {
|
||
const regionCode = '1';
|
||
const firstNumber = '+1202555000';
|
||
const isNumberOnSignal = false;
|
||
const getAbsoluteAttachmentPath = (path: string) => `absolute:${path}`;
|
||
|
||
it('eliminates avatar if it has had an attachment download error', () => {
|
||
const contact = {
|
||
name: {
|
||
displayName: 'displayName',
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
organization: 'Somewhere, Inc.',
|
||
avatar: {
|
||
isProfile: true,
|
||
avatar: {
|
||
error: true,
|
||
contentType: IMAGE_GIF,
|
||
},
|
||
},
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'displayName',
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
organization: 'Somewhere, Inc.',
|
||
avatar: undefined,
|
||
firstNumber,
|
||
isNumberOnSignal,
|
||
number: undefined,
|
||
};
|
||
const actual = embeddedContactSelector(contact, {
|
||
regionCode,
|
||
firstNumber,
|
||
isNumberOnSignal,
|
||
getAbsoluteAttachmentPath,
|
||
});
|
||
assert.deepEqual(actual, expected);
|
||
});
|
||
|
||
it('does not calculate absolute path if avatar is pending', () => {
|
||
const contact = {
|
||
name: {
|
||
displayName: 'displayName',
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
organization: 'Somewhere, Inc.',
|
||
avatar: {
|
||
isProfile: true,
|
||
avatar: {
|
||
pending: true,
|
||
contentType: IMAGE_GIF,
|
||
},
|
||
},
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'displayName',
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
organization: 'Somewhere, Inc.',
|
||
avatar: {
|
||
isProfile: true,
|
||
avatar: {
|
||
pending: true,
|
||
path: undefined,
|
||
contentType: IMAGE_GIF,
|
||
},
|
||
},
|
||
firstNumber,
|
||
isNumberOnSignal,
|
||
number: undefined,
|
||
};
|
||
const actual = embeddedContactSelector(contact, {
|
||
regionCode,
|
||
firstNumber,
|
||
isNumberOnSignal,
|
||
getAbsoluteAttachmentPath,
|
||
});
|
||
assert.deepEqual(actual, expected);
|
||
});
|
||
|
||
it('calculates absolute path', () => {
|
||
const contact = {
|
||
name: {
|
||
displayName: 'displayName',
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
organization: 'Somewhere, Inc.',
|
||
avatar: {
|
||
isProfile: true,
|
||
avatar: {
|
||
path: 'somewhere',
|
||
contentType: IMAGE_GIF,
|
||
},
|
||
},
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'displayName',
|
||
givenName: 'givenName',
|
||
familyName: 'familyName',
|
||
},
|
||
organization: 'Somewhere, Inc.',
|
||
avatar: {
|
||
isProfile: true,
|
||
avatar: {
|
||
path: 'absolute:somewhere',
|
||
contentType: IMAGE_GIF,
|
||
},
|
||
},
|
||
firstNumber,
|
||
isNumberOnSignal: true,
|
||
number: undefined,
|
||
};
|
||
const actual = embeddedContactSelector(contact, {
|
||
regionCode,
|
||
firstNumber,
|
||
isNumberOnSignal: true,
|
||
getAbsoluteAttachmentPath,
|
||
});
|
||
assert.deepEqual(actual, expected);
|
||
});
|
||
});
|
||
|
||
describe('parseAndWriteAvatar', () => {
|
||
it('handles message with no avatar in contact', async () => {
|
||
const upgradeAttachment = sinon
|
||
.stub()
|
||
.throws(new Error("Shouldn't be called"));
|
||
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
|
||
|
||
const message = {
|
||
...getDefaultMessageAttrs(),
|
||
contact: [
|
||
{
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: NUMBER,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
const result = await upgradeVersion(message.contact[0], {
|
||
message,
|
||
logger,
|
||
regionCode: '1',
|
||
writeNewAttachmentData,
|
||
});
|
||
assert.deepEqual(result, message.contact[0]);
|
||
});
|
||
|
||
it('turns phone numbers to e164 format', async () => {
|
||
const upgradeAttachment = sinon
|
||
.stub()
|
||
.throws(new Error("Shouldn't be called"));
|
||
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
|
||
|
||
const message = {
|
||
...getDefaultMessageAttrs(),
|
||
contact: [
|
||
{
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: '(202) 555-0099',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: '+12025550099',
|
||
},
|
||
],
|
||
};
|
||
const result = await upgradeVersion(message.contact[0], {
|
||
message,
|
||
regionCode: 'US',
|
||
logger,
|
||
writeNewAttachmentData,
|
||
});
|
||
assert.deepEqual(result, expected);
|
||
});
|
||
|
||
it('removes contact avatar if it has no sub-avatar', async () => {
|
||
const upgradeAttachment = sinon
|
||
.stub()
|
||
.throws(new Error("Shouldn't be called"));
|
||
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
|
||
|
||
const message = {
|
||
...getDefaultMessageAttrs(),
|
||
contact: [
|
||
{
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: NUMBER,
|
||
},
|
||
],
|
||
avatar: ({
|
||
isProfile: true,
|
||
} as unknown) as Avatar,
|
||
},
|
||
],
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: NUMBER,
|
||
},
|
||
],
|
||
};
|
||
const result = await upgradeVersion(message.contact[0], {
|
||
regionCode: '1',
|
||
writeNewAttachmentData,
|
||
message,
|
||
logger,
|
||
});
|
||
assert.deepEqual(result, expected);
|
||
});
|
||
|
||
it('writes avatar to disk', async () => {
|
||
const upgradeAttachment = async () => {
|
||
return {
|
||
path: 'abc/abcdefg',
|
||
contentType: IMAGE_PNG,
|
||
};
|
||
};
|
||
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
|
||
|
||
const message = {
|
||
...getDefaultMessageAttrs(),
|
||
contact: [
|
||
{
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: NUMBER,
|
||
},
|
||
],
|
||
email: [
|
||
{
|
||
type: 2,
|
||
value: 'someone@somewhere.com',
|
||
},
|
||
],
|
||
address: [
|
||
{
|
||
type: 1,
|
||
street: '5 Somewhere Ave.',
|
||
},
|
||
],
|
||
avatar: ({
|
||
otherKey: 'otherValue',
|
||
avatar: {
|
||
contentType: 'image/png',
|
||
data: Buffer.from('It’s easy if you try'),
|
||
},
|
||
} as unknown) as Avatar,
|
||
},
|
||
],
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: NUMBER,
|
||
},
|
||
],
|
||
email: [
|
||
{
|
||
type: 2,
|
||
value: 'someone@somewhere.com',
|
||
},
|
||
],
|
||
address: [
|
||
{
|
||
type: 1,
|
||
street: '5 Somewhere Ave.',
|
||
},
|
||
],
|
||
avatar: {
|
||
otherKey: 'otherValue',
|
||
isProfile: false,
|
||
avatar: {
|
||
contentType: IMAGE_PNG,
|
||
path: 'abc/abcdefg',
|
||
},
|
||
},
|
||
};
|
||
|
||
const result = await upgradeVersion(message.contact[0], {
|
||
regionCode: '1',
|
||
writeNewAttachmentData,
|
||
message,
|
||
logger,
|
||
});
|
||
assert.deepEqual(result, expected);
|
||
});
|
||
|
||
it('removes number element if it ends up with no value', async () => {
|
||
const upgradeAttachment = sinon
|
||
.stub()
|
||
.throws(new Error("Shouldn't be called"));
|
||
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
|
||
|
||
const message = {
|
||
...getDefaultMessageAttrs(),
|
||
contact: [
|
||
{
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
({
|
||
type: 1,
|
||
} as unknown) as Phone,
|
||
],
|
||
email: [
|
||
{
|
||
type: 0,
|
||
value: 'someone@somewhere.com',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
email: [
|
||
{
|
||
type: 1,
|
||
value: 'someone@somewhere.com',
|
||
},
|
||
],
|
||
};
|
||
const result = await upgradeVersion(message.contact[0], {
|
||
regionCode: '1',
|
||
writeNewAttachmentData,
|
||
message,
|
||
logger,
|
||
});
|
||
assert.deepEqual(result, expected);
|
||
});
|
||
|
||
it('drops address if it has no real values', async () => {
|
||
const upgradeAttachment = sinon
|
||
.stub()
|
||
.throws(new Error("Shouldn't be called"));
|
||
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
|
||
|
||
const message = {
|
||
...getDefaultMessageAttrs(),
|
||
contact: [
|
||
{
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: NUMBER,
|
||
},
|
||
],
|
||
address: [
|
||
{
|
||
type: 1,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
{
|
||
value: NUMBER,
|
||
type: 1,
|
||
},
|
||
],
|
||
};
|
||
const result = await upgradeVersion(message.contact[0], {
|
||
regionCode: '1',
|
||
writeNewAttachmentData,
|
||
message,
|
||
logger,
|
||
});
|
||
assert.deepEqual(result, expected);
|
||
});
|
||
|
||
it('removes invalid elements if no values remain in contact', async () => {
|
||
const upgradeAttachment = sinon
|
||
.stub()
|
||
.throws(new Error("Shouldn't be called"));
|
||
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
|
||
|
||
const message = {
|
||
...getDefaultMessageAttrs(),
|
||
source: NUMBER,
|
||
sourceDevice: 1,
|
||
sent_at: 1232132,
|
||
contact: [
|
||
{
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [
|
||
({
|
||
type: 1,
|
||
} as unknown) as Phone,
|
||
],
|
||
email: [
|
||
({
|
||
type: 1,
|
||
} as unknown) as Email,
|
||
],
|
||
},
|
||
],
|
||
};
|
||
const expected = {
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
};
|
||
const result = await upgradeVersion(message.contact[0], {
|
||
regionCode: '1',
|
||
writeNewAttachmentData,
|
||
message,
|
||
logger,
|
||
});
|
||
assert.deepEqual(result, expected);
|
||
});
|
||
|
||
it('handles a contact with just organization', async () => {
|
||
const upgradeAttachment = sinon
|
||
.stub()
|
||
.throws(new Error("Shouldn't be called"));
|
||
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
|
||
|
||
const message = {
|
||
...getDefaultMessageAttrs(),
|
||
contact: [
|
||
{
|
||
organization: 'Somewhere Consulting',
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: NUMBER,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
const result = await upgradeVersion(message.contact[0], {
|
||
regionCode: '1',
|
||
writeNewAttachmentData,
|
||
message,
|
||
logger,
|
||
});
|
||
assert.deepEqual(result, message.contact[0]);
|
||
});
|
||
});
|
||
|
||
describe('_validate', () => {
|
||
it('returns error if contact has no name.displayName or organization', () => {
|
||
const messageId = 'the-message-id';
|
||
const contact = {
|
||
name: {
|
||
givenName: 'Someone',
|
||
},
|
||
number: [
|
||
{
|
||
type: 1,
|
||
value: NUMBER,
|
||
},
|
||
],
|
||
};
|
||
const expected =
|
||
"Message the-message-id: Contact had neither 'displayName' nor 'organization'";
|
||
|
||
const result = _validate(contact, { messageId });
|
||
assert.deepEqual(result?.message, expected);
|
||
});
|
||
|
||
it('logs if no values remain in contact', async () => {
|
||
const messageId = 'the-message-id';
|
||
const contact = {
|
||
name: {
|
||
displayName: 'Someone Somewhere',
|
||
},
|
||
number: [],
|
||
email: [],
|
||
};
|
||
const expected =
|
||
'Message the-message-id: Contact had no included numbers, email or addresses';
|
||
|
||
const result = _validate(contact, { messageId });
|
||
assert.deepEqual(result?.message, expected);
|
||
});
|
||
});
|
||
});
|