Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,324 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global
Bootstrap,
BootstrapFixture,
DataAdapters,
ERROR_SYNC_APP_SYNC_IN_PROGRESS,
ERROR_SYNC_INVALID_REQUEST_OPTIONS,
MocksHelper,
MockIACPort,
MockLazyLoader,
MockSyncEngine
*/
require('/shared/js/sync/errors.js');
require('/shared/test/unit/mocks/mock_lazy_loader.js');
requireApp('system/test/unit/mock_iac_handler.js');
requireApp('sharedtest/test/unit/sync/fixtures/bootstrap.js');
requireApp('sharedtest/test/unit/sync/sync-engine-mock.js');
var mocksForBootstrap = new MocksHelper([
'IACHandler',
'IACPort',
'LazyLoader',
'SyncEngine'
]).init();
suite('Bootstrap', function() {
mocksForBootstrap.attachTestHelpers();
suite('when script is loaded', function() {
var addEventListenerSpy;
suiteSetup(function(done) {
addEventListenerSpy = sinon.spy(window, 'addEventListener');
// The other suites rely on this suite running first for bootstrap.js
// to be loaded.
require('/shared/js/sync/bootstrap.js', done);
});
suiteTeardown(function() {
addEventListenerSpy.restore();
});
test('DataAdapters global is exposed', function() {
expect(DataAdapters).to.be.an('object');
});
test('IAC event listener is set', function() {
expect(addEventListenerSpy.called).to.equal(true);
expect(addEventListenerSpy.args[0][0]).to.equal(
'iac-gaia::sync::request'
);
});
});
suite('Bootstrap.handleSyncRequest', function() {
function run(shouldFail = false) {
MockSyncEngine.shouldFail = shouldFail;
return Bootstrap.handleSyncRequest({
assertion: 'test-assertion',
keys: { kA: 'test-kA', kB: 'test-kB' },
collections: {
'test-collection': {}
},
URL: 'test-URL'
});
}
function buildRequest() {
return {
URL: 'is a string',
assertion: 'is a string',
keys: {
kA: 'is a string',
kB: 'is a string'
},
collections: {
foo: {}
}
};
}
test('checks request is an object', function(done) {
var spy = this.sinon.spy(MockLazyLoader, 'load');
expect(Bootstrap.handleSyncRequest()).to.be
.rejectedWith(ERROR_SYNC_INVALID_REQUEST_OPTIONS)
.and.notify(function(err) {
expect(spy.args[0][0]).to.equal('shared/js/sync/errors.js');
done(err);
});
});
test('checks request.URL is a string', function(done) {
var spy = this.sinon.spy(MockLazyLoader, 'load');
var request = buildRequest();
delete request.URL;
expect(Bootstrap.handleSyncRequest()).to.be
.rejectedWith(ERROR_SYNC_INVALID_REQUEST_OPTIONS)
.and.notify(function(err) {
expect(spy.args[0][0]).to.equal('shared/js/sync/errors.js');
done(err);
});
});
test('checks request.assertion is a string', function(done) {
var spy = this.sinon.spy(MockLazyLoader, 'load');
var request = buildRequest();
delete request.assertion;
expect(Bootstrap.handleSyncRequest()).to.be
.rejectedWith(ERROR_SYNC_INVALID_REQUEST_OPTIONS)
.and.notify(function(err) {
expect(spy.args[0][0]).to.equal('shared/js/sync/errors.js');
done(err);
});
});
test('checks request.keys.kB is a string', function(done) {
var spy = this.sinon.spy(MockLazyLoader, 'load');
var request = buildRequest();
delete request.keys.kB;
expect(Bootstrap.handleSyncRequest()).to.be
.rejectedWith(ERROR_SYNC_INVALID_REQUEST_OPTIONS)
.and.notify(function(err) {
expect(spy.args[0][0]).to.equal('shared/js/sync/errors.js');
done(err);
});
});
test('checks request.collections is an object (not Array)', function(done) {
var spy = this.sinon.spy(MockLazyLoader, 'load');
var request = buildRequest();
request.collections = [ 'foo' ];
expect(Bootstrap.handleSyncRequest()).to.be
.rejectedWith(ERROR_SYNC_INVALID_REQUEST_OPTIONS)
.and.notify(function(err) {
expect(spy.args[0][0]).to.equal('shared/js/sync/errors.js');
done(err);
});
});
test('loads main scripts', function(done) {
var spy = this.sinon.spy(MockLazyLoader, 'load');
run().then(function() {
expect(spy.args[0][0]).to.deep.equal(BootstrapFixture.mainScripts);
done();
});
});
test('loads DataAdapter scripts', function(done) {
var spy = this.sinon.spy(MockLazyLoader, 'load');
run().then(function() {
expect(spy.args[1][0]).to.deep.equal('js/adapters/test-collection.js');
done();
});
});
test('constructs SyncEngine', function(done) {
run().then(() => {
expect(MockSyncEngine.constructorOptions).to.deep.equal({
kB: 'test-kB',
URL: 'test-URL',
assertion: 'test-assertion',
adapters: DataAdapters
});
done();
});
});
test('calls SyncEngine#syncNow', function(done) {
run().then(() => {
expect(MockSyncEngine.syncOptions).to.deep.equal({
'test-collection': {}
});
done();
});
});
test('will report failures', function(done) {
expect(run(true)).to.be.rejectedWith('mock sync failure')
.and.notify(done);
});
test('will run only once at a time', function(done) {
var spy = this.sinon.spy(MockLazyLoader, 'load');
var firstSucceeded = false;
var secondFailed = false;
run().then(() => {
firstSucceeded = true;
if (secondFailed) {
done();
}
});
run().catch(err => {
expect(spy.withArgs('shared/js/sync/errors.js').called).to.equal(true);
expect(err.message).to.equal(ERROR_SYNC_APP_SYNC_IN_PROGRESS);
secondFailed = true;
if (firstSucceeded) {
done();
}
});
});
test('will run again after success', function(done) {
run().then(() => {
expect(run()).to.eventually.equal(undefined).and.notify(done);
});
});
test('will run again after failure', function(done) {
run(true).catch(err => {
expect(err.message).to.equal('mock sync failure');
return run();
}).then(done);
});
});
suite('IAC request', () => {
const IAC_EVENT = 'iac-gaia::sync::request';
var consoleErrorSpy;
var postMessageSpy;
var closeStub;
setup(function() {
consoleErrorSpy = sinon.spy(console, 'error');
postMessageSpy = sinon.spy(MockIACPort, 'postMessage');
closeStub = sinon.stub(window, 'close');
});
teardown(function() {
consoleErrorSpy.restore();
postMessageSpy.restore();
closeStub.restore();
});
test('invalid sync request', function() {
window.dispatchEvent(new CustomEvent(IAC_EVENT));
expect(consoleErrorSpy.called).to.equal(true);
expect(consoleErrorSpy.args[0][0]).to.equal('Wrong IAC request');
expect(postMessageSpy.called).to.equal(false);
expect(closeStub.called).to.equal(true);
});
test('Unknown IAC request', function() {
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
name: 'whatever',
id: id
}
}));
expect(consoleErrorSpy.called).to.equal(true);
expect(consoleErrorSpy.args[0][0]).to.equal('Unknown IAC request');
expect(postMessageSpy.called).to.equal(false);
expect(closeStub.called).to.equal(true);
});
test('successful sync request', function(done) {
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
name: 'sync',
id: id,
URL: 'url',
assertion: 'assertion',
keys: { kB: 'kB' },
collections: { collection: {} }
}
}));
setTimeout(function() {
expect(consoleErrorSpy.called).to.equal(false);
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0]).to.deep.equal({
id: id
});
expect(postMessageSpy.args[0][0].error).to.equal(undefined);
expect(closeStub.called).to.equal(true);
done();
});
});
test('failed sync request', function(done) {
var id = Date.now();
MockSyncEngine.shouldFail = true;
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
name: 'sync',
id: id,
URL: 'url',
assertion: 'assertion',
keys: { kB: 'kB' },
collections: { collection: {} }
}
}));
setTimeout(function() {
expect(consoleErrorSpy.called).to.equal(false);
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0].id).to.equal(id);
expect(postMessageSpy.args[0][0].error.message)
.to.equal('mock sync failure');
expect(closeStub.called).to.equal(true);
done();
});
});
test('Cancel sync request', function() {
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
name: 'cancel',
id: id
}
}));
expect(closeStub.called).to.equal(true);
expect(postMessageSpy.called).to.equal(false);
});
});
});

View file

@ -0,0 +1,385 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global
crypto,
CryptoKey,
FxSyncWebCrypto,
FxSyncWebCryptoFixture
*/
require('/shared/js/sync/crypto/stringconversion.js');
require('/shared/js/sync/crypto/keyderivation.js');
require('/shared/js/sync/crypto/fxsyncwebcrypto.js');
requireApp('sharedtest/test/unit/sync/fixtures/fxsyncwebcrypto.js');
suite('FxSyncWebCrypto', () => {
suite('constructor', () => {
test('creates an object with the right methods', () => {
const fswc = new FxSyncWebCrypto();
expect(fswc).to.be.an('object');
expect(fswc.setKeys).to.be.a('function');
expect(fswc.decrypt).to.be.a('function');
expect(fswc.encrypt).to.be.a('function');
});
});
suite('(private) importKeyBundle', () => {
test('imports two ArrayBuffers into a key bundle', done => {
var aesKeyBA = new Uint8Array(32);
var hmacKeyBA = new Uint8Array(32);
crypto.getRandomValues(aesKeyBA);
crypto.getRandomValues(hmacKeyBA);
FxSyncWebCrypto.importKeyBundle(aesKeyBA.buffer, hmacKeyBA.buffer)
.then(keyBundle => {
expect(keyBundle).to.be.an('object');
expect(keyBundle.aes).to.be.instanceOf(CryptoKey);
expect(keyBundle.hmac).to.be.instanceOf(CryptoKey);
done();
});
});
test('throws an error when you pass it undefined', done => {
FxSyncWebCrypto.importKeyBundle().then(() => {
expect(false, 'Should not reach here.');
}, err => {
expect(err.message).to.equal('Could not import key bundle.');
done();
});
});
});
suite('setKeys', () => {
test('populates mainSyncKey and defaultDecryptionKey correctly',
done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
fswc.setKeys(fixture.kB, fixture.cryptoKeys).then(() => {
expect(fswc.bulkKeyBundle).to.be.an('object');
expect(fswc.bulkKeyBundle.default).to.be.instanceof(Array);
expect(fswc.bulkKeyBundle.defaultAsKeyBundle).to.be.an('object');
expect(fswc.bulkKeyBundle.defaultAsKeyBundle.aes).to.be.
instanceof(CryptoKey);
expect(fswc.bulkKeyBundle.defaultAsKeyBundle.hmac).to.be.
instanceof(CryptoKey);
done();
});
});
test('rejects promise if cryptoKeys hmac is wrong', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const cryptoKeysWrong = JSON.parse(JSON.stringify(fixture.cryptoKeys));
cryptoKeysWrong.hmac = 'deadbeef';
const promise = fswc.setKeys(fixture.kB, cryptoKeysWrong);
expect(promise).to.be.rejectedWith(
'SyncKeys hmac could not be verified with current main key').and.
notify(done);
});
test('rejects promise if cryptoKeys ciphertext is wrong (1)', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const cryptoKeysWrong = JSON.parse(JSON.stringify(fixture.cryptoKeys));
cryptoKeysWrong.ciphertext = 'deadbeef';
const promise = fswc.setKeys(fixture.kB, cryptoKeysWrong);
expect(promise).to.be.rejectedWith(
'SyncKeys hmac could not be verified with current main key').and.
notify(done);
});
test('rejects promise if cryptoKeys ciphertext is wrong (2)', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const cryptoKeysWrong = JSON.parse(JSON.stringify(fixture.cryptoKeys));
cryptoKeysWrong.ciphertext = 'deadbee';
const promise = fswc.setKeys(fixture.kB, cryptoKeysWrong);
expect(promise).to.be.rejectedWith(
'Could not parse ciphertext as a base64 string').and.
notify(done);
});
test('rejects promise if cryptoKeys IV is wrong', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const cryptoKeysWrong = JSON.parse(JSON.stringify(fixture.cryptoKeys));
cryptoKeysWrong.IV = 'deadbeef';
const promise = fswc.setKeys(fixture.kB, cryptoKeysWrong);
expect(promise).to.be.rejectedWith(
'Could not decrypt crypto keys using AES part of stretched kB key').
and.notify(done);
});
});
suite('decrypt', () => {
test('can verify and decrypt a record', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
fswc.setKeys(fixture.kB, fixture.cryptoKeys).then(() => {
return fswc.decrypt(fixture.historyEntryEnc.payload,
fixture.historyEntryEnc.collectionName);
}).then(decryptedRecord => {
expect(decryptedRecord).to.be.an('object');
expect(decryptedRecord.id).to.equal(fixture.historyEntryDec.payload.id);
expect(decryptedRecord.histUri).to.equal(
fixture.historyEntryDec.payload.histUri);
expect(decryptedRecord.title).to.equal(
fixture.historyEntryDec.payload.title);
expect(decryptedRecord.visits).to.be.instanceof(Array);
expect(decryptedRecord.visits.length).to.equal(1);
expect(decryptedRecord.visits[0].date).to.equal(
fixture.historyEntryDec.payload.visits[0].date);
expect(decryptedRecord.visits[0].type).to.equal(
fixture.historyEntryDec.payload.visits[0].type);
done();
});
});
test('rejects promise if collectionName is not a string', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(fixture.historyEntryEnc.payload, 5);
});
expect(promise).to.be.rejectedWith('collectionName is not a string').and.
notify(done);
});
test('rejects promise if payload is not an object', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).then(
() => {
return fswc.decrypt(5, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith('PayloadStrings is not an object').and.
notify(done);
});
test('rejects promise if payload.ciphertext is not a string',
done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: 5,
IV: fixture.historyEntryEnc.payload.IV,
hmac: fixture.historyEntryEnc.payload.hmac
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(`Could not parse ciphertext as a base6\
4 string`).and.notify(done);
});
test('rejects promise if payload.ciphertext is not a Base64 string',
done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: 'foo',
IV: fixture.historyEntryEnc.payload.IV,
hmac: fixture.historyEntryEnc.payload.hmac
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(`Could not parse ciphertext as a base6\
4 string`).and.notify(done);
});
test('rejects promise if payload.IV is not a string', done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: fixture.historyEntryEnc.payload.ciphertext,
IV: null,
hmac: fixture.historyEntryEnc.payload.hmac
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).then(
() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(`Could not parse IV as a base64 string\
`).and.notify(done);
});
test('rejects promise if payload.IV is not a Base64 string',
done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: fixture.historyEntryEnc.payload.ciphertext,
IV: 'foo',
hmac: fixture.historyEntryEnc.payload.hmac
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(`Could not parse IV as a base64 string\
`).and.notify(done);
});
test('rejects promise if payload.hmac is not a string', done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: fixture.historyEntryEnc.payload.ciphertext,
IV: fixture.historyEntryEnc.payload.IV,
hmac: { foo: 'bar' }
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(
'Could not parse hmac as a hex string').and.notify(done);
});
test('rejects promise if payload.hmac is not a hex string (1)',
done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: fixture.historyEntryEnc.payload.ciphertext,
IV: fixture.historyEntryEnc.payload.IV,
hmac: 'fee' // should detect odd number of chars
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(
'Could not parse hmac as a hex string').and.notify(done);
});
test('rejects promise if payload.hmac is not a hex string (2)',
done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: fixture.historyEntryEnc.payload.ciphertext,
IV: fixture.historyEntryEnc.payload.IV,
hmac: 'fooz' // should detect verification failure due to non-hex string
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(
'Record verification failed with current hmac key for history').and.
notify(done);
});
test('rejects promise if record hmac is wrong', done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: fixture.historyEntryEnc.payload.ciphertext,
IV: fixture.historyEntryEnc.payload.IV,
hmac: 'deadbeef'
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(`Record verification failed with curre\
nt hmac key for history`).and.notify(done);
});
test('rejects promise if record ciphertext is wrong', done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: 'deadbeef',
IV: fixture.historyEntryEnc.payload.IV,
hmac: fixture.historyEntryEnc.payload.hmac
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(`Record verification failed with curre\
nt hmac key for history`).and.notify(done);
});
test('rejects promise if record IV is wrong', done => {
const fixture = FxSyncWebCryptoFixture;
const payload = {
ciphertext: fixture.historyEntryEnc.payload.ciphertext,
IV: 'deadbeef',
hmac: fixture.historyEntryEnc.payload.hmac
};
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.decrypt(payload, fixture.historyEntryEnc.collectionName);
});
expect(promise).to.be.rejectedWith(`Could not decrypt record using AES pa\
rt of key bundle for collection history`).and.notify(done);
});
});
suite('encrypt', () => {
test('can sign and encrypt a record', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
fswc.setKeys(fixture.kB, fixture.cryptoKeys).then(() => {
return fswc.encrypt(fixture.historyEntryDec.payload,
fixture.historyEntryDec.collectionName);
}).then(encryptedRecord => {
// We cannot predict how the payload will be JSON-stringified into a
// cleartext, so instead, see if we can at least decrypt it again
return fswc.decrypt(encryptedRecord,
fixture.historyEntryDec.collectionName);
}).then(redecryptedRecord => {
expect(redecryptedRecord).to.deep.equal(
fixture.historyEntryDec.payload);
done();
});
});
test('rejects promise if record is not an object', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.encrypt('boo', fixture.historyEntryDec.collectionName);
});
expect(promise).to.be.rejectedWith('Record should be an object').and.
notify(done);
});
test('rejects promise if record cannot be JSON-stringified',
done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
const myObject = {};
myObject.cyclicReference = myObject;
return fswc.encrypt(myObject, fixture.historyEntryDec.collectionName);
});
expect(promise).to.be.rejectedWith('Record cannot be JSON-stringified').
and.notify(done);
});
test('rejects promise if collectionName is not a string', done => {
const fixture = FxSyncWebCryptoFixture;
const fswc = new FxSyncWebCrypto();
const promise = fswc.setKeys(fixture.kB, fixture.cryptoKeys).
then(() => {
return fswc.encrypt(fixture.historyEntryDec.payload, 5);
});
expect(promise).to.be.rejectedWith('collectionName is not a string').
and.notify(done);
});
});
});

View file

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global
KeyDerivation,
FxSyncWebCryptoFixture
*/
require('/shared/js/sync/crypto/stringconversion.js');
require('/shared/js/sync/crypto/keyderivation.js');
require('/shared/js/sync/crypto/main.js');
requireApp('sharedtest/test/unit/sync/fixtures/fxsyncwebcrypto.js');
suite('hkdf', () => {
suite('hkdf', () => {
test('can calculate a hkdf result correctly', done => {
const fixture = FxSyncWebCryptoFixture.hkdf;
KeyDerivation.hkdf(fixture.kB, fixture.infoStr, new Uint8Array(64), 64).
then(function(bytes) {
var hex = '';
for (var i=0; i <bytes.length; ++i) {
const zeropad = (bytes[i] < 0x10) ? '0' : '';
hex += zeropad + bytes[i].toString(16);
}
expect(hex).to.equal(fixture.outputHex);
done();
});
});
test('rejects its promise if ikm is wrong', done => {
const fixture = FxSyncWebCryptoFixture.hkdf;
fixture.kB = 'foo';
const promise = KeyDerivation.hkdf(fixture.kB, fixture.infoStr,
new Uint8Array(64), 64);
expect(promise).to.be.rejectedWith(Error).and.notify(done);
});
test('rejects its promise if info is wrong', done => {
const fixture = FxSyncWebCryptoFixture.hkdf;
fixture.kB = 'foo';
const promise = KeyDerivation.hkdf(fixture.kB, fixture.infoStr,
new Uint8Array(64), 64);
expect(promise).to.be.rejectedWith(Error).and.notify(done);
});
test('rejects its promise if salt is wrong', done => {
const fixture = FxSyncWebCryptoFixture.hkdf;
fixture.kB = 'foo';
const promise = KeyDerivation.hkdf(fixture.kB, fixture.infoStr,
new Uint8Array(64), 64);
expect(promise).to.be.rejectedWith(Error).and.notify(done);
});
test('rejects its promise if length is wrong', done => {
const fixture = FxSyncWebCryptoFixture.hkdf;
const promise = KeyDerivation.hkdf(fixture.kB, fixture.infoStr,
new Uint8Array(64), 32);
expect(promise).to.be.rejectedWith(Error).and.notify(done);
});
});
});

View file

