sharedb 6.0.4 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agent.js +66 -29
- package/lib/backend.js +20 -6
- package/lib/client/doc.js +14 -1
- package/lib/client/presence/remote-doc-presence.js +7 -1
- package/lib/ot.js +53 -14
- package/lib/projections.js +6 -6
- package/lib/pubsub/index.js +15 -3
- package/lib/submit-request.js +6 -0
- package/package.json +1 -1
- package/test/agent.js +240 -0
- package/test/backend.js +86 -3
- package/test/client/doc.js +215 -26
- package/test/client/presence/doc-presence.js +54 -0
- package/test/client/presence/presence.js +48 -0
- package/test/client/query-subscribe.js +56 -18
- package/test/client/snapshot-timestamp-request.js +38 -1
- package/test/middleware.js +96 -0
- package/test/ot.js +62 -0
- package/test/projections.js +18 -0
- package/test/pubsub-memory.js +45 -0
- package/test/pubsub.js +45 -0
|
@@ -6,6 +6,7 @@ var presenceTestType = require('./presence-test-type');
|
|
|
6
6
|
var errorHandler = require('../../util').errorHandler;
|
|
7
7
|
var PresencePauser = require('./presence-pauser');
|
|
8
8
|
var sinon = require('sinon');
|
|
9
|
+
var ACTIONS = require('../../../lib/message-actions').ACTIONS;
|
|
9
10
|
types.register(presenceTestType.type);
|
|
10
11
|
|
|
11
12
|
describe('DocPresence', function() {
|
|
@@ -971,6 +972,59 @@ describe('DocPresence', function() {
|
|
|
971
972
|
], done);
|
|
972
973
|
});
|
|
973
974
|
|
|
975
|
+
it('emits an error and drops stale remote presence that cannot be transformed', function(done) {
|
|
976
|
+
var localPresence1 = presence1.create('presence-1');
|
|
977
|
+
var catchUpCount = 0;
|
|
978
|
+
|
|
979
|
+
function sendUntypedPresenceAtVersion(version) {
|
|
980
|
+
var message = localPresence1._message();
|
|
981
|
+
message.d = null;
|
|
982
|
+
message.p = {index: 0};
|
|
983
|
+
message.v = version;
|
|
984
|
+
connection1.send(message);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async.series([
|
|
988
|
+
presence2.subscribe.bind(presence2),
|
|
989
|
+
function(next) {
|
|
990
|
+
connection2.on('send', function(message) {
|
|
991
|
+
if (message.a !== ACTIONS.presenceRequest) return;
|
|
992
|
+
catchUpCount++;
|
|
993
|
+
if (catchUpCount === 1) next();
|
|
994
|
+
});
|
|
995
|
+
sendUntypedPresenceAtVersion(0);
|
|
996
|
+
},
|
|
997
|
+
function(next) {
|
|
998
|
+
doc2.once('op', function() {
|
|
999
|
+
next();
|
|
1000
|
+
});
|
|
1001
|
+
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
|
|
1002
|
+
},
|
|
1003
|
+
function(next) {
|
|
1004
|
+
presence2.once('error', function(error) {
|
|
1005
|
+
expect(error).to.be.an.instanceOf(Error);
|
|
1006
|
+
expect(error.code).to.equal('ERR_DOC_TYPE_NOT_RECOGNIZED');
|
|
1007
|
+
next();
|
|
1008
|
+
});
|
|
1009
|
+
sendUntypedPresenceAtVersion(1);
|
|
1010
|
+
},
|
|
1011
|
+
function(next) {
|
|
1012
|
+
doc2.once('op', function() {
|
|
1013
|
+
next();
|
|
1014
|
+
});
|
|
1015
|
+
doc1.submitOp({index: 0, value: 'The'}, errorHandler(done));
|
|
1016
|
+
},
|
|
1017
|
+
function(next) {
|
|
1018
|
+
presence2.once('receive', function(id, presence) {
|
|
1019
|
+
expect(presence).to.eql({index: 1});
|
|
1020
|
+
expect(catchUpCount).to.equal(1);
|
|
1021
|
+
next();
|
|
1022
|
+
});
|
|
1023
|
+
localPresence1.submit({index: 1}, errorHandler(done));
|
|
1024
|
+
}
|
|
1025
|
+
], done);
|
|
1026
|
+
});
|
|
1027
|
+
|
|
974
1028
|
it('sends null presence when the doc is destroyed', function(done) {
|
|
975
1029
|
var localPresence1 = presence1.create('presence-1');
|
|
976
1030
|
|
|
@@ -486,6 +486,54 @@ describe('Presence', function() {
|
|
|
486
486
|
], done);
|
|
487
487
|
});
|
|
488
488
|
|
|
489
|
+
it('does not leak Streams when subscribing the same presence multiple times in parallel', function(done) {
|
|
490
|
+
var streamsCount = backend.pubsub.streamsCount;
|
|
491
|
+
async.series([
|
|
492
|
+
function(next) {
|
|
493
|
+
presence1.subscribe();
|
|
494
|
+
// Trick it into sending a duplicate request
|
|
495
|
+
presence1.wantSubscribe = false;
|
|
496
|
+
presence1.subscribe(next);
|
|
497
|
+
},
|
|
498
|
+
function(next) {
|
|
499
|
+
expect(backend.pubsub.streamsCount).to.equal(streamsCount + 1);
|
|
500
|
+
next();
|
|
501
|
+
},
|
|
502
|
+
presence1.unsubscribe.bind(presence1),
|
|
503
|
+
function(next) {
|
|
504
|
+
expect(backend.pubsub.streamsCount).to.equal(streamsCount);
|
|
505
|
+
next();
|
|
506
|
+
}
|
|
507
|
+
], done);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('does not broadcast a disconnect when subscribing the same presence multiple times in parallel', function(done) {
|
|
511
|
+
var localPresence1 = presence1.create('presence-1');
|
|
512
|
+
|
|
513
|
+
async.series([
|
|
514
|
+
presence2.subscribe.bind(presence2),
|
|
515
|
+
function(next) {
|
|
516
|
+
localPresence1.submit({index: 1}, errorHandler(done));
|
|
517
|
+
presence2.once('receive', function() {
|
|
518
|
+
next();
|
|
519
|
+
});
|
|
520
|
+
},
|
|
521
|
+
function(next) {
|
|
522
|
+
presence1.subscribe();
|
|
523
|
+
// Trick it into sending a duplicate request
|
|
524
|
+
presence1.wantSubscribe = false;
|
|
525
|
+
presence1.subscribe(next);
|
|
526
|
+
},
|
|
527
|
+
function(next) {
|
|
528
|
+
localPresence1.submit({index: 2}, errorHandler(done));
|
|
529
|
+
presence2.once('receive', function(id, presence) {
|
|
530
|
+
expect(presence).to.eql({index: 2});
|
|
531
|
+
next();
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
], done);
|
|
535
|
+
});
|
|
536
|
+
|
|
489
537
|
it('throws an error when trying to create a presence with a non-string ID', function() {
|
|
490
538
|
expect(function() {
|
|
491
539
|
presence1.create(123);
|
|
@@ -127,11 +127,13 @@ module.exports = function(options) {
|
|
|
127
127
|
|
|
128
128
|
it('does not duplicate messages', function(done) {
|
|
129
129
|
var connection = this.backend.connect();
|
|
130
|
+
this.backend.db.pollInterval = 0;
|
|
131
|
+
this.backend.db.pollDebounce = 0;
|
|
130
132
|
var count = 0;
|
|
131
133
|
var query = connection.createSubscribeQuery(
|
|
132
134
|
'dogs',
|
|
133
135
|
this.matchAllDbQuery,
|
|
134
|
-
|
|
136
|
+
null,
|
|
135
137
|
function(err) {
|
|
136
138
|
if (err) return done(err);
|
|
137
139
|
connection.get('dogs', '1').on('error', done).create({});
|
|
@@ -519,13 +521,17 @@ function commonTests(options) {
|
|
|
519
521
|
});
|
|
520
522
|
});
|
|
521
523
|
|
|
522
|
-
it('pollDebounce option reduces subsequent poll interval', function(done) {
|
|
524
|
+
it('query middleware pollDebounce option reduces subsequent poll interval', function(done) {
|
|
523
525
|
var clock = util.useFakeTimers();
|
|
524
526
|
var connection = this.backend.connect();
|
|
525
527
|
this.backend.db.canPollDoc = function() {
|
|
526
528
|
return false;
|
|
527
529
|
};
|
|
528
|
-
|
|
530
|
+
this.backend.use('query', function(context, next) {
|
|
531
|
+
context.options.pollDebounce = 2000;
|
|
532
|
+
next();
|
|
533
|
+
});
|
|
534
|
+
var query = connection.createSubscribeQuery('items', this.matchAllDbQuery);
|
|
529
535
|
query.on('error', done);
|
|
530
536
|
var batchSizes = [];
|
|
531
537
|
var total = 0;
|
|
@@ -618,21 +624,21 @@ function commonTests(options) {
|
|
|
618
624
|
});
|
|
619
625
|
});
|
|
620
626
|
|
|
621
|
-
it('pollInterval updates a subscribed query after an unpublished create', function(done) {
|
|
627
|
+
it('query middleware pollInterval updates a subscribed query after an unpublished create', function(done) {
|
|
622
628
|
var clock = util.useFakeTimers();
|
|
623
629
|
var connection = this.backend.connect();
|
|
624
630
|
this.backend.suppressPublish = true;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
635
|
-
);
|
|
631
|
+
this.backend.use('query', function(context, next) {
|
|
632
|
+
context.options.pollDebounce = 0;
|
|
633
|
+
context.options.pollInterval = 50;
|
|
634
|
+
next();
|
|
635
|
+
});
|
|
636
|
+
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
|
|
637
|
+
if (err) return done(err);
|
|
638
|
+
connection.get('dogs', 'fido').on('error', done).create({}, function() {
|
|
639
|
+
clock.tick(51);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
636
642
|
query.on('error', done);
|
|
637
643
|
query.on('insert', function(docs) {
|
|
638
644
|
expect(util.pluck(docs, 'id')).eql(['fido']);
|
|
@@ -659,13 +665,17 @@ function commonTests(options) {
|
|
|
659
665
|
});
|
|
660
666
|
});
|
|
661
667
|
|
|
662
|
-
it('pollInterval captures additional unpublished creates', function(done) {
|
|
668
|
+
it('query middleware pollInterval captures additional unpublished creates', function(done) {
|
|
663
669
|
var clock = util.useFakeTimers();
|
|
664
670
|
var connection = this.backend.connect();
|
|
665
671
|
this.backend.suppressPublish = true;
|
|
672
|
+
this.backend.use('query', function(context, next) {
|
|
673
|
+
context.options.pollInterval = 1000;
|
|
674
|
+
next();
|
|
675
|
+
});
|
|
666
676
|
var count = 0;
|
|
667
677
|
|
|
668
|
-
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery,
|
|
678
|
+
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
|
|
669
679
|
if (err) return done(err);
|
|
670
680
|
var doc = connection.get('dogs', count.toString()).on('error', done);
|
|
671
681
|
doc.create({}, function(e) {
|
|
@@ -686,6 +696,34 @@ function commonTests(options) {
|
|
|
686
696
|
clock.tick(1);
|
|
687
697
|
});
|
|
688
698
|
|
|
699
|
+
it('does not pass client pollInterval or pollDebounce to query middleware', function(done) {
|
|
700
|
+
var connection = this.backend.connect();
|
|
701
|
+
var queryOptions;
|
|
702
|
+
this.backend.use('query', function(context, next) {
|
|
703
|
+
queryOptions = context.options;
|
|
704
|
+
next();
|
|
705
|
+
});
|
|
706
|
+
connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 1, pollDebounce: 0}, function(err) {
|
|
707
|
+
if (err) return done(err);
|
|
708
|
+
expect(queryOptions).not.to.have.any.keys('pollInterval', 'pollDebounce');
|
|
709
|
+
done();
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('ignores pollInterval from client query options', function(done) {
|
|
714
|
+
var clock = util.useFakeTimers();
|
|
715
|
+
var connection = this.backend.connect();
|
|
716
|
+
var db = this.backend.db;
|
|
717
|
+
db.pollInterval = 0;
|
|
718
|
+
sinon.spy(db, 'queryPoll');
|
|
719
|
+
connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
|
|
720
|
+
if (err) return done(err);
|
|
721
|
+
clock.tick(1000);
|
|
722
|
+
expect(db.queryPoll.called).to.be.false;
|
|
723
|
+
done();
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
689
727
|
it('query extra is returned to client', function(done) {
|
|
690
728
|
var connection = this.backend.connect();
|
|
691
729
|
this.backend.db.query = function(collection, query, fields, options, callback) {
|
|
@@ -906,7 +944,7 @@ function commonTests(options) {
|
|
|
906
944
|
connection.createSubscribeQuery(
|
|
907
945
|
'dogs',
|
|
908
946
|
this.matchAllDbQuery,
|
|
909
|
-
|
|
947
|
+
null,
|
|
910
948
|
function(err) {
|
|
911
949
|
if (err) {
|
|
912
950
|
expect(err.message).to.be.equal('TEST_ERROR');
|
|
@@ -128,6 +128,17 @@ describe('SnapshotTimestampRequest', function() {
|
|
|
128
128
|
], done);
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
it('fetches ops up to the current version when there are no milestones', function(done) {
|
|
132
|
+
sinon.spy(backend.db, 'getOps');
|
|
133
|
+
var halfwayBetweenDays2and3 = (day2 + day3) * 0.5;
|
|
134
|
+
|
|
135
|
+
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', halfwayBetweenDays2and3, function(error) {
|
|
136
|
+
if (error) return done(error);
|
|
137
|
+
expect(backend.db.getOps.calledWith('books', 'time-machine', 0, 3)).to.equal(true);
|
|
138
|
+
done();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
131
142
|
it('fetches the day 3 version when asking for a time after day 3', function(done) {
|
|
132
143
|
var connection = backend.connect();
|
|
133
144
|
async.waterfall([
|
|
@@ -229,8 +240,11 @@ describe('SnapshotTimestampRequest', function() {
|
|
|
229
240
|
});
|
|
230
241
|
|
|
231
242
|
it('returns an empty snapshot if trying to fetch a non-existent document', function(done) {
|
|
243
|
+
sinon.spy(backend.db, 'getOps');
|
|
244
|
+
|
|
232
245
|
backend.connect().fetchSnapshotByTimestamp('books', 'does-not-exist', day1, function(error, snapshot) {
|
|
233
246
|
if (error) return done(error);
|
|
247
|
+
expect(backend.db.getOps.called).to.be.false;
|
|
234
248
|
expect(snapshot).to.eql({
|
|
235
249
|
id: 'does-not-exist',
|
|
236
250
|
v: 0,
|
|
@@ -469,6 +483,7 @@ describe('SnapshotTimestampRequest', function() {
|
|
|
469
483
|
it('fetches a snapshot that matches a milestone snapshot', function(done) {
|
|
470
484
|
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');
|
|
471
485
|
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');
|
|
486
|
+
sinon.spy(db, 'getOps');
|
|
472
487
|
|
|
473
488
|
backendWithMilestones.connect()
|
|
474
489
|
.fetchSnapshotByTimestamp('books', 'mocking-bird', day2, function(error, snapshot) {
|
|
@@ -476,6 +491,7 @@ describe('SnapshotTimestampRequest', function() {
|
|
|
476
491
|
|
|
477
492
|
expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true);
|
|
478
493
|
expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true);
|
|
494
|
+
expect(db.getOps.called).to.be.false;
|
|
479
495
|
|
|
480
496
|
expect(snapshot.v).to.equal(2);
|
|
481
497
|
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lea'});
|
|
@@ -483,6 +499,27 @@ describe('SnapshotTimestampRequest', function() {
|
|
|
483
499
|
});
|
|
484
500
|
});
|
|
485
501
|
|
|
502
|
+
it('skips fetching ops when milestone times are out of version order', function(done) {
|
|
503
|
+
var milestoneAtVersion = function(version) {
|
|
504
|
+
return function(collection, id, _timestamp, callback) {
|
|
505
|
+
milestoneDb.getMilestoneSnapshot(collection, id, version, callback);
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
sinon.stub(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime').callsFake(milestoneAtVersion(4));
|
|
509
|
+
sinon.stub(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime').callsFake(milestoneAtVersion(2));
|
|
510
|
+
sinon.spy(db, 'getOps');
|
|
511
|
+
var halfwayBetweenDays3and4 = (day3 + day4) * 0.5;
|
|
512
|
+
|
|
513
|
+
backendWithMilestones.connect()
|
|
514
|
+
.fetchSnapshotByTimestamp('books', 'mocking-bird', halfwayBetweenDays3and4, function(error, snapshot) {
|
|
515
|
+
if (error) return done(error);
|
|
516
|
+
expect(db.getOps.called).to.be.false;
|
|
517
|
+
expect(snapshot.v).to.equal(4);
|
|
518
|
+
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee', year: 1959});
|
|
519
|
+
done();
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
486
523
|
it('fetches a snapshot before any milestones', function(done) {
|
|
487
524
|
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');
|
|
488
525
|
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');
|
|
@@ -513,7 +550,7 @@ describe('SnapshotTimestampRequest', function() {
|
|
|
513
550
|
|
|
514
551
|
expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true);
|
|
515
552
|
expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true);
|
|
516
|
-
expect(db.getOps.calledWith('books', 'mocking-bird', 4,
|
|
553
|
+
expect(db.getOps.calledWith('books', 'mocking-bird', 4, 5)).to.equal(true);
|
|
517
554
|
|
|
518
555
|
expect(snapshot.v).to.equal(5);
|
|
519
556
|
expect(snapshot.data).to.eql({
|
package/test/middleware.js
CHANGED
|
@@ -5,6 +5,7 @@ var types = require('../lib/types');
|
|
|
5
5
|
var errorHandler = util.errorHandler;
|
|
6
6
|
var ShareDBError = require('../lib/error');
|
|
7
7
|
var sinon = require('sinon');
|
|
8
|
+
var crypto = require('crypto');
|
|
8
9
|
var ACTIONS = require('../lib/message-actions').ACTIONS;
|
|
9
10
|
|
|
10
11
|
var ERROR_CODE = ShareDBError.CODES;
|
|
@@ -63,6 +64,101 @@ describe('middleware', function() {
|
|
|
63
64
|
done();
|
|
64
65
|
});
|
|
65
66
|
});
|
|
67
|
+
|
|
68
|
+
it('uses a clientId set on connect as the connection id and op src', function(done) {
|
|
69
|
+
var backend = this.backend;
|
|
70
|
+
backend.use('connect', function(request, next) {
|
|
71
|
+
request.agent.clientId = 'alice:1';
|
|
72
|
+
next();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
var srcs = [];
|
|
76
|
+
backend.use('submit', function(request, next) {
|
|
77
|
+
srcs.push(request.op.src);
|
|
78
|
+
next();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
backend.connect(null, null, function(connection) {
|
|
82
|
+
expect(connection.id).to.equal('alice:1');
|
|
83
|
+
connection.get('dogs', 'fido').create({age: 3}, function(error) {
|
|
84
|
+
if (error) return done(error);
|
|
85
|
+
expect(srcs).to.eql(['alice:1']);
|
|
86
|
+
done();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('restricting op sources', function() {
|
|
93
|
+
beforeEach(function() {
|
|
94
|
+
function srcFor(userId) {
|
|
95
|
+
return userId + ':' + crypto.randomUUID();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isSrcOf(userId, src) {
|
|
99
|
+
if (typeof src !== 'string') return false;
|
|
100
|
+
var separator = src.lastIndexOf(':');
|
|
101
|
+
return separator !== -1 && src.slice(0, separator) === String(userId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.backend.use('connect', function(context, next) {
|
|
105
|
+
var userId = context.req.userId;
|
|
106
|
+
context.agent.custom.userId = userId;
|
|
107
|
+
context.agent.clientId = srcFor(userId);
|
|
108
|
+
next();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.backend.use('receive', function(context, next) {
|
|
112
|
+
var message = context.data;
|
|
113
|
+
var userId = context.agent.custom.userId;
|
|
114
|
+
if (message.a === ACTIONS.handshake && message.id != null && !isSrcOf(userId, message.id)) {
|
|
115
|
+
delete message.id;
|
|
116
|
+
}
|
|
117
|
+
if (message.a === ACTIONS.op && message.src != null && !isSrcOf(userId, message.src)) {
|
|
118
|
+
return next(new Error('Op src belongs to another user'));
|
|
119
|
+
}
|
|
120
|
+
next();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('keeps a reclaimed id that names the same user', function(done) {
|
|
125
|
+
var backend = this.backend;
|
|
126
|
+
backend.connect(null, {userId: 42}, function(connection) {
|
|
127
|
+
var id = connection.id;
|
|
128
|
+
expect(id).to.match(/^42:/);
|
|
129
|
+
connection.close();
|
|
130
|
+
backend.connect(connection, {userId: 42}, function() {
|
|
131
|
+
expect(connection.id).to.equal(id);
|
|
132
|
+
done();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('replaces a reclaimed id that names another user', function(done) {
|
|
138
|
+
var backend = this.backend;
|
|
139
|
+
backend.connect(null, {userId: 42}, function(connection) {
|
|
140
|
+
connection.close();
|
|
141
|
+
connection.id = '7:stale';
|
|
142
|
+
backend.connect(connection, {userId: 42}, function() {
|
|
143
|
+
expect(connection.id).to.match(/^42:/);
|
|
144
|
+
expect(connection.id).to.equal(connection.agent.clientId);
|
|
145
|
+
done();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('rejects an op whose src names another user', function(done) {
|
|
151
|
+
this.backend.connect(null, {userId: 42}, function(connection) {
|
|
152
|
+
connection.get('dogs', 'fido').on('error', function(error) {
|
|
153
|
+
expect(error.message).to.equal('Op src belongs to another user');
|
|
154
|
+
done();
|
|
155
|
+
});
|
|
156
|
+
connection.send({
|
|
157
|
+
a: ACTIONS.op, c: 'dogs', d: 'fido', v: 0, src: '7:1', seq: 1,
|
|
158
|
+
create: {type: 'json0', data: {}}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
66
162
|
});
|
|
67
163
|
|
|
68
164
|
describe('readSnapshots', function() {
|
package/test/ot.js
CHANGED
|
@@ -159,6 +159,22 @@ describe('ot', function() {
|
|
|
159
159
|
expect({}.polluted).to.equal(undefined);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
it('returns an error for a path segment that cannot be converted to a key', function() {
|
|
163
|
+
var error;
|
|
164
|
+
expect(function() {
|
|
165
|
+
error = ot.apply(snapshot, {v: 6, op: [{p: ['colour', {toString: 'x'}], oi: 'yes'}]});
|
|
166
|
+
}).to.not.throw();
|
|
167
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_NOT_APPLIED);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns an error for an op whose length cannot be converted to a number', function() {
|
|
171
|
+
var error;
|
|
172
|
+
expect(function() {
|
|
173
|
+
error = ot.apply(snapshot, {v: 6, op: {length: {toString: 'x'}}});
|
|
174
|
+
}).to.not.throw();
|
|
175
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_NOT_APPLIED);
|
|
176
|
+
});
|
|
177
|
+
|
|
162
178
|
['__proto__', 'constructor'].forEach(function(badProp) {
|
|
163
179
|
it('rejects ' + badProp + ' as a path segment', function() {
|
|
164
180
|
var error = ot.apply(snapshot, {v: 6, op: [{p: [badProp, 'polluted'], oi: 'yes'}]});
|
|
@@ -187,6 +203,52 @@ describe('ot', function() {
|
|
|
187
203
|
});
|
|
188
204
|
});
|
|
189
205
|
|
|
206
|
+
describe('checkSubmittedOpForType', function() {
|
|
207
|
+
it('rejects an array-like json0 op', function() {
|
|
208
|
+
var op = {op: {0: {p: ['colour'], oi: 'red'}, length: 1}};
|
|
209
|
+
var error = ot.checkSubmittedOpForType(type.uri, op);
|
|
210
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_BADLY_FORMED);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('rejects a json0 op that is not an array', function() {
|
|
214
|
+
var error = ot.checkSubmittedOpForType(type.uri, {op: {p: ['colour'], oi: 'red'}});
|
|
215
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_BADLY_FORMED);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('rejects a json0 op component that is not an object', function() {
|
|
219
|
+
var error = ot.checkSubmittedOpForType(type.uri, {op: [null]});
|
|
220
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_BADLY_FORMED);
|
|
221
|
+
expect(error.message).to.equal('Missing path');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('rejects a dangerous path segment', function() {
|
|
225
|
+
var error = ot.checkSubmittedOpForType(type.uri, {op: [{p: ['__proto__', 'x'], oi: 'yes'}]});
|
|
226
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_NOT_APPLIED);
|
|
227
|
+
expect(error.message).to.equal('Invalid path segment');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('returns an error for a path segment that cannot be converted to a key', function() {
|
|
231
|
+
var error;
|
|
232
|
+
expect(function() {
|
|
233
|
+
error = ot.checkSubmittedOpForType(type.uri, {op: [{p: ['colour', {toString: 'x'}], oi: 'yes'}]});
|
|
234
|
+
}).to.not.throw();
|
|
235
|
+
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_NOT_APPLIED);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('accepts a valid json0 op', function() {
|
|
239
|
+
expect(ot.checkSubmittedOpForType(type.uri, {op: [{p: ['colour'], oi: 'red'}]})).equal();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('leaves ops for other types alone', function() {
|
|
243
|
+
expect(ot.checkSubmittedOpForType(presenceType.uri, {op: {index: 0, value: 'hi'}})).equal();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('leaves creates and deletes alone', function() {
|
|
247
|
+
expect(ot.checkSubmittedOpForType(type.uri, {create: {type: type.uri}})).equal();
|
|
248
|
+
expect(ot.checkSubmittedOpForType(type.uri, {del: true})).equal();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
190
252
|
describe('no-op', function() {
|
|
191
253
|
it('works on existing docs', function() {
|
|
192
254
|
var doc = {v: 6, type: type.uri, data: 'Hi'};
|
package/test/projections.js
CHANGED
|
@@ -383,6 +383,24 @@ describe('projection utility methods', function() {
|
|
|
383
383
|
{x: true},
|
|
384
384
|
{op: [{p: [], oi: {y: 1}}]}
|
|
385
385
|
);
|
|
386
|
+
test(
|
|
387
|
+
{0: true},
|
|
388
|
+
{op: [{p: [0], na: 1}]}
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
[
|
|
393
|
+
null,
|
|
394
|
+
{},
|
|
395
|
+
[null],
|
|
396
|
+
[{}],
|
|
397
|
+
[{p: null}],
|
|
398
|
+
[{p: 'x'}],
|
|
399
|
+
[{p: [{toString: 'x'}]}]
|
|
400
|
+
].forEach(function(edit) {
|
|
401
|
+
it('does not allow the malformed op ' + JSON.stringify(edit), function() {
|
|
402
|
+
expect(projections.isOpAllowed(null, {x: true}, {op: edit})).equal(false);
|
|
403
|
+
});
|
|
386
404
|
});
|
|
387
405
|
});
|
|
388
406
|
|
package/test/pubsub-memory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
var MemoryPubSub = require('../lib/pubsub/memory');
|
|
2
2
|
var PubSub = require('../lib/pubsub');
|
|
3
3
|
var expect = require('chai').expect;
|
|
4
|
+
var sinon = require('sinon');
|
|
4
5
|
|
|
5
6
|
require('./pubsub')(function(callback) {
|
|
6
7
|
callback(null, new MemoryPubSub());
|
|
@@ -29,6 +30,50 @@ describe('PubSub base class', function() {
|
|
|
29
30
|
pubsub.subscribe('x');
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
it('returns the _subscribe error to every concurrent subscriber', function(done) {
|
|
34
|
+
var pubsub = new PubSub();
|
|
35
|
+
sinon.spy(pubsub, '_subscribe');
|
|
36
|
+
var errors = 0;
|
|
37
|
+
function onSubscribe(err) {
|
|
38
|
+
expect(err.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED');
|
|
39
|
+
if (++errors < 2) return;
|
|
40
|
+
expect(pubsub._subscribe).to.have.been.calledOnce;
|
|
41
|
+
done();
|
|
42
|
+
}
|
|
43
|
+
pubsub.subscribe('x', onSubscribe);
|
|
44
|
+
pubsub.subscribe('x', onSubscribe);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('calls _subscribe again after a failed subscribe', function(done) {
|
|
48
|
+
var pubsub = new PubSub();
|
|
49
|
+
pubsub.subscribe('x', function(err) {
|
|
50
|
+
expect(err.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED');
|
|
51
|
+
pubsub._subscribe = function(channel, callback) {
|
|
52
|
+
callback();
|
|
53
|
+
};
|
|
54
|
+
pubsub.subscribe('x', function(err) {
|
|
55
|
+
if (err) return done(err);
|
|
56
|
+
expect(pubsub.streamsCount).to.equal(1);
|
|
57
|
+
done();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('can resubscribe from a callback that destroyed the last stream', function(done) {
|
|
63
|
+
var pubsub = new PubSub();
|
|
64
|
+
pubsub._subscribe = function(channel, callback) {
|
|
65
|
+
callback();
|
|
66
|
+
};
|
|
67
|
+
pubsub._unsubscribe = function(channel, callback) {
|
|
68
|
+
callback();
|
|
69
|
+
};
|
|
70
|
+
pubsub.subscribe('x', function(err, stream) {
|
|
71
|
+
if (err) return done(err);
|
|
72
|
+
stream.destroy();
|
|
73
|
+
pubsub.subscribe('x', done);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
32
77
|
it('emits an error if _unsubscribe is unimplemented', function(done) {
|
|
33
78
|
var pubsub = new PubSub();
|
|
34
79
|
pubsub._subscribe = function(channel, callback) {
|
package/test/pubsub.js
CHANGED
|
@@ -60,6 +60,51 @@ module.exports = function(create) {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it('calls _subscribe once for concurrent subscribes to a channel', function(done) {
|
|
64
|
+
var pubsub = this.pubsub;
|
|
65
|
+
var _subscribe = pubsub._subscribe;
|
|
66
|
+
var subscribeCalls = 0;
|
|
67
|
+
pubsub._subscribe = function() {
|
|
68
|
+
subscribeCalls++;
|
|
69
|
+
return _subscribe.apply(this, arguments);
|
|
70
|
+
};
|
|
71
|
+
var received = [];
|
|
72
|
+
var finished = 0;
|
|
73
|
+
function onSubscribe(err, stream) {
|
|
74
|
+
if (err) return done(err);
|
|
75
|
+
var messages = [];
|
|
76
|
+
received.push(messages);
|
|
77
|
+
stream.on('data', function(data) {
|
|
78
|
+
messages.push(data.n);
|
|
79
|
+
if (data.n !== 2 || ++finished < 2) return;
|
|
80
|
+
expect(received).eql([[1, 2], [1, 2]]);
|
|
81
|
+
done();
|
|
82
|
+
});
|
|
83
|
+
if (received.length < 2) return;
|
|
84
|
+
expect(subscribeCalls).equal(1);
|
|
85
|
+
pubsub.publish(['x'], {n: 1});
|
|
86
|
+
pubsub.publish(['x'], {n: 2});
|
|
87
|
+
}
|
|
88
|
+
pubsub.subscribe('x', onSubscribe);
|
|
89
|
+
pubsub.subscribe('x', onSubscribe);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('keeps a channel subscribed when a concurrent subscriber destroys its stream', function(done) {
|
|
93
|
+
var pubsub = this.pubsub;
|
|
94
|
+
pubsub.subscribe('x', function(err, stream) {
|
|
95
|
+
if (err) return done(err);
|
|
96
|
+
stream.destroy();
|
|
97
|
+
});
|
|
98
|
+
pubsub.subscribe('x', function(err, stream) {
|
|
99
|
+
if (err) return done(err);
|
|
100
|
+
stream.on('data', function(data) {
|
|
101
|
+
expect(data).eql({test: true});
|
|
102
|
+
done();
|
|
103
|
+
});
|
|
104
|
+
pubsub.publish(['x'], {test: true});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
63
108
|
it('stream.destroy() unsubscribes from a channel', function(done) {
|
|
64
109
|
var pubsub = this.pubsub;
|
|
65
110
|
pubsub.subscribe('x', function(err, stream) {
|