move taskWithTimeout to typescript

pull/1707/head
Audric Ackermann 4 years ago
parent 34835ef68f
commit 09d9db38e8
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -80,7 +80,6 @@ module.exports = grunt => {
'libtextsecure/event_target.js',
'libtextsecure/http-resources.js',
'libtextsecure/message_receiver.js',
'libtextsecure/task_with_timeout.js',
],
dest: 'js/libtextsecure.js',
},
@ -346,17 +345,6 @@ module.exports = grunt => {
runTests(environment, done);
});
grunt.registerTask(
'lib-unit-tests',
'Run libtextsecure unit tests w/Electron',
function thisNeeded() {
const environment = grunt.option('env') || 'test-lib';
const done = this.async();
runTests(environment, done);
}
);
grunt.registerMultiTask('test-release', 'Test packaged releases', function thisNeeded() {
const dir = grunt.option('dir') || 'release';
const environment = grunt.option('env') || 'production';
@ -440,7 +428,7 @@ module.exports = grunt => {
grunt.registerTask('tx', ['exec:tx-pull-new', 'exec:tx-pull', 'locale-patch']);
grunt.registerTask('dev', ['default', 'watch']);
grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']);
grunt.registerTask('test', ['unit-tests']);
grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
grunt.registerTask('default', [
'exec:build-protobuf',

@ -111,18 +111,6 @@
}
inherit(ReplayableError, TimestampError);
function PublicChatError(message) {
this.name = 'PublicChatError';
this.message = message;
Error.call(this, message);
// Maintains proper stack trace, where our error was thrown (only available on V8)
// via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
if (Error.captureStackTrace) {
Error.captureStackTrace(this);
}
}
window.textsecure.SendMessageNetworkError = SendMessageNetworkError;
window.textsecure.ReplayableError = ReplayableError;
window.textsecure.EmptySwarmError = EmptySwarmError;
@ -130,5 +118,4 @@
window.textsecure.HTTPError = HTTPError;
window.textsecure.NotFoundError = NotFoundError;
window.textsecure.TimestampError = TimestampError;
window.textsecure.PublicChatError = PublicChatError;
})();

@ -11,6 +11,4 @@ export interface LibTextsecure {
HTTPError: any;
NotFoundError: any;
TimestampError: any;
PublicChatError: any;
createTaskWithTimeout(task: any, id: any, options?: any): Promise<any>;
}

@ -1,23 +0,0 @@
{
"env": {
"browser": true,
"node": false,
"mocha": true,
},
"parserOptions": {
"sourceType": "script"
},
"rules": {
"strict": "off",
"more/no-then": "off",
},
"globals": {
"assert": true,
"assertEqualArrayBuffers": true,
"dcodeIO": true,
"getString": true,
"hexToArrayBuffer": true,
"PROTO_ROOT": true,
"stringToArrayBuffer": true,
}
}

@ -1,61 +0,0 @@
/* global mocha, chai, assert */
mocha
.setup('bdd')
.fullTrace()
.timeout(10000);
window.assert = chai.assert;
window.PROTO_ROOT = '../../protos';
const OriginalReporter = mocha._reporter;
const SauceReporter = function Constructor(runner) {
const failedTests = [];
runner.on('end', () => {
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', (test, err) => {
const flattenTitles = item => {
const titles = [];
while (item.parent.title) {
titles.push(item.parent.title);
// eslint-disable-next-line no-param-reassign
item = item.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test),
});
});
// eslint-disable-next-line no-new
new OriginalReporter(runner);
};
SauceReporter.prototype = OriginalReporter.prototype;
mocha.reporter(SauceReporter);
/*
* global helpers for tests
*/
window.assertEqualArrayBuffers = (ab1, ab2) => {
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
};
window.hexToArrayBuffer = str => {
const ret = new ArrayBuffer(str.length / 2);
const array = new Uint8Array(ret);
for (let i = 0; i < str.length / 2; i += 1) {
array[i] = parseInt(str.substr(i * 2, 2), 16);
}
return ret;
};

File diff suppressed because one or more lines are too long

@ -1,56 +0,0 @@
/* global libsignal, textsecure */
describe('encrypting and decrypting profile data', () => {
const NAME_PADDED_LENGTH = 26;
describe('encrypting and decrypting profile names', () => {
it('pads, encrypts, decrypts, and unpads a short string', () => {
const name = 'Alice';
const buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfileName(buffer, key).then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto.decryptProfileName(encrypted, key).then(decrypted => {
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), 'Alice');
});
});
});
it('works for empty string', () => {
const name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfileName(name.buffer, key).then(encrypted => {
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
return textsecure.crypto.decryptProfileName(encrypted, key).then(decrypted => {
assert.strictEqual(decrypted.byteLength, 0);
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), '');
});
});
});
});
describe('encrypting and decrypting profile avatars', () => {
it('encrypts and decrypts', () => {
const buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto.decryptProfile(encrypted, key).then(decrypted => {
assertEqualArrayBuffers(buffer, decrypted);
});
});
});
it('throws when decrypting with the wrong key', () => {
const buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
const key = libsignal.crypto.getRandomBytes(32);
const badKey = libsignal.crypto.getRandomBytes(32);
return textsecure.crypto.encryptProfile(buffer, key).then(encrypted => {
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
return textsecure.crypto.decryptProfile(encrypted, badKey).catch(error => {
assert.strictEqual(error.name, 'ProfileDecryptError');
});
});
});
});
});