@ -0,0 +1,137 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global StringConversion */
require('/shared/js/sync/crypto/stringconversion.js');
require('/shared/js/sync/crypto/keyderivation.js');
require('/shared/js/sync/crypto/main.js');
requireApp('sharedtest/test/unit/sync/fixtures/fxsyncwebcrypto.js');
suite('utils', () => {
suite('rawStringToUint8Array', () => {
test('converts a ASCII string to a ByteArray', () => {
const ba = StringConversion.rawStringToUint8Array('\x00\xEA\x80\xFF');
expect(ba).to.be.instanceOf(Uint8Array);
expect(ba.length).to.equal(4);
expect(ba[0]).to.equal(0x00);
expect(ba[1]).to.equal(0xEA);
expect(ba[2]).to.equal(0x80);
expect(ba[3]).to.equal(0xFF);
});
test('throws an error when input is not a string', () => {
expect(StringConversion.rawStringToUint8Array.bind(undefined, 5)).to.
throw(Error);
});
});
suite('stringToUtf8Uint8Array', () => {
test('converts a string to a UTF-8 ByteArray', () => {
var ba = StringConversion.stringToUtf8Uint8Array('€');
expect(ba).to.be.instanceOf(Uint8Array);
expect(ba.length).to.equal(3);
expect(ba[0]).to.equal(0xe2);
expect(ba[1]).to.equal(0x82);
expect(ba[2]).to.equal(0xac);
});
test('throws an error when input is not a string', () => {
expect(StringConversion.stringToUtf8Uint8Array.bind(undefined, 5)).to.
throw(Error);
});
});
suite('base64StringToUint8Array', () => {
test('converts a Base64 string to a ByteArray', () => {
const ba = StringConversion.base64StringToUint8Array('Af9=');
expect(ba).to.be.instanceOf(Uint8Array);
expect(ba.length).to.equal(2);
expect(ba[0]).to.equal(1);
expect(ba[1]).to.equal(255);
});
test('throws an error when input is not a Base64 string', () => {
expect(StringConversion.base64StringToUint8Array.
bind(undefined, 'hello')).to.throw(Error);
});
});
suite('hexStringToUint8Array', () => {
test('converts a hex string to a ByteArray', () => {
const ba = StringConversion.hexStringToUint8Array('af93');
expect(ba).to.be.instanceOf(Uint8Array);
expect(ba.length).to.equal(2);
expect(ba[0]).to.equal(175);
expect(ba[1]).to.equal(147);
});
test('throws an error when input is not a hex string', () => {
expect(StringConversion.hexStringToUint8Array.bind(undefined, 'hello')).
to.throw(Error);
});
});
suite('uint8ArrayToBase64String', () => {
test('converts a Uint8Array to a Base64', () => {
const ba = StringConversion.hexStringToUint8Array('01ff');
const str = StringConversion.uint8ArrayToBase64String(ba);
expect(str).to.be.a('string');
expect(str).to.equal('Af8=');
});
test('throws an error when input is not a Uint8Array', () => {
expect(StringConversion.uint8ArrayToBase64String.bind(undefined,
new ArrayBuffer(2))).
to.throw(Error);
});
});
suite('uint8ArrayToHexString', () => {
test('converts a Uint8Array to a Base64', () => {
const ba = StringConversion.base64StringToUint8Array('Af8=');
const str = StringConversion.uint8ArrayToHexString(ba);
expect(str).to.be.a('string');
expect(str).to.equal('01ff');
});
test('throws an error when input is not an Uint8Array', () => {
expect(StringConversion.uint8ArrayToHexString.bind(undefined,
new ArrayBuffer(2))).to.
throw(Error);
});
});
suite('utf8Uint8ArrayToString', () => {
test('converts an UTF-8 Uint8Array to a string', () => {
var array = new Uint8Array(3);
array[0] = 0xe2;
array[1] = 0x82;
array[2] = 0xac;
var str = StringConversion.utf8Uint8ArrayToString(array);
expect(str).to.be.a('string');
expect(str).to.equal('€');
});
test('throws an error when input is not an Uint8Array', () => {
expect(StringConversion.utf8Uint8ArrayToString.bind(
undefined,
new ArrayBuffer(2))).to.throw(Error);
});
});
suite('arrayBufferToBase64String', () => {
test('converts an ArrayBuffer to a Base64', () => {
const ba = StringConversion.hexStringToUint8Array('01ff');
const str = StringConversion.arrayBufferToBase64String(ba.buffer);
expect(str).to.be.a('string');
expect(str).to.equal('Af8=');
});
test('throws an error when input is not an ArrayBuffer', () => {
expect(StringConversion.arrayBufferToBase64String.bind(
undefined,
new Uint8Array(2))).to.throw(Error);
});
});
suite('arrayBufferToHexString', () => {
test('converts an ArrayBuffer to a Base64', () => {
const ba = StringConversion.base64StringToUint8Array('Af8=');
const str = StringConversion.arrayBufferToHexString(ba.buffer);
expect(str).to.be.a('string');
expect(str).to.equal('01ff');
});
test('throws an error when input is not an ArrayBuffer', () => {
expect(StringConversion.arrayBufferToHexString.bind(undefined,
new Uint8Array(2))).
to.throw(Error);
});
});
});

View file

@ -0,0 +1,18 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* exported
BootstrapFixture
*/
var BootstrapFixture = {
mainScripts: [
'shared/js/sync/crypto/stringconversion.js',
'shared/js/sync/crypto/keyderivation.js',
'shared/js/sync/crypto/fxsyncwebcrypto.js',
'shared/js/sync/ext/kinto.min.js',
'shared/js/sync/engine.js'
]
};

View file

@ -0,0 +1,64 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* exported
FxSyncWebCryptoFixture
*/
const FxSyncWebCryptoFixture = (() => {
// NB: This data is from an FxSync account, freshly created for this purpose,
// and then synced once using FxDesktop with a fresh empty Firefox profile:
const hex2ba = (hexStr) => {
var numBytes = hexStr.length / 2;
var byteArray = new Uint8Array(numBytes);
for (var i = 0; i < numBytes; i++) {
byteArray[i] = parseInt(hexStr.substr(i * 2, 2), 16);
}
return byteArray;
};
return {
kB: '85c4f8c1d8e3e2186824c127af786891dd03c6e05b1b45f28f7181211bf2affb',
hkdf: {
kB: hex2ba(`85c4f8c1d8e3e2186824c127af786891dd03c6e05b1b45f28f7181211bf2a\
ffb`),
infoStr: hex2ba(
`6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f6f6c647379\
6e63`), // 'identity.mozilla.com/picl/v1/oldsync'
outputHex: `d63eb2610f1e5edc21b37fed22b93b0f9ea379840fb5c3c5eb95991b9b3bf\
ab69cbabfda0762bdf0b27431d81960c8cf5dab7d482901213a5a1b003eb0081ed0`
},
cryptoKeys: {
ciphertext: `PP5yNUYwJJoLcsL5o85i6RZfvanYDrwtChDD/LdKTZ8JOLubZ9DyRv3HMetS\
kbhL3HLvVm/FJ1Z4F2Z6IKQCxAc5dNnLsBIUUxhOHLbT0x9/jfnqZ8fLtlbkogI3ZlNvbc8iUF1aX+b\
oe0Pv43vM0VvzxrnJDYzZ2a6jm9nbzUn0ldV9sv6vuvGHE6dANnRkZ3wA/q0q8UvjdwpzXBixAw==`,
IV: 'FmosM+XBNy81/9oEAgI4Uw==',
hmac: '01a816e4577c6cf3f97b66b4382d0a3e7e9178c75a3d38ed9ac8ad6397c2ecce'
},
historyEntryEnc: {
payload: {
ciphertext: `o6aTLJdfhW4nz1hhnE2ZkvPb9N9lP6JEez+oufMGWIVAgQFaEMB3r/Oboc\
vRti2dwe7yo2qly9d8aILHuOTgerFZuC5Bu5b0KmvHnVBzLxcPcqF20mFY2iQIApEI82Cu2VfwZuX4t\
nCLMlxfllEzWvQPWai8e5OKmThdzKdHAmaS+sHBj5FjTe5mxuX4U1c8EJUS3a4fSV5NY0EolUkSuMLw\
9sXu++7Uiu8OJH/JNWis6getxKcE+J61iaHJO/raiPyYIB/tSi3yrIjb0FqLeG8tXCrrZ9VD/2AWPmJ\
5/6bpt80dSXgke4qvKePNoCYp`,
IV: 'pP/NMhj5fwREqes4I9H0tw==',
hmac: '97d16085c1bf347cb7530342ccf85647609be4a8f55d2b8fe409b1756240c06b'
},
collectionName: 'history'
},
historyEntryDec: {
payload: {
id: '_9sCUbahs0ay',
histUri: `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refer\
ence/Global_Objects/Object/proto`,
title: 'Object.prototype.__proto__ € - JavaScript | MDN',
visits:[ { date: 1439366063808983, type:1 } ]
},
collectionName: 'history'
}
};
})();

View file

