Timeline: repair oldest/newest metrics if we fetch nothing
This commit is contained in:
parent
56ae4a41eb
commit
6832b8acca
47 changed files with 579 additions and 173 deletions
208
ts/test-node/components/LeftPane_test.tsx
Normal file
208
ts/test-node/components/LeftPane_test.tsx
Normal file
|
@ -0,0 +1,208 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { LeftPane, RowType, HeaderType } from '../../components/LeftPane';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
describe('LeftPane', () => {
|
||||
const defaultProps = {
|
||||
archivedConversations: [],
|
||||
conversations: [],
|
||||
i18n,
|
||||
openConversationInternal: () => null,
|
||||
pinnedConversations: [],
|
||||
renderExpiredBuildDialog: () => <div />,
|
||||
renderMainHeader: () => <div />,
|
||||
renderMessageSearchResult: () => <div />,
|
||||
renderNetworkStatus: () => <div />,
|
||||
renderRelinkDialog: () => <div />,
|
||||
renderUpdateDialog: () => <div />,
|
||||
showArchivedConversations: () => null,
|
||||
showInbox: () => null,
|
||||
startNewConversation: () => null,
|
||||
};
|
||||
|
||||
describe('getRowFromIndex', () => {
|
||||
describe('given only pinned chats', () => {
|
||||
it('returns pinned chats, not headers', () => {
|
||||
const leftPane = new LeftPane({
|
||||
...defaultProps,
|
||||
pinnedConversations: [
|
||||
{
|
||||
id: 'philly-convo',
|
||||
isPinned: true,
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Philip Glass',
|
||||
type: 'direct',
|
||||
},
|
||||
{
|
||||
id: 'robbo-convo',
|
||||
isPinned: true,
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Robert Moog',
|
||||
type: 'direct',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(leftPane.getRowFromIndex(0), {
|
||||
index: 0,
|
||||
type: RowType.PinnedConversation,
|
||||
});
|
||||
assert.deepEqual(leftPane.getRowFromIndex(1), {
|
||||
index: 1,
|
||||
type: RowType.PinnedConversation,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given only non-pinned chats', () => {
|
||||
it('returns conversations, not headers', () => {
|
||||
const leftPane = new LeftPane({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
{
|
||||
id: 'fred-convo',
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Fred Willard',
|
||||
type: 'direct',
|
||||
},
|
||||
{
|
||||
id: 'robbo-convo',
|
||||
isPinned: false,
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Robert Moog',
|
||||
type: 'direct',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(leftPane.getRowFromIndex(0), {
|
||||
index: 0,
|
||||
type: RowType.Conversation,
|
||||
});
|
||||
assert.deepEqual(leftPane.getRowFromIndex(1), {
|
||||
index: 1,
|
||||
type: RowType.Conversation,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given only pinned and non-pinned chats', () => {
|
||||
it('returns headers and conversations', () => {
|
||||
const leftPane = new LeftPane({
|
||||
...defaultProps,
|
||||
conversations: [
|
||||
{
|
||||
id: 'fred-convo',
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Fred Willard',
|
||||
type: 'direct',
|
||||
},
|
||||
],
|
||||
pinnedConversations: [
|
||||
{
|
||||
id: 'philly-convo',
|
||||
isPinned: true,
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Philip Glass',
|
||||
type: 'direct',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(leftPane.getRowFromIndex(0), {
|
||||
headerType: HeaderType.Pinned,
|
||||
type: RowType.Header,
|
||||
});
|
||||
assert.deepEqual(leftPane.getRowFromIndex(1), {
|
||||
index: 0,
|
||||
type: RowType.PinnedConversation,
|
||||
});
|
||||
assert.deepEqual(leftPane.getRowFromIndex(2), {
|
||||
headerType: HeaderType.Chats,
|
||||
type: RowType.Header,
|
||||
});
|
||||
assert.deepEqual(leftPane.getRowFromIndex(3), {
|
||||
index: 0,
|
||||
type: RowType.Conversation,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given not showing archive with archived conversation', () => {
|
||||
it('returns an archive button last', () => {
|
||||
const leftPane = new LeftPane({
|
||||
...defaultProps,
|
||||
archivedConversations: [
|
||||
{
|
||||
id: 'jerry-convo',
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Jerry Jordan',
|
||||
type: 'direct',
|
||||
},
|
||||
],
|
||||
conversations: [
|
||||
{
|
||||
id: 'fred-convo',
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Fred Willard',
|
||||
type: 'direct',
|
||||
},
|
||||
],
|
||||
showArchived: false,
|
||||
});
|
||||
|
||||
assert.deepEqual(leftPane.getRowFromIndex(1), {
|
||||
type: RowType.ArchiveButton,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given showing archive and archive chats', () => {
|
||||
it('returns archived conversations', () => {
|
||||
const leftPane = new LeftPane({
|
||||
...defaultProps,
|
||||
archivedConversations: [
|
||||
{
|
||||
id: 'fred-convo',
|
||||
isSelected: false,
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
title: 'Fred Willard',
|
||||
type: 'direct',
|
||||
},
|
||||
],
|
||||
showArchived: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(leftPane.getRowFromIndex(0), {
|
||||
index: 0,
|
||||
type: RowType.ArchivedConversation,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,230 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { shuffle } from 'lodash';
|
||||
|
||||
import { IMAGE_JPEG } from '../../../types/MIME';
|
||||
import {
|
||||
groupMediaItemsByDate,
|
||||
Section,
|
||||
} from '../../../components/conversation/media-gallery/groupMediaItemsByDate';
|
||||
import { MediaItemType } from '../../../components/LightboxGallery';
|
||||
|
||||
const toMediaItem = (date: Date): MediaItemType => ({
|
||||
objectURL: date.toUTCString(),
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: date.getTime(),
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
});
|
||||
|
||||
describe('groupMediaItemsByDate', () => {
|
||||
it('should group mediaItems', () => {
|
||||
const referenceTime = new Date('2018-04-12T18:00Z').getTime(); // Thu
|
||||
const input: Array<MediaItemType> = shuffle([
|
||||
// Today
|
||||
toMediaItem(new Date('2018-04-12T12:00Z')), // Thu
|
||||
toMediaItem(new Date('2018-04-12T00:01Z')), // Thu
|
||||
// This week
|
||||
toMediaItem(new Date('2018-04-11T23:59Z')), // Wed
|
||||
toMediaItem(new Date('2018-04-09T00:01Z')), // Mon
|
||||
// This month
|
||||
toMediaItem(new Date('2018-04-08T23:59Z')), // Sun
|
||||
toMediaItem(new Date('2018-04-01T00:01Z')),
|
||||
// March 2018
|
||||
toMediaItem(new Date('2018-03-31T23:59Z')),
|
||||
toMediaItem(new Date('2018-03-01T14:00Z')),
|
||||
// February 2011
|
||||
toMediaItem(new Date('2011-02-28T23:59Z')),
|
||||
toMediaItem(new Date('2011-02-01T10:00Z')),
|
||||
]);
|
||||
|
||||
const expected: Array<Section> = [
|
||||
{
|
||||
type: 'today',
|
||||
mediaItems: [
|
||||
{
|
||||
objectURL: 'Thu, 12 Apr 2018 12:00:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1523534400000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
{
|
||||
objectURL: 'Thu, 12 Apr 2018 00:01:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1523491260000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'yesterday',
|
||||
mediaItems: [
|
||||
{
|
||||
objectURL: 'Wed, 11 Apr 2018 23:59:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1523491140000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'thisWeek',
|
||||
mediaItems: [
|
||||
{
|
||||
objectURL: 'Mon, 09 Apr 2018 00:01:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1523232060000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'thisMonth',
|
||||
mediaItems: [
|
||||
{
|
||||
objectURL: 'Sun, 08 Apr 2018 23:59:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1523231940000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
{
|
||||
objectURL: 'Sun, 01 Apr 2018 00:01:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1522540860000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'yearMonth',
|
||||
year: 2018,
|
||||
month: 2,
|
||||
mediaItems: [
|
||||
{
|
||||
objectURL: 'Sat, 31 Mar 2018 23:59:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1522540740000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
{
|
||||
objectURL: 'Thu, 01 Mar 2018 14:00:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1519912800000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'yearMonth',
|
||||
year: 2011,
|
||||
month: 1,
|
||||
mediaItems: [
|
||||
{
|
||||
objectURL: 'Mon, 28 Feb 2011 23:59:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1298937540000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
{
|
||||
objectURL: 'Tue, 01 Feb 2011 10:00:00 GMT',
|
||||
index: 0,
|
||||
message: {
|
||||
id: 'id',
|
||||
received_at: 1296554400000,
|
||||
attachments: [],
|
||||
},
|
||||
attachment: {
|
||||
fileName: 'fileName',
|
||||
contentType: IMAGE_JPEG,
|
||||
url: 'url',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const actual = groupMediaItemsByDate(referenceTime, input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
17
ts/test-node/helpers.ts
Normal file
17
ts/test-node/helpers.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
export async function assertRejects(fn: () => Promise<unknown>): Promise<void> {
|
||||
let err: unknown;
|
||||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
assert(
|
||||
err instanceof Error,
|
||||
'Expected promise to reject with an Error, but it resolved'
|
||||
);
|
||||
}
|
115
ts/test-node/license_comments_test.ts
Normal file
115
ts/test-node/license_comments_test.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import * as readline from 'readline';
|
||||
import * as childProcess from 'child_process';
|
||||
import pMap from 'p-map';
|
||||
|
||||
const exec = promisify(childProcess.exec);
|
||||
|
||||
const EXTENSIONS_TO_CHECK = new Set([
|
||||
'.eslintignore',
|
||||
'.gitattributes',
|
||||
'.gitignore',
|
||||
'.nvmrc',
|
||||
'.prettierignore',
|
||||
'.sh',
|
||||
'.snyk',
|
||||
'.yarnclean',
|
||||
'.yml',
|
||||
'.js',
|
||||
'.scss',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.html',
|
||||
'.md',
|
||||
'.plist',
|
||||
]);
|
||||
const FILES_TO_IGNORE = new Set([
|
||||
'ISSUE_TEMPLATE.md',
|
||||
'Mp3LameEncoder.min.js',
|
||||
'PULL_REQUEST_TEMPLATE.md',
|
||||
'WebAudioRecorderMp3.js',
|
||||
]);
|
||||
|
||||
const rootPath = path.join(__dirname, '..', '..');
|
||||
|
||||
async function getGitFiles(): Promise<Array<string>> {
|
||||
return (await exec('git ls-files', { cwd: rootPath, env: {} })).stdout
|
||||
.split(/\n/g)
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.map(file => path.join(rootPath, file));
|
||||
}
|
||||
|
||||
// This is not technically the real extension.
|
||||
function getExtension(file: string): string {
|
||||
if (file.startsWith('.')) {
|
||||
return getExtension(`x.${file}`);
|
||||
}
|
||||
return path.extname(file);
|
||||
}
|
||||
|
||||
function readFirstTwoLines(file: string): Promise<Array<string>> {
|
||||
return new Promise(resolve => {
|
||||
const lines: Array<string> = [];
|
||||
|
||||
const lineReader = readline.createInterface({
|
||||
input: fs.createReadStream(file),
|
||||
});
|
||||
lineReader.on('line', line => {
|
||||
lines.push(line);
|
||||
if (lines.length >= 2) {
|
||||
lineReader.close();
|
||||
}
|
||||
});
|
||||
lineReader.on('close', () => {
|
||||
resolve(lines);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('license comments', () => {
|
||||
it('includes a license comment at the top of every relevant file', async function test() {
|
||||
// This usually executes quickly but can be slow in some cases, such as Windows CI.
|
||||
this.timeout(10000);
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
await pMap(
|
||||
await getGitFiles(),
|
||||
async (file: string) => {
|
||||
if (
|
||||
FILES_TO_IGNORE.has(path.basename(file)) ||
|
||||
path.relative(rootPath, file).startsWith('components')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = getExtension(file);
|
||||
if (!EXTENSIONS_TO_CHECK.has(extension)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [firstLine, secondLine] = await readFirstTwoLines(file);
|
||||
|
||||
assert.match(
|
||||
firstLine,
|
||||
RegExp(`Copyright (?:\\d{4}-)?${currentYear} Signal Messenger, LLC`),
|
||||
`First line of ${file} is missing correct license header comment`
|
||||
);
|
||||
assert.include(
|
||||
secondLine,
|
||||
'SPDX-License-Identifier: AGPL-3.0-only',
|
||||
`Second line of ${file} is missing correct license header comment`
|
||||
);
|
||||
},
|
||||
// Without this, we may run into "too many open files" errors.
|
||||
{ concurrency: 100 }
|
||||
);
|
||||
});
|
||||
});
|
44
ts/test-node/linkPreviews/isLinkPreviewDateValid_test.ts
Normal file
44
ts/test-node/linkPreviews/isLinkPreviewDateValid_test.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isLinkPreviewDateValid } from '../../linkPreviews/isLinkPreviewDateValid';
|
||||
|
||||
describe('isLinkPreviewDateValid', () => {
|
||||
it('returns false for non-numbers', () => {
|
||||
assert.isFalse(isLinkPreviewDateValid(null));
|
||||
assert.isFalse(isLinkPreviewDateValid(undefined));
|
||||
assert.isFalse(isLinkPreviewDateValid(Date.now().toString()));
|
||||
assert.isFalse(isLinkPreviewDateValid(new Date()));
|
||||
});
|
||||
|
||||
it('returns false for zero', () => {
|
||||
assert.isFalse(isLinkPreviewDateValid(0));
|
||||
assert.isFalse(isLinkPreviewDateValid(-0));
|
||||
});
|
||||
|
||||
it('returns false for NaN', () => {
|
||||
assert.isFalse(isLinkPreviewDateValid(0 / 0));
|
||||
});
|
||||
|
||||
it('returns false for any infinite value', () => {
|
||||
assert.isFalse(isLinkPreviewDateValid(Infinity));
|
||||
assert.isFalse(isLinkPreviewDateValid(-Infinity));
|
||||
});
|
||||
|
||||
it('returns false for timestamps more than a day from now', () => {
|
||||
const twoDays = 2 * 24 * 60 * 60 * 1000;
|
||||
assert.isFalse(isLinkPreviewDateValid(Date.now() + twoDays));
|
||||
});
|
||||
|
||||
it('returns true for timestamps before tomorrow', () => {
|
||||
assert.isTrue(isLinkPreviewDateValid(Date.now()));
|
||||
assert.isTrue(isLinkPreviewDateValid(Date.now() + 123));
|
||||
assert.isTrue(isLinkPreviewDateValid(Date.now() - 123));
|
||||
assert.isTrue(isLinkPreviewDateValid(new Date(1995, 3, 20).valueOf()));
|
||||
assert.isTrue(isLinkPreviewDateValid(new Date(1970, 3, 20).valueOf()));
|
||||
assert.isTrue(isLinkPreviewDateValid(new Date(1969, 3, 20).valueOf()));
|
||||
assert.isTrue(isLinkPreviewDateValid(1));
|
||||
});
|
||||
});
|
442
ts/test-node/quill/emoji/completion_test.tsx
Normal file
442
ts/test-node/quill/emoji/completion_test.tsx
Normal file
|
@ -0,0 +1,442 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { EmojiCompletion } from '../../../quill/emoji/completion';
|
||||
import { EmojiData } from '../../../components/emoji/lib';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const globalAsAny = global as any;
|
||||
|
||||
describe('emojiCompletion', () => {
|
||||
let emojiCompletion: EmojiCompletion;
|
||||
const mockOnPickEmoji = sinon.spy();
|
||||
const mockSetEmojiPickerElement = sinon.spy();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mockQuill: any;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.oldDocument = globalAsAny.document;
|
||||
globalAsAny.document = {
|
||||
body: {
|
||||
appendChild: () => null,
|
||||
},
|
||||
createElement: () => null,
|
||||
};
|
||||
|
||||
mockQuill = {
|
||||
getLeaf: sinon.stub(),
|
||||
getSelection: sinon.stub(),
|
||||
keyboard: {
|
||||
addBinding: sinon.stub(),
|
||||
},
|
||||
on: sinon.stub(),
|
||||
setSelection: sinon.stub(),
|
||||
updateContents: sinon.stub(),
|
||||
};
|
||||
const options = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onPickEmoji: mockOnPickEmoji as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setEmojiPickerElement: mockSetEmojiPickerElement as any,
|
||||
skinTone: 0,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
emojiCompletion = new EmojiCompletion(mockQuill as any, options);
|
||||
|
||||
// Stub rendering to avoid missing DOM until we bring in Enzyme
|
||||
emojiCompletion.render = sinon.stub();
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
mockOnPickEmoji.resetHistory();
|
||||
mockSetEmojiPickerElement.resetHistory();
|
||||
(emojiCompletion.render as sinon.SinonStub).resetHistory();
|
||||
|
||||
if (this.oldDocument === undefined) {
|
||||
delete globalAsAny.document;
|
||||
} else {
|
||||
globalAsAny.document = this.oldDocument;
|
||||
}
|
||||
});
|
||||
|
||||
describe('getCurrentLeafTextPartitions', () => {
|
||||
it('returns left and right text', () => {
|
||||
mockQuill.getSelection.returns({ index: 0, length: 0 });
|
||||
const blot = {
|
||||
text: ':smile:',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 2]);
|
||||
const [
|
||||
leftLeafText,
|
||||
rightLeafText,
|
||||
] = emojiCompletion.getCurrentLeafTextPartitions();
|
||||
assert.equal(leftLeafText, ':s');
|
||||
assert.equal(rightLeafText, 'mile:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onTextChange', () => {
|
||||
let insertEmojiStub: sinon.SinonStub<
|
||||
[EmojiData, number, number, (boolean | undefined)?],
|
||||
void
|
||||
>;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
emojiCompletion.results = [{ short_name: 'joy' } as any];
|
||||
emojiCompletion.index = 5;
|
||||
insertEmojiStub = sinon
|
||||
.stub(emojiCompletion, 'insertEmoji')
|
||||
.callThrough();
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
insertEmojiStub.restore();
|
||||
});
|
||||
|
||||
describe('given an emoji is not starting (no colon)', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 3,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: 'smi',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 3]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a colon in a string (but not an emoji)', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 5,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: '10:30',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 5]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji is starting but does not have 2 characters', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 2,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: ':s',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 2]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji is starting but does not match a short name', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 4,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: ':smy',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 4]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji is starting and matches short names', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 4,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: ':smi',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 4]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('stores the results and renders', () => {
|
||||
assert.equal(emojiCompletion.results.length, 10);
|
||||
assert.equal((emojiCompletion.render as sinon.SinonStub).called, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji was just completed', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 7,
|
||||
length: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it matches a short name', () => {
|
||||
const text = ':smile:';
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 7]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, 7);
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it matches a short name inside a larger string', () => {
|
||||
const text = 'have a :smile: nice day';
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getSelection.returns({
|
||||
index: 13,
|
||||
length: 0,
|
||||
});
|
||||
mockQuill.getLeaf.returns([blot, 13]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(index, 7);
|
||||
assert.equal(range, 7);
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
|
||||
it('sets the quill selection to the right cursor position', () => {
|
||||
const [index, range] = mockQuill.setSelection.args[0];
|
||||
|
||||
assert.equal(index, 8);
|
||||
assert.equal(range, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it does not match a short name', () => {
|
||||
const text = ':smyle:';
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 7]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji was just completed from inside the colons', () => {
|
||||
const validEmoji = ':smile:';
|
||||
const invalidEmoji = ':smyle:';
|
||||
const middleCursorIndex = validEmoji.length - 3;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: middleCursorIndex,
|
||||
length: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it matches a short name', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text: validEmoji,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, middleCursorIndex]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, validEmoji.length);
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it does not match a short name', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text: invalidEmoji,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, middleCursorIndex]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a completeable emoji and colon was just pressed', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 6,
|
||||
length: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it matches a short name', () => {
|
||||
const text = ':smile';
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 6]);
|
||||
|
||||
emojiCompletion.onTextChange(true);
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, 6);
|
||||
});
|
||||
|
||||
it('does not show results', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeEmoji', () => {
|
||||
let insertEmojiStub: sinon.SinonStub<
|
||||
[EmojiData, number, number, (boolean | undefined)?],
|
||||
void
|
||||
>;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
emojiCompletion.results = [
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ short_name: 'smile' } as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ short_name: 'smile_cat' } as any,
|
||||
];
|
||||
emojiCompletion.index = 1;
|
||||
insertEmojiStub = sinon.stub(emojiCompletion, 'insertEmoji');
|
||||
});
|
||||
|
||||
describe('given a valid token', () => {
|
||||
const text = ':smi';
|
||||
const index = text.length;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, index]);
|
||||
|
||||
emojiCompletion.completeEmoji();
|
||||
});
|
||||
|
||||
it('inserts the currently selected emoji at the current cursor position', () => {
|
||||
const [emoji, insertIndex, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile_cat');
|
||||
assert.equal(insertIndex, 0);
|
||||
assert.equal(range, text.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a valid token is not present', () => {
|
||||
const text = 'smi';
|
||||
const index = text.length;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, index]);
|
||||
|
||||
emojiCompletion.completeEmoji();
|
||||
});
|
||||
|
||||
it('does not insert anything', () => {
|
||||
assert.equal(insertEmojiStub.called, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
137
ts/test-node/quill/memberRepository_test.ts
Normal file
137
ts/test-node/quill/memberRepository_test.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { MemberRepository } from '../../quill/memberRepository';
|
||||
|
||||
const memberMahershala: ConversationType = {
|
||||
id: '555444',
|
||||
uuid: 'abcdefg',
|
||||
title: 'Pal',
|
||||
firstName: 'Mahershala',
|
||||
profileName: 'Mr Ali',
|
||||
name: 'Friend',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const memberShia: ConversationType = {
|
||||
id: '333222',
|
||||
uuid: 'hijklmno',
|
||||
title: 'Buddy',
|
||||
firstName: 'Shia',
|
||||
profileName: 'Sr LaBeouf',
|
||||
name: 'Duder',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const members: Array<ConversationType> = [memberMahershala, memberShia];
|
||||
|
||||
const singleMember: ConversationType = {
|
||||
id: '666777',
|
||||
uuid: 'pqrstuv',
|
||||
title: 'The Guy',
|
||||
firstName: 'Jeff',
|
||||
profileName: 'Jr Klaus',
|
||||
name: 'Him',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
describe('MemberRepository', () => {
|
||||
describe('#updateMembers', () => {
|
||||
it('updates with given members', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
assert.deepEqual(memberRepository.getMembers(), members);
|
||||
|
||||
const updatedMembers = [...members, singleMember];
|
||||
memberRepository.updateMembers(updatedMembers);
|
||||
assert.deepEqual(memberRepository.getMembers(), updatedMembers);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getMemberById', () => {
|
||||
it('returns undefined when there is no search id', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
assert.isUndefined(memberRepository.getMemberById());
|
||||
});
|
||||
|
||||
it('returns a matched member', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
assert.isDefined(memberRepository.getMemberById('555444'));
|
||||
});
|
||||
|
||||
it('returns undefined when it does not have the member', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
assert.isUndefined(memberRepository.getMemberById('nope'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getMemberByUuid', () => {
|
||||
it('returns undefined when there is no search uuid', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
assert.isUndefined(memberRepository.getMemberByUuid());
|
||||
});
|
||||
|
||||
it('returns a matched member', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
assert.isDefined(memberRepository.getMemberByUuid('abcdefg'));
|
||||
});
|
||||
|
||||
it('returns undefined when it does not have the member', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
assert.isUndefined(memberRepository.getMemberByUuid('nope'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#search', () => {
|
||||
describe('given a prefix-matching string on last name', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
const results = memberRepository.search('a');
|
||||
assert.deepEqual(results, [memberMahershala]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a prefix-matching string on first name', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
const results = memberRepository.search('ma');
|
||||
assert.deepEqual(results, [memberMahershala]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a prefix-matching string on profile name', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
const results = memberRepository.search('sr');
|
||||
assert.deepEqual(results, [memberShia]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a prefix-matching string on title', () => {
|
||||
it('returns the match', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
const results = memberRepository.search('d');
|
||||
assert.deepEqual(results, [memberShia]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a match in the middle of a name', () => {
|
||||
it('returns zero matches', () => {
|
||||
const memberRepository = new MemberRepository(members);
|
||||
const results = memberRepository.search('e');
|
||||
assert.deepEqual(results, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
269
ts/test-node/quill/mentions/completion_test.tsx
Normal file
269
ts/test-node/quill/mentions/completion_test.tsx
Normal file
|
@ -0,0 +1,269 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import Delta from 'quill-delta';
|
||||
import sinon, { SinonStub } from 'sinon';
|
||||
import Quill, { KeyboardStatic } from 'quill';
|
||||
|
||||
import { MutableRefObject } from 'react';
|
||||
import {
|
||||
MentionCompletion,
|
||||
MentionCompletionOptions,
|
||||
} from '../../../quill/mentions/completion';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { MemberRepository } from '../../../quill/memberRepository';
|
||||
|
||||
const me: ConversationType = {
|
||||
id: '666777',
|
||||
uuid: 'pqrstuv',
|
||||
title: 'Fred Savage',
|
||||
firstName: 'Fred',
|
||||
profileName: 'Fred S.',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const members: Array<ConversationType> = [
|
||||
{
|
||||
id: '555444',
|
||||
uuid: 'abcdefg',
|
||||
title: 'Mahershala Ali',
|
||||
firstName: 'Mahershala',
|
||||
profileName: 'Mahershala A.',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
},
|
||||
{
|
||||
id: '333222',
|
||||
uuid: 'hijklmno',
|
||||
title: 'Shia LaBeouf',
|
||||
firstName: 'Shia',
|
||||
profileName: 'Shia L.',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
},
|
||||
me,
|
||||
];
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
document: {
|
||||
body: {
|
||||
appendChild: unknown;
|
||||
};
|
||||
createElement: unknown;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('MentionCompletion', () => {
|
||||
const mockSetMentionPickerElement = sinon.spy();
|
||||
|
||||
let mockQuill: Omit<
|
||||
Partial<{ [K in keyof Quill]: SinonStub }>,
|
||||
'keyboard'
|
||||
> & {
|
||||
keyboard: Partial<{ [K in keyof KeyboardStatic]: SinonStub }>;
|
||||
};
|
||||
let mentionCompletion: MentionCompletion;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
global.document = {
|
||||
body: {
|
||||
appendChild: sinon.spy(),
|
||||
},
|
||||
createElement: sinon.spy(),
|
||||
};
|
||||
|
||||
const memberRepositoryRef: MutableRefObject<MemberRepository> = {
|
||||
current: new MemberRepository(members),
|
||||
};
|
||||
|
||||
const options: MentionCompletionOptions = {
|
||||
i18n: sinon.stub(),
|
||||
me,
|
||||
memberRepositoryRef,
|
||||
setMentionPickerElement: mockSetMentionPickerElement,
|
||||
};
|
||||
|
||||
mockQuill = {
|
||||
getContents: sinon.stub(),
|
||||
getLeaf: sinon.stub(),
|
||||
getSelection: sinon.stub(),
|
||||
keyboard: { addBinding: sinon.stub() },
|
||||
on: sinon.stub(),
|
||||
setSelection: sinon.stub(),
|
||||
updateContents: sinon.stub(),
|
||||
};
|
||||
|
||||
mentionCompletion = new MentionCompletion(
|
||||
(mockQuill as unknown) as Quill,
|
||||
options
|
||||
);
|
||||
|
||||
sinon.stub(mentionCompletion, 'render');
|
||||
});
|
||||
|
||||
describe('onTextChange', () => {
|
||||
let possiblyShowMemberResultsStub: sinon.SinonStub<[], ConversationType[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
possiblyShowMemberResultsStub = sinon.stub(
|
||||
mentionCompletion,
|
||||
'possiblyShowMemberResults'
|
||||
);
|
||||
});
|
||||
|
||||
describe('given a change that should show members', () => {
|
||||
const newContents = new Delta().insert('@a');
|
||||
|
||||
beforeEach(() => {
|
||||
mockQuill.getContents?.returns(newContents);
|
||||
|
||||
possiblyShowMemberResultsStub.returns(members);
|
||||
});
|
||||
|
||||
it('shows member results', () => {
|
||||
mentionCompletion.onTextChange();
|
||||
|
||||
assert.equal(mentionCompletion.results, members);
|
||||
assert.equal(mentionCompletion.index, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a change that should clear results', () => {
|
||||
const newContents = new Delta().insert('foo ');
|
||||
|
||||
let clearResultsStub: SinonStub<[], void>;
|
||||
|
||||
beforeEach(() => {
|
||||
mentionCompletion.results = members;
|
||||
|
||||
mockQuill.getContents?.returns(newContents);
|
||||
|
||||
possiblyShowMemberResultsStub.returns([]);
|
||||
|
||||
clearResultsStub = sinon.stub(mentionCompletion, 'clearResults');
|
||||
});
|
||||
|
||||
it('clears member results', () => {
|
||||
mentionCompletion.onTextChange();
|
||||
|
||||
assert.equal(clearResultsStub.called, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeMention', () => {
|
||||
describe('given a completable mention', () => {
|
||||
let insertMentionStub: SinonStub<
|
||||
[ConversationType, number, number, (boolean | undefined)?],
|
||||
void
|
||||
>;
|
||||
|
||||
beforeEach(() => {
|
||||
mentionCompletion.results = members;
|
||||
mockQuill.getSelection?.returns({ index: 5 });
|
||||
mockQuill.getLeaf?.returns([{ text: '@shia' }, 5]);
|
||||
|
||||
insertMentionStub = sinon.stub(mentionCompletion, 'insertMention');
|
||||
});
|
||||
|
||||
it('inserts the currently selected mention at the current cursor position', () => {
|
||||
mentionCompletion.completeMention(1);
|
||||
|
||||
const [
|
||||
member,
|
||||
distanceFromCursor,
|
||||
adjustCursorAfterBy,
|
||||
withTrailingSpace,
|
||||
] = insertMentionStub.getCall(0).args;
|
||||
|
||||
assert.equal(member, members[1]);
|
||||
assert.equal(distanceFromCursor, 0);
|
||||
assert.equal(adjustCursorAfterBy, 5);
|
||||
assert.equal(withTrailingSpace, true);
|
||||
});
|
||||
|
||||
it('can infer the member to complete with', () => {
|
||||
mentionCompletion.index = 1;
|
||||
mentionCompletion.completeMention();
|
||||
|
||||
const [
|
||||
member,
|
||||
distanceFromCursor,
|
||||
adjustCursorAfterBy,
|
||||
withTrailingSpace,
|
||||
] = insertMentionStub.getCall(0).args;
|
||||
|
||||
assert.equal(member, members[1]);
|
||||
assert.equal(distanceFromCursor, 0);
|
||||
assert.equal(adjustCursorAfterBy, 5);
|
||||
assert.equal(withTrailingSpace, true);
|
||||
});
|
||||
|
||||
describe('from the middle of a string', () => {
|
||||
beforeEach(() => {
|
||||
mockQuill.getSelection?.returns({ index: 9 });
|
||||
mockQuill.getLeaf?.returns([{ text: 'foo @shia bar' }, 9]);
|
||||
});
|
||||
|
||||
it('inserts correctly', () => {
|
||||
mentionCompletion.completeMention(1);
|
||||
|
||||
const [
|
||||
member,
|
||||
distanceFromCursor,
|
||||
adjustCursorAfterBy,
|
||||
withTrailingSpace,
|
||||
] = insertMentionStub.getCall(0).args;
|
||||
|
||||
assert.equal(member, members[1]);
|
||||
assert.equal(distanceFromCursor, 4);
|
||||
assert.equal(adjustCursorAfterBy, 5);
|
||||
assert.equal(withTrailingSpace, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a completable mention starting with a capital letter', () => {
|
||||
const text = '@Sh';
|
||||
const index = text.length;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection?.returns({ index });
|
||||
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf?.returns([blot, index]);
|
||||
|
||||
mentionCompletion.completeMention(1);
|
||||
});
|
||||
|
||||
it('inserts the currently selected mention at the current cursor position', () => {
|
||||
const [
|
||||
member,
|
||||
distanceFromCursor,
|
||||
adjustCursorAfterBy,
|
||||
withTrailingSpace,
|
||||
] = insertMentionStub.getCall(0).args;
|
||||
|
||||
assert.equal(member, members[1]);
|
||||
assert.equal(distanceFromCursor, 0);
|
||||
assert.equal(adjustCursorAfterBy, 3);
|
||||
assert.equal(withTrailingSpace, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
189
ts/test-node/quill/mentions/matchers_test.ts
Normal file
189
ts/test-node/quill/mentions/matchers_test.ts
Normal file
|
@ -0,0 +1,189 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { RefObject } from 'react';
|
||||
import Delta from 'quill-delta';
|
||||
|
||||
import { matchMention } from '../../../quill/mentions/matchers';
|
||||
import { MemberRepository } from '../../../quill/memberRepository';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
|
||||
class FakeTokenList<T> extends Array<T> {
|
||||
constructor(elements: Array<T>) {
|
||||
super();
|
||||
elements.forEach(element => this.push(element));
|
||||
}
|
||||
|
||||
contains(searchElement: T) {
|
||||
return this.includes(searchElement);
|
||||
}
|
||||
}
|
||||
|
||||
const createMockElement = (
|
||||
className: string,
|
||||
dataset: Record<string, string>
|
||||
): HTMLElement =>
|
||||
(({
|
||||
classList: new FakeTokenList([className]),
|
||||
dataset,
|
||||
} as unknown) as HTMLElement);
|
||||
|
||||
const createMockAtMentionElement = (
|
||||
dataset: Record<string, string>
|
||||
): HTMLElement => createMockElement('module-message-body__at-mention', dataset);
|
||||
|
||||
const createMockMentionBlotElement = (
|
||||
dataset: Record<string, string>
|
||||
): HTMLElement => createMockElement('mention-blot', dataset);
|
||||
|
||||
const memberMahershala: ConversationType = {
|
||||
id: '555444',
|
||||
uuid: 'abcdefg',
|
||||
title: 'Mahershala Ali',
|
||||
firstName: 'Mahershala',
|
||||
profileName: 'Mahershala A.',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const memberShia: ConversationType = {
|
||||
id: '333222',
|
||||
uuid: 'hijklmno',
|
||||
title: 'Shia LaBeouf',
|
||||
firstName: 'Shia',
|
||||
profileName: 'Shia L.',
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
markedUnread: false,
|
||||
areWeAdmin: false,
|
||||
};
|
||||
|
||||
const members: Array<ConversationType> = [memberMahershala, memberShia];
|
||||
|
||||
const memberRepositoryRef: RefObject<MemberRepository> = {
|
||||
current: new MemberRepository(members),
|
||||
};
|
||||
|
||||
const matcher = matchMention(memberRepositoryRef);
|
||||
|
||||
interface Mention {
|
||||
uuid: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface MentionInsert {
|
||||
mention: Mention;
|
||||
}
|
||||
|
||||
const isMention = (insert?: unknown): insert is MentionInsert => {
|
||||
if (insert) {
|
||||
if (Object.getOwnPropertyNames(insert).includes('mention')) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const EMPTY_DELTA = new Delta();
|
||||
|
||||
describe('matchMention', () => {
|
||||
it('handles an AtMentionify from clipboard', () => {
|
||||
const result = matcher(
|
||||
createMockAtMentionElement({
|
||||
id: memberMahershala.id,
|
||||
title: memberMahershala.title,
|
||||
}),
|
||||
EMPTY_DELTA
|
||||
);
|
||||
const { ops } = result;
|
||||
|
||||
assert.isNotEmpty(ops);
|
||||
|
||||
const [op] = ops;
|
||||
const { insert } = op;
|
||||
|
||||
if (isMention(insert)) {
|
||||
const { title, uuid } = insert.mention;
|
||||
|
||||
assert.equal(title, memberMahershala.title);
|
||||
assert.equal(uuid, memberMahershala.uuid);
|
||||
} else {
|
||||
assert.fail('insert is invalid');
|
||||
}
|
||||
});
|
||||
|
||||
it('handles an MentionBlot from clipboard', () => {
|
||||
const result = matcher(
|
||||
createMockMentionBlotElement({
|
||||
uuid: memberMahershala.uuid || '',
|
||||
title: memberMahershala.title,
|
||||
}),
|
||||
EMPTY_DELTA
|
||||
);
|
||||
const { ops } = result;
|
||||
|
||||
assert.isNotEmpty(ops);
|
||||
|
||||
const [op] = ops;
|
||||
const { insert } = op;
|
||||
|
||||
if (isMention(insert)) {
|
||||
const { title, uuid } = insert.mention;
|
||||
|
||||
assert.equal(title, memberMahershala.title);
|
||||
assert.equal(uuid, memberMahershala.uuid);
|
||||
} else {
|
||||
assert.fail('insert is invalid');
|
||||
}
|
||||
});
|
||||
|
||||
it('converts a missing AtMentionify to string', () => {
|
||||
const result = matcher(
|
||||
createMockAtMentionElement({
|
||||
id: 'florp',
|
||||
title: 'Nonexistent',
|
||||
}),
|
||||
EMPTY_DELTA
|
||||
);
|
||||
const { ops } = result;
|
||||
|
||||
assert.isNotEmpty(ops);
|
||||
|
||||
const [op] = ops;
|
||||
const { insert } = op;
|
||||
|
||||
if (isMention(insert)) {
|
||||
assert.fail('insert is invalid');
|
||||
} else {
|
||||
assert.equal(insert, '@Nonexistent');
|
||||
}
|
||||
});
|
||||
|
||||
it('converts a missing MentionBlot to string', () => {
|
||||
const result = matcher(
|
||||
createMockMentionBlotElement({
|
||||
uuid: 'florp',
|
||||
title: 'Nonexistent',
|
||||
}),
|
||||
EMPTY_DELTA
|
||||
);
|
||||
const { ops } = result;
|
||||
|
||||
assert.isNotEmpty(ops);
|
||||
|
||||
const [op] = ops;
|
||||
const { insert } = op;
|
||||
|
||||
if (isMention(insert)) {
|
||||
assert.fail('insert is invalid');
|
||||
} else {
|
||||
assert.equal(insert, '@Nonexistent');
|
||||
}
|
||||
});
|
||||
|
||||
it('passes other clipboard elements through', () => {
|
||||
const result = matcher(createMockElement('ignore', {}), EMPTY_DELTA);
|
||||
assert.equal(result, EMPTY_DELTA);
|
||||
});
|
||||
});
|
241
ts/test-node/quill/util_test.ts
Normal file
241
ts/test-node/quill/util_test.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import {
|
||||
getDeltaToRemoveStaleMentions,
|
||||
getTextAndMentionsFromOps,
|
||||
getDeltaToRestartMention,
|
||||
} from '../../quill/util';
|
||||
|
||||
describe('getDeltaToRemoveStaleMentions', () => {
|
||||
const memberUuids = ['abcdef', 'ghijkl'];
|
||||
|
||||
describe('given text', () => {
|
||||
it('retains the text', () => {
|
||||
const originalOps = [
|
||||
{
|
||||
insert: 'whoa, nobody here',
|
||||
},
|
||||
];
|
||||
|
||||
const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);
|
||||
|
||||
assert.deepEqual(ops, [{ retain: 17 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given stale and valid mentions', () => {
|
||||
it('retains the valid and replaces the stale', () => {
|
||||
const originalOps = [
|
||||
{
|
||||
insert: {
|
||||
mention: { uuid: '12345', title: 'Klaus' },
|
||||
},
|
||||
},
|
||||
{ insert: { mention: { uuid: 'abcdef', title: 'Werner' } } },
|
||||
];
|
||||
|
||||
const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);
|
||||
|
||||
assert.deepEqual(ops, [
|
||||
{ delete: 1 },
|
||||
{ insert: '@Klaus' },
|
||||
{ retain: 1 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given emoji embeds', () => {
|
||||
it('retains the embeds', () => {
|
||||
const originalOps = [
|
||||
{
|
||||
insert: {
|
||||
emoji: '😂',
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: {
|
||||
emoji: '🍋',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);
|
||||
|
||||
assert.deepEqual(ops, [{ retain: 1 }, { retain: 1 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given other ops', () => {
|
||||
it('passes them through', () => {
|
||||
const originalOps = [
|
||||
{
|
||||
delete: 5,
|
||||
},
|
||||
];
|
||||
|
||||
const { ops } = getDeltaToRemoveStaleMentions(originalOps, memberUuids);
|
||||
|
||||
assert.deepEqual(ops, originalOps);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTextAndMentionsFromOps', () => {
|
||||
describe('given only text', () => {
|
||||
it('returns only text trimmed', () => {
|
||||
const ops = [{ insert: ' The ' }, { insert: ' text \n' }];
|
||||
const [resultText, resultMentions] = getTextAndMentionsFromOps(ops);
|
||||
assert.equal(resultText, 'The text');
|
||||
assert.equal(resultMentions.length, 0);
|
||||
});
|
||||
|
||||
it('returns trimmed of trailing newlines', () => {
|
||||
const ops = [{ insert: ' The\ntext\n\n\n' }];
|
||||
const [resultText, resultMentions] = getTextAndMentionsFromOps(ops);
|
||||
assert.equal(resultText, 'The\ntext');
|
||||
assert.equal(resultMentions.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given text, emoji, and mentions', () => {
|
||||
it('returns the trimmed text with placeholders and mentions', () => {
|
||||
const ops = [
|
||||
{
|
||||
insert: {
|
||||
emoji: '😂',
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: ' wow, funny, ',
|
||||
},
|
||||
{
|
||||
insert: {
|
||||
mention: {
|
||||
uuid: 'abcdef',
|
||||
title: '@fred',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const [resultText, resultMentions] = getTextAndMentionsFromOps(ops);
|
||||
assert.equal(resultText, '😂 wow, funny, \uFFFC');
|
||||
assert.deepEqual(resultMentions, [
|
||||
{
|
||||
length: 1,
|
||||
mentionUuid: 'abcdef',
|
||||
replacementText: '@fred',
|
||||
start: 15,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given only mentions', () => {
|
||||
it('returns the trimmed text with placeholders and mentions', () => {
|
||||
const ops = [
|
||||
{
|
||||
insert: {
|
||||
mention: {
|
||||
uuid: 'abcdef',
|
||||
title: '@fred',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const [resultText, resultMentions] = getTextAndMentionsFromOps(ops);
|
||||
assert.equal(resultText, '\uFFFC');
|
||||
assert.deepEqual(resultMentions, [
|
||||
{
|
||||
length: 1,
|
||||
mentionUuid: 'abcdef',
|
||||
replacementText: '@fred',
|
||||
start: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not trim newlines padding mentions', () => {
|
||||
const ops = [
|
||||
{ insert: 'test \n' },
|
||||
{
|
||||
insert: {
|
||||
mention: {
|
||||
uuid: 'abcdef',
|
||||
title: '@fred',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ insert: '\n test' },
|
||||
];
|
||||
const [resultText, resultMentions] = getTextAndMentionsFromOps(ops);
|
||||
assert.equal(resultText, 'test \n\uFFFC\n test');
|
||||
assert.deepEqual(resultMentions, [
|
||||
{
|
||||
length: 1,
|
||||
mentionUuid: 'abcdef',
|
||||
replacementText: '@fred',
|
||||
start: 6,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeltaToRestartMention', () => {
|
||||
describe('given text and emoji', () => {
|
||||
it('returns the correct retains, a delete, and an @', () => {
|
||||
const originalOps = [
|
||||
{
|
||||
insert: {
|
||||
emoji: '😂',
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: {
|
||||
mention: {
|
||||
uuid: 'ghijkl',
|
||||
title: '@sam',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: ' wow, funny, ',
|
||||
},
|
||||
{
|
||||
insert: {
|
||||
mention: {
|
||||
uuid: 'abcdef',
|
||||
title: '@fred',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { ops } = getDeltaToRestartMention(originalOps);
|
||||
|
||||
assert.deepEqual(ops, [
|
||||
{
|
||||
retain: 1,
|
||||
},
|
||||
{
|
||||
retain: 1,
|
||||
},
|
||||
{
|
||||
retain: 13,
|
||||
},
|
||||
{
|
||||
retain: 1,
|
||||
},
|
||||
{
|
||||
delete: 1,
|
||||
},
|
||||
{
|
||||
insert: '@',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
11
ts/test-node/tslint.json
Normal file
11
ts/test-node/tslint.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": ["../../tslint.json"],
|
||||
"rules": {
|
||||
// To allow the use of devDependencies here
|
||||
"no-implicit-dependencies": false,
|
||||
|
||||
// All tests use arrow functions, and they can be long
|
||||
"max-func-body-length": false
|
||||
}
|
||||
}
|
182
ts/test-node/types/Attachment_test.ts
Normal file
182
ts/test-node/types/Attachment_test.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as Attachment from '../../types/Attachment';
|
||||
import * as MIME from '../../types/MIME';
|
||||
import { SignalService } from '../../protobuf';
|
||||
import { stringToArrayBuffer } from '../../../js/modules/string_to_array_buffer';
|
||||
|
||||
describe('Attachment', () => {
|
||||
describe('getFileExtension', () => {
|
||||
it('should return file extension from content type', () => {
|
||||
const input: Attachment.Attachment = {
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.IMAGE_GIF,
|
||||
};
|
||||
assert.strictEqual(Attachment.getFileExtension(input), 'gif');
|
||||
});
|
||||
|
||||
it('should return file extension for QuickTime videos', () => {
|
||||
const input: Attachment.Attachment = {
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
assert.strictEqual(Attachment.getFileExtension(input), 'mov');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSuggestedFilename', () => {
|
||||
context('for attachment with filename', () => {
|
||||
it('should return existing filename if present', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'funny-cat.mov',
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
const actual = Attachment.getSuggestedFilename({ attachment });
|
||||
const expected = 'funny-cat.mov';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
context('for attachment without filename', () => {
|
||||
it('should generate a filename based on timestamp', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
|
||||
const actual = Attachment.getSuggestedFilename({
|
||||
attachment,
|
||||
timestamp,
|
||||
});
|
||||
const expected = 'signal-1970-01-01-000000.mov';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
context('for attachment with index', () => {
|
||||
it('should generate a filename based on timestamp', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
|
||||
const actual = Attachment.getSuggestedFilename({
|
||||
attachment,
|
||||
timestamp,
|
||||
index: 3,
|
||||
});
|
||||
const expected = 'signal-1970-01-01-000000_003.mov';
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isVisualMedia', () => {
|
||||
it('should return true for images', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'meme.gif',
|
||||
data: stringToArrayBuffer('gif'),
|
||||
contentType: MIME.IMAGE_GIF,
|
||||
};
|
||||
assert.isTrue(Attachment.isVisualMedia(attachment));
|
||||
});
|
||||
|
||||
it('should return true for videos', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'meme.mp4',
|
||||
data: stringToArrayBuffer('mp4'),
|
||||
contentType: MIME.VIDEO_MP4,
|
||||
};
|
||||
assert.isTrue(Attachment.isVisualMedia(attachment));
|
||||
});
|
||||
|
||||
it('should return false for voice message attachment', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'Voice Message.aac',
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('voice message'),
|
||||
contentType: MIME.AUDIO_AAC,
|
||||
};
|
||||
assert.isFalse(Attachment.isVisualMedia(attachment));
|
||||
});
|
||||
|
||||
it('should return false for other attachments', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'foo.json',
|
||||
data: stringToArrayBuffer('{"foo": "bar"}'),
|
||||
contentType: MIME.APPLICATION_JSON,
|
||||
};
|
||||
assert.isFalse(Attachment.isVisualMedia(attachment));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFile', () => {
|
||||
it('should return true for JSON', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'foo.json',
|
||||
data: stringToArrayBuffer('{"foo": "bar"}'),
|
||||
contentType: MIME.APPLICATION_JSON,
|
||||
};
|
||||
assert.isTrue(Attachment.isFile(attachment));
|
||||
});
|
||||
|
||||
it('should return false for images', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'meme.gif',
|
||||
data: stringToArrayBuffer('gif'),
|
||||
contentType: MIME.IMAGE_GIF,
|
||||
};
|
||||
assert.isFalse(Attachment.isFile(attachment));
|
||||
});
|
||||
|
||||
it('should return false for videos', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'meme.mp4',
|
||||
data: stringToArrayBuffer('mp4'),
|
||||
contentType: MIME.VIDEO_MP4,
|
||||
};
|
||||
assert.isFalse(Attachment.isFile(attachment));
|
||||
});
|
||||
|
||||
it('should return false for voice message attachment', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'Voice Message.aac',
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('voice message'),
|
||||
contentType: MIME.AUDIO_AAC,
|
||||
};
|
||||
assert.isFalse(Attachment.isFile(attachment));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isVoiceMessage', () => {
|
||||
it('should return true for voice message attachment', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'Voice Message.aac',
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('voice message'),
|
||||
contentType: MIME.AUDIO_AAC,
|
||||
};
|
||||
assert.isTrue(Attachment.isVoiceMessage(attachment));
|
||||
});
|
||||
|
||||
it('should return true for legacy Android voice message attachment', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
data: stringToArrayBuffer('voice message'),
|
||||
contentType: MIME.AUDIO_MP3,
|
||||
};
|
||||
assert.isTrue(Attachment.isVoiceMessage(attachment));
|
||||
});
|
||||
|
||||
it('should return false for other attachments', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
fileName: 'foo.gif',
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.IMAGE_GIF,
|
||||
};
|
||||
assert.isFalse(Attachment.isVoiceMessage(attachment));
|
||||
});
|
||||
});
|
||||
});
|
185
ts/test-node/types/Contact_test.ts
Normal file
185
ts/test-node/types/Contact_test.ts
Normal file
|
@ -0,0 +1,185 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { contactSelector, getName } from '../../types/Contact';
|
||||
|
||||
describe('Contact', () => {
|
||||
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('contactSelector', () => {
|
||||
const regionCode = '1';
|
||||
const signalAccount = '+1202555000';
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
name: {
|
||||
displayName: 'displayName',
|
||||
givenName: 'givenName',
|
||||
familyName: 'familyName',
|
||||
},
|
||||
organization: 'Somewhere, Inc.',
|
||||
avatar: undefined,
|
||||
signalAccount,
|
||||
number: undefined,
|
||||
};
|
||||
const actual = contactSelector(contact, {
|
||||
regionCode,
|
||||
signalAccount,
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
name: {
|
||||
displayName: 'displayName',
|
||||
givenName: 'givenName',
|
||||
familyName: 'familyName',
|
||||
},
|
||||
organization: 'Somewhere, Inc.',
|
||||
avatar: {
|
||||
isProfile: true,
|
||||
avatar: {
|
||||
pending: true,
|
||||
path: undefined,
|
||||
},
|
||||
},
|
||||
signalAccount,
|
||||
number: undefined,
|
||||
};
|
||||
const actual = contactSelector(contact, {
|
||||
regionCode,
|
||||
signalAccount,
|
||||
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',
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
name: {
|
||||
displayName: 'displayName',
|
||||
givenName: 'givenName',
|
||||
familyName: 'familyName',
|
||||
},
|
||||
organization: 'Somewhere, Inc.',
|
||||
avatar: {
|
||||
isProfile: true,
|
||||
avatar: {
|
||||
path: 'absolute:somewhere',
|
||||
},
|
||||
},
|
||||
signalAccount,
|
||||
number: undefined,
|
||||
};
|
||||
const actual = contactSelector(contact, {
|
||||
regionCode,
|
||||
signalAccount,
|
||||
getAbsoluteAttachmentPath,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
152
ts/test-node/types/Settings_test.ts
Normal file
152
ts/test-node/types/Settings_test.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import os from 'os';
|
||||
import Sinon from 'sinon';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as Settings from '../../types/Settings';
|
||||
|
||||
describe('Settings', () => {
|
||||
let sandbox: Sinon.SinonSandbox;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = Sinon.createSandbox();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('getAudioNotificationSupport', () => {
|
||||
it('returns native support on macOS', () => {
|
||||
sandbox.stub(process, 'platform').value('darwin');
|
||||
assert.strictEqual(
|
||||
Settings.getAudioNotificationSupport(),
|
||||
Settings.AudioNotificationSupport.Native
|
||||
);
|
||||
});
|
||||
|
||||
it('returns no support on Windows 7', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('7.0.0');
|
||||
assert.strictEqual(
|
||||
Settings.getAudioNotificationSupport(),
|
||||
Settings.AudioNotificationSupport.None
|
||||
);
|
||||
});
|
||||
|
||||
it('returns native support on Windows 8', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('8.0.0');
|
||||
assert.strictEqual(
|
||||
Settings.getAudioNotificationSupport(),
|
||||
Settings.AudioNotificationSupport.Native
|
||||
);
|
||||
});
|
||||
|
||||
it('returns custom support on Linux', () => {
|
||||
sandbox.stub(process, 'platform').value('linux');
|
||||
assert.strictEqual(
|
||||
Settings.getAudioNotificationSupport(),
|
||||
Settings.AudioNotificationSupport.Custom
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAudioNotificationSupported', () => {
|
||||
it('returns true on macOS', () => {
|
||||
sandbox.stub(process, 'platform').value('darwin');
|
||||
assert.isTrue(Settings.isAudioNotificationSupported());
|
||||
});
|
||||
|
||||
it('returns false on Windows 7', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('7.0.0');
|
||||
assert.isFalse(Settings.isAudioNotificationSupported());
|
||||
});
|
||||
|
||||
it('returns true on Windows 8', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('8.0.0');
|
||||
assert.isTrue(Settings.isAudioNotificationSupported());
|
||||
});
|
||||
|
||||
it('returns true on Linux', () => {
|
||||
sandbox.stub(process, 'platform').value('linux');
|
||||
assert.isTrue(Settings.isAudioNotificationSupported());
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNotificationGroupingSupported', () => {
|
||||
it('returns true on macOS', () => {
|
||||
sandbox.stub(process, 'platform').value('darwin');
|
||||
assert.isTrue(Settings.isNotificationGroupingSupported());
|
||||
});
|
||||
|
||||
it('returns true on Windows 7', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('7.0.0');
|
||||
assert.isFalse(Settings.isNotificationGroupingSupported());
|
||||
});
|
||||
|
||||
it('returns true on Windows 8', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('8.0.0');
|
||||
assert.isTrue(Settings.isNotificationGroupingSupported());
|
||||
});
|
||||
|
||||
it('returns true on Linux', () => {
|
||||
sandbox.stub(process, 'platform').value('linux');
|
||||
assert.isTrue(Settings.isNotificationGroupingSupported());
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHideMenuBarSupported', () => {
|
||||
it('returns false on macOS', () => {
|
||||
sandbox.stub(process, 'platform').value('darwin');
|
||||
assert.isFalse(Settings.isHideMenuBarSupported());
|
||||
});
|
||||
|
||||
it('returns true on Windows 7', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('7.0.0');
|
||||
assert.isTrue(Settings.isHideMenuBarSupported());
|
||||
});
|
||||
|
||||
it('returns true on Windows 8', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('8.0.0');
|
||||
assert.isTrue(Settings.isHideMenuBarSupported());
|
||||
});
|
||||
|
||||
it('returns true on Linux', () => {
|
||||
sandbox.stub(process, 'platform').value('linux');
|
||||
assert.isTrue(Settings.isHideMenuBarSupported());
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDrawAttentionSupported', () => {
|
||||
it('returns false on macOS', () => {
|
||||
sandbox.stub(process, 'platform').value('darwin');
|
||||
assert.isFalse(Settings.isDrawAttentionSupported());
|
||||
});
|
||||
|
||||
it('returns true on Windows 7', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('7.0.0');
|
||||
assert.isTrue(Settings.isDrawAttentionSupported());
|
||||
});
|
||||
|
||||
it('returns true on Windows 8', () => {
|
||||
sandbox.stub(process, 'platform').value('win32');
|
||||
sandbox.stub(os, 'release').returns('8.0.0');
|
||||
assert.isTrue(Settings.isDrawAttentionSupported());
|
||||
});
|
||||
|
||||
it('returns true on Linux', () => {
|
||||
sandbox.stub(process, 'platform').value('linux');
|
||||
assert.isTrue(Settings.isDrawAttentionSupported());
|
||||
});
|
||||
});
|
||||
});
|
207
ts/test-node/types/message/initializeAttachmentMetadata_test.ts
Normal file
207
ts/test-node/types/message/initializeAttachmentMetadata_test.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as Message from '../../../types/message/initializeAttachmentMetadata';
|
||||
import { IncomingMessage } from '../../../types/Message';
|
||||
import { SignalService } from '../../../protobuf';
|
||||
import * as MIME from '../../../types/MIME';
|
||||
import { stringToArrayBuffer } from '../../../../js/modules/string_to_array_buffer';
|
||||
|
||||
describe('Message', () => {
|
||||
describe('initializeAttachmentMetadata', () => {
|
||||
it('should classify visual media attachments', async () => {
|
||||
const input: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [
|
||||
{
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
data: stringToArrayBuffer('foo'),
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
};
|
||||
const expected: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [
|
||||
{
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
data: stringToArrayBuffer('foo'),
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
hasAttachments: 1,
|
||||
hasVisualMediaAttachments: 1,
|
||||
hasFileAttachments: undefined,
|
||||
};
|
||||
|
||||
const actual = await Message.initializeAttachmentMetadata(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should classify file attachments', async () => {
|
||||
const input: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [
|
||||
{
|
||||
contentType: MIME.APPLICATION_OCTET_STREAM,
|
||||
data: stringToArrayBuffer('foo'),
|
||||
fileName: 'foo.bin',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
};
|
||||
const expected: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [
|
||||
{
|
||||
contentType: MIME.APPLICATION_OCTET_STREAM,
|
||||
data: stringToArrayBuffer('foo'),
|
||||
fileName: 'foo.bin',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
hasAttachments: 1,
|
||||
hasVisualMediaAttachments: undefined,
|
||||
hasFileAttachments: 1,
|
||||
};
|
||||
|
||||
const actual = await Message.initializeAttachmentMetadata(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should classify voice message attachments', async () => {
|
||||
const input: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [
|
||||
{
|
||||
contentType: MIME.AUDIO_AAC,
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('foo'),
|
||||
fileName: 'Voice Message.aac',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
};
|
||||
const expected: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [
|
||||
{
|
||||
contentType: MIME.AUDIO_AAC,
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('foo'),
|
||||
fileName: 'Voice Message.aac',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
hasAttachments: 1,
|
||||
hasVisualMediaAttachments: undefined,
|
||||
hasFileAttachments: undefined,
|
||||
};
|
||||
|
||||
const actual = await Message.initializeAttachmentMetadata(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('does not include long message attachments', async () => {
|
||||
const input: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [
|
||||
{
|
||||
contentType: MIME.LONG_MESSAGE,
|
||||
data: stringToArrayBuffer('foo'),
|
||||
fileName: 'message.txt',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
};
|
||||
const expected: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [
|
||||
{
|
||||
contentType: MIME.LONG_MESSAGE,
|
||||
data: stringToArrayBuffer('foo'),
|
||||
fileName: 'message.txt',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
hasAttachments: 0,
|
||||
hasVisualMediaAttachments: undefined,
|
||||
hasFileAttachments: undefined,
|
||||
};
|
||||
|
||||
const actual = await Message.initializeAttachmentMetadata(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('handles not attachments', async () => {
|
||||
const input: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [],
|
||||
};
|
||||
const expected: IncomingMessage = {
|
||||
type: 'incoming',
|
||||
conversationId: 'foo',
|
||||
id: '11111111-1111-1111-1111-111111111111',
|
||||
timestamp: 1523317140899,
|
||||
received_at: 1523317140899,
|
||||
sent_at: 1523317140800,
|
||||
attachments: [],
|
||||
hasAttachments: 0,
|
||||
hasVisualMediaAttachments: undefined,
|
||||
hasFileAttachments: undefined,
|
||||
};
|
||||
|
||||
const actual = await Message.initializeAttachmentMetadata(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
130
ts/test-node/updater/common_test.ts
Normal file
130
ts/test-node/updater/common_test.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import {
|
||||
createTempDir,
|
||||
getUpdateFileName,
|
||||
getVersion,
|
||||
isUpdateFileNameValid,
|
||||
validatePath,
|
||||
} from '../../updater/common';
|
||||
|
||||
describe('updater/signatures', () => {
|
||||
const windows = `version: 1.23.2
|
||||
files:
|
||||
- url: signal-desktop-win-1.23.2.exe
|
||||
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
|
||||
size: 92020776
|
||||
path: signal-desktop-win-1.23.2.exe
|
||||
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
|
||||
releaseDate: '2019-03-29T16:58:08.210Z'
|
||||
`;
|
||||
const mac = `version: 1.23.2
|
||||
files:
|
||||
- url: signal-desktop-mac-1.23.2.zip
|
||||
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
|
||||
size: 105179791
|
||||
blockMapSize: 111109
|
||||
path: signal-desktop-mac-1.23.2.zip
|
||||
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
|
||||
releaseDate: '2019-03-29T16:57:16.997Z'
|
||||
`;
|
||||
const windowsBeta = `version: 1.23.2-beta.1
|
||||
files:
|
||||
- url: signal-desktop-beta-win-1.23.2-beta.1.exe
|
||||
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
|
||||
size: 92028656
|
||||
path: signal-desktop-beta-win-1.23.2-beta.1.exe
|
||||
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
|
||||
releaseDate: '2019-03-29T01:56:00.544Z'
|
||||
`;
|
||||
const macBeta = `version: 1.23.2-beta.1
|
||||
files:
|
||||
- url: signal-desktop-beta-mac-1.23.2-beta.1.zip
|
||||
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
|
||||
size: 105182398
|
||||
blockMapSize: 110894
|
||||
path: signal-desktop-beta-mac-1.23.2-beta.1.zip
|
||||
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
|
||||
releaseDate: '2019-03-29T01:53:23.881Z'
|
||||
`;
|
||||
|
||||
describe('#getVersion', () => {
|
||||
it('successfully gets version', () => {
|
||||
const expected = '1.23.2';
|
||||
assert.strictEqual(getVersion(windows), expected);
|
||||
assert.strictEqual(getVersion(mac), expected);
|
||||
|
||||
const expectedBeta = '1.23.2-beta.1';
|
||||
assert.strictEqual(getVersion(windowsBeta), expectedBeta);
|
||||
assert.strictEqual(getVersion(macBeta), expectedBeta);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUpdateFileName', () => {
|
||||
it('successfully gets version', () => {
|
||||
assert.strictEqual(
|
||||
getUpdateFileName(windows),
|
||||
'signal-desktop-win-1.23.2.exe'
|
||||
);
|
||||
assert.strictEqual(
|
||||
getUpdateFileName(mac),
|
||||
'signal-desktop-mac-1.23.2.zip'
|
||||
);
|
||||
assert.strictEqual(
|
||||
getUpdateFileName(windowsBeta),
|
||||
'signal-desktop-beta-win-1.23.2-beta.1.exe'
|
||||
);
|
||||
assert.strictEqual(
|
||||
getUpdateFileName(macBeta),
|
||||
'signal-desktop-beta-mac-1.23.2-beta.1.zip'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isUpdateFileNameValid', () => {
|
||||
it('returns true for normal filenames', () => {
|
||||
assert.strictEqual(
|
||||
isUpdateFileNameValid('signal-desktop-win-1.23.2.exe'),
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
isUpdateFileNameValid('signal-desktop-mac-1.23.2-beta.1.zip'),
|
||||
true
|
||||
);
|
||||
});
|
||||
it('returns false for problematic names', () => {
|
||||
assert.strictEqual(
|
||||
isUpdateFileNameValid('../signal-desktop-win-1.23.2.exe'),
|
||||
false
|
||||
);
|
||||
assert.strictEqual(
|
||||
isUpdateFileNameValid('%signal-desktop-mac-1.23.2-beta.1.zip'),
|
||||
false
|
||||
);
|
||||
assert.strictEqual(
|
||||
isUpdateFileNameValid('@signal-desktop-mac-1.23.2-beta.1.zip'),
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validatePath', () => {
|
||||
it('succeeds for simple children', async () => {
|
||||
const base = await createTempDir();
|
||||
validatePath(base, `${base}/child`);
|
||||
validatePath(base, `${base}/child/grandchild`);
|
||||
});
|
||||
it('returns false for problematic names', async () => {
|
||||
const base = await createTempDir();
|
||||
assert.throws(() => {
|
||||
validatePath(base, `${base}/../child`);
|
||||
});
|
||||
assert.throws(() => {
|
||||
validatePath(base, '/root');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
17
ts/test-node/updater/curve_test.ts
Normal file
17
ts/test-node/updater/curve_test.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { keyPair, sign, verify } from '../../updater/curve';
|
||||
|
||||
describe('updater/curve', () => {
|
||||
it('roundtrips', () => {
|
||||
const message = Buffer.from('message');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
const signature = sign(privateKey, message);
|
||||
const verified = verify(publicKey, message, signature);
|
||||
|
||||
assert.strictEqual(verified, true);
|
||||
});
|
||||
});
|
209
ts/test-node/updater/signature_test.ts
Normal file
209
ts/test-node/updater/signature_test.ts
Normal file
|
@ -0,0 +1,209 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { copy } from 'fs-extra';
|
||||
|
||||
import {
|
||||
_getFileHash,
|
||||
getSignaturePath,
|
||||
loadHexFromPath,
|
||||
verifySignature,
|
||||
writeHexToPath,
|
||||
writeSignature,
|
||||
} from '../../updater/signature';
|
||||
import { createTempDir, deleteTempDir } from '../../updater/common';
|
||||
import { keyPair } from '../../updater/curve';
|
||||
|
||||
describe('updater/signatures', () => {
|
||||
it('_getFileHash returns correct hash', async () => {
|
||||
const filePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const expected =
|
||||
'7bc77f27d92d00b4a1d57c480ca86dacc43d57bc318339c92119d1fbf6b557a5';
|
||||
|
||||
const hash = await _getFileHash(filePath);
|
||||
|
||||
assert.strictEqual(expected, Buffer.from(hash).toString('hex'));
|
||||
});
|
||||
|
||||
it('roundtrips binary file writes', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const path = join(tempDir, 'something.bin');
|
||||
const { publicKey } = keyPair();
|
||||
|
||||
await writeHexToPath(path, publicKey);
|
||||
|
||||
const fromDisk = await loadHexFromPath(path);
|
||||
|
||||
assert.strictEqual(
|
||||
Buffer.from(fromDisk).compare(Buffer.from(publicKey)),
|
||||
0
|
||||
);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('roundtrips signature', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const signaturePath = getSignaturePath(updatePath);
|
||||
assert.strictEqual(existsSync(signaturePath), true);
|
||||
|
||||
const verified = await verifySignature(updatePath, version, publicKey);
|
||||
assert.strictEqual(verified, true);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails signature verification if version changes', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
const brokenVersion = 'v1.23.3';
|
||||
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const verified = await verifySignature(
|
||||
updatePath,
|
||||
brokenVersion,
|
||||
publicKey
|
||||
);
|
||||
assert.strictEqual(verified, false);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails signature verification if signature tampered with', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const signaturePath = getSignaturePath(updatePath);
|
||||
const signature = Buffer.from(await loadHexFromPath(signaturePath));
|
||||
signature[4] += 3;
|
||||
await writeHexToPath(signaturePath, signature);
|
||||
|
||||
const verified = await verifySignature(updatePath, version, publicKey);
|
||||
assert.strictEqual(verified, false);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails signature verification if binary file tampered with', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const brokenSourcePath = join(
|
||||
__dirname,
|
||||
'../../../fixtures/pixabay-Soap-Bubble-7141.mp4'
|
||||
);
|
||||
await copy(brokenSourcePath, updatePath);
|
||||
|
||||
const verified = await verifySignature(updatePath, version, publicKey);
|
||||
assert.strictEqual(verified, false);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails signature verification if signed by different key', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey } = keyPair();
|
||||
const { privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const verified = await verifySignature(updatePath, version, publicKey);
|
||||
assert.strictEqual(verified, false);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
53
ts/test-node/util/LatestQueue_test.ts
Normal file
53
ts/test-node/util/LatestQueue_test.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { LatestQueue } from '../../util/LatestQueue';
|
||||
|
||||
describe('LatestQueue', () => {
|
||||
it('if the queue is empty, new tasks are started immediately', done => {
|
||||
new LatestQueue().add(async () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('only enqueues the latest operation', done => {
|
||||
const queue = new LatestQueue();
|
||||
|
||||
const spy = sinon.spy();
|
||||
|
||||
let openFirstTaskGate: undefined | (() => void);
|
||||
const firstTaskGate = new Promise(resolve => {
|
||||
openFirstTaskGate = resolve;
|
||||
});
|
||||
if (!openFirstTaskGate) {
|
||||
throw new Error('Test is misconfigured; cannot grab inner resolve');
|
||||
}
|
||||
|
||||
queue.add(async () => {
|
||||
await firstTaskGate;
|
||||
spy('first');
|
||||
});
|
||||
|
||||
queue.add(async () => {
|
||||
spy('second');
|
||||
});
|
||||
|
||||
queue.add(async () => {
|
||||
spy('third');
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(spy);
|
||||
|
||||
openFirstTaskGate();
|
||||
|
||||
queue.onceEmpty(() => {
|
||||
sinon.assert.calledTwice(spy);
|
||||
sinon.assert.calledWith(spy, 'first');
|
||||
sinon.assert.calledWith(spy, 'third');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
32
ts/test-node/util/combineNames_test.ts
Normal file
32
ts/test-node/util/combineNames_test.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { combineNames } from '../../util/combineNames';
|
||||
|
||||
describe('combineNames', () => {
|
||||
it('returns undefined if no names provided', () => {
|
||||
assert.strictEqual(combineNames('', ''), undefined);
|
||||
});
|
||||
|
||||
it('returns first name only if family name not provided', () => {
|
||||
assert.strictEqual(combineNames('Alice'), 'Alice');
|
||||
});
|
||||
|
||||
it('returns returns combined names', () => {
|
||||
assert.strictEqual(combineNames('Alice', 'Jones'), 'Alice Jones');
|
||||
});
|
||||
|
||||
it('returns given name first if names in Chinese', () => {
|
||||
assert.strictEqual(combineNames('振宁', '杨'), '杨振宁');
|
||||
});
|
||||
|
||||
it('returns given name first if names in Japanese', () => {
|
||||
assert.strictEqual(combineNames('泰夫', '木田'), '木田泰夫');
|
||||
});
|
||||
|
||||
it('returns given name first if names in Korean', () => {
|
||||
assert.strictEqual(combineNames('채원', '도윤'), '도윤채원');
|
||||
});
|
||||
});
|
60
ts/test-node/util/getAnimatedPngDataIfExists_test.ts
Normal file
60
ts/test-node/util/getAnimatedPngDataIfExists_test.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { getAnimatedPngDataIfExists } from '../../util/getAnimatedPngDataIfExists';
|
||||
|
||||
describe('getAnimatedPngDataIfExists', () => {
|
||||
const fixture = (filename: string): Promise<Buffer> => {
|
||||
const fixturePath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'fixtures',
|
||||
filename
|
||||
);
|
||||
return fs.promises.readFile(fixturePath);
|
||||
};
|
||||
|
||||
it('returns null for empty buffers', () => {
|
||||
assert.isNull(getAnimatedPngDataIfExists(Buffer.alloc(0)));
|
||||
});
|
||||
|
||||
it('returns null for non-PNG files', async () => {
|
||||
await Promise.all(
|
||||
[
|
||||
'kitten-1-64-64.jpg',
|
||||
'512x515-thumbs-up-lincoln.webp',
|
||||
'giphy-GVNvOUpeYmI7e.gif',
|
||||
'pixabay-Soap-Bubble-7141.mp4',
|
||||
'lorem-ipsum.txt',
|
||||
].map(async filename => {
|
||||
assert.isNull(getAnimatedPngDataIfExists(await fixture(filename)));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null for non-animated PNG files', async () => {
|
||||
assert.isNull(
|
||||
getAnimatedPngDataIfExists(await fixture('20x200-yellow.png'))
|
||||
);
|
||||
});
|
||||
|
||||
it('returns data for animated PNG files', async () => {
|
||||
assert.deepEqual(
|
||||
getAnimatedPngDataIfExists(
|
||||
await fixture('Animated_PNG_example_bouncing_beach_ball.png')
|
||||
),
|
||||
{ numPlays: Infinity }
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
getAnimatedPngDataIfExists(await fixture('apng_with_2_plays.png')),
|
||||
{ numPlays: 2 }
|
||||
);
|
||||
});
|
||||
});
|
50
ts/test-node/util/getOwn_test.ts
Normal file
50
ts/test-node/util/getOwn_test.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
|
||||
describe('getOwn', () => {
|
||||
class Person {
|
||||
public birthYear: number;
|
||||
|
||||
constructor(birthYear: number) {
|
||||
this.birthYear = birthYear;
|
||||
}
|
||||
|
||||
getAge() {
|
||||
return new Date().getFullYear() - this.birthYear;
|
||||
}
|
||||
}
|
||||
|
||||
it('returns undefined when asking for a non-existent property', () => {
|
||||
const obj: Record<string, number> = { bar: 123 };
|
||||
assert.isUndefined(getOwn(obj, 'foo'));
|
||||
});
|
||||
|
||||
it('returns undefined when asking for a non-own property', () => {
|
||||
const obj: Record<string, number> = { bar: 123 };
|
||||
assert.isUndefined(getOwn(obj, 'hasOwnProperty'));
|
||||
|
||||
const person = new Person(1880);
|
||||
assert.isUndefined(getOwn(person, 'getAge'));
|
||||
});
|
||||
|
||||
it('returns own properties', () => {
|
||||
const obj: Record<string, number> = { foo: 123 };
|
||||
assert.strictEqual(getOwn(obj, 'foo'), 123);
|
||||
|
||||
const person = new Person(1880);
|
||||
assert.strictEqual(getOwn(person, 'birthYear'), 1880);
|
||||
});
|
||||
|
||||
it('works even if `hasOwnProperty` has been overridden for the object', () => {
|
||||
const obj: Record<string, unknown> = {
|
||||
foo: 123,
|
||||
hasOwnProperty: () => true,
|
||||
};
|
||||
assert.strictEqual(getOwn(obj, 'foo'), 123);
|
||||
assert.isUndefined(getOwn(obj, 'bar'));
|
||||
});
|
||||
});
|
45
ts/test-node/util/getTextWithMentions_test.ts
Normal file
45
ts/test-node/util/getTextWithMentions_test.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { getTextWithMentions } from '../../util/getTextWithMentions';
|
||||
|
||||
describe('getTextWithMentions', () => {
|
||||
describe('given mention replacements', () => {
|
||||
it('replaces them', () => {
|
||||
const bodyRanges = [
|
||||
{
|
||||
length: 1,
|
||||
mentionUuid: 'abcdef',
|
||||
replacementText: 'fred',
|
||||
start: 4,
|
||||
},
|
||||
];
|
||||
const text = "Hey \uFFFC, I'm here";
|
||||
expect(getTextWithMentions(bodyRanges, text)).to.eql(
|
||||
"Hey @fred, I'm here"
|
||||
);
|
||||
});
|
||||
|
||||
it('sorts them to go from back to front', () => {
|
||||
const bodyRanges = [
|
||||
{
|
||||
length: 1,
|
||||
mentionUuid: 'blarg',
|
||||
replacementText: 'jerry',
|
||||
start: 0,
|
||||
},
|
||||
{
|
||||
length: 1,
|
||||
mentionUuid: 'abcdef',
|
||||
replacementText: 'fred',
|
||||
start: 7,
|
||||
},
|
||||
];
|
||||
const text = "\uFFFC says \uFFFC, I'm here";
|
||||
expect(getTextWithMentions(bodyRanges, text)).to.eql(
|
||||
"@jerry says @fred, I'm here"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
37
ts/test-node/util/getUserAgent_test.ts
Normal file
37
ts/test-node/util/getUserAgent_test.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { getUserAgent } from '../../util/getUserAgent';
|
||||
|
||||
describe('getUserAgent', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
this.sandbox = sinon.createSandbox();
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
this.sandbox.restore();
|
||||
});
|
||||
|
||||
it('returns the right User-Agent on Windows', function test() {
|
||||
this.sandbox.stub(process, 'platform').get(() => 'win32');
|
||||
assert.strictEqual(getUserAgent('1.2.3'), 'Signal-Desktop/1.2.3 Windows');
|
||||
});
|
||||
|
||||
it('returns the right User-Agent on macOS', function test() {
|
||||
this.sandbox.stub(process, 'platform').get(() => 'darwin');
|
||||
assert.strictEqual(getUserAgent('1.2.3'), 'Signal-Desktop/1.2.3 macOS');
|
||||
});
|
||||
|
||||
it('returns the right User-Agent on Linux', function test() {
|
||||
this.sandbox.stub(process, 'platform').get(() => 'linux');
|
||||
assert.strictEqual(getUserAgent('1.2.3'), 'Signal-Desktop/1.2.3 Linux');
|
||||
});
|
||||
|
||||
it('omits the platform on unsupported platforms', function test() {
|
||||
this.sandbox.stub(process, 'platform').get(() => 'freebsd');
|
||||
assert.strictEqual(getUserAgent('1.2.3'), 'Signal-Desktop/1.2.3');
|
||||
});
|
||||
});
|
51
ts/test-node/util/isFileDangerous_test.ts
Normal file
51
ts/test-node/util/isFileDangerous_test.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isFileDangerous } from '../../util/isFileDangerous';
|
||||
|
||||
describe('isFileDangerous', () => {
|
||||
it('returns false for images', () => {
|
||||
assert.strictEqual(isFileDangerous('dog.gif'), false);
|
||||
assert.strictEqual(isFileDangerous('cat.jpg'), false);
|
||||
});
|
||||
|
||||
it('returns false for documents', () => {
|
||||
assert.strictEqual(isFileDangerous('resume.docx'), false);
|
||||
assert.strictEqual(isFileDangerous('price_list.pdf'), false);
|
||||
});
|
||||
|
||||
it('returns true for executable files', () => {
|
||||
assert.strictEqual(isFileDangerous('run.exe'), true);
|
||||
assert.strictEqual(isFileDangerous('install.pif'), true);
|
||||
});
|
||||
|
||||
it('returns true for Microsoft settings files', () => {
|
||||
assert.strictEqual(isFileDangerous('downl.SettingContent-ms'), true);
|
||||
});
|
||||
|
||||
it('returns false for non-dangerous files that end in ".", which can happen on Windows', () => {
|
||||
assert.strictEqual(isFileDangerous('dog.png.'), false);
|
||||
assert.strictEqual(isFileDangerous('resume.docx.'), false);
|
||||
});
|
||||
|
||||
it('returns true for dangerous files that end in ".", which can happen on Windows', () => {
|
||||
assert.strictEqual(isFileDangerous('run.exe.'), true);
|
||||
assert.strictEqual(isFileDangerous('install.pif.'), true);
|
||||
});
|
||||
|
||||
it('returns false for empty filename', () => {
|
||||
assert.strictEqual(isFileDangerous(''), false);
|
||||
});
|
||||
|
||||
it('returns false for exe at various parts of filename', () => {
|
||||
assert.strictEqual(isFileDangerous('.exemanifesto.txt'), false);
|
||||
assert.strictEqual(isFileDangerous('runexe'), false);
|
||||
assert.strictEqual(isFileDangerous('run_exe'), false);
|
||||
});
|
||||
|
||||
it('returns true for upper-case EXE', () => {
|
||||
assert.strictEqual(isFileDangerous('run.EXE'), true);
|
||||
});
|
||||
});
|
22
ts/test-node/util/isMuted_test.ts
Normal file
22
ts/test-node/util/isMuted_test.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isMuted } from '../../util/isMuted';
|
||||
|
||||
describe('isMuted', () => {
|
||||
it('returns false if passed undefined', () => {
|
||||
assert.isFalse(isMuted(undefined));
|
||||
});
|
||||
|
||||
it('returns false if passed a date in the past', () => {
|
||||
assert.isFalse(isMuted(0));
|
||||
assert.isFalse(isMuted(Date.now() - 123));
|
||||
});
|
||||
|
||||
it('returns false if passed a date in the future', () => {
|
||||
assert.isTrue(isMuted(Date.now() + 123));
|
||||
assert.isTrue(isMuted(Date.now() + 123456));
|
||||
});
|
||||
});
|
45
ts/test-node/util/isPathInside_test.ts
Normal file
45
ts/test-node/util/isPathInside_test.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isPathInside } from '../../util/isPathInside';
|
||||
|
||||
describe('isPathInside', () => {
|
||||
it('returns false if the child path is not inside the parent path', () => {
|
||||
assert.isFalse(isPathInside('x', 'a/b'));
|
||||
assert.isFalse(isPathInside('a/b', '/a/b'));
|
||||
assert.isFalse(isPathInside('/a/b', 'a/b'));
|
||||
assert.isFalse(isPathInside('/x', '/a/b'));
|
||||
assert.isFalse(isPathInside('/x/y', '/a/b'));
|
||||
assert.isFalse(isPathInside('/a/x', '/a/b'));
|
||||
assert.isFalse(isPathInside('/a/bad', '/a/b'));
|
||||
assert.isFalse(isPathInside('/a/x', '/a/b'));
|
||||
assert.isFalse(isPathInside('/a/b', '/a/b'));
|
||||
assert.isFalse(isPathInside('/a/b/c/..', '/a/b'));
|
||||
assert.isFalse(isPathInside('/', '/'));
|
||||
assert.isFalse(isPathInside('/x/..', '/'));
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
assert.isFalse(isPathInside('C:\\a\\x\\y', 'C:\\a\\b'));
|
||||
assert.isFalse(isPathInside('D:\\a\\b\\c', 'C:\\a\\b'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns true if the child path is inside the parent path', () => {
|
||||
assert.isTrue(isPathInside('a/b/c', 'a/b'));
|
||||
assert.isTrue(isPathInside('a/b/c/d', 'a/b'));
|
||||
assert.isTrue(isPathInside('/a/b/c', '/a/b'));
|
||||
assert.isTrue(isPathInside('/a/b/c', '/a/b/'));
|
||||
assert.isTrue(isPathInside('/a/b/c/', '/a/b'));
|
||||
assert.isTrue(isPathInside('/a/b/c/', '/a/b/'));
|
||||
assert.isTrue(isPathInside('/a/b/c/d', '/a/b'));
|
||||
assert.isTrue(isPathInside('/a/b/c/d/..', '/a/b'));
|
||||
assert.isTrue(isPathInside('/x/y/z/z/y', '/'));
|
||||
assert.isTrue(isPathInside('x/y/z/z/y', '/'));
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
assert.isTrue(isPathInside('C:\\a\\b\\c', 'C:\\a\\b'));
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { nonRenderedRemoteParticipant } from '../../../util/ringrtc/nonRenderedRemoteParticipant';
|
||||
|
||||
describe('nonRenderedRemoteParticipant', () => {
|
||||
it('returns a video request object a width and height of 0', () => {
|
||||
assert.deepEqual(nonRenderedRemoteParticipant({ demuxId: 123 }), {
|
||||
demuxId: 123,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { normalizeGroupCallTimestamp } from '../../../util/ringrtc/normalizeGroupCallTimestamp';
|
||||
|
||||
describe('normalizeGroupCallTimestamp', () => {
|
||||
it('returns undefined if passed NaN', () => {
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(NaN));
|
||||
});
|
||||
|
||||
it('returns undefined if passed 0', () => {
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(0));
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(-0));
|
||||
});
|
||||
|
||||
it('returns undefined if passed a negative number', () => {
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(-1));
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(-123));
|
||||
});
|
||||
|
||||
it('returns undefined if passed a string that cannot be parsed as a number', () => {
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(''));
|
||||
assert.isUndefined(normalizeGroupCallTimestamp('uhhh'));
|
||||
});
|
||||
|
||||
it('returns undefined if passed a BigInt of 0', () => {
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(BigInt(0)));
|
||||
});
|
||||
|
||||
it('returns undefined if passed a negative BigInt', () => {
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(BigInt(-1)));
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(BigInt(-123)));
|
||||
});
|
||||
|
||||
it('returns undefined if passed a non-parseable type', () => {
|
||||
[
|
||||
undefined,
|
||||
null,
|
||||
{},
|
||||
[],
|
||||
[123],
|
||||
Symbol('123'),
|
||||
{ [Symbol.toPrimitive]: () => 123 },
|
||||
// eslint-disable-next-line no-new-wrappers
|
||||
new Number(123),
|
||||
].forEach(value => {
|
||||
assert.isUndefined(normalizeGroupCallTimestamp(value));
|
||||
});
|
||||
});
|
||||
|
||||
it('returns positive numbers passed in', () => {
|
||||
assert.strictEqual(normalizeGroupCallTimestamp(1), 1);
|
||||
assert.strictEqual(normalizeGroupCallTimestamp(123), 123);
|
||||
});
|
||||
|
||||
it('parses strings as numbers', () => {
|
||||
assert.strictEqual(normalizeGroupCallTimestamp('1'), 1);
|
||||
assert.strictEqual(normalizeGroupCallTimestamp('123'), 123);
|
||||
});
|
||||
|
||||
it('only parses the first 15 characters of a string', () => {
|
||||
assert.strictEqual(
|
||||
normalizeGroupCallTimestamp('12345678901234567890123456789'),
|
||||
123456789012345
|
||||
);
|
||||
});
|
||||
|
||||
it('converts positive BigInts to numbers', () => {
|
||||
assert.strictEqual(normalizeGroupCallTimestamp(BigInt(1)), 1);
|
||||
assert.strictEqual(normalizeGroupCallTimestamp(BigInt(123)), 123);
|
||||
});
|
||||
});
|
176
ts/test-node/util/sgnlHref_test.ts
Normal file
176
ts/test-node/util/sgnlHref_test.ts
Normal file
|
@ -0,0 +1,176 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import Sinon from 'sinon';
|
||||
import { LoggerType } from '../../types/Logging';
|
||||
|
||||
import { isSgnlHref, parseSgnlHref } from '../../util/sgnlHref';
|
||||
|
||||
function shouldNeverBeCalled() {
|
||||
assert.fail('This should never be called');
|
||||
}
|
||||
|
||||
const explodingLogger: LoggerType = {
|
||||
fatal: shouldNeverBeCalled,
|
||||
error: shouldNeverBeCalled,
|
||||
warn: shouldNeverBeCalled,
|
||||
info: shouldNeverBeCalled,
|
||||
debug: shouldNeverBeCalled,
|
||||
trace: shouldNeverBeCalled,
|
||||
};
|
||||
|
||||
describe('sgnlHref', () => {
|
||||
describe('isSgnlHref', () => {
|
||||
it('returns false for non-strings', () => {
|
||||
const logger = {
|
||||
...explodingLogger,
|
||||
warn: Sinon.spy(),
|
||||
};
|
||||
|
||||
const castToString = (value: unknown): string => value as string;
|
||||
|
||||
assert.isFalse(isSgnlHref(castToString(undefined), logger));
|
||||
assert.isFalse(isSgnlHref(castToString(null), logger));
|
||||
assert.isFalse(isSgnlHref(castToString(123), logger));
|
||||
|
||||
Sinon.assert.calledThrice(logger.warn);
|
||||
});
|
||||
|
||||
it('returns false for invalid URLs', () => {
|
||||
assert.isFalse(isSgnlHref('', explodingLogger));
|
||||
assert.isFalse(isSgnlHref('sgnl', explodingLogger));
|
||||
assert.isFalse(isSgnlHref('sgnl://::', explodingLogger));
|
||||
});
|
||||
|
||||
it('returns false if the protocol is not "sgnl:"', () => {
|
||||
assert.isFalse(isSgnlHref('https://example', explodingLogger));
|
||||
assert.isFalse(
|
||||
isSgnlHref(
|
||||
'https://signal.art/addstickers/?pack_id=abc',
|
||||
explodingLogger
|
||||
)
|
||||
);
|
||||
assert.isFalse(isSgnlHref('signal://example', explodingLogger));
|
||||
});
|
||||
|
||||
it('returns true if the protocol is "sgnl:"', () => {
|
||||
assert.isTrue(isSgnlHref('sgnl://', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example.com', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('SGNL://example', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example?foo=bar', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example/', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example#', explodingLogger));
|
||||
|
||||
assert.isTrue(isSgnlHref('sgnl:foo', explodingLogger));
|
||||
|
||||
assert.isTrue(isSgnlHref('sgnl://user:pass@example', explodingLogger));
|
||||
assert.isTrue(isSgnlHref('sgnl://example.com:1234', explodingLogger));
|
||||
assert.isTrue(
|
||||
isSgnlHref('sgnl://example.com/extra/path/data', explodingLogger)
|
||||
);
|
||||
assert.isTrue(
|
||||
isSgnlHref('sgnl://example/?foo=bar#hash', explodingLogger)
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts URL objects', () => {
|
||||
const invalid = new URL('https://example.com');
|
||||
assert.isFalse(isSgnlHref(invalid, explodingLogger));
|
||||
const valid = new URL('sgnl://example');
|
||||
assert.isTrue(isSgnlHref(valid, explodingLogger));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSgnlHref', () => {
|
||||
it('returns a null command for invalid URLs', () => {
|
||||
['', 'sgnl', 'https://example/?foo=bar'].forEach(href => {
|
||||
assert.deepEqual(parseSgnlHref(href, explodingLogger), {
|
||||
command: null,
|
||||
args: new Map<never, never>(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('parses the command for URLs with no arguments', () => {
|
||||
[
|
||||
'sgnl://foo',
|
||||
'sgnl://foo/',
|
||||
'sgnl://foo?',
|
||||
'SGNL://foo?',
|
||||
'sgnl://user:pass@foo',
|
||||
'sgnl://foo/path/data#hash-data',
|
||||
].forEach(href => {
|
||||
assert.deepEqual(parseSgnlHref(href, explodingLogger), {
|
||||
command: 'foo',
|
||||
args: new Map<string, string>(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a command's arguments", () => {
|
||||
assert.deepEqual(
|
||||
parseSgnlHref(
|
||||
'sgnl://Foo?bar=baz&qux=Quux&num=123&empty=&encoded=hello%20world',
|
||||
explodingLogger
|
||||
),
|
||||
{
|
||||
command: 'Foo',
|
||||
args: new Map([
|
||||
['bar', 'baz'],
|
||||
['qux', 'Quux'],
|
||||
['num', '123'],
|
||||
['empty', ''],
|
||||
['encoded', 'hello world'],
|
||||
]),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('treats the port as part of the command', () => {
|
||||
assert.propertyVal(
|
||||
parseSgnlHref('sgnl://foo:1234', explodingLogger),
|
||||
'command',
|
||||
'foo:1234'
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores duplicate query parameters', () => {
|
||||
assert.deepPropertyVal(
|
||||
parseSgnlHref('sgnl://x?foo=bar&foo=totally-ignored', explodingLogger),
|
||||
'args',
|
||||
new Map([['foo', 'bar']])
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores other parts of the URL', () => {
|
||||
[
|
||||
'sgnl://foo?bar=baz',
|
||||
'sgnl://foo/?bar=baz',
|
||||
'sgnl://foo/lots/of/path?bar=baz',
|
||||
'sgnl://foo?bar=baz#hash',
|
||||
'sgnl://user:pass@foo?bar=baz',
|
||||
].forEach(href => {
|
||||
assert.deepEqual(parseSgnlHref(href, explodingLogger), {
|
||||
command: 'foo',
|
||||
args: new Map([['bar', 'baz']]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't do anything fancy with arrays or objects in the query string", () => {
|
||||
// The `qs` module does things like this, which we don't want.
|
||||
assert.deepPropertyVal(
|
||||
parseSgnlHref('sgnl://x?foo[]=bar&foo[]=baz', explodingLogger),
|
||||
'args',
|
||||
new Map([['foo[]', 'bar']])
|
||||
);
|
||||
assert.deepPropertyVal(
|
||||
parseSgnlHref('sgnl://x?foo[bar][baz]=foobarbaz', explodingLogger),
|
||||
'args',
|
||||
new Map([['foo[bar][baz]', 'foobarbaz']])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
36
ts/test-node/util/sleep_test.ts
Normal file
36
ts/test-node/util/sleep_test.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { useFakeTimers } from 'sinon';
|
||||
|
||||
import { sleep } from '../../util/sleep';
|
||||
|
||||
describe('sleep', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
// This isn't a hook.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
this.clock = useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
this.clock.restore();
|
||||
});
|
||||
|
||||
it('returns a promise that resolves after the specified number of milliseconds', async function test() {
|
||||
let isDone = false;
|
||||
|
||||
(async () => {
|
||||
await sleep(123);
|
||||
isDone = true;
|
||||
})();
|
||||
|
||||
assert.isFalse(isDone);
|
||||
|
||||
await this.clock.tickAsync(100);
|
||||
assert.isFalse(isDone);
|
||||
|
||||
await this.clock.tickAsync(25);
|
||||
assert.isTrue(isDone);
|
||||
});
|
||||
});
|
95
ts/test-node/util/sniffImageMimeType_test.ts
Normal file
95
ts/test-node/util/sniffImageMimeType_test.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { assert } from 'chai';
|
||||
import {
|
||||
IMAGE_BMP,
|
||||
IMAGE_GIF,
|
||||
IMAGE_ICO,
|
||||
IMAGE_JPEG,
|
||||
IMAGE_PNG,
|
||||
IMAGE_WEBP,
|
||||
} from '../../types/MIME';
|
||||
|
||||
import { sniffImageMimeType } from '../../util/sniffImageMimeType';
|
||||
|
||||
describe('sniffImageMimeType', () => {
|
||||
const fixture = (filename: string): Promise<Buffer> => {
|
||||
const fixturePath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'fixtures',
|
||||
filename
|
||||
);
|
||||
return fs.promises.readFile(fixturePath);
|
||||
};
|
||||
|
||||
it('returns undefined for empty buffers', () => {
|
||||
assert.isUndefined(sniffImageMimeType(new Uint8Array()));
|
||||
});
|
||||
|
||||
it('returns undefined for non-image files', async () => {
|
||||
await Promise.all(
|
||||
['pixabay-Soap-Bubble-7141.mp4', 'lorem-ipsum.txt'].map(
|
||||
async filename => {
|
||||
assert.isUndefined(sniffImageMimeType(await fixture(filename)));
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('sniffs ICO files', async () => {
|
||||
assert.strictEqual(
|
||||
sniffImageMimeType(await fixture('kitten-1-64-64.ico')),
|
||||
IMAGE_ICO
|
||||
);
|
||||
});
|
||||
|
||||
it('sniffs BMP files', async () => {
|
||||
assert.strictEqual(sniffImageMimeType(await fixture('2x2.bmp')), IMAGE_BMP);
|
||||
});
|
||||
|
||||
it('sniffs GIF files', async () => {
|
||||
assert.strictEqual(
|
||||
sniffImageMimeType(await fixture('giphy-GVNvOUpeYmI7e.gif')),
|
||||
IMAGE_GIF
|
||||
);
|
||||
});
|
||||
|
||||
it('sniffs WEBP files', async () => {
|
||||
assert.strictEqual(
|
||||
sniffImageMimeType(await fixture('512x515-thumbs-up-lincoln.webp')),
|
||||
IMAGE_WEBP
|
||||
);
|
||||
});
|
||||
|
||||
it('sniffs PNG files', async () => {
|
||||
await Promise.all(
|
||||
[
|
||||
'freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png',
|
||||
'Animated_PNG_example_bouncing_beach_ball.png',
|
||||
].map(async filename => {
|
||||
assert.strictEqual(
|
||||
sniffImageMimeType(await fixture(filename)),
|
||||
IMAGE_PNG
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('sniffs JPEG files', async () => {
|
||||
assert.strictEqual(
|
||||
sniffImageMimeType(await fixture('kitten-1-64-64.jpg')),
|
||||
IMAGE_JPEG
|
||||
);
|
||||
});
|
||||
|
||||
it('handles ArrayBuffers', async () => {
|
||||
const arrayBuffer = (await fixture('kitten-1-64-64.jpg')).buffer;
|
||||
assert.strictEqual(sniffImageMimeType(arrayBuffer), IMAGE_JPEG);
|
||||
});
|
||||
});
|
67
ts/test-node/util/windowsZoneIdentifier_test.ts
Normal file
67
ts/test-node/util/windowsZoneIdentifier_test.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as fse from 'fs-extra';
|
||||
import * as Sinon from 'sinon';
|
||||
import { assertRejects } from '../helpers';
|
||||
|
||||
import { writeWindowsZoneIdentifier } from '../../util/windowsZoneIdentifier';
|
||||
|
||||
describe('writeWindowsZoneIdentifier', () => {
|
||||
before(function thisNeeded() {
|
||||
if (process.platform !== 'win32') {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async function thisNeeded() {
|
||||
this.sandbox = Sinon.createSandbox();
|
||||
this.tmpdir = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'signal-test-')
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async function thisNeeded() {
|
||||
this.sandbox.restore();
|
||||
await fse.remove(this.tmpdir);
|
||||
});
|
||||
|
||||
it('writes zone transfer ID 3 (internet) to the Zone.Identifier file', async function thisNeeded() {
|
||||
const file = path.join(this.tmpdir, 'file.txt');
|
||||
await fse.outputFile(file, 'hello');
|
||||
|
||||
await writeWindowsZoneIdentifier(file);
|
||||
|
||||
assert.strictEqual(
|
||||
await fs.promises.readFile(`${file}:Zone.Identifier`, 'utf8'),
|
||||
'[ZoneTransfer]\r\nZoneId=3'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if there is an existing Zone.Identifier file', async function thisNeeded() {
|
||||
const file = path.join(this.tmpdir, 'file.txt');
|
||||
await fse.outputFile(file, 'hello');
|
||||
await fs.promises.writeFile(`${file}:Zone.Identifier`, '# already here');
|
||||
|
||||
await assertRejects(() => writeWindowsZoneIdentifier(file));
|
||||
});
|
||||
|
||||
it('fails if the original file does not exist', async function thisNeeded() {
|
||||
const file = path.join(this.tmpdir, 'file-never-created.txt');
|
||||
|
||||
await assertRejects(() => writeWindowsZoneIdentifier(file));
|
||||
});
|
||||
|
||||
it('fails if not on Windows', async function thisNeeded() {
|
||||
this.sandbox.stub(process, 'platform').get(() => 'darwin');
|
||||
|
||||
const file = path.join(this.tmpdir, 'file.txt');
|
||||
await fse.outputFile(file, 'hello');
|
||||
|
||||
await assertRejects(() => writeWindowsZoneIdentifier(file));
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue