feathersjs/feathers-offline-realtime

View on GitHub
test/commons/helpers/optimistic-mutator-online.test.js

Summary

Maintainability
D
2 days
Test Coverage

const assert = require('chai').assert;
const feathers = require('feathers');
const memory = require('feathers-memory');
const hooks = require('feathers-hooks');
const errors = require('feathers-errors');

const optimisticMutator = require('../../../src/optimistic-mutator');

const sampleLen = 5;

let app;
let clientService;

function services1 () {
  app = this;

  app.configure(fromServiceNonPaginatedConfig);
}

function fromServiceNonPaginatedConfig () {
  const app = this;

  app.use('/from', memory({}));

  app.service('from').hooks({
    before: {
      all: context => {
        if (context.params.query && context.params.query._fail) {
          throw new errors.BadRequest('Fail requested');
        }
      }
    }
  });
}

export default function (Replicator, desc) {
  describe(`${desc} - optimistic mutation online`, () => {
    let data;
    let fromService;
    let replicator;

    beforeEach(() => {
      const app = feathers()
        .configure(hooks())
        .configure(services1);

      fromService = app.service('from');

      data = [];
      for (let i = 0, len = sampleLen; i < len; i += 1) {
        data.push({ id: i, uuid: 1000 + i, order: i });
      }
    });

    describe('not connected', () => {
      let events;

      beforeEach(() => {
        events = [];

        return fromService.create(clone(data))
          .then(() => {
            replicator = new Replicator(fromService, { sort: Replicator.sort('order'), uuid: true });

            app.use('clientService', optimisticMutator({ replicator }));

            clientService = app.service('clientService');

            replicator.on('events', (records, last) => {
              events[events.length] = last;
            });
          });
      });

      it('create fails', () => {
        return clientService.create({ id: 99, uuid: 1099, order: 99 })
          .then(() => {
            assert(false, 'Unexpectedly succeeded.');
          })
          .catch(err => {
            assert.equal(err.className, 'bad-request');
          });
      });
    });

    describe('without publication', () => {
      let events;

      beforeEach(() => {
        events = [];

        return fromService.create(clone(data))
          .then(() => {
            replicator = new Replicator(fromService, { sort: Replicator.sort('order'), uuid: true });

            app.use('clientService', optimisticMutator({ replicator }));

            clientService = app.service('clientService');

            replicator.on('events', (records, last) => {
              events[events.length] = last;
            });
          });
      });

      it('find works', () => {
        return replicator.connect()
          .then(() => clientService.find({ query: { order: { $lt: 3 } } }))
          .then(result => {
            const records = replicator.store.records;

            assert.deepEqual(result, data.slice(0, 3));
            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' }
            ]);

            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);
          })
          .then(() => replicator.disconnect());
      });

      it('get works', () => {
        return replicator.connect()
          .then(() => clientService.get(1000))
          .then(result => {
            const records = replicator.store.records;

            assert.deepEqual(result, { id: 0, uuid: 1000, order: 0 });
            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' }
            ]);

            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);
          })
          .then(() => replicator.disconnect());
      });

      it('create works', () => {
        return replicator.connect()
          .then(() => clientService.create({ id: 99, uuid: 1099, order: 99 }))
          .then(delay())
          .then(result => {
            const records = replicator.store.records;

            data[sampleLen] = { id: 99, uuid: 1099, order: 99 };

            assert.deepEqual(result, { id: 99, uuid: 1099, order: 99 });
            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'created', action: 'mutated', record: { id: 99, uuid: 1099, order: 99 } },
              { source: 0, eventName: 'created', action: 'mutated', record: { id: 99, uuid: 1099, order: 99 } }
            ]);

            assert.lengthOf(records, sampleLen + 1);
            assert.deepEqual(records, data);
          })
          .then(() => replicator.disconnect());
      });

      it('create adds missing uuid', () => {
        return replicator.connect()
          .then(() => clientService.create({ id: 99, order: 99 }))
          .then(data => {
            assert.isString(data.uuid);
          })
          .then(() => replicator.disconnect());
      });

      it('update works', () => {
        return replicator.connect()
          .then(() => clientService.update(1000, { id: 0, uuid: 1000, order: 99 }))
          .then(delay())
          .then(result => {
            const records = replicator.store.records;
            data.splice(0, 1);
            data[data.length] = { id: 0, uuid: 1000, order: 99 };

            assert.deepEqual(result, { id: 0, uuid: 1000, order: 99 });
            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'updated', action: 'mutated', record: { id: 0, uuid: 1000, order: 99 } },
              { source: 0, eventName: 'updated', action: 'mutated', record: { id: 0, uuid: 1000, order: 99 } }
            ]);
          });
      });

      it('patch works', () => {
        return replicator.connect()
          .then(() => clientService.patch(1001, { order: 99 }))
          .then(delay())
          .then(result => {
            const records = replicator.store.records;
            data.splice(1, 1);
            data[data.length] = { id: 1, uuid: 1001, order: 99 };

            assert.deepEqual(result, { id: 1, uuid: 1001, order: 99 });
            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'patched', action: 'mutated', record: { id: 1, uuid: 1001, order: 99 } },
              { source: 0, eventName: 'patched', action: 'mutated', record: { id: 1, uuid: 1001, order: 99 } }
            ]);
          });
      });

      it('remove works', () => {
        return replicator.connect()
          .then(() => clientService.remove(1002))
          .then(delay())
          .then(result => {
            const records = replicator.store.records;
            data.splice(2, 1);

            assert.deepEqual(result, { id: 2, uuid: 1002, order: 2 });
            assert.lengthOf(records, sampleLen - 1);
            assert.deepEqual(records, data);

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'removed', action: 'remove', record: { id: 2, uuid: 1002, order: 2 } },
              { source: 0, eventName: 'removed', action: 'remove', record: { id: 2, uuid: 1002, order: 2 } }
            ]);
          });
      });
    });

    describe('without publication, null id', () => {
      let events;

      beforeEach(() => {
        events = [];

        return fromService.create(clone(data))
          .then(() => {
            replicator = new Replicator(fromService, {sort: Replicator.sort('order'), uuid: true});

            app.use('clientService', optimisticMutator({replicator}));

            clientService = app.service('clientService');

            replicator.on('events', (records, last) => {
              events[events.length] = last;
            });
          });
      });

      it('create works', () => {
        return replicator.connect()
          .then(() => clientService.create([
            { id: 98, uuid: 1098, order: 98 },
            { id: 99, uuid: 1099, order: 99 }
          ]))
          .then(delay())
          .then(result => {
            const records = replicator.store.records;

            data[sampleLen] = { id: 98, uuid: 1098, order: 98 };
            data[sampleLen + 1] = { id: 99, uuid: 1099, order: 99 };

            assert.deepEqual(result, [
              { id: 98, uuid: 1098, order: 98 },
              { id: 99, uuid: 1099, order: 99 }
            ]);
            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'created', action: 'mutated', record: { id: 98, uuid: 1098, order: 98 } },
              { source: 1, eventName: 'created', action: 'mutated', record: { id: 99, uuid: 1099, order: 99 } },
              { source: 0, eventName: 'created', action: 'mutated', record: { id: 98, uuid: 1098, order: 98 } },
              { source: 0, eventName: 'created', action: 'mutated', record: { id: 99, uuid: 1099, order: 99 } }
            ]);

            assert.lengthOf(records, sampleLen + 2);
            assert.deepEqual(records, data);
          })
          .then(() => replicator.disconnect());
      });

      it('patch works', () => {
        return replicator.connect()
          .then(() => clientService.patch(null, { foo: 1 }, { query: { order: { $gt: 0, $lt: 4 } } }))
          .then(delay())
          .then(result => {
            const records = replicator.store.records;

            data[1].foo = 1;
            data[2].foo = 1;
            data[3].foo = 1;

            assert.deepEqual(result, [
              { id: 1, uuid: 1001, order: 1, foo: 1 },
              { id: 2, uuid: 1002, order: 2, foo: 1 },
              { id: 3, uuid: 1003, order: 3, foo: 1 }
            ]);

            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'patched', action: 'mutated', record: { id: 1, uuid: 1001, order: 1, foo: 1 } },
              { source: 1, eventName: 'patched', action: 'mutated', record: { id: 2, uuid: 1002, order: 2, foo: 1 } },
              { source: 1, eventName: 'patched', action: 'mutated', record: { id: 3, uuid: 1003, order: 3, foo: 1 } },
              { source: 0, eventName: 'patched', action: 'mutated', record: { id: 1, uuid: 1001, order: 1, foo: 1 } },
              { source: 0, eventName: 'patched', action: 'mutated', record: { id: 2, uuid: 1002, order: 2, foo: 1 } },
              { source: 0, eventName: 'patched', action: 'mutated', record: { id: 3, uuid: 1003, order: 3, foo: 1 } }
            ]);
          });
      });

      it('remove works', () => {
        return replicator.connect()
          .then(() => clientService.remove(null, { query: { order: { $gt: 0, $lt: 4 } } }))
          .then(delay())
          .then(result => {
            const records = replicator.store.records;
            data.splice(1, 3);

            assert.deepEqual(result, [
              { id: 1, uuid: 1001, order: 1 },
              { id: 2, uuid: 1002, order: 2 },
              { id: 3, uuid: 1003, order: 3 }
            ]);

            assert.lengthOf(records, sampleLen - 3);
            assert.deepEqual(records, data);

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'removed', action: 'remove', record: { id: 1, uuid: 1001, order: 1 } },
              { source: 1, eventName: 'removed', action: 'remove', record: { id: 2, uuid: 1002, order: 2 } },
              { source: 1, eventName: 'removed', action: 'remove', record: { id: 3, uuid: 1003, order: 3 } },
              { source: 0, eventName: 'removed', action: 'remove', record: { id: 1, uuid: 1001, order: 1 } },
              { source: 0, eventName: 'removed', action: 'remove', record: { id: 2, uuid: 1002, order: 2 } },
              { source: 0, eventName: 'removed', action: 'remove', record: { id: 3, uuid: 1003, order: 3 } }
            ]);
          });
      });
    });

    describe('without publication & remote error', () => {
      let events;

      beforeEach(() => {
        events = [];

        return fromService.create(clone(data))
          .then(() => {
            replicator = new Replicator(fromService, { sort: Replicator.sort('order'), uuid: true });

            app.use('clientService', optimisticMutator({ replicator }));

            clientService = app.service('clientService');

            replicator.on('events', (records, last) => {
              events[events.length] = last;
            });
          });
      });

      it('get fails correctly', () => {
        return replicator.connect()
          .then(() => clientService.get(9999))
          .then(() => {
            assert(false, 'Unexpectedly succeeded');
          })
          .catch(err => {
            assert.equal(err.className, 'not-found');
          })
          .then(() => replicator.disconnect());
      });

      it('create recovers', () => {
        return replicator.connect()
          .then(() => clientService.create({ id: 99, uuid: 1099, order: 99 }, { query: { _fail: true } }))
          .then(delay())
          .then(() => {
            const records = replicator.store.records;

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'created', action: 'mutated', record: { id: 99, uuid: 1099, order: 99 } },
              { source: 2, eventName: 'removed', action: 'remove', record: { id: 99, uuid: 1099, order: 99 } }
            ]);

            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);
          })
          .then(() => replicator.disconnect());
      });

      it('update recovers', () => {
        return replicator.connect()
          .then(() => clientService.update(1000, { id: 0, uuid: 1000, order: 99 }, { query: { _fail: true } }))
          .then(delay())
          .then(() => {
            const records = replicator.store.records;

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'updated', action: 'mutated', record: { id: 0, uuid: 1000, order: 99 } },
              { source: 2, eventName: 'updated', action: 'mutated', record: { id: 0, uuid: 1000, order: 0 } }
            ]);

            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);
          })
          .then(() => replicator.disconnect());
      });

      it('patch recovers', () => {
        return replicator.connect()
          .then(() => clientService.patch(1001, { order: 99 }, { query: { _fail: true } }))
          .then(delay())
          .then(() => {
            const records = replicator.store.records;

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'patched', action: 'mutated', record: { id: 1, uuid: 1001, order: 99 } },
              { source: 2, eventName: 'updated', action: 'mutated', record: { id: 1, uuid: 1001, order: 1 } }
            ]);

            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);
          })
          .then(() => replicator.disconnect());
      });

      it('remove recovers', () => {
        return replicator.connect()
          .then(() => clientService.remove(1002, { query: { _fail: true } }))
          .then(delay())
          .then(() => {
            const records = replicator.store.records;

            assert.deepEqual(events, [
              { action: 'snapshot' },
              { action: 'add-listeners' },
              { source: 1, eventName: 'removed', action: 'remove', record: { id: 2, uuid: 1002, order: 2 } },
              { source: 2, eventName: 'created', action: 'mutated', record: { id: 2, uuid: 1002, order: 2 } }
            ]);

            assert.lengthOf(records, sampleLen);
            assert.deepEqual(records, data);
          })
          .then(() => replicator.disconnect());
      });
    });
  });
}

// Helpers

function clone (obj) {
  return JSON.parse(JSON.stringify(obj));
}

function delay (ms = 0) {
  return data => new Promise(resolve => {
    setTimeout(() => {
      resolve(data);
    }, ms);
  });
}