@ -0,0 +1,652 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global
AdapterMock
*/
/* exported
SynctoServerFixture
*/
var SynctoServerFixture = (function() {
var syncEngineOptions = {
URL: 'http://localhost:8000/v1/',
assertion: 'test-assertion-mock',
// Taken from https://bugzilla.mozilla.org/show_bug.cgi?id=959919#c13.
kB: 'fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d',
adapters: {
history: (typeof AdapterMock === 'undefined' ? null : AdapterMock())
}
};
// Taken from https://bugzilla.mozilla.org/show_bug.cgi?id=959919#c13.
var xClientState = '6ae94683571c7a7c54dab4700aa3995f';
var metaGlobalPayloadWithHistoryEngine = JSON.stringify({
syncID: 'NOuEmrZxVWxl',
storageVersion: 5,
declined:[],
engines: {
clients: { version: 1, syncID: '-qRIYq3pRFaF' },
prefs: { version:2, syncID: 'J2d8YxLBQ68M' },
passwords: { version:1, syncID: 'y3sQX0uYGXwz' },
tabs: { version: 1, syncID: 'MGdVuWFjTRpP' },
bookmarks: { version: 2, syncID: 'OmUGbrBTvZbn' },
addons: { version: 1, syncID: '90lUL4MPuhpx' },
forms: { version:1, syncID: 'Q_mWdmGZtuX9' },
history: { version: 1, syncID: '2_MOTXJfjA9Q' }
}
});
var metaGlobalPayloadWithoutHistoryEngine = JSON.stringify({
syncID: 'NOuEmrZxVWxl',
storageVersion: 5,
declined:[],
engines: {
clients: { version: 1, syncID: '-qRIYq3pRFaF' },
prefs: { version:2, syncID: 'J2d8YxLBQ68M' },
passwords: { version:1, syncID: 'y3sQX0uYGXwz' },
tabs: { version: 1, syncID: 'MGdVuWFjTRpP' },
bookmarks: { version: 2, syncID: 'OmUGbrBTvZbn' },
addons: { version: 1, syncID: '90lUL4MPuhpx' },
forms: { version:1, syncID: 'Q_mWdmGZtuX9' }
}
});
var remoteData = {
meta: {
id: 'global',
last_modified: 1234567890123,
payload: metaGlobalPayloadWithHistoryEngine
},
crypto: {
id: 'keys',
last_modified: 1234567890123,
payload: JSON.stringify({
ciphertext: `PP5yNUYwJJoLcsL5o85i6RZfvanYDrwtChDD/LdKTZ8JOLubZ9DyRv3HMe\
tSkbhL3HLvVm/FJ1Z4F2Z6IKQCxAc5dNnLsBIUUxhOHLbT0x9/jfnqZ8fLtlbkogI3ZlNvbc8iUF1aX\
+boe0Pv43vM0VvzxrnJDYzZ2a6jm9nbzUn0ldV9sv6vuvGHE6dANnRkZ3wA/q0q8UvjdwpzXBixAw=\
=`,
IV: 'FmosM+XBNy81/9oEAgI4Uw==',
hmac: '01a816e4577c6cf3f97b66b4382d0a3e7e9178c75a3d38ed9ac8ad6397c2ecce'
})
},
history: {
id: '_9sCUbahs0ay',
last_modified: 1234567890123,
payload: JSON.stringify({
ciphertext: `o6aTLJdfhW4nz1hhnE2ZkvPb9N9lP6JEez+oufMGWIVAgQFaEMB3r/Oboc\
vRti2dwe7yo2qly9d8aILHuOTgerFZuC5Bu5b0KmvHnVBzLxcPcqF20mFY2iQIApEI82Cu2VfwZuX4t\
nCLMlxfllEzWvQPWai8e5OKmThdzKdHAmaS+sHBj5FjTe5mxuX4U1c8EJUS3a4fSV5NY0EolUkSuMLw\
9sXu++7Uiu8OJH/JNWis6getxKcE+J61iaHJO/raiPyYIB/tSi3yrIjb0FqLeG8tXCrrZ9VD/2AWPmJ\
5/6bpt80dSXgke4qvKePNoCYp`,
IV: 'pP/NMhj5fwREqes4I9H0tw==',
hmac: '97d16085c1bf347cb7530342ccf85647609be4a8f55d2b8fe409b1756240c06b'
})
},
'wrong-payload-meta': {
id: 'global',
last_modified: 1234567890123,
payload: 'whoopsie!'
},
'wrong-payload-crypto': {
id: 'keys',
last_modified: 1234567890123,
payload: 'whoopsie!'
},
'wrong-payload-history': {
id: '_9sCUbahs0ay',
last_modified: 1234567890123,
payload: 'whoopsie!'
},
'wrong-ciphertext': {
id: '_9sCUbahs0ay',
last_modified: 1234567890123,
payload: JSON.stringify({
ciphertext: 'deadbeef',
IV: 'kXL3hb11ltD+Jl0YFk+PlQ==',
hmac: 'cb727efe7a3f0307921cecbd1a97c03f06a4d75c42026089494d84fcf92dbff9'
})
},
'wrong-id': {
id: {},
last_modified: 1234567890123,
payload: JSON.stringify({
ciphertext: `o/VpkqMj1tlT8t2youwsS2FgvQeonoHxqjGsRTu1+4swfyBq/QsnKfgOOM\
mDIXZiPC3hOCNUlf/NtQiEe55hzJZEKLBshaLfXotai6KrprwrmykfiXnwn73n+nYNs8BXL5awDHoaJ\
ToyFgF4PYokl7mwN7YC2xFiPgwO7Z2u/8r5RfnPV9MoafqvlvUkW+Tqs+QHeHS/iuSA0P2h/j5ynt9v\
4xDWLVfEMce0KOKHQ5Qj7BmEPAieWP1trkkDmTdVi2euWrs+fuG4C6PgY4A2j2DbNLVIloqpDVkqM2f\
gh0YOM9L2NC/uiKEb1Ynr2Fos`,
IV: 'kXL3hb11ltD+Jl0YFk+PlQ==',
hmac: 'cb727efe7a3f0307921cecbd1a97c03f06a4d75c42026089494d84fcf92dbff9'
})
}
};
var record = {
'places': {
id: 'places',
sortindex: 1000000,
last_modified: 1444990319980,
payload: `{"ciphertext":"oDeFFF9Mhq2ASsACJpTOroCT8R9r7DHJjv0RJI4tI4/kc7tW\
TGrTMKCXBQu/3olgLKjbue4sUDHVmwlcwdMMbwis1Pt2FrChm1+kmmzKrndKsgDKqf0d2jfjSIeeO8Z\
fS0mSYFzQifI63khTftZr1fWkz0ajuwzlajXTKKcwYhruXaZE8YCoxu/cXoUtbKtgAxMCY6heNB1VwM\
QrWuXlkFWxOaMJZC9Qot93ef6xH84=","IV":"9nq13kP7b59dCRLLiCR8Mw==","hmac":"35133a2\
00238f6476f5b6305130cff76fdf47d66742ddffa88a8f0fefcb7d24b"}`
},
'unfiled': {
id: 'unfiled',
sortindex: 1000000,
last_modified: 1443799918500,
payload: `{"ciphertext":"+5oLXV7WdW9iadskJlISpQgOUAFABGC6BlUoJx/Lj/Ys4JkB\
T7Ga/9Ufv5ToPxsi8pDbk0Cy/neqV0g3Dj82xRqqADhaWTIbrNMAvDIE3navdtXafhxYKEcwRa8+nII\
uRc8si1c90cvdqCEKUIhhZUctqfNNJRUFbjhBMNEHQ0F6f02CL5RF2DyOt8Lg83Ui4P2CBiFkyIJRA9\
1B17K55P41w7Y4AUWUPCZHBIERzY0=","IV":"7otPN3W23riiDnXgSsxCfg==","hmac":"82d1f8e\
54c5d0ab09e31e6e7beaf45ca8d8ffb2d924614eafaf50732500408f0"}`
},
'-Z5mm8AxeIkq': {
id: '-Z5mm8AxeIkq',
sortindex: 175,
last_modified: 1443799918500,
payload: `{"ciphertext":"+gEPw/xy2oXxirZHU57m2J4Q7jhtiEZrxxjvO+6mqh+LWUVp\
Lj2sER7vFt8ioA3OlPWcBpUertzgWArZ4zLSrvOiMrDWX651R7Oi//+ksGGEBkOVHzU7NKldDTrQtO1\
slUaLVMCEto+KD9lQLbFBLo2r3yvB7wslOFDQqbp5cLn++My5bK2YML2bzVgyU2Ug0il6aN/SQrSKr+\
BAxMlg4V9vcJ+uJ/R0pdTSUx3/g3l8U1T6e4sqirOdsXMbckyMjG9scr36owBQqP6fIp97DCVQTRrhR\
nJhSk/v+9sSg5XKkDdx3ZjFfodNjHfmoafvdpDIAVPUlqSRV9WnPuFDVp2/j1OZ3hVac5GxIjwMnweE\
3+9S5oYdkjrtg0dA/YJ3ZXGyX21b53ZVACYV7E82Pal+pdD5iMxHn3zd4LYimwDBumX06pssJYHOhMx\
LIS6cG0U0HHsTuDFXR1FAWbMWpZeR1D/BOizW1KcOSacin4GV0zd2aDCDTk4xKE9Wd30q2o8HnLjNUU\
HO/St1osonSnj2hHx+YiHt4IGOA4481hu3NzXD1mwtutsSj/yvRrv/VCaMDVqY2MCTxXP5nfUEY2ohO\
nlJ7dOCAQgq8vjBROay+nscOmLEk8gNAygneFYBJA72YGy8HdLzoZcWcqoaed63m6TtqaxxWc+3CINr\
tZri9v/hK7qoVnYiVD8D8KVL","IV":"Uw4HT5/LP2B3Qq9goOai7g==","hmac":"52dd9f3b60870\
ac74b4d0b642dc34b7a872bf0a1bb2b410ba874373c68c88d5e"}`
},
'toolbar': {
id: 'toolbar',
sortindex: 1000000,
last_modified: 1444990337260,
payload: `{"ciphertext":"x8CVxJRSPsTsc5zhslndISQnnZvNkVu6g4HbVRlh7L966I4Z\
74bT0me1twfGN8Oowwm9nmg70wQQJwNIur+DYZEIXXCD9koq7LIsD/vj9c33gWYB6MucS/cI6cg7RMx\
NM7bieq01K8c10Sdl92MHH3WHgmqFo3AXXi44qpHnDC6mhMriAPdFIm4HdgeOURmmrykqMDUSatYVwE\
4LhXD61inIz0nXmygUJhVYNV707SwGpg/YfUCAyNUuUwlXBWjMVdFojPCHmgRy0vYRPnX7XiHE/Gw4W\
ViBuRG35yNHM1WeJIsMbNGwQIZ1w61mT4Y8Tdy/qTifj9ixz8B2VXbNaAtURgWvCDwR/P3PMkTNYXs=\
","IV":"wwGivoJN0JzlN43cphN8Vg==","hmac":"4b1520e74900c9cb3952778928fce0b09f853\
c1e8da78779ca9a61bae4e91ea0"}`
},
'5AiZMklaq0fL': {
id: '5AiZMklaq0fL',
sortindex: 1000000,
last_modified: 1444990361000,
payload: `{"ciphertext":"qr2Hq4V9bMi8ZrmTr19P5QziQj4CcDx6gI4qJuEMLgVmuci4\
Wcrz8uQiMtstyWFh0lK7WZlTLMdqwKnWbR/cqIR3tnfBrgP/IpCupnPgYcNBQR4ubtXBMO4+rRXz6kE\
pMlkDRuBJOXI3Uv4fmuy9fmCsyHjnXWLMoMPiS7ynlB82CVFLlTo4RGXs65+dK024xE5hnp0IE+sBHG\
hFtFWGyP2sJyY10c2f8SyGTdBY8o8=","IV":"S784aq1OWKzIR6QNY8Tqdg==","hmac":"623cd30\
5ffe2a1915e0a3d30551c36dd1db9cc7a1a4e91a8b4767451001cfa4e"}`
},
'6q8JaloClXcH': {
id: '6q8JaloClXcH',
sortindex: 1000000,
last_modified: 1444990406800,
payload: `{"ciphertext":"qn83DQj8GkLmg1g8j1V7dahpxX8ejxZ0c8s7l/Gqlr7ioqXG\
7r8C5YfMcPHpgLOJSFKIC2+a/t7mz391Bwmye4BPRPx1St1+ZRGDapP6X6IInm8kYdiXPeeAoC0zExw\
ZCaQBTWUI8x+l0sGJR3e2V84G6VIISkTxm2h6ywjcAYfNifnjyLIds1gxakGQ0gTYJ5gPXnQpQdBp4O\
ytRA1R721FWluxM3BYQ72UJFt8hMw=","IV":"UckfGiIKrwL0NBuJ4vpoBw==","hmac":"536e341\
32c8dcb980a0050597310f36a306d288288068d7782001649fa1f9609"}`
},
'msopyVa0ckP_': {
id: 'msopyVa0ckP_',
sortindex: 140,
last_modified: 1444990424710,
payload: `{"ciphertext":"/LclynYgBvPDqO0Ch2ylAZufLr+t42vYA37CilJMCxYnjCYY\
DzlMz1BOXooksM0PrZ+5vgLFZOl1sgjH4A/AS397dvn9EYugyNJgAm+oYAPfeDswXtp9xQ757oeC0zi\
AXHuq55bT3ClCv/yCWcK4HmL0l9MEePmXxOcR/QlBMq+Qn0iDNvUl9rPTeJw70G6MgJbSHscz9GXRqs\
Y/34BfxYV+aDdOGx2Wp877hK+EgrWes+yFUt+TjsuR8xiJk+SwASggEfw4GMDe4bo5EG2uDQTQCUww5\
qh32hg6OmE+DIj1wEBSCjw6yf4Yni32tn8vB1c4bo2xzV4KOXN8qfRFHeYCtKebdCNLHk7NwxEwDYM=\
","IV":"kbyuFrOfffGftlLAD/4Skg==","hmac":"d1ae13411f3577c6e62b8670b4b8a40365fa4\
7598368f141276c38936088fce3"}`
},
'AGyc2XZiNuVL': {
id: 'AGyc2XZiNuVL',
sortindex: 140,
last_modified: 1444990424710,
payload: `{"ciphertext":"UHL8oLNqhrxmxP3w/LiOUDe3SpuwwA4AbqIYX/faT5XaaWN\
Dyfw8ZhxuKaQfLyYa+mkdQRfYckR7eyl9/S352vrEU/GdR9kQ+y6tyyrYUfKHbPAZLHuRrdu8IPcVt+\
8SMn1Lao9fKAoutHM/oSqlQV9d1ZPtxcIJcCgnJmTR1nq//P5mj9OwGbXwKAh8JboS8wbS42ryQ/Llh\
oXf4HarDnePZsvLjNOmF0KH+VvHBfszTQDFoMPqgiDhnPjJunNU2EXI+WclfCjyUP6BhU5dqfUfWq1H\
qj4y6BV7Ajav8nyn7U7UvM8q3AnaEQSFM90nJeVx/e0+0EC7h1mZEuXhirRWFpReGxYYJ0lpDQ6OhPc\
=","IV":"xJ3PE16G3cX2Y20YPBTlog==","hmac":"f0cc1a9b065ad3dd7856c95cdd63d8e48904\
3f5dfa279b34afb99c9aaf4cf08f"}`
}
};
var bookmarksEntriesDec = {
'places': {
id: 'places',
type: 'folder',
title: '',
description: null,
children: ['toolbar_____', 'unfiled_____'],
parentid: '9sS1iwhx4w0q'
},
'unfiled': {
id: 'unfiled',
type: 'folder',
parentName: '',
title: 'Unsorted Bookmarks',
description: null,
children: ['-Z5mm8AxeIkq'],
parentid: 'places'
},
'-Z5mm8AxeIkq': {
id: '-Z5mm8AxeIkq',
type: 'bookmark',
title: 'HiNet首頁 -中華電信HiNet網路服務入口',
parentName: 'Unsorted Bookmarks',
bmkUri: 'http://www.hinet.net/',
tags: [],
keyword: null,
description: `中華電信HiNet網路服務入口,台灣最大ISP,提供寬頻上網,光世代(光纖\
)、ADSL服務,及信箱、新聞、hichannel、Xuite等生活娛樂資訊。並延伸至企業、政府、資安、雲\
端網絡應用,是您上網的第一站。`,
loadInSidebar: false,
parentid: 'unfiled'
},
'toolbar': {
id: 'toolbar',
type: 'folder',
parentName: '',
title: 'Bookmarks Toolbar',
description: `Add bookmarks to this folder to see them displayed on the B\
ookmarks Toolbar`,
children: [
'QH1IawO-JKtz',
'olmlydfUhcP7',
'5AiZMklaq0fL',
'6q8JaloClXcH'
],
parentid: 'places'
},
'5AiZMklaq0fL': {
id: '5AiZMklaq0fL',
type: 'folder',
parentName: 'Bookmarks Toolbar',
title: 'folder toolbar 1',
description: null,
children: ['msopyVa0ckP_'],
parentid: 'toolbar'
},
'6q8JaloClXcH': {
id: '6q8JaloClXcH',
type: 'folder',
parentName: 'Bookmarks Toolbar',
title: 'folder toolbar 2',
description: null,
children: ['AGyc2XZiNuVL'],
parentid: 'toolbar'
},
'msopyVa0ckP_': {
id: 'msopyVa0ckP_',
type: 'bookmark',
title: 'Redecentralize in folder toolbar 1',
parentName: 'folder toolbar 1',
bmkUri: 'https://redecentralize.org/',
tags: ['nicer'],
keyword: 'cooler',
description: 'yeah',
loadInSidebar: false,
parentid: '5AiZMklaq0fL'
},
'AGyc2XZiNuVL': {
id:'AGyc2XZiNuVL',
type: 'bookmark',
title: 'Redecentralize in folder toolbar 2',
parentName: 'folder toolbar 2',
bmkUri: 'https://redecentralize.org/',
tags: ['nicer'],
keyword: 'cooler',
description: 'yeaher',
loadInSidebar: true,
parentid: '6q8JaloClXcH'
}
};
var bookmarksExpectedDataStore = {
'https://redecentralize.org/': {
id: 'https://redecentralize.org/',
url: 'https://redecentralize.org/',
name: 'Redecentralize in folder toolbar 2',
type: 'url',
iconable: false,
icon: '',
createdLocally: false,
fxsyncRecords: {
msopyVa0ckP_: {
id: 'msopyVa0ckP_',
type: 'bookmark',
title: 'Redecentralize in folder toolbar 1',
parentName: 'folder toolbar 1',
bmkUri: 'https://redecentralize.org/',
tags: [ 'nicer' ],
keyword: 'cooler',
description: 'yeah',
loadInSidebar: false,
parentid: '5AiZMklaq0fL',
timestamp: 1444990424710
},
AGyc2XZiNuVL: {
id: 'AGyc2XZiNuVL',
type: 'bookmark',
title: 'Redecentralize in folder toolbar 2',
parentName: 'folder toolbar 2',
bmkUri: 'https://redecentralize.org/',
tags: [ 'nicer' ],
keyword: 'cooler',
description: 'yeaher',
loadInSidebar: true,
parentid: '6q8JaloClXcH',
timestamp: 1444990424710
}
}
},
'folder|6q8JaloClXcH': {
id: 'folder|6q8JaloClXcH',
name: 'folder toolbar 2',
type: 'others',
iconable: false,
icon: '',
createdLocally: false,
fxsyncRecords: {
'6q8JaloClXcH': {
id: '6q8JaloClXcH',
type: 'folder',
parentName: 'Bookmarks Toolbar',
title: 'folder toolbar 2',
description: null,
children: [ 'AGyc2XZiNuVL' ],
parentid: 'toolbar',
timestamp: 1444990406800
}
}
},
'folder|5AiZMklaq0fL': {
id: 'folder|5AiZMklaq0fL',
name: 'folder toolbar 1',
type: 'others',
iconable: false,
icon: '',
createdLocally: false,
fxsyncRecords: {
'5AiZMklaq0fL': {
id: '5AiZMklaq0fL',
type: 'folder',
parentName: 'Bookmarks Toolbar',
title: 'folder toolbar 1',
description: null,
children: [ 'msopyVa0ckP_' ],
parentid: 'toolbar',
timestamp: 1444990361000
}
}
},
'folder|toolbar': {
id: 'folder|toolbar',
name: 'Bookmarks Toolbar',
type: 'others',
iconable: false,
icon: '',
createdLocally: false,
fxsyncRecords: {
toolbar: {
id: 'toolbar',
type: 'folder',
parentName: '',
title: 'Bookmarks Toolbar',
description: `Add bookmarks to this folder to see them disp\
layed on the Bookmarks Toolbar`,
children: [
'QH1IawO-JKtz',
'olmlydfUhcP7',
'5AiZMklaq0fL',
'6q8JaloClXcH'
],
parentid: 'places',
timestamp: 1444990337260
}
}
},
'folder|places': {
id: 'folder|places',
name: '',
type: 'others',
iconable: false,
icon: '',
createdLocally: false,
fxsyncRecords: {
places: {
id: 'places',
type: 'folder',
title: '',
description: null,
children: [
'menu________',
'toolbar_____',
'tags________',
'unfiled_____',
'PYeSTK7_j6uu'
],
parentid: '9sS1iwhx4w0q',
timestamp: 1444990319980
}
}
},
'folder|unfiled': {
id: 'folder|unfiled',
name: 'Unsorted Bookmarks',
type: 'others',
iconable: false,
icon: '',
createdLocally: false,
fxsyncRecords: {
unfiled: {
id: 'unfiled',
type: 'folder',
parentName: '',
title: 'Unsorted Bookmarks',
description: null,
children: [
'HfLLCKqFA18a',
'-Z5mm8AxeIkq',
'z-G0UXZVu9o1'
],
parentid: 'places',
timestamp: 1443799918500
}
}
},
'http://www.hinet.net/': {
id: 'http://www.hinet.net/',
url: 'http://www.hinet.net/',
name: 'HiNet首頁 -中華電信HiNet網路服務入口',
type: 'url',
iconable: false,
icon: '',
createdLocally: false,
fxsyncRecords: {
'-Z5mm8AxeIkq': {
id: '-Z5mm8AxeIkq',
type: 'bookmark',
title: 'HiNet首頁 -中華電信HiNet網路服務入口',
parentName: 'Unsorted Bookmarks',
bmkUri: 'http://www.hinet.net/',
tags: [],
keyword: null,
description: `中華電信HiNet網路服務入口,台灣最大ISP,提供寬頻上\
網,光世代(光纖)、ADSL服務,及信箱、新聞、hichannel、Xuite等生活娛樂資訊。並延伸至企業\
、政府、資安、雲端網絡應用,是您上網的第一站。`,
loadInSidebar: false,
parentid: 'unfiled',
timestamp: 1443799918500
}
}
}
};
// Key name is too long for one line of code:
bookmarksExpectedDataStore[`https://developer.mozilla.org/en-US/docs/Web/Java\
Script/Reference/Global_Objects/Object/proto`] = {
url: `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Glo\
bal_Objects/Object/proto`,
title: 'Object.prototype.__proto__ € - JavaScript | MDN',
visits: [ 1439366063808 ],
fxsyncId: '_9sCUbahs0ay',
createdLocally: false
};
var bookmarksExpectedAsyncStorage = {
'518fef27c6bbc0220aab0f00b1a37308::collections::bookmarks::revisionid':
'{new-revision-id}',
'518fef27c6bbc0220aab0f00b1a37308::synctoid::bookmarks::msopyVa0ckP_':
'https://redecentralize.org/',
'518fef27c6bbc0220aab0f00b1a37308::synctoid::bookmarks::AGyc2XZiNuVL':
'https://redecentralize.org/',
'518fef27c6bbc0220aab0f00b1a37308::synctoid::bookmarks::6q8JaloClXcH':
'folder|6q8JaloClXcH',
'518fef27c6bbc0220aab0f00b1a37308::synctoid::bookmarks::5AiZMklaq0fL':
'folder|5AiZMklaq0fL',
'518fef27c6bbc0220aab0f00b1a37308::synctoid::bookmarks::toolbar':
'folder|toolbar',
'518fef27c6bbc0220aab0f00b1a37308::synctoid::bookmarks::places':
'folder|places',
'518fef27c6bbc0220aab0f00b1a37308::synctoid::bookmarks::unfiled':
'folder|unfiled',
'518fef27c6bbc0220aab0f00b1a37308::synctoid::bookmarks::-Z5mm8AxeIkq':
'http://www.hinet.net/',
'518fef27c6bbc0220aab0f00b1a37308::collections::bookmarks::mtime':
1444990424710
};
var bookmarksRecords = [];
for (var i in record) {
bookmarksRecords.push(record[i]);
}
var responses = {};
responses.unreachable = {
status: 0,
headers: { get(header) {} },
text() {}
};
responses['http://example.com/v1/'] = {
status: 200,
statusText: 'OK',
headers: { get(header) {} },
text() { return 'bogus'; }
};
responses['https://syncto.dev.mozaws.net/v1/'] = {
status: 200,
headers: { get(header) {} },
text() {
return JSON.stringify({
url: 'https://syncto.dev.mozaws.net/v1/',
documentation: 'https://syncto.readthedocs.org/',
version: '1.0.0',
settings: {
'syncto.batch_max_requests': 25
},
hello: 'syncto'
});
}
};
responses[`https://syncto.dev.mozaws.net/v1/buckets/518fef27c6bbc0220aab0f00b\
1a37308/collections/meta/records`] = {
status: 200,
statusText: 'OK',
headers: { get(header) {} },
text() {
return JSON.stringify({ data: [ remoteData.meta ] });
}
};
responses[`https://syncto.dev.mozaws.net/v1/buckets/518fef27c6bbc0220aab0f00b\
1a37308/collections/crypto/records`] = {
status: 200,
statusText: 'OK',
headers: { get(header) {} },
text() {
return JSON.stringify({ data: [ remoteData.crypto ] });
}
};
responses[`https://syncto.dev.mozaws.net/v1/buckets/518fef27c6bbc0220aab0f00b\
1a37308/collections/history/records`] = {
status: 200,
statusText: 'OK',
headers: { get(header) {} },
text() {
return JSON.stringify({ data: [ remoteData.history ] });
}
};
responses[`https://syncto.dev.mozaws.net/v1/buckets/518fef27c6bbc0220aab0f00b\
1a37308/collections/bookmarks/records`] = {
status: 200,
statusText: 'OK',
headers: { get(header) {} },
text() {
return JSON.stringify({ data: bookmarksRecords });
}
};
var historyEntryDec = {
payload: {
id: '_9sCUbahs0ay',
histUri: `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Referen\
ce/Global_Objects/Object/proto`,
title: 'Object.prototype.__proto__ - JavaScript | MDN',
visits:[ { date: 1439366063808983, type:1 } ]
},
collectionName: 'history'
};
function fetchArgsExpected(collectionNames) {
var ret = [
[
'https://syncto.dev.mozaws.net/v1/',
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
mode: undefined
}
]
];
function collReq(collName) {
return [ `https://syncto.dev.mozaws.net/v1/buckets/518fef27c6bbc0220aab0f\
00b1a37308/collections/${collName}/records`,
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'BrowserID assertion'
},
mode: undefined
}
];
}
collectionNames.forEach(collectionName => {
ret.push(collReq(collectionName));
});
return ret;
}
return {
syncEngineOptions,
xClientState,
remoteData,
responses,
historyEntryDec,
bookmarksEntriesDec,
bookmarksExpectedDataStore,
bookmarksExpectedAsyncStorage,
fetchArgsExpected,
metaGlobalPayloadWithHistoryEngine,
metaGlobalPayloadWithoutHistoryEngine
};
})();