@ -1,45 +0,0 @@
<html>
<head>
<meta charset='utf-8'>
<title>libtextsecure test runner</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
</head>
<body>
<div id="mocha">
</div>
<div id="tests">
</div>
<script type="text/javascript" src="test.js"></script>
<script type="text/javascript" src="../components.js"></script>
<script type="text/javascript" src="../libsignal-protocol.js"></script>
<script type="text/javascript" src="../crypto.js"></script>
<script type="text/javascript" src="../protobufs.js" data-cover></script>
<script type="text/javascript" src="../errors.js" data-cover></script>
<script type="text/javascript" src="../storage.js" data-cover></script>
<script type="text/javascript" src="../event_target.js" data-cover></script>
<script type="text/javascript" src="../helpers.js" data-cover></script>
<script type="text/javascript" src="../stringview.js" data-cover></script>
<script type="text/javascript" src="../task_with_timeout.js" data-cover></script>
<script type="text/javascript" src="errors_test.js"></script>
<script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="task_with_timeout_test.js"></script>
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
<!-- NOTE: blanket doesn't support modern syntax and will choke until we find a replacement. :0( -->
<!-- <script type="text/javascript" src="blanket_mocha.js"></script> -->
<!-- Uncomment to start tests without code coverage enabled -->
<script type="text/javascript">
mocha.run();
</script>
</body>
</html>

@ -1,72 +0,0 @@
/* global textsecure */
describe('createTaskWithTimeout', () => {
it('resolves when promise resolves', () => {
const task = () => Promise.resolve('hi!');
const taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(result => {
assert.strictEqual(result, 'hi!');
});
});
it('flows error from promise back', () => {
const error = new Error('original');
const task = () => Promise.reject(error);
const taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().catch(flowedError => {
assert.strictEqual(error, flowedError);
});
});
it('rejects if promise takes too long (this one logs error to console)', () => {
let complete = false;
const task = () =>
new Promise(resolve => {
setTimeout(() => {
complete = true;
resolve();
}, 3000);
});
const taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10,
});
return taskWithTimeout().then(
() => {
throw new Error('it was not supposed to resolve!');
},
() => {
assert.strictEqual(complete, false);
}
);
});
it('resolves if task returns something falsey', () => {
const task = () => {};
const taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout();
});
it('resolves if task returns a non-promise', () => {
const task = () => 'hi!';
const taskWithTimeout = textsecure.createTaskWithTimeout(task);
return taskWithTimeout().then(result => {
assert.strictEqual(result, 'hi!');
});
});
it('rejects if task throws (and does not log about taking too long)', () => {
const error = new Error('Task is throwing!');
const task = () => {
throw error;
};
const taskWithTimeout = textsecure.createTaskWithTimeout(task, this.name, {
timeout: 10,
});
return taskWithTimeout().then(
() => {
throw new Error('Overall task should reject!');
},
flowedError => {
assert.strictEqual(flowedError, error);
}
);
});
});

