You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/test/backup_test.js

640 lines
19 KiB
JavaScript

/* global Signal, Whisper, assert, textsecure, _, libsignal */
/* eslint-disable no-console */
'use strict';
describe('Backup', () => {
describe('_sanitizeFileName', () => {
it('leaves a basic string alone', () => {
const initial = "Hello, how are you #5 ('fine' + great).jpg";
const expected = initial;
assert.strictEqual(Signal.Backup._sanitizeFileName(initial), expected);
});
it('replaces all unknown characters', () => {
const initial = '!@$%^&*=';
const expected = '________';
assert.strictEqual(Signal.Backup._sanitizeFileName(initial), expected);
});
});
describe('_trimFileName', () => {
it('handles a file with no extension', () => {
const initial = '0123456789012345678901234567890123456789';
const expected = '012345678901234567890123456789';
assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
});
it('handles a file with a long extension', () => {
const initial =
'0123456789012345678901234567890123456789.01234567890123456789';
const expected = '012345678901234567890123456789';
assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
});
it('handles a file with a normal extension', () => {
const initial = '01234567890123456789012345678901234567890123456789.jpg';
const expected = '012345678901234567890123.jpg';
assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
});
});
describe('_getExportAttachmentFileName', () => {
it('uses original filename if attachment has one', () => {
const message = {
body: 'something',
};
const index = 0;
const attachment = {
fileName: 'blah.jpg',
};
const expected = 'blah.jpg';
const actual = Signal.Backup._getExportAttachmentFileName(
message,
index,
attachment
);
assert.strictEqual(actual, expected);
});
it('uses attachment id if no filename', () => {
const message = {
body: 'something',
};
const index = 0;
const attachment = {
id: '123',
};
const expected = '123';
const actual = Signal.Backup._getExportAttachmentFileName(
message,
index,
attachment
);
assert.strictEqual(actual, expected);
});
it('uses filename and contentType if available', () => {
const message = {
body: 'something',
};
const index = 0;
const attachment = {
id: '123',
contentType: 'image/jpeg',
};
const expected = '123.jpeg';
const actual = Signal.Backup._getExportAttachmentFileName(
message,
index,
attachment
);
assert.strictEqual(actual, expected);
});
it('handles strange contentType', () => {
const message = {
body: 'something',
};
const index = 0;
const attachment = {
id: '123',
contentType: 'something',
};
const expected = '123.something';
const actual = Signal.Backup._getExportAttachmentFileName(
message,
index,
attachment
);
assert.strictEqual(actual, expected);
});
});
describe('_getAnonymousAttachmentFileName', () => {
it('uses message id', () => {
const message = {
id: 'id-45',
body: 'something',
};
const index = 0;
const attachment = {
fileName: 'blah.jpg',
};
const expected = 'id-45';
const actual = Signal.Backup._getAnonymousAttachmentFileName(
message,
index,
attachment
);
assert.strictEqual(actual, expected);
});
it('appends index if it is above zero', () => {
const message = {
id: 'id-45',
body: 'something',
};
const index = 1;
const attachment = {
fileName: 'blah.jpg',
};
const expected = 'id-45-1';
const actual = Signal.Backup._getAnonymousAttachmentFileName(
message,
index,
attachment
);
assert.strictEqual(actual, expected);
});
});
describe('_getConversationDirName', () => {
it('uses name if available', () => {
const conversation = {
active_at: 123,
name: '0123456789012345678901234567890123456789',
id: 'id',
};
const expected = '123 (012345678901234567890123456789 id)';
assert.strictEqual(
Signal.Backup._getConversationDirName(conversation),
expected
);
});
it('uses just id if name is not available', () => {
const conversation = {
active_at: 123,
id: 'id',
};
const expected = '123 (id)';
assert.strictEqual(
Signal.Backup._getConversationDirName(conversation),
expected
);
});
it('uses inactive for missing active_at', () => {
const conversation = {
name: 'name',
id: 'id',
};
const expected = 'inactive (name id)';
assert.strictEqual(
Signal.Backup._getConversationDirName(conversation),
expected
);
});
});
describe('_getConversationLoggingName', () => {
it('uses plain id if conversation is private', () => {
const conversation = {
active_at: 123,
id: 'id',
type: 'private',
};
const expected = '123 (id)';
assert.strictEqual(
Signal.Backup._getConversationLoggingName(conversation),
expected
);
});
it('uses just id if name is not available', () => {
const conversation = {
active_at: 123,
id: 'groupId',
type: 'group',
};
const expected = '123 ([REDACTED_GROUP]pId)';
assert.strictEqual(
Signal.Backup._getConversationLoggingName(conversation),
expected
);
});
it('uses inactive for missing active_at', () => {
const conversation = {
id: 'id',
type: 'private',
};
const expected = 'inactive (id)';
assert.strictEqual(
Signal.Backup._getConversationLoggingName(conversation),
expected
);
});
});
describe('end-to-end', () => {
it('exports then imports to produce the same data we started with', async function thisNeeded() {
this.timeout(6000);
const {
attachmentsPath,
fse,
glob,
path,
tmp,
isTravis,
isWindows,
} = window.test;
// Skip this test on travis windows
// because it always fails due to lstat permission error.
// Don't know how to fix it so this is a temp work around.
if (isTravis && isWindows) {
console.log(
'Skipping exports then imports to produce the same data we started'
);
this.skip();
return;
}
const {
upgradeMessageSchema,
loadAttachmentData,
} = window.Signal.Migrations;
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
const attachmentsPattern = path.join(attachmentsPath, '**');
const OUR_NUMBER = '+12025550000';
const CONTACT_ONE_NUMBER = '+12025550001';
const CONTACT_TWO_NUMBER = '+12025550002';
const toArrayBuffer = nodeBuffer =>
nodeBuffer.buffer.slice(
nodeBuffer.byteOffset,
nodeBuffer.byteOffset + nodeBuffer.byteLength
);
const getFixture = target => toArrayBuffer(fse.readFileSync(target));
const FIXTURES = {
gif: getFixture('fixtures/giphy-7GFfijngKbeNy.gif'),
mp4: getFixture('fixtures/pixabay-Soap-Bubble-7141.mp4'),
jpg: getFixture('fixtures/koushik-chowdavarapu-105425-unsplash.jpg'),
mp3: getFixture('fixtures/incompetech-com-Agnus-Dei-X.mp3'),
txt: getFixture('fixtures/lorem-ipsum.txt'),
png: getFixture(
'fixtures/freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png'
),
};
async function wrappedLoadAttachment(attachment) {
return _.omit(await loadAttachmentData(attachment), ['path']);
}
async function clearAllData() {
await textsecure.storage.protocol.removeAllData();
await fse.emptyDir(attachmentsPath);
}
function removeId(model) {
return _.omit(model, ['id']);
}
const getUndefinedKeys = object =>
Object.entries(object)
.filter(([, value]) => value === undefined)
.map(([name]) => name);
const omitUndefinedKeys = object =>
_.omit(object, getUndefinedKeys(object));
// We want to know which paths have two slashes, since that tells us which files
// in the attachment fan-out are files vs. directories.
const TWO_SLASHES = /[^/]*\/[^/]*\/[^/]*/;
// On windows, attachmentsPath has a normal windows path format (\ separators), but
// glob returns only /. We normalize to / separators for our manipulations.
const normalizedBase = attachmentsPath.replace(/\\/g, '/');
function removeDirs(dirs) {
return _.filter(dirs, fullDir => {
const dir = fullDir.replace(normalizedBase, '');
return TWO_SLASHES.test(dir);
});
}
function _mapQuotedAttachments(mapper) {
return async (message, context) => {
if (!message.quote) {
return message;
}
const wrappedMapper = async attachment => {
if (!attachment || !attachment.thumbnail) {
return attachment;
}
return Object.assign({}, attachment, {
thumbnail: await mapper(attachment.thumbnail, context),
});
};
const quotedAttachments =
(message.quote && message.quote.attachments) || [];
return Object.assign({}, message, {
quote: Object.assign({}, message.quote, {
attachments: await Promise.all(
quotedAttachments.map(wrappedMapper)
),
}),
});
};
}
async function loadAllFilesFromDisk(message) {
const loadThumbnails = _mapQuotedAttachments(thumbnail => {
// we want to be bulletproof to thumbnails without data
if (!thumbnail.path) {
return thumbnail;
}
return wrappedLoadAttachment(thumbnail);
});
return Object.assign({}, await loadThumbnails(message), {
contact: await Promise.all(
(message.contact || []).map(async contact => {
return contact && contact.avatar && contact.avatar.avatar
? Object.assign({}, contact, {
6 years ago
avatar: Object.assign({}, contact.avatar, {
avatar: await wrappedLoadAttachment(
contact.avatar.avatar
),
}),
})
: contact;
})
),
attachments: await Promise.all(
(message.attachments || []).map(async attachment => {
await wrappedLoadAttachment(attachment);
if (attachment.thumbnail) {
await wrappedLoadAttachment(attachment.thumbnail);
}
if (attachment.screenshot) {
await wrappedLoadAttachment(attachment.screenshot);
}
return attachment;
})
),
6 years ago
preview: await Promise.all(
(message.preview || []).map(async item => {
if (item.image) {
await wrappedLoadAttachment(item.image);
}
return item;
})
),
});
}
let backupDir;
try {
// Seven total:
// - Five from image/video attachments
// - One from embedded contact avatar
6 years ago
// - One from embedded quoted attachment thumbnail
// - One from a link preview image
const ATTACHMENT_COUNT = 8;
const MESSAGE_COUNT = 1;
const CONVERSATION_COUNT = 1;
const messageWithAttachments = {
conversationId: CONTACT_ONE_NUMBER,
body: 'Totally!',
source: OUR_NUMBER,
received_at: 1524185933350,
timestamp: 1524185933350,
errors: [],
attachments: [
// Note: generates two more files: screenshot and thumbnail
{
contentType: 'video/mp4',
fileName: 'video.mp4',
data: FIXTURES.mp4,
},
// Note: generates one more file: thumbnail
{
contentType: 'image/png',
fileName: 'landscape.png',
data: FIXTURES.png,
},
],
hasAttachments: 1,
hasVisualMediaAttachments: 1,
quote: {
text: "Isn't it cute?",
author: CONTACT_ONE_NUMBER,
id: 12345678,
attachments: [
{
contentType: 'audio/mp3',
fileName: 'song.mp3',
},
{
contentType: 'image/gif',
fileName: 'avatar.gif',
thumbnail: {
contentType: 'image/png',
data: FIXTURES.gif,
},
},
],
},
contact: [
{
name: {
displayName: 'Someone Somewhere',
},
number: [
{
value: CONTACT_TWO_NUMBER,
type: 1,
},
],
avatar: {
isProfile: false,
avatar: {
contentType: 'image/png',
data: FIXTURES.png,
},
},
},
],
6 years ago
preview: [
{
url: 'https://www.instagram.com/p/BsOGulcndj-/',
title:
'EGG GANG 🌍 on Instagram: “Lets set a world record together and get the most liked post on Instagram. Beating the current world record held by Kylie Jenner (18…”',
image: {
contentType: 'image/jpeg',
data: FIXTURES.jpg,
},
},
],
};
console.log('Backup test: Clear all data');
await clearAllData();
console.log('Backup test: Create models, save to db/disk');
const message = await upgradeMessageSchema(messageWithAttachments);
console.log({ message });
await window.Signal.Data.saveMessage(message, {
Message: Whisper.Message,
});
const conversation = {
active_at: 1524185933350,
color: 'orange',
expireTimer: 0,
id: CONTACT_ONE_NUMBER,
name: 'Someone Somewhere',
profileAvatar: {
contentType: 'image/jpeg',
data: FIXTURES.jpeg,
size: 64,
},
profileKey: 'BASE64KEY',
profileName: 'Someone! 🤔',
profileSharing: true,
timestamp: 1524185933350,
type: 'private',
unreadCount: 0,
verified: 0,
sealedSender: 0,
version: 2,
};
console.log({ conversation });
await window.Signal.Data.saveConversation(conversation, {
Conversation: Whisper.Conversation,
});
console.log(
'Backup test: Ensure that all attachments were saved to disk'
);
const attachmentFiles = removeDirs(glob.sync(attachmentsPattern));
console.log({ attachmentFiles });
assert.strictEqual(ATTACHMENT_COUNT, attachmentFiles.length);
console.log('Backup test: Export!');
backupDir = tmp.dirSync().name;
console.log({ backupDir });
await Signal.Backup.exportToDirectory(backupDir, {
key: staticKeyPair.pubKey,
});
console.log('Backup test: Ensure that messages.tar.gz exists');
const archivePath = path.join(backupDir, 'messages.tar.gz');
const messageZipExists = fse.existsSync(archivePath);
assert.strictEqual(true, messageZipExists);
console.log(
'Backup test: Ensure that all attachments made it to backup dir'
);
const backupAttachmentPattern = path.join(backupDir, 'attachments/*');
const backupAttachments = glob.sync(backupAttachmentPattern);
console.log({ backupAttachments });
assert.strictEqual(ATTACHMENT_COUNT, backupAttachments.length);
console.log('Backup test: Clear all data');
await clearAllData();
console.log('Backup test: Import!');
await Signal.Backup.importFromDirectory(backupDir, {
key: staticKeyPair.privKey,
});
console.log('Backup test: Check conversations');
const conversationCollection = await window.Signal.Data.getAllConversations(
{
ConversationCollection: Whisper.ConversationCollection,
}
);
assert.strictEqual(conversationCollection.length, CONVERSATION_COUNT);
// We need to ommit any custom fields we have added
const ommited = [
'profileAvatar',
'swarmNodes',
'friendRequestStatus',
Session v1.0 changes (#802) * correct typo in readme * include log * decrypt file server response, remove debug, handle crypt before _sendToProxy, improve json parsing failure logging * support file uploads on file proxy, fix _sendToProxy calling * bump form-data to 3.0 * initial refactor of feaure flag detection statements in serverRequest() * fix send-message line-height with multiple lines * fix lint * fix position of delete account modal * Profile picture upload, fixes and copy * Various changes suggested by redesign overview * Scrolling button updated and animations to modals * Display subscriber count for open chats * Prevent illegal username and passwords * Delete channel / group merge * Solidification of minor changes w appview injections * hide description field in group panel for now * fix join publicgroups pulls * increase min height respecting ratio * allow space inside a display name but not at start or end * fix height of leftpane overlay view * add back typing indicator and read receipt setting under privacy * Auto-focus new open chat input box * Password lock screen and delete data screen * touchups * Resolving Bilb revisions * Disable link previews as default per Kee on signup * remove date, we have git * add missing semicolon * _sendToProxy pass headers/handle response refactor, lint * fix my yarn conflict/resolve * include IV in server response * Sealed sender support * Support sealed sender for friend requests * fix lint * Remove unused destinationRegistrationId; lint * Update messages.json * pull RSS through file proxy * fix unit tests: remove not used count in scrolldown view and assert svg present * Disable auto-joining default loki open groups * session-id-editable-textarea * fix the textscramble for sessionID on registration * speed up lint, add lint-full/format-full, make sure use lint-full * add skipToken to establishConnection options, smuggle out secureRpcPubKey * get latest version through snode proxy, remove clearfix from ExpiredAlertBanner * expose semver and LokiAppDotNetServerAPI because we can't get ourKey from storage early enough * update note * fix upgrade link, wrap expiredWarning in span for styling, use br to clear the float, trim trailing whitespace * designalify * designalify * designalify user agent * continue designalification * make expired banner legible * remove ugly TLS hack * disable unauthorization rejection when making https requests limited to lokiRpc * Update main.js Aspect ratio amendment * Constants rework * local commit * event listeners * address missing comma for lint * fix header sessions message section * fix profile image size conversation list with pending friend request * textarea centering * refresh files in group in group panel * Looking into keyboard navigation * Remove P2P * cache eslint on `lint` but not `ready` * Cleanup media view formatting * force locale to be EN until our files are updated and translated * Simplification of keyup * Updated all icon references * SASS fixup * fix disabled state of message input on sent friendrequest * trim pubkey when user can enter one to remove whitespaces * remove lZ in path which fixes errors on svg and does not alter rendering * fix text scramble animation on registration * reload app on ctrl-r or f5 from anywhere * add back file which should have not been deleted * fix lint and clean code * fix lint * add .loki to have a self-signed cert * Remove mixpanel * use local shortcut instead of global shortcut otherwise, ctrl+r is only caught bu the last loaded instance * open the conversation when accepting a friend request also, it does what is needed to show the new friend in the friend list * make sure token comms are done over fileProxy, other notes, logging adjustment * leftpane sections titles are Wasa bold * minor refactor * onboarding messageview * linter * fix padding buttons overlay * do not render session-id-editable border when textarea disabled * textarea sessionID SpaceMono font * various touchups * fix font of description to sfprodisplay * reduce triple dots conversation header icon size * reduce size of conversationHeader title font size * fix font for session-search-input * make conversationlistitem title font wasa * fix green and white border under title in leftpane * fix panel-text-divider font-size and family * disable completely borders for profile images * make profile image which where 48pixels big 36 noew, as no more border * Complete conditional message onboarding * cache file deletions * Link preview warning on setting toggle * Messages.json amendments * Join channel generalisation * Localise global vars * remove eslintcache * rm global launchcount * Remove source field from envelope * Session public chat icon * CLosed groups ui initial listprops * Desktop: enable useSnodeProxy feature flag * file proxy needs to be able to talk to snode - disable TLS check for fileProxy - lokiHttpsAgent => snodeHttpsAgent (since we use for two different things now) * enable useSealedSender too per Maxim * lint * lint * window.extension.expiredPromise version * better error checking * use promise version to see if we're expired * fix typo * lint * put back seemingly now required process.env.NODE_TLS_REJECT_UNAUTHORIZED * fix querystring in file-proxy * lint * fix typo * Remove more references to signal.org * make sure TLS is forced on open groups, improve serverRequest error message * Closed groups UI * function params changes * turn off snode proxy logging * include useful info on error * actually validate URL before starting up a bunch of timers * Closed groups overlay integration * move comments from connecting_to_server_dialog_view * use attempt from window object to reduce code duplication * refactor out validServer() * lint * lint caught typo * Rename BACKGROUND_FRIEND_REQUEST to SESSION_REQUEST. Don't trigger friend request logic if a message is aimed at a group. * Linting * Closed group joining completed w/o backend * Fix friend request messages being sent to users you don't have a session in closed groups. Disable typing messages and read receipts in groups. Send out session request messages if you don't have a session with a member in the group. * Remove unneeded boolean condition. * Closed group update message stylgin * constants renaming * Message deletion fix * gruntify * fix grunt error * expose isRss, don't close uncloseable Rss conversation on deleteMessages * remove copyId and block user on RSS feeds * remove options from RSS feed that don't make any sense and don't work * fix grunt error * squelch RSS duplicate messages * extension.expiredStatus(), adjustable timers, improve guards * allowing sending of messages if we're still waiting to hear back * markRandomNodeUnreachable() refactor, notes/logging * improve logging * improve logging * no need to validate empty token, support lokinet/getession file domains, mark broken snodes as bad, improve logging * try to address travis-osx lint complaints * not designed to have a period at the end of titleIsNow * put period back at the end * Catch a stray loki messenger * fix stray loki messenger * loki messenger isnt a thing * lint * Fix open group joining. * guards incase there are no members yet, fixes dialog not showing up * fixed file server holding up message sender init. fix joining closed groups. * Clean * Don't wait for file server to return tokens when establishing home connection. * Disable join public chat prompt Co-authored-by: Audric Ackermann <audric.bilb@gmail.com> Co-authored-by: Ryan Tharp <neuro@interx.net> Co-authored-by: Vince <vincent@loki.network> Co-authored-by: Maxim Shishmarev <msgmaxim@gmail.com>
5 years ago
'groupAdmins',
'isKickedFromGroup',
'unlockTimestamp',
'sessionResetStatus',
6 years ago
'isOnline',
];
const conversationFromDB = conversationCollection.at(0).attributes;
console.log({ conversationFromDB, conversation });
assert.deepEqual(
_.omit(conversationFromDB, ommited),
_.omit(conversation, ommited)
);
console.log('Backup test: Check messages');
const messageCollection = await window.Signal.Data.getAllMessages({
MessageCollection: Whisper.MessageCollection,
});
assert.strictEqual(messageCollection.length, MESSAGE_COUNT);
const messageFromDB = removeId(messageCollection.at(0).attributes);
const expectedMessage = messageFromDB;
console.log({ messageFromDB, expectedMessage });
assert.deepEqual(messageFromDB, expectedMessage);
console.log('Backup test: ensure that all attachments were imported');
const recreatedAttachmentFiles = removeDirs(
glob.sync(attachmentsPattern)
);
console.log({ recreatedAttachmentFiles });
assert.strictEqual(ATTACHMENT_COUNT, recreatedAttachmentFiles.length);
assert.deepEqual(attachmentFiles, recreatedAttachmentFiles);
console.log(
'Backup test: Check that all attachments were successfully imported'
);
const messageWithAttachmentsFromDB = await loadAllFilesFromDisk(
messageFromDB
);
const expectedMessageWithAttachments = await loadAllFilesFromDisk(
omitUndefinedKeys(message)
);
console.log({
messageWithAttachmentsFromDB,
expectedMessageWithAttachments,
});
assert.deepEqual(
_.omit(messageWithAttachmentsFromDB, ['sent']),
expectedMessageWithAttachments
);
console.log('Backup test: Clear all data');
await clearAllData();
console.log('Backup test: Complete!');
} finally {
if (backupDir) {
console.log({ backupDir });
console.log('Deleting', backupDir);
await fse.remove(backupDir);
}
}
});
});
});