View file

@ -0,0 +1,418 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global
FxSyncWebCryptoFixture,
MocksHelper,
MockIACPort,
MockasyncStorage,
MockDatastore,
MockNavigatorDatastore,
SynctoServerFixture
*/
requireApp('system/test/unit/mock_iac_handler.js');
require('/shared/test/unit/mocks/mock_navigator_datastore.js');
require('/apps/system/test/unit/mock_asyncStorage.js');
require('/shared/test/unit/mocks/mock_lazy_loader.js');
require('/shared/js/sync/bootstrap.js');
require('/shared/js/sync/engine.js');
require('/shared/js/sync/errors.js');
require('/shared/js/sync/crypto/stringconversion.js');
require('/shared/js/sync/crypto/keyderivation.js');
require('/shared/js/sync/crypto/fxsyncwebcrypto.js');
require('/shared/js/sync/ext/kinto.min.js');
requireApp('sync/js/adapters/history.js');
requireApp('sync/js/adapters/bookmarks.js');
requireApp('sharedtest/test/unit/sync/fixtures/synctoserver.js');
requireApp('sharedtest/test/unit/sync/fixtures/fxsyncwebcrypto.js');
var mocksForBootstrap = new MocksHelper([
'IACHandler',
'IACPort',
'LazyLoader'
]).init();
suite('Sync app', function() {
mocksForBootstrap.attachTestHelpers();
suite('Sync request', function() {
const IAC_EVENT = 'iac-gaia::sync::request';
var fetchStub, realFetch, LazyLoader, realGetDataStores, realAsyncStorage;
var addEventListenerSpy, consoleErrorSpy, postMessageSpy, closeStub,
windowClosedPromise, dataStoreSpy;
setup(function() {
addEventListenerSpy = sinon.spy(window, 'addEventListener');
realAsyncStorage = window.asyncStorage;
window.asyncStorage = MockasyncStorage;
consoleErrorSpy = sinon.spy(console, 'error');
postMessageSpy = sinon.spy(MockIACPort, 'postMessage');
closeStub = sinon.stub(window, 'close', () => {
if (windowClosedPromise) {
windowClosedPromise.resolve();
}
});
LazyLoader = {
load() {
return Promise.resolve();
}
};
realGetDataStores = navigator.getDataStores;
navigator.getDataStores = MockNavigatorDatastore.getDataStores;
dataStoreSpy = sinon.spy(navigator, 'getDataStores');
realFetch = window.fetch;
// Spying directly on window.fetch is not allowed, so wrapping it in an
// anonymous function first.
fetchStub = this.sinon.spy(function() {
if (fetchStub.mockUnreachable) {
return Promise.resolve(SynctoServerFixture.responses.unreachable);
} else {
return Promise.resolve(SynctoServerFixture.responses[arguments[0]]);
}
});
window.fetch = fetchStub;
});
teardown(function() {
addEventListenerSpy.restore();
window.asyncStorage.mTeardown();
window.asyncStorage = realAsyncStorage;
consoleErrorSpy.restore();
postMessageSpy.restore();
dataStoreSpy.restore();
navigator.getDataStores = realGetDataStores;
window.fetch = realFetch;
closeStub.restore();
windowClosedPromise = null;
});
function windowClosed() {
if (closeStub.called) {
return Promise.resolve();
}
return new Promise(resolve => {
windowClosedPromise = { resolve };
});
}
function clearSyncAppData(kB) {
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
id,
name: 'clear',
URL: '',
assertion: '',
keys: { kB },
collections: {
history: {},
bookmarks: {}
}
}
}));
return windowClosed();
}
test('invalid sync request', function(done) {
window.dispatchEvent(new CustomEvent(IAC_EVENT));
windowClosed().then(() => {
expect(fetchStub.called).to.equal(false);
done();
});
});
test('url without version', function(done) {
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
id,
name: 'sync',
URL: 'url',
assertion: 'assertion',
keys: { kB: 'kB' },
collections: {
history: { readOnly: true }
}
}
}));
windowClosed().then(() => {
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0].id).to.equal(id);
expect(postMessageSpy.args[0][0].error.message).to.equal(`The remote UR\
L must contain the version: url`);
done();
});
});
test('server unreachable', function(done) {
fetchStub.mockUnreachable = true;
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
id,
name: 'sync',
URL: 'http://example.com/v1/',
assertion: 'assertion',
keys: { kB: 'kB' },
collections: {
history: { readOnly: true }
}
}
}));
windowClosed().then(() => {
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0].id).to.equal(id);
expect(postMessageSpy.args[0][0].error.message).to.equal(`try later`);
fetchStub.mockUnreachable = false;
clearSyncAppData('kB').then(done);
});
});
test('bogus server response', function(done) {
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
id,
name: 'sync',
URL: 'http://example.com/v1/',
assertion: 'assertion',
keys: { kB: 'kB' },
collections: {
history: { readOnly: true }
}
}
}));
windowClosed().then(() => {
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0].id).to.equal(id);
expect(postMessageSpy.args[0][0].error.message).to.equal(`try later`);
clearSyncAppData('kB').then(done, done);
});
});
test('sync zero collections', function(done) {
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
id,
name: 'sync',
URL: 'https://syncto.dev.mozaws.net/v1/',
assertion: 'assertion',
keys: { kB: FxSyncWebCryptoFixture.kB },
collections: {
}
}
}));
windowClosed().then(() => {
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0].id).to.equal(id);
expect(postMessageSpy.args[0][0].error).to.equal(undefined);
var fetchArgsExpected = SynctoServerFixture.fetchArgsExpected(
['meta']);
expect(fetchStub.args.length).to.equal(fetchArgsExpected.length);
for (var i = 0; i < fetchStub.args.length; i++) {
expect(fetchStub.args[i].length).to.equal(
fetchArgsExpected[i].length);
for (var j = 0; j < fetchStub.args[i].length; j++) {
expect(fetchStub.args[i][j]).to.deep.equal(
fetchArgsExpected[i][j]);
}
}
return clearSyncAppData(FxSyncWebCryptoFixture.kB).then(done, done);
});
});
test('sync history', function(done) {
// TODO: Test known tombstones, unknown tombstones, updates, quota errors.
// Leaving race conditions for now, since on the TV the sync app is the
// only one to write to the places DS.
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
id,
name: 'sync',
URL: 'https://syncto.dev.mozaws.net/v1/',
assertion: 'assertion',
keys: { kB: FxSyncWebCryptoFixture.kB },
collections: {
history: { readonly: true }
}
}
}));
windowClosed().then(() => {
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0].id).to.equal(id);
expect(postMessageSpy.args[0][0].error).to.equal(undefined);
var fetchArgsExpected = SynctoServerFixture.fetchArgsExpected(
['meta', 'crypto', 'history']);
// FIXME: We are not mocking IndexedDB, so it's possible the crypto
// collection was cached in the test environment and does not get
// fetched.
if (fetchStub.args.length == 3) {
fetchArgsExpected = SynctoServerFixture.fetchArgsExpected(
['meta', 'history']);
}
expect(fetchStub.args.length).to.equal(fetchArgsExpected.length);
for (var i = 0; i < fetchStub.args.length; i++) {
expect(fetchStub.args[i].length).to.equal(
fetchArgsExpected[i].length);
for (var j = 0; j < fetchStub.args[i].length; j++) {
expect(fetchStub.args[i][j]).to.deep.equal(
fetchArgsExpected[i][j]);
}
}
expect(MockasyncStorage.mItems[`518fef27c6bbc0220aab0f00b1a37308::\
synctoid::history::_9sCUbahs0ay`]).to.equal(`https://developer.mozilla.org/en-U\
S/docs/Web/JavaScript/Reference/Global_Objects/Object/proto`);
expect(MockasyncStorage.mItems[`518fef27c6bbc0220aab0f00b1a37308::\
collections::history::mtime`]).to.equal(1234567890123);
return Promise.all([
// FIXME: Can only mock one DataStore per test
navigator.getDataStores('places').then(stores => {
var recordKeys = Object.keys(stores[0]._records);
expect(recordKeys.length).to.equal(1);
expect(recordKeys[0]).to.equal(`https://developer.mozilla.org/en-US\
/docs/Web/JavaScript/Reference/Global_Objects/Object/proto`);
expect(stores[0]._records[`https://developer.mozilla.org/en-US/docs\
/Web/JavaScript/Reference/Global_Objects/Object/proto`]).to.deep.equal({
url: `https://developer.mozilla.org/en-US/docs/Web/JavaScript/R\
eference/Global_Objects/Object/proto`,
title: 'Object.prototype.__proto__ € - JavaScript | MDN',
visits: [ 1439366063808 ],
fxsyncId: '_9sCUbahs0ay',
createdLocally: false
});
})
]);
}).then(() => undefined).then(() => {
return clearSyncAppData(FxSyncWebCryptoFixture.kB);
}).then(done, done);
});
test('sync bookmarks', function(done) {
// TODO: Test known tombstones, unknown tombstones, updates, quota errors.
// Leaving race conditions for now, since on the TV the sync app is the
// only one to write to the bookmarks_store DS.
MockDatastore._tasks[1].revisionId = '{new-revision-id}';
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
id,
name: 'sync',
URL: 'https://syncto.dev.mozaws.net/v1/',
assertion: 'assertion',
keys: { kB: FxSyncWebCryptoFixture.kB },
collections: {
bookmarks: { readonly: true }
}
}
}));
windowClosed().then(() => {
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0].id).to.equal(id);
expect(postMessageSpy.args[0][0].error).to.equal(undefined);
var fetchArgsExpected = SynctoServerFixture.fetchArgsExpected(
['meta', 'crypto', 'bookmarks']);
// FIXME: We are not mocking IndexedDB, so it's possible the crypto
// collection was cached in the test environment and does not get
// fetched.
if (fetchStub.args.length == 3) {
fetchArgsExpected = SynctoServerFixture.fetchArgsExpected(
['meta', 'bookmarks']);
}
expect(fetchStub.args.length).to.equal(fetchArgsExpected.length);
for (var i = 0; i < fetchStub.args.length; i++) {
expect(fetchStub.args[i].length).to.equal(
fetchArgsExpected[i].length);
for (var j = 0; j < fetchStub.args[i].length; j++) {
expect(fetchStub.args[i][j]).to.deep.equal(
fetchArgsExpected[i][j]);
}
}
expect(MockasyncStorage.mItems)
.to.deep.equal(SynctoServerFixture.bookmarksExpectedAsyncStorage);
return Promise.all([
navigator.getDataStores('bookmarks_store').then(stores => {
expect(stores[0]._records).to.deep.equal(
SynctoServerFixture.bookmarksExpectedDataStore);
})
]);
}).then(() => undefined).then(() => {
return clearSyncAppData(FxSyncWebCryptoFixture.kB);
}).then(done, done);
});
suite('collection not in meta/global engines', function() {
setup(function() {
SynctoServerFixture.remoteData.meta.payload =
SynctoServerFixture.metaGlobalPayloadWithoutHistoryEngine;
});
teardown(function() {
SynctoServerFixture.remoteData.meta.payload =
SynctoServerFixture.metaGlobalPayloadWithHistoryEngine;
});
test('skips that collection', function(done) {
var id = Date.now();
window.dispatchEvent(new CustomEvent(IAC_EVENT, {
detail: {
id,
name: 'sync',
URL: 'https://syncto.dev.mozaws.net/v1/',
assertion: 'assertion',
keys: { kB: FxSyncWebCryptoFixture.kB },
collections: {
history: { readonly: true }
}
}
}));
windowClosed().then(() => {
expect(postMessageSpy.called).to.equal(true);
expect(postMessageSpy.args[0][0].id).to.equal(id);
expect(postMessageSpy.args[0][0].error).to.equal(undefined);
var fetchArgsExpected = SynctoServerFixture.fetchArgsExpected(
['meta', 'crypto']);
// FIXME: We are not mocking IndexedDB, so it's possible the crypto
// collection was cached in the test environment and does not get
// fetched.
if (fetchStub.args.length == 2) {
fetchArgsExpected = SynctoServerFixture.fetchArgsExpected(
['meta']);
}
expect(fetchStub.args.length).to.equal(fetchArgsExpected.length);
for (var i = 0; i < fetchStub.args.length; i++) {
expect(fetchStub.args[i].length).to.equal(
fetchArgsExpected[i].length);
for (var j = 0; j < fetchStub.args[i].length; j++) {
expect(fetchStub.args[i][j]).to.deep.equal(
fetchArgsExpected[i][j]);
}
}
return clearSyncAppData(FxSyncWebCryptoFixture.kB).then(done, done);
});
});
});
});
});

