1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-12 20:57:12 +01:00
metamask-extension/ui/store/action-queue/index.test.js

359 lines
11 KiB
JavaScript
Raw Normal View History

import sinon from 'sinon';
import {
dropQueue,
callBackgroundMethod,
submitRequestToBackground,
_setBackgroundConnection,
} from '.';
// This file tests only MV3 queue scenario
// MV2 tests are already covered by '../actions.test.js'
jest.mock('../../../shared/modules/mv3.utils', () => {
return {
isManifestV3: () => true,
};
});
describe('ActionQueue', () => {
afterEach(() => {
sinon.restore();
dropQueue(true);
});
describe('dropQueue', () => {
it('rejects all pending actions by default', async () => {
const background = {
connectionStream: {
readable: false,
},
backgroundFunction: sinon.stub().yields(),
};
_setBackgroundConnection(background);
const result = submitRequestToBackground('backgroundFunction');
dropQueue();
await expect(result).rejects.toThrow(
'Background operation cancelled while waiting for connection.',
);
expect(background.backgroundFunction.called).toStrictEqual(false);
});
});
describe('submitRequestToBackground', () => {
it('calls promisified background method if the stream is connected', async () => {
const background = {
connectionStream: {
readable: true,
},
backgroundFunction1: sinon.stub().yields(),
};
_setBackgroundConnection(background);
submitRequestToBackground('backgroundFunction1');
expect(background.backgroundFunction1.called).toStrictEqual(true);
});
it('does not calls promisified background method if the stream is not connected', async () => {
const background = {
connectionStream: {
readable: false,
},
backgroundFunction2: sinon.stub().yields(),
};
_setBackgroundConnection(background);
submitRequestToBackground('backgroundFunction2');
expect(background.backgroundFunction2.called).toStrictEqual(false);
});
it('calls promisified background method on stream reconnection', async () => {
const background = {
connectionStream: {
readable: false,
},
backgroundFunction3: sinon.stub().yields(),
};
_setBackgroundConnection(background);
const requestPromise = submitRequestToBackground('backgroundFunction3');
background.connectionStream = {
readable: true,
};
_setBackgroundConnection(background);
await requestPromise;
expect(background.backgroundFunction3.calledOnce).toStrictEqual(true);
});
it('resolves if backgroundFunction resolves', async () => {
const background = {
connectionStream: {
readable: true,
},
backgroundFunction4: (cb) => {
return cb(null, 'test');
},
};
_setBackgroundConnection(background);
await expect(
submitRequestToBackground('backgroundFunction4'),
).resolves.toStrictEqual('test');
});
it('rejects if backgroundFunction throws exception', async () => {
expect.assertions(1);
const background = {
connectionStream: {
readable: true,
},
backgroundFunction: () => {
throw Error('test');
},
};
_setBackgroundConnection(background);
await expect(
submitRequestToBackground('backgroundFunction'),
).rejects.toThrow('test');
});
it('calls methods in parallel when connection available', async () => {
const trace = {};
const background = {
connectionStream: {
readable: true,
},
first: (cb) => {
setTimeout(() => {
trace.firstDone = Date.now();
cb(null, 'first');
}, 5);
},
second: (cb) => {
trace.secondStarted = Date.now();
setTimeout(() => cb(null, 'second'), 10);
},
};
_setBackgroundConnection(background);
const scheduled = Promise.all([
submitRequestToBackground('first'),
submitRequestToBackground('second'),
]);
await scheduled;
expect(trace.firstDone).toBeGreaterThan(trace.secondStarted);
});
it('processes the queue sequentially when connection is restored', async () => {
const trace = {};
const background = {
connectionStream: {
readable: false,
},
first: (cb) => {
setTimeout(() => {
trace.firstDone = Date.now();
cb(null, 'first');
}, 5);
},
second: (cb) => {
trace.secondStarted = Date.now();
setTimeout(() => cb(null, 'second'), 10);
},
};
_setBackgroundConnection(background);
const scheduled = Promise.all([
submitRequestToBackground('first'),
submitRequestToBackground('second'),
]);
background.connectionStream.readable = true;
_setBackgroundConnection(background);
await scheduled;
expect(trace.firstDone).toBeLessThanOrEqual(trace.secondStarted);
});
it('ensures actions in queue will not repeat once finished', async () => {
const trace = { calls: 0 };
const background = {
connectionStream: {
readable: false,
},
first: (cb) => {
trace.calls += 1;
setTimeout(() => {
trace.firstDone = Date.now();
cb(null, 'first');
}, 5);
},
second: (cb) => {
trace.calls += 1;
trace.secondStarted = Date.now();
setTimeout(() => cb(null, 'second'), 10);
},
};
_setBackgroundConnection(background);
const scheduled = Promise.all([
submitRequestToBackground('first'),
submitRequestToBackground('second'),
]);
background.connectionStream.readable = true;
_setBackgroundConnection(background);
await scheduled;
_setBackgroundConnection(background); // once all actions finished, this triggers draining the queue again
expect(trace.firstDone).toBeLessThanOrEqual(trace.secondStarted);
expect(trace.calls).toStrictEqual(2);
});
it('stops processng the queue if connection is lost', async () => {
const trace = {};
const background = {
connectionStream: {
readable: false,
},
first: (cb) => {
setTimeout(() => {
trace.firstDone = true;
background.connectionStream.readable = false;
cb(Error('lost connection'));
}, 5);
},
second: sinon.stub().yields(),
};
_setBackgroundConnection(background);
const scheduled = Promise.race([
submitRequestToBackground('first').catch(() => ({})),
submitRequestToBackground('second'),
]);
background.connectionStream.readable = true;
_setBackgroundConnection(background);
await scheduled;
await Promise.resolve('one more tick'); // One asynchronous tick to avoid depending on implementation details
expect(trace.firstDone).toStrictEqual(true);
expect(background.second.called).toStrictEqual(false);
});
// Failing test for a race condition related to how items are removed from queue
it('avoids race conditions', async () => {
const trace = { first: 0, second: 0 };
const flowControl = {};
const background = {
connectionStream: {
readable: false,
},
first: (cb) => {
trace.first += 1;
setTimeout(() => {
flowControl.triggerRaceCondition();
cb(null, 'first');
}, 5);
},
second: (cb) => {
trace.second += 1;
setTimeout(() => cb(null, 'second'), 10);
},
third: sinon.stub().yields(),
};
flowControl.triggerRaceCondition = () => {
flowControl.waitFor = submitRequestToBackground('third');
};
_setBackgroundConnection(background);
const scheduled = Promise.all([
submitRequestToBackground('first'),
submitRequestToBackground('second'),
]);
background.connectionStream.readable = true;
_setBackgroundConnection(background);
await scheduled;
await flowControl.waitFor;
expect(trace.first).toStrictEqual(1);
expect(trace.second).toStrictEqual(1);
expect(background.third.calledOnce).toStrictEqual(true);
});
});
describe('callBackgroundMethod', () => {
afterEach(() => {
sinon.restore();
});
it('calls background method if the stream is connected', async () => {
const background = {
connectionStream: {
readable: true,
},
backgroundFunction: sinon.stub().yields(),
};
_setBackgroundConnection(background);
callBackgroundMethod('backgroundFunction', [], () => ({}));
expect(background.backgroundFunction.called).toStrictEqual(true);
});
it('does not call background method if the stream is not connected', async () => {
const background = {
connectionStream: {
readable: false,
},
backgroundFunction: sinon.stub(),
};
_setBackgroundConnection(background);
callBackgroundMethod('backgroundFunction', [], () => ({}));
expect(background.backgroundFunction.called).toStrictEqual(false);
});
it('calls background method on stream reconnection', async () => {
const background = {
connectionStream: {
readable: false,
},
backgroundFunction: sinon.stub().yields(),
};
_setBackgroundConnection(background);
callBackgroundMethod('backgroundFunction', [], () => ({}));
expect(background.backgroundFunction.called).toStrictEqual(false);
background.connectionStream = {
readable: true,
};
_setBackgroundConnection(background);
expect(background.backgroundFunction.calledOnce).toStrictEqual(true);
});
it('resolves if backgroundFunction called resolves', async () => {
const background = {
connectionStream: {
readable: true,
},
backgroundFunction: (cb) => {
return cb(null, 'successViaCallback');
},
};
_setBackgroundConnection(background);
const value = await new Promise((resolve) => {
callBackgroundMethod('backgroundFunction', [], (_err, result) => {
resolve(result);
});
});
expect(value).toStrictEqual('successViaCallback');
});
it('rejects if backgroundFunction called rejects', async () => {
const errorViaCallback = Error('errorViaCallback');
const background = {
connectionStream: {
readable: true,
},
backgroundFunction: (cb) => {
return cb(errorViaCallback);
},
};
_setBackgroundConnection(background);
const value = await new Promise((resolve) => {
callBackgroundMethod('backgroundFunction', [], (err) => {
resolve(err);
});
});
expect(value).toStrictEqual(errorViaCallback);
});
});
});