@ -351,8 +351,6 @@ async function createWindow() {
if (config.environment === 'test') {
mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html']));
} else if (config.environment === 'test-lib') {
mainWindow.loadURL(prepareURL([__dirname, 'libtextsecure', 'test', 'index.html']));
} else if (config.environment.includes('test-integration')) {
mainWindow.loadURL(prepareURL([__dirname, 'background_test.html']));
} else {
@ -377,7 +375,6 @@ async function createWindow() {
// If the application is terminating, just do the default
if (
config.environment === 'test' ||
config.environment === 'test-lib' ||
config.environment.includes('test-integration') ||
(mainWindow.readyForShutdown && windowState.shouldQuit())
) {
@ -503,11 +500,7 @@ function showPasswordWindow() {
passwordWindow.on('close', e => {
// If the application is terminating, just do the default
if (
config.environment === 'test' ||
config.environment === 'test-lib' ||
windowState.shouldQuit()
) {
if (config.environment === 'test' || windowState.shouldQuit()) {
return;
}
@ -632,11 +625,7 @@ app.on('ready', async () => {
const userDataPath = await getRealPath(app.getPath('userData'));
const installPath = await getRealPath(app.getAppPath());
if (
process.env.NODE_ENV !== 'test' &&
process.env.NODE_ENV !== 'test-lib' &&
!process.env.NODE_ENV.includes('test-integration')
) {
if (process.env.NODE_ENV !== 'test' && !process.env.NODE_ENV.includes('test-integration')) {
installFileHandler({
protocol: electronProtocol,
userDataPath,
@ -805,7 +794,6 @@ app.on('window-all-closed', () => {
if (
process.platform !== 'darwin' ||
config.environment === 'test' ||
config.environment === 'test-lib' ||
config.environment.includes('test-integration')
) {
app.quit();

@ -18,143 +18,6 @@
</div>
</div>
<script type="text/x-tmpl-mustache" id="banner">
<div class="body">
<span class="icon warning"></span>
{{ message }}
<span class="icon dismiss"></span>
</div>
</script>
<script type="text/x-tmpl-mustache" id="nickname-dialog">
<div class="content">
<div class="message">{{ message }}</div>
<input type="text" name="name" class="name" placeholder="Type a name" value="{{ name }}">
<div class="buttons">
<button class="clear" tabindex="3">{{ clear }}</button>
<button class="cancel" tabindex="2">{{ cancel }}</button>
<button class="ok" tabindex="1">{{ ok }}</button>
</div>
</div>
</script>
<script type="text/x-tmpl-mustache" id="beta-disclaimer-dialog">
<div class="content">
<div class="betaDisclaimerView" style="display: none;">
<h2>
Thanks for testing Session!
</h2>
<p>
Thanks for testing Session! This software is a beta version of the full Session software suite, and so is missing some of the features the full version will have.
</p>
<p>
<b>
This version of Session provides no guarantees of metadata privacy.
</b>
</p>
<p>
While your messages are secured using end to end encryption, in this beta version of Loki messenger, <b>third parties (like your ISP or the Service Node network) can see who youre talking to</b> and when youre sending or receiving messages.
</p>
<p>
It is also possible that <b>third parties could correlate your public key to your IP address</b> and your real identity if they learn your public key.
</p>
<p>
However, no one except you and your intended recipients will be able to see the contents of your messages. We recommend using existing methods, like Tor or I2P to mask your IP address while using Session beta version.
</p>
<p>
As a beta, this software is still experimental. When things aren't working for you, or you feel confused by the app, please let us know by filing an issue on <a href="https://github.com/loki-project/loki-messenger">Github</a> or making suggestions on <a href="https://discordapp.com/invite/67GXfD6">Discord</a>.
</p>
<button class="ok" tabindex="1">{{ ok }}</button>
</div>
</div>
</script>
<script type="text/x-tmpl-mustache" id="identicon-svg">
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="{{ color }}" />
<text text-anchor="middle" fill="white" font-family="sans-serif" font-size="24px" x="50" y="50" baseline-shift="-8px">
{{ content }}
</text>
</svg>
</script>
<script type="text/x-tmpl-mustache" id="import-flow-template">
{{#isStep2}}
<div id="step2" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon"></span>
<div class="header">{{ chooseHeader }}</div>
<div class="body-text">{{ choose }}</div>
</div>
<div class="nav">
<div>
<a class="button choose">{{ chooseButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep2}}
{{#isStep3}}
<div id="step3" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon import"></span>
<div class="header">{{ importingHeader }}</div>
</div>
<div class="progress">
</div>
</div>
</div>
{{/isStep3}}
{{#isStep4}}
<div id="step4" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon"></span>
<div class="header">{{ completeHeader }}</div>
</div>
<div class="nav">
{{#restartButton}}
<div>
<a class="button restart">{{ restartButton }}</a>
</div>
{{/restartButton}}
{{#registerButton}}
<div>
<a class="button register">{{ registerButton }}</a>
</div>
{{/registerButton}}
</div>
</div>
</div>
{{/isStep4}}
{{#isError}}
<div id="error" class="step">
<div class="clearfix inner error-dialog">
<div class="step-body">
<span class="banner-icon alert-outline"></span>
<div class="header">{{ errorHeader }}</div>
<div class="body-text-wide">
{{ errorMessageFirst }}
<p>{{ errorMessageSecond }}</p>
</div>
</div>
<div class="nav">
<div>
<a class="button choose">{{ chooseButton }}</a>
</div>
</div>
</div>
</div>
{{/isError}}
</script>
<script type="text/javascript" src="../js/components.js"></script>

@ -43,6 +43,7 @@ import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUt
import { NotificationForConvoOption } from '../components/conversation/ConversationHeader';
import { useDispatch } from 'react-redux';
import { updateConfirmModal } from '../state/ducks/modalDialog';
import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
export enum ConversationTypeEnum {
GROUP = 'group',
@ -455,10 +456,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
// tslint:disable-next-line: no-promise-as-boolean
const previous = this.pending || Promise.resolve();
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
callback,
`conversation ${this.idForLogging()}`
);
const taskWithTimeout = createTaskWithTimeout(callback, `conversation ${this.idForLogging()}`);
this.pending = previous.then(taskWithTimeout, taskWithTimeout);
const current = this.pending;

@ -38,6 +38,7 @@ import { handleMessageJob } from './queuedJob';
import { fromBase64ToArray } from '../session/utils/String';
import { removeMessagePadding } from '../session/crypto/BufferPadding';
import { isDuplicateBasedOnHash } from './hashDuplicateFilter';
import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
// TODO: check if some of these exports no longer needed
@ -93,7 +94,7 @@ function queueEnvelope(envelope: EnvelopePlus) {
window?.log?.info('queueing envelope', id);
const task = handleEnvelope.bind(null, envelope);
const taskWithTimeout = window.textsecure.createTaskWithTimeout(task, `queueEnvelope ${id}`);
const taskWithTimeout = createTaskWithTimeout(task, `queueEnvelope ${id}`);
try {
envelopeQueue.add(taskWithTimeout);
@ -248,10 +249,7 @@ function queueDecryptedEnvelope(envelope: any, plaintext: ArrayBuffer) {
window?.log?.info('queueing decrypted envelope', id);
const task = handleDecryptedEnvelope.bind(null, envelope, plaintext);
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
task,
`queueEncryptedEnvelope ${id}`
);
const taskWithTimeout = createTaskWithTimeout(task, `queueEncryptedEnvelope ${id}`);
try {
envelopeQueue.add(taskWithTimeout);
} catch (error) {

@ -0,0 +1,66 @@
export const createTaskWithTimeout = (task: any, id: string, givenTimeout?: number) => {
const timeout = givenTimeout || 1000 * 60 * 3; // three minutes
const errorForStack = new Error('for stack');
return async () =>
new Promise((resolve, reject) => {
let complete = false;
let timer: NodeJS.Timeout | null = global.setTimeout(() => {
if (!complete) {
const message = `${id || ''} task did not complete in time. Calling stack: ${
errorForStack.stack
}`;
window?.log?.error(message);
reject(new Error(message));
return;
}
return null;
}, timeout);
const clearTimer = () => {
try {
const localTimer = timer;
if (localTimer) {
timer = null;
clearTimeout(localTimer);
}
} catch (error) {
window?.log?.error(
id || '',
'task ran into problem canceling timer. Calling stack:',
errorForStack.stack
);
}
};
const success = (result: any) => {
clearTimer();
complete = true;
resolve(result);
return;
};
const failure = (error: any) => {
clearTimer();
complete = true;
reject(error);
return;
};
let promise;
try {
promise = task();
} catch (error) {
clearTimer();
throw error;
}
if (!promise || !promise.then) {
clearTimer();
complete = true;
resolve(promise);
return;
}
return promise.then(success, failure);
});
};

@ -0,0 +1,81 @@
// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression
import chai from 'chai';
// tslint:disable-next-line: no-require-imports no-var-requires
import chaiAsPromised from 'chai-as-promised';
import { createTaskWithTimeout } from '../../../../session/utils/TaskWithTimeout';
chai.use(chaiAsPromised as any);
chai.should();
const { assert } = chai;
const taskName = 'whatever';
describe('createTaskWithTimeout', () => {
it('resolves when promise resolves', async () => {
const task = () => Promise.resolve('hi!');
const taskWithTimeout = createTaskWithTimeout(task, 'task_123');
await taskWithTimeout().then((result: any) => {
assert.strictEqual(result, 'hi!');
});
});
it('flows error from promise back', async () => {
const error = new Error('original');
const task = () => Promise.reject(error);
const taskWithTimeout = createTaskWithTimeout(task, 'task_123');
await taskWithTimeout().catch((flowedError: any) => {
assert.strictEqual(error, flowedError);
});
});
it('rejects if promise takes too long (this one logs error to console)', async () => {
let complete = false;
const task = async () =>
new Promise(resolve => {
setTimeout(() => {
complete = true;
resolve(null);
}, 3000);
});
const taskWithTimeout = createTaskWithTimeout(task, taskName, 10);
await taskWithTimeout().then(
() => {
throw new Error('it was not supposed to resolve!');
},
() => {
assert.strictEqual(complete, false);
}
);
});
it('resolves if task returns something falsey', async () => {
// tslint:disable-next-line: no-empty
const task = () => {};
const taskWithTimeout = createTaskWithTimeout(task, taskName);
await taskWithTimeout();
});
it('resolves if task returns a non-promise', async () => {
const task = () => 'hi!';
const taskWithTimeout = createTaskWithTimeout(task, taskName);
await taskWithTimeout().then((result: any) => {
assert.strictEqual(result, 'hi!');
});
});
it('rejects if task throws (and does not log about taking too long)', async () => {
const error = new Error('Task is throwing!');
const task = () => {
throw error;
};
const taskWithTimeout = createTaskWithTimeout(task, taskName, 10);
await taskWithTimeout().then(
() => {
throw new Error('Overall task should reject!');
},
(flowedError: any) => {
assert.strictEqual(flowedError, error);
}
);
});
});
Loading…
Cancel
Save