View file

@ -0,0 +1,26 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* exported
MockSyncEngine
*/
var MockSyncEngine = (() => {
var MockSyncEngine = function(options) {
MockSyncEngine.constructorOptions = options;
};
MockSyncEngine.prototype = {
syncNow: function(options) {
MockSyncEngine.syncOptions = options;
if (MockSyncEngine.shouldFail) {
return Promise.reject(new Error('mock sync failure'));
} else {
return Promise.resolve();
}
}
};
MockSyncEngine.shouldFail = false;
return MockSyncEngine;
})();

View file

@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* exported
AdapterMock
*/
var AdapterMock = (action, args) => {
return {
update(kintoColl, options) {
AdapterMock.options = options;
if (action === 'noop') {
// Even if the adapter makes changes, we want to see that
// SyncEngine does not sync those changes up if the adapter
// return false.
return kintoColl.update.apply(kintoColl, args).then(() => {
return false;
});
}
if (action) {
return kintoColl[action].apply(kintoColl, args).then(() => {
return true;
});
}
return Promise.resolve(false);
},
handleConflict(conflict) {
return Promise.resolve(conflict.local);
},
reset() {
return Promise.resolve();
}
};
};

View file

@ -0,0 +1,51 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global SynctoServerFixture */
/* exported FxSyncWebCrypto */
requireApp('sharedtest/test/unit/sync/fixtures/synctoserver.js');
var FxSyncWebCrypto = function() {};
FxSyncWebCrypto.prototype = {
setKeys: function(kB, cryptoKeys) {
this.shouldWork = true;
if (kB !== SynctoServerFixture.syncEngineOptions.kB) {
this.shouldWork = false;
}
var correctCryptoKeys = JSON.parse(
SynctoServerFixture.remoteData.crypto.payload);
if (cryptoKeys.ciphertext !== correctCryptoKeys.ciphertext ||
cryptoKeys.IV !== correctCryptoKeys.IV ||
cryptoKeys.hmac !== correctCryptoKeys.hmac) {
this.shouldWork = false;
}
if (this.shouldWork) {
this.bulkKeyBundle = true;
return Promise.resolve();
}
return Promise.reject(`SyncKeys hmac could not be verified with current mai\
n key`);
},
encrypt: function(payload) {
if (this.shouldWork) {
return Promise.resolve({ mockEncrypted: JSON.stringify(payload) });
}
return Promise.reject();
},
decrypt: function(record) {
var decryptablePayload = JSON.parse(
SynctoServerFixture.remoteData.history.payload);
if (this.shouldWork &&
record.ciphertext === decryptablePayload.ciphertext &&
record.IV === decryptablePayload.IV &&
record.hmac === decryptablePayload.hmac) {
return Promise.resolve(
SynctoServerFixture.historyEntryDec.payload);
}
return Promise.reject(new Error(
'payload.ciphertext is not a Base64 string'));
}
};

