|
1 | 1 | var Backend = require('../../lib/backend');
|
2 | 2 | var expect = require('expect.js');
|
| 3 | +var util = require('../util') |
3 | 4 |
|
4 | 5 | describe('client query subscribe', function() {
|
5 | 6 |
|
@@ -212,4 +213,133 @@ describe('client query subscribe', function() {
|
212 | 213 |
|
213 | 214 | });
|
214 | 215 |
|
| 216 | + describe('submitting an invalid op', function () { |
| 217 | + var doc; |
| 218 | + var invalidOp; |
| 219 | + var validOp; |
| 220 | + |
| 221 | + beforeEach(function (done) { |
| 222 | + // This op is invalid because we try to perform a list deletion |
| 223 | + // on something that isn't a list |
| 224 | + invalidOp = {p: ['name'], ld: 'Scooby'}; |
| 225 | + |
| 226 | + validOp = {p:['snacks'], oi: true}; |
| 227 | + |
| 228 | + doc = this.connection.get('dogs', 'scooby'); |
| 229 | + doc.create({ name: 'Scooby' }, function (error) { |
| 230 | + if (error) return done(error); |
| 231 | + doc.whenNothingPending(done); |
| 232 | + }); |
| 233 | + }); |
| 234 | + |
| 235 | + it('returns an error to the submitOp callback', function (done) { |
| 236 | + doc.submitOp(invalidOp, function (error) { |
| 237 | + expect(error.message).to.equal('Referenced element not a list'); |
| 238 | + done(); |
| 239 | + }); |
| 240 | + }); |
| 241 | + |
| 242 | + it('rolls the doc back to a usable state', function (done) { |
| 243 | + util.callInSeries([ |
| 244 | + function (next) { |
| 245 | + doc.submitOp(invalidOp, function (error) { |
| 246 | + expect(error).to.be.ok(); |
| 247 | + next(); |
| 248 | + }); |
| 249 | + }, |
| 250 | + function (next) { |
| 251 | + doc.whenNothingPending(next); |
| 252 | + }, |
| 253 | + function (next) { |
| 254 | + expect(doc.data).to.eql({name: 'Scooby'}); |
| 255 | + doc.submitOp(validOp, next); |
| 256 | + }, |
| 257 | + function (next) { |
| 258 | + expect(doc.data).to.eql({name: 'Scooby', snacks: true}); |
| 259 | + next(); |
| 260 | + }, |
| 261 | + done |
| 262 | + ]); |
| 263 | + }); |
| 264 | + |
| 265 | + it('rescues an irreversible op collision', function (done) { |
| 266 | + // This test case attempts to reconstruct the following corner case, with |
| 267 | + // two independent references to the same document. We submit two simultaneous, but |
| 268 | + // incompatible operations (eg one of them changes the data structure the other op is |
| 269 | + // attempting to manipulate). |
| 270 | + // |
| 271 | + // The second document to attempt to submit should have its op rejected, and its |
| 272 | + // state successfully rolled back to a usable state. |
| 273 | + var doc1 = this.backend.connect().get('dogs', 'snoopy'); |
| 274 | + var doc2 = this.backend.connect().get('dogs', 'snoopy'); |
| 275 | + |
| 276 | + var pauseSubmit = false; |
| 277 | + var fireSubmit; |
| 278 | + this.backend.use('submit', function (request, callback) { |
| 279 | + if (pauseSubmit) { |
| 280 | + fireSubmit = function () { |
| 281 | + pauseSubmit = false; |
| 282 | + callback(); |
| 283 | + }; |
| 284 | + } else { |
| 285 | + fireSubmit = null; |
| 286 | + callback(); |
| 287 | + } |
| 288 | + }); |
| 289 | + |
| 290 | + util.callInSeries([ |
| 291 | + function (next) { |
| 292 | + doc1.create({colours: ['white']}, next); |
| 293 | + }, |
| 294 | + function (next) { |
| 295 | + doc1.whenNothingPending(next); |
| 296 | + }, |
| 297 | + function (next) { |
| 298 | + doc2.fetch(next); |
| 299 | + }, |
| 300 | + function (next) { |
| 301 | + doc2.whenNothingPending(next); |
| 302 | + }, |
| 303 | + // Both documents start off at the same v1 state, with colours as a list |
| 304 | + function (next) { |
| 305 | + expect(doc1.data).to.eql({colours: ['white']}); |
| 306 | + expect(doc2.data).to.eql({colours: ['white']}); |
| 307 | + next(); |
| 308 | + }, |
| 309 | + // doc1 successfully submits an op which changes our list into a string in v2 |
| 310 | + function (next) { |
| 311 | + doc1.submitOp({p: ['colours'], oi: 'white,black'}, next); |
| 312 | + }, |
| 313 | + // This next step is a little fiddly. We abuse the middleware to pause the op submission and |
| 314 | + // ensure that we get this repeatable sequence of events: |
| 315 | + // 1. doc2 is still on v1, where 'colours' is a list (but it's a string in v2) |
| 316 | + // 2. doc2 submits an op that assumes 'colours' is still a list |
| 317 | + // 3. doc2 fetches v2 before the op submission completes - 'colours' is no longer a list locally |
| 318 | + // 4. doc2's op is rejected by the server, because 'colours' is not a list on the server |
| 319 | + // 5. doc2 attempts to roll back the inflight op by turning a list insertion into a list deletion |
| 320 | + // 6. doc2 applies this list deletion to a field that is no longer a list |
| 321 | + // 7. type.apply throws, because this is an invalid op |
| 322 | + function (next) { |
| 323 | + pauseSubmit = true; |
| 324 | + doc2.submitOp({p: ['colours', '0'], li: 'black'}, function (error) { |
| 325 | + expect(error.message).to.equal('Referenced element not a list'); |
| 326 | + next(); |
| 327 | + }); |
| 328 | + |
| 329 | + doc2.fetch(function (error) { |
| 330 | + if (error) return next(error); |
| 331 | + fireSubmit(); |
| 332 | + }); |
| 333 | + }, |
| 334 | + // Validate that - despite the error in doc2.submitOp - doc2 has been returned to a |
| 335 | + // workable state in v2 |
| 336 | + function (next) { |
| 337 | + expect(doc1.data).to.eql({colours: 'white,black'}); |
| 338 | + expect(doc2.data).to.eql(doc1.data); |
| 339 | + doc2.submitOp({p: ['colours'], oi: 'white,black,red'}, next); |
| 340 | + }, |
| 341 | + done |
| 342 | + ]); |
| 343 | + }); |
| 344 | + }); |
215 | 345 | });
|
0 commit comments