View file

@ -0,0 +1,256 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global SynctoServerFixture */
/* exported Kinto */
var Kinto = (function() {
var mockProblem = null;
var listData = {};
var KintoCollectionMock = function(collectionName, options = {}) {
this.collectionName = collectionName;
this.data = null;
this.fireConflicts = options.fireConflicts || [];
this._idSchemaUsed = options.idSchema; // Not mocking default UUID IdSchema.
this._remoteTransformersUsed = options.remoteTransformers;
};
KintoCollectionMock.prototype = {
sync: function() {
var dataRecordIn = JSON.parse(JSON.stringify(
SynctoServerFixture.remoteData[this.collectionName]));
var transformOut = (item) => {
if (!this._remoteTransformersUsed) {
return Promise.resolve(item);
}
// We're only mocking the case where there is just one
// remoteTransformer.
return this._remoteTransformersUsed[0].encode(item);
};
var transformIn = () => {
if (!this._remoteTransformersUsed) {
return Promise.resolve();
}
try {
// We're only mocking the case where there is just one
// remoteTransformer.
return this._remoteTransformersUsed[0].decode(dataRecordIn).
then(decoded => {
dataRecordIn = decoded;
});
} catch(err) {
return Promise.reject(err);
}
};
var pushOut = () => {
if (!listData[this.collectionName]) {
return Promise.resolve();
}
return Promise.all(listData[this.collectionName].data.map(item => {
var dataRecordOut = JSON.parse(JSON.stringify(item));
transformOut(dataRecordOut).then(encoded => {
this.pushData.push(encoded);
});
}));
};
var checkIdSchema = () => {
if (!this._idSchemaUsed.validate(dataRecordIn.id)) {
return Promise.reject(new Error('Invalid id: ' +
dataRecordIn.id));
}
};
var storeListData = () => {
if (!listData[this.collectionName]) {
listData[this.collectionName] = { data: [ dataRecordIn ] };
}
return Promise.resolve({ ok: true, conflicts: this.fireConflicts });
};
var markSyncCount = (syncResults) => {
if (!Kinto.syncCount[this.collectionName]) {
Kinto.syncCount[this.collectionName] = 0;
}
Kinto.syncCount[this.collectionName]++;
return Promise.resolve(syncResults);
};
var reportError = (error) => {
listData[this.collectionName] = { data: [] };
return Promise.resolve({ ok: false, conflicts: [], errors: [ error ] });
};
this.pushData = [];
return pushOut().
then(transformIn).
then(checkIdSchema).
then(storeListData).
then(markSyncCount).
catch(reportError);
},
resolve: function(conflict, resolution) {
listData[this.collectionName] = { data: [ resolution ] };
return Promise.resolve();
},
list: function() {
return Promise.resolve(listData[this.collectionName]);
},
get: function(id) {
if (listData[this.collectionName]) {
return Promise.resolve({
data: listData[this.collectionName].data[0]
});
}
return Promise.reject(new Error(`Record with id=${id} not found.`));
},
create: function(obj, options = {}) {
if (options.forceId) {
if(!this._idSchemaUsed.validate(options.forceId)) {
return Promise.reject(new Error('Invalid id: ' + options.forceId));
}
obj.id = options.forceId;
} else {
obj.id = this._idSchemaUsed.generate();
}
listData[this.collectionName].data.push(obj);
return Promise.resolve();
},
update: function(obj) {
for (var i = 0; i < listData[this.collectionName].data.length; i++) {
if (listData[this.collectionName].data[i].id === obj.id) {
listData[this.collectionName].data[i] = obj;
return Promise.resolve();
}
}
return Promise.reject('not found!');
},
delete: function(id) {
for (var i = 0; i < listData[this.collectionName].data.length; i++) {
if (listData[this.collectionName].data[i].id === id) {
listData[this.collectionName].data.splice(i, 1);
return Promise.resolve();
}
}
return Promise.reject('not found!');
},
clear: function() {
Kinto.clearCalled = true;
listData[this.collectionName] = { data: [] };
return Promise.resolve();
}
};
var UnreachableKintoCollectionMock = function() {};
UnreachableKintoCollectionMock.prototype = {
sync() {
return Promise.reject(new Error(`HTTP 0; TypeError: NetworkError when att\
empting to fetch resource.`));
},
list() {},
get(id) {
return Promise.reject(new Error(`Record with id=${id} not found.`));
},
create() {},
update() {},
delete() {},
clear() {
Kinto.clearCalled = true;
},
};
var HttpCodeKintoCollectionMock = function(status) {
this.status = status;
};
HttpCodeKintoCollectionMock.prototype = {
sync() {
var err = new Error();
err.response = {
status: this.status
};
return Promise.reject(err);
},
list() {},
get(id) {
return Promise.reject(new Error(`Record with id=${id} not found.`));
},
create() {},
update() {},
delete() {},
clear() {
Kinto.clearCalled = true;
},
};
var Kinto = function(kintoOptions) {
this.options = kintoOptions;
this.collection = function(collectionName, collectionOptions = {}) {
var mockProblemCase = () => {
var httpCode;
if (mockProblem &&
mockProblem.collectionName === collectionName) {
httpCode = parseInt(mockProblem.problem);
if (isNaN(httpCode)) {
if (mockProblem.problem === 'conflicts') {
collectionOptions.fireConflicts = [{
local: { bar: 'local' },
remote: SynctoServerFixture.historyEntryDec.payload
}];
return new KintoCollectionMock(collectionName, collectionOptions);
}
return new KintoCollectionMock(mockProblem.problem,
collectionOptions);
}
return new HttpCodeKintoCollectionMock(httpCode);
}
};
var unauthCase = () => {
if (kintoOptions.headers.Authorization !==
'BrowserID test-assertion-mock') {
return new HttpCodeKintoCollectionMock(401);
}
};
var unreachableCase = () => {
if (kintoOptions.remote !== 'http://localhost:8000/v1/') {
return new UnreachableKintoCollectionMock(collectionName);
}
};
var defaultCase = () => {
return new KintoCollectionMock(collectionName, collectionOptions);
};
return mockProblemCase() ||
unauthCase() ||
unreachableCase() ||
defaultCase();
};
};
Kinto.syncCount = {};
Kinto.clearCalled = false;
Kinto.setMockProblem = (problem) => {
mockProblem = problem;
Kinto.syncCount = {};
Kinto.clearCalled = false;
listData = {};
};
return Kinto;
})();

View file

@ -0,0 +1,397 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
/* global
AdapterMock,
FxSyncWebCrypto,
Kinto,
SyncEngine,
SynctoServerFixture
*/
requireApp('sharedtest/test/unit/sync/sync-engine/adapter-mock.js');
requireApp('sharedtest/test/unit/sync/fixtures/synctoserver.js');
requireApp('sharedtest/test/unit/sync/sync-engine/fxsyncwebcrypto-mock.js');
requireApp('sharedtest/test/unit/sync/sync-engine/kinto-mock.js');
require('/shared/js/sync/engine.js');
var cloneObject = (obj) => {
return JSON.parse(JSON.stringify(obj));
};
suite('SyncEngine', function() {
suite('constructor', function() {
test('constructs a SyncEngine object', function(done) {
const options = SynctoServerFixture.syncEngineOptions;
const se = new SyncEngine(options);
expect(se).to.be.an('object');
expect(se._kB).to.equal(options.kB);
expect(se._collections).to.be.an('object');
expect(se._controlCollections).to.be.an('object');
expect(se._fswc).to.be.instanceOf(FxSyncWebCrypto);
expect(se._adapters).to.deep.equal(options.adapters);
expect(se._ready).to.equal(false);
done();
});
test('checks that options is an Object', function(done) {
try {
var se = new SyncEngine(5);
} catch(err) {
expect(se).to.equal(undefined);
expect(err.message).to.equal('options should be an Object');
done();
}
});
['URL', 'assertion', 'kB'].forEach(field => {
test(`checks that ${field} is a String`, function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials[field] = 7.2;
try {
var se = new SyncEngine(credentials);
} catch(err) {
expect(se).to.equal(undefined);
expect(err.message).to.equal(`options.${field} should be a String`);
done();
}
});
});
test('checks that options.adapters is an Object', function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters = 'foo';
try {
var se = new SyncEngine(credentials);
} catch(err) {
expect(se).to.equal(undefined);
expect(err.message).to.equal('options.adapters should be an Object');
done();
}
});
test('checks that options.adapters members are Objects', function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters = {
foo: 'bar'
};
try {
var se = new SyncEngine(credentials);
} catch(err) {
expect(se).to.equal(undefined);
expect(err.message).to.equal(
'options.adapters.foo should be an Object');
done();
}
});
['update', 'handleConflict'].forEach(methodName => {
test(`checks that options.adapters[x].${methodName} are Functions`,
function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters = {
foo: AdapterMock()
};
credentials.adapters.foo[methodName] = 8;
try {
var se = new SyncEngine(credentials);
} catch(err) {
expect(se).to.equal(undefined);
expect(err.message).to.equal(`options.adapters.foo.${methodName} shou\
ld be a Function`);
done();
}
});
});
});
suite('syncNow', function() {
setup(function() {
Kinto.setMockProblem();
});
test('resolves its promise', function(done) {
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
expect(se.syncNow({ history: {} })).to.eventually.
equal(undefined).and.notify(done);
});
test('initializes the Kinto object', function(done) {
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
se.syncNow({ history: {} }).then(function() {
expect(se._kinto).to.be.instanceOf(Kinto);
expect(se._kinto.options).to.deep.equal({
bucket: SynctoServerFixture.xClientState,
remote: SynctoServerFixture.syncEngineOptions.URL,
timeout: 180 * 1000,
headers: {
'Authorization': 'BrowserID ' +
SynctoServerFixture.syncEngineOptions.assertion
}
});
done();
});
});
test('sets the correct xClientState as the bucket', function(done) {
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
se.syncNow({ history: {} }).then(function() {
expect(se._kinto.options.bucket).to.equal(
SynctoServerFixture.xClientState);
done();
});
});
test('Passes options to the DataAdapter', function(done) {
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
se.syncNow({ history: { readonly: true } }).then(function() {
expect(AdapterMock.options).to.deep.equal({
readonly: true,
userid: SynctoServerFixture.xClientState
});
done();
});
});
test('Syncs crypto collection only first time', function(done) {
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
se.syncNow({ history: {} }).then(() => {
expect(Kinto.syncCount.meta).to.equal(1);
expect(Kinto.syncCount.crypto).to.equal(1);
expect(Kinto.syncCount.history).to.equal(1);
var se2 = new SyncEngine(SynctoServerFixture.syncEngineOptions);
return se2.syncNow({ history: {} });
}).then(() => {
expect(Kinto.syncCount.meta).to.equal(2);
expect(Kinto.syncCount.crypto).to.equal(1);
expect(Kinto.syncCount.history).to.equal(2);
done();
});
});
test('retrieves and decrypts the remote data', function(done) {
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
se.syncNow({ history: {} }).then(() => {
expect(se._collections.history).to.be.an('object');
return se._collections.history.list();
}).then(list => {
expect(list).to.be.an('object');
expect(list.data).to.be.instanceOf(Array);
expect(list.data.length).to.equal(1);
expect(Object.keys(list.data[0]).sort())
.to.deep.equal(['id', 'last_modified', 'payload']);
expect(list.data[0].payload).to.be.an('object');
expect(list.data[0].payload.histUri).to.be.a('string');
done();
});
});
test('encrypts and pushes added records', function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters.history = AdapterMock('create', [
{ payload: 'foo' }
]);
var se = new SyncEngine(credentials);
se.syncNow({ history: {} }).then(() => {
return se._collections.history.list();
}).then(list => {
expect(list.data.length).to.equal(2);
expect(se._collections.history.pushData.length).to.equal(2);
expect(list.data[0].payload.histUri).to.be.a('string');
expect(list.data[1].payload).to.equal('foo');
expect(se._collections.history.pushData[1].payload).to.equal(
'{"mockEncrypted":"\\"foo\\""}');
done();
});
});
test('enforces FxSyncIdSchema on added records', function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters.history = AdapterMock('create', [
{ payload: 'foo' },
{ forceId: 8.4 }
]);
var se = new SyncEngine(credentials);
expect(se.syncNow({ history: {} })).to.be.rejectedWith(Error, `Invalid id\
: 8.4`).and.notify(done);
});
test('encrypts and pushes updated records', function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters.history = AdapterMock('update', [ {
id: SynctoServerFixture.remoteData.history.id,
payload: 'foo'
}]);
var se = new SyncEngine(credentials);
se.syncNow({ 'history': {} }).then(() => {
return se._collections.history.list();
}).then(list => {
expect(list.data.length).to.equal(1);
expect(se._collections.history.pushData.length).to.equal(1);
expect(se._collections.history.pushData[0].payload).to.equal(
'{"mockEncrypted":"\\"foo\\""}');
done();
});
});
test('only uploads encrypted payload and id', function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters.history = AdapterMock('update', [ {
id: SynctoServerFixture.remoteData.history.id,
payload: 'foo',
strayField: 'bar'
}]);
var se = new SyncEngine(credentials);
se.syncNow({ history: {} }).then(() => {
return se._collections.history.list();
}).then(list => {
expect(list.data.length).to.equal(1);
expect(Object.keys(se._collections.history.pushData[0]).sort())
.to.deep.equal(['id', 'payload']);
done();
});
});
test('pushes deletes of records', function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters.history = AdapterMock('delete', [
SynctoServerFixture.remoteData.history.id
]);
var se = new SyncEngine(credentials);
se.syncNow({ history: {} }).then(() => {
return se._collections.history.list();
}).then(list => {
expect(list.data.length).to.equal(0);
expect(se._collections.history.pushData.length).to.equal(0);
done();
});
});
test('encrypts and pushes conflict resolutions', function(done) {
Kinto.setMockProblem({ collectionName: 'history', problem: 'conflicts' });
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
se.syncNow({ history: {} }).then(() => {
return se._collections.history.list();
}).then(list => {
expect(list.data.length).to.equal(1);
expect(list.data[0].bar).to.equal('local');
expect(se._collections.history.pushData[0].payload).to.equal('{}');
Kinto.setMockProblem();
done();
});
});
test('does not push if nothing changed', function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters.history = AdapterMock('noop', [ {
id: SynctoServerFixture.remoteData.history.id,
foo: 'bar'
}]);
var se = new SyncEngine(credentials);
se.syncNow({ 'history': {} }).then(() => {
return se._collections.history.list();
}).then(list => {
expect(list.data.length).to.equal(1);
expect(se._collections.history.pushData.length).to.equal(0);
done();
});
});
test(`rejects its promise if collectionOptions is not an object`,
function(done) {
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
expect(se.syncNow('foo')).to.be.rejectedWith(Error, `collectionOptions sh\
ould be an object`).and.notify(done);
});
['URL', 'assertion', 'kB'].forEach(function(field) {
test(`rejects its promise if ${field} is wrong`, function(done) {
var credentials = cloneObject(SynctoServerFixture.syncEngineOptions);
credentials.adapters = {
history: AdapterMock()
};
credentials[field] = 'whoopsie';
var se = new SyncEngine(credentials);
se.syncNow({ history: {} }).catch(err => {
if (field === 'assertion') {
expect(err).to.be.instanceOf(SyncEngine.AuthError);
} else if (field === 'URL') {
expect(err).to.be.instanceOf(SyncEngine.TryLaterError);
} else {
expect(err).to.equal(`SyncKeys hmac could not be verified with curr\
ent main key`);
}
done();
});
});
});
['meta', 'crypto'].forEach(function(collectionName) {
[
'401',
'404',
'500',
'503',
'wrong-payload-meta',
'wrong-payload-crypto',
'wrong-ciphertext',
'wrong-id'
].forEach(function(problem) {
test(`rejects its promise if ${collectionName} response is ${problem}`,
function(done) {
Kinto.setMockProblem({ collectionName, problem });
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
se.syncNow({ history: {} }).catch(err => {
if (problem === '401') {
expect(err.message).to.equal('unauthorized');
} else if (['404', '500', '503'].indexOf(problem) !== -1) {
expect(err.message).to.equal('try later');
} else {
expect(err.message).to.equal('unrecoverable');
}
done();
});
});
});
});
[
'401',
'404',
'500',
'503',
'wrong-payload-history',
'wrong-ciphertext',
'wrong-id'
].forEach(function(problem) {
test(`rejects its promise if response is ${problem}`, function(done) {
Kinto.setMockProblem({ collectionName:'history', problem });
var se = new SyncEngine(SynctoServerFixture.syncEngineOptions);
se.syncNow({ history: {} }).catch(err => {
if (problem === '401') {
expect(err.message).to.equal('unauthorized');
} else if (['404', '500', '503'].indexOf(problem) !== -1) {
expect(err.message).to.equal('try later');
} else {
expect(err.message).to.equal('unrecoverable');
}
if (['401', '404', '500', '503'].indexOf(problem) === -1) {
expect(Kinto.clearCalled).to.equal(true);
} else {
expect(Kinto.clearCalled).to.equal(false);
}
done();
});
});
});
});
});