huridocs/uwazi

View on GitHub
app/api/common.v2/database/specs/MongoDataSourceSync.spec.ts

Summary

Maintainability
C
1 day
Test Coverage
/* eslint-disable max-statements */
import { StandardLogger } from 'api/log.v2/infrastructure/StandardLogger';
import { getIdMapper } from 'api/utils/fixturesFactory';
import { testingEnvironment } from 'api/utils/testingEnvironment';
import testingDB from 'api/utils/testing_db';
import { MongoClient, ObjectId } from 'mongodb';
import { MongoDataSource } from '../MongoDataSource';
import { MongoTransactionManager } from '../MongoTransactionManager';
import { getClient, getConnection, getTenant } from '../getConnectionForCurrentTenant';

const id = getIdMapper();

const blankState = [
  {
    _id: id('collection id 1'),
    data: 'some old data',
  },
  {
    _id: id('collection id 2'),
    data: 'some old data 2',
  },
];

const updateLogsBlankState = [
  {
    _id: new ObjectId(),
    timestamp: 123,
    namespace: 'collection',
    mongoId: id('collection id 1'),
    deleted: false,
  },
  {
    _id: new ObjectId(),
    timestamp: 1234,
    namespace: 'collection',
    mongoId: id('collection id 2'),
    deleted: false,
  },
];

const createTransactionManager = (client?: MongoClient) =>
  new MongoTransactionManager(client ?? getClient(), new StandardLogger(() => {}, getTenant()));

beforeEach(async () => {
  await testingEnvironment.setUp({
    collection: blankState,
    updatelogs: updateLogsBlankState,
  });
});

afterAll(async () => {
  await testingEnvironment.tearDown();
});

class DataSource extends MongoDataSource<{ data: string }> {
  protected collectionName = 'collection';

  collection() {
    return this.getCollection();
  }
}

const updateLog = (partialUpdateLog: {
  mongoId: ObjectId;
  timestamp?: number;
  deleted?: boolean;
}) => ({
  _id: expect.any(ObjectId),
  timestamp: 1,
  namespace: 'collection',
  deleted: false,
  ...partialUpdateLog,
});

const updatedTimestamp = 2;

describe('collection with automatic log to updatelogs', () => {
  describe('should log creations/updates/deletions on updatelogs collection', () => {
    beforeEach(() => {
      jest.spyOn(Date, 'now').mockReturnValue(1);
    });

    const casesForUpdates = [
      {
        description: 'insertOne',
        callback: async (ds: DataSource) =>
          ds.collection().insertOne({ _id: id('some_data'), data: 'some data' }),
        expectedDBState: [...updateLogsBlankState, updateLog({ mongoId: id('some_data') })],
        expectedResult: { acknowledged: true, insertedId: id('some_data') },
      },
      {
        description: 'insertMany',
        callback: async (ds: DataSource) =>
          ds.collection().insertMany([
            { _id: id('data 1'), data: 'data 1' },
            { _id: id('data 2'), data: 'data 2' },
          ]),
        expectedDBState: [
          ...updateLogsBlankState,
          updateLog({ mongoId: id('data 1') }),
          updateLog({ mongoId: id('data 2') }),
        ],
        expectedResult: {
          acknowledged: true,
          insertedCount: 2,
          insertedIds: { 0: id('data 1'), 1: id('data 2') },
        },
      },
      {
        description: 'updateOne',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .updateOne({ _id: id('collection id 1') }, { $set: { data: 'updated data' } });
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp },
          updateLogsBlankState[1],
        ],
        expectedResult: { acknowledged: true, matchedCount: 1, modifiedCount: 1 },
      },
      {
        description: 'updateOne without previous updatelog',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          await getConnection().collection('updatelogs').deleteMany();
          return ds
            .collection()
            .updateOne({ _id: id('collection id 1') }, { $set: { data: 'updated data' } });
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp, _id: expect.any(ObjectId) },
        ],
        expectedDBStateOnTransactionError: [],
        expectedResult: { acknowledged: true, matchedCount: 1, modifiedCount: 1 },
      },
      {
        description: 'updateOne with upsert',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .updateOne(
              { _id: id('non existent') },
              { $set: { data: 'updated data' } },
              { upsert: true }
            );
        },
        expectedDBState: [
          ...updateLogsBlankState,
          updateLog({ mongoId: id('non existent'), timestamp: updatedTimestamp }),
        ],
        expectedResult: { acknowledged: true, upsertedCount: 1, upsertedId: id('non existent') },
      },
      {
        description: 'updateOne with 0 matches',
        callback: async (ds: DataSource) =>
          ds
            .collection()
            .updateOne({ _id: id('non existent') }, { $set: { data: 'updated data' } }),
        expectedDBState: updateLogsBlankState,
        expectedResult: { acknowledged: true, matchedCount: 0 },
      },
      {
        description: 'replaceOne',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .replaceOne({ _id: id('collection id 1') }, { data: 'replaced document' });
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp },
          updateLogsBlankState[1],
        ],
        expectedResult: { acknowledged: true, matchedCount: 1, modifiedCount: 1 },
      },
      {
        description: 'replaceOne with upsert',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .replaceOne(
              { _id: id('non existent id') },
              { data: 'replaced document' },
              { upsert: true }
            );
        },
        expectedDBState: [
          ...updateLogsBlankState,
          updateLog({ mongoId: id('non existent id'), timestamp: updatedTimestamp }),
        ],
        expectedResult: { acknowledged: true, upsertedCount: 1, upsertedId: id('non existent id') },
      },
      {
        description: 'replaceOne without previous updatelog',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          await getConnection().collection('updatelogs').deleteMany();
          return ds
            .collection()
            .replaceOne({ _id: id('collection id 1') }, { data: 'updated data' });
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp, _id: expect.any(ObjectId) },
        ],
        expectedResult: { acknowledged: true, upsertedCount: 0 },
      },
      {
        description: 'findOneAndUpdate',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .findOneAndUpdate(
              { _id: id('collection id 1') },
              { $set: { data: 'updated data' } },
              { includeResultMetadata: true }
            );
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp },
          updateLogsBlankState[1],
        ],
        expectedResult: { ok: 1, value: { _id: id('collection id 1'), data: 'some old data' } },
      },
      {
        description: 'findOneAndUpdate with upsert',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .findOneAndUpdate(
              { _id: id('non existent id') },
              { $set: { data: 'upserted data' } },
              { upsert: true, includeResultMetadata: true }
            );
        },
        expectedDBState: [
          ...updateLogsBlankState,
          updateLog({ mongoId: id('non existent id'), timestamp: updatedTimestamp }),
        ],
        expectedResult: {
          lastErrorObject: { n: 1, updatedExisting: false, upserted: id('non existent id') },
          ok: 1,
        },
      },
      {
        description: 'findOneAndUpdate without previous updatelog ',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          await getConnection().collection('updatelogs').deleteMany();
          return ds
            .collection()
            .findOneAndUpdate(
              { _id: id('collection id 1') },
              { $set: { data: 'updated data' } },
              { includeResultMetadata: true }
            );
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp, _id: expect.any(ObjectId) },
        ],
        expectedResult: { ok: 1, value: { _id: id('collection id 1'), data: 'some old data' } },
      },
      {
        description: 'findOneAndReplace',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .findOneAndReplace(
              { _id: id('collection id 1') },
              { data: 'updated data' },
              { includeResultMetadata: true }
            );
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp },
          updateLogsBlankState[1],
        ],
        expectedResult: { ok: 1, value: { _id: id('collection id 1'), data: 'some old data' } },
      },
      {
        description: 'findOneAndReplace with upsert',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .findOneAndReplace(
              { _id: id('non existent id') },
              { data: 'upserted data' },
              { upsert: true, includeResultMetadata: true }
            );
        },
        expectedDBState: [
          ...updateLogsBlankState,
          updateLog({ mongoId: id('non existent id'), timestamp: updatedTimestamp }),
        ],
        expectedResult: {
          lastErrorObject: { n: 1, updatedExisting: false, upserted: id('non existent id') },
          ok: 1,
        },
      },
      {
        description: 'findOneAndReplace without previous updatelogs',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          await getConnection().collection('updatelogs').deleteMany();
          return ds
            .collection()
            .findOneAndReplace(
              { _id: id('collection id 1') },
              { data: 'updated data' },
              { includeResultMetadata: true }
            );
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp, _id: expect.any(ObjectId) },
        ],
        expectedResult: { ok: 1, value: { _id: id('collection id 1'), data: 'some old data' } },
      },
      {
        description: 'updateMany',
        callback: async (ds: DataSource) => {
          await ds.collection().insertMany([
            { _id: id('data 1'), data: 'data 1' },
            { _id: id('data 2'), data: 'data 2' },
          ]);
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds.collection().updateMany({}, { $set: { data: 'updated data' } });
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp },
          { ...updateLogsBlankState[1], timestamp: updatedTimestamp },
          updateLog({ mongoId: id('data 1'), timestamp: updatedTimestamp }),
          updateLog({ mongoId: id('data 2'), timestamp: updatedTimestamp }),
        ],
        expectedResult: { acknowledged: true, matchedCount: 4, modifiedCount: 4 },
      },
      {
        description: 'updateMany with upsert',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .updateMany(
              { _id: id('non existent id') },
              { $set: { _id: id('non existent id'), data: 'updated data' } },
              { upsert: true }
            );
        },
        expectedDBState: [
          ...updateLogsBlankState,
          updateLog({ mongoId: id('non existent id'), timestamp: updatedTimestamp }),
        ],
        expectedResult: { acknowledged: true, upsertedCount: 1, upsertedId: id('non existent id') },
      },
      {
        description: 'updateMany without previous updatelogs',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          await getConnection().collection('updatelogs').deleteMany();
          return ds.collection().updateMany({}, { $set: { data: 'updated data' } });
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp, _id: expect.any(ObjectId) },
          { ...updateLogsBlankState[1], timestamp: updatedTimestamp, _id: expect.any(ObjectId) },
        ],
        expectedResult: { acknowledged: true, matchedCount: 2, modifiedCount: 2 },
      },
      {
        description: 'updateMany with 0 matches',
        callback: async (ds: DataSource) =>
          ds
            .collection()
            .updateMany({ property: 'does not exist' }, { $set: { data: 'updated data' } }),
        expectedDBState: updateLogsBlankState,
        expectedResult: { acknowledged: true, matchedCount: 0 },
      },
      {
        description: 'deleteOne',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds.collection().deleteOne({ _id: id('collection id 1') });
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], deleted: true, timestamp: updatedTimestamp },
          updateLogsBlankState[1],
        ],
        expectedResult: { acknowledged: true },
      },
      {
        description: 'deleteOne without previous updatelogs',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          await getConnection().collection('updatelogs').deleteMany();
          return ds.collection().deleteOne({ _id: id('collection id 1') });
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          {
            ...updateLogsBlankState[0],
            deleted: true,
            timestamp: updatedTimestamp,
            _id: expect.any(ObjectId),
          },
        ],
        expectedResult: { acknowledged: true },
      },
      {
        description: 'deleteMany',
        callback: async (ds: DataSource) => {
          await ds.collection().insertOne({ _id: id('data 1'), data: 'data 1' });
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .deleteMany({ _id: { $in: [id('collection id 1'), id('data 1')] } });
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], deleted: true, timestamp: updatedTimestamp },
          updateLogsBlankState[1],
          updateLog({ mongoId: id('data 1'), timestamp: updatedTimestamp, deleted: true }),
        ],
        expectedResult: { acknowledged: true },
      },
      {
        description: 'deleteMany without previous updatelogs',
        callback: async (ds: DataSource) => {
          await getConnection().collection('updatelogs').deleteMany();
          await ds.collection().insertOne({ _id: id('data 1'), data: 'data 1' });
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .deleteMany({ _id: { $in: [id('collection id 1'), id('data 1')] } });
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          updateLog({ mongoId: id('data 1'), timestamp: updatedTimestamp, deleted: true }),
          {
            ...updateLogsBlankState[0],
            deleted: true,
            timestamp: updatedTimestamp,
            _id: expect.any(ObjectId),
          },
        ],
        expectedResult: { acknowledged: true },
      },
      {
        description: 'findOneAndDelete',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .findOneAndDelete({ _id: id('collection id 1') }, { includeResultMetadata: true });
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], deleted: true, timestamp: updatedTimestamp },
          updateLogsBlankState[1],
        ],
        expectedResult: {
          ok: 1,
          value: { _id: id('collection id 1'), data: 'some old data' },
        },
      },
      {
        description: 'findOneAndDelete without previous updatelogs',
        callback: async (ds: DataSource) => {
          await getConnection().collection('updatelogs').deleteMany();
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .findOneAndDelete({ _id: id('collection id 1') }, { includeResultMetadata: true });
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          {
            ...updateLogsBlankState[0],
            deleted: true,
            timestamp: updatedTimestamp,
            _id: expect.any(ObjectId),
          },
        ],
        expectedResult: {
          ok: 1,
          value: { _id: id('collection id 1'), data: 'some old data' },
        },
      },
      {
        description: 'bulkWrite (insert)',
        callback: async (ds: DataSource) => {
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .bulkWrite([
              { insertOne: { document: { _id: id('insert one'), data: 'insert one' } } },
              { insertOne: { document: { _id: id('insert many 1'), data: 'insert many 1' } } },
              { insertOne: { document: { _id: id('insert many 2'), data: 'insert many 2' } } },
            ]);
        },
        expectedDBState: [
          ...updateLogsBlankState,
          updateLog({ mongoId: id('insert one'), timestamp: updatedTimestamp }),
          updateLog({ mongoId: id('insert many 1'), timestamp: updatedTimestamp }),
          updateLog({ mongoId: id('insert many 2'), timestamp: updatedTimestamp }),
        ],
        expectedResult: {
          insertedCount: 3,
          insertedIds: { 0: id('insert one'), 1: id('insert many 1'), 2: id('insert many 2') },
        },
      },
      {
        description: 'bulkWrite (delete)',
        callback: async (ds: DataSource) => {
          await ds.collection().insertMany([
            { _id: id('data 1'), data: 'data 1' },
            { _id: id('data 2'), data: 'data 2' },
          ]);
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .bulkWrite([
              { deleteOne: { filter: { _id: id('collection id 1') } } },
              { deleteMany: { filter: { _id: { $in: [id('data 1'), id('data 2')] } } } },
            ]);
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp, deleted: true },
          updateLogsBlankState[1],
          updateLog({ mongoId: id('data 1'), timestamp: updatedTimestamp, deleted: true }),
          updateLog({ mongoId: id('data 2'), timestamp: updatedTimestamp, deleted: true }),
        ],
        expectedResult: { deletedCount: 3 },
      },
      {
        description: 'bulkWrite (delete) without previous updatelogs',
        callback: async (ds: DataSource) => {
          await getConnection().collection('updatelogs').deleteMany();
          await ds.collection().insertMany([
            { _id: id('data 1'), data: 'data 1' },
            { _id: id('data 2'), data: 'data 2' },
          ]);
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds
            .collection()
            .bulkWrite([
              { deleteOne: { filter: { _id: id('collection id 1') } } },
              { deleteMany: { filter: { _id: { $in: [id('data 1'), id('data 2')] } } } },
            ]);
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          updateLog({ mongoId: id('data 1'), timestamp: updatedTimestamp, deleted: true }),
          updateLog({ mongoId: id('data 2'), timestamp: updatedTimestamp, deleted: true }),
          {
            ...updateLogsBlankState[0],
            timestamp: updatedTimestamp,
            deleted: true,
            _id: expect.any(ObjectId),
          },
        ],
        expectedResult: { deletedCount: 3 },
      },
      {
        description: 'bulkWrite (update/replace)',
        callback: async (ds: DataSource) => {
          await ds.collection().insertMany([
            { _id: id('data 1'), data: 'data 1' },
            { _id: id('data 2'), data: 'data 2' },
          ]);
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds.collection().bulkWrite([
            {
              updateOne: {
                filter: { _id: id('collection id 1') },
                update: { $set: { data: 'collection id 1 updated' } },
              },
            },
            {
              updateOne: {
                filter: { _id: id('upserted') },
                update: { $set: { data: 'upserted' } },
                upsert: true,
              },
            },
            {
              updateMany: {
                filter: { _id: { $in: [id('data 1'), id('data 2')] } },
                update: { $set: { data: 'data 1 and 2 updated' } },
              },
            },
          ]);
        },
        expectedDBState: [
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp },
          updateLogsBlankState[1],
          updateLog({ mongoId: id('data 1'), timestamp: updatedTimestamp }),
          updateLog({ mongoId: id('data 2'), timestamp: updatedTimestamp }),
          updateLog({ mongoId: id('upserted'), timestamp: updatedTimestamp }),
        ],
        expectedResult: {
          matchedCount: 3,
          modifiedCount: 3,
          upsertedCount: 1,
          upsertedIds: { 1: id('upserted') },
        },
      },
      {
        description: 'bulkWrite (update/replace) without previous updatelogs',
        callback: async (ds: DataSource) => {
          await ds.collection().insertMany([
            { _id: id('data 1'), data: 'data 1' },
            { _id: id('data 2'), data: 'data 2' },
          ]);
          await getConnection().collection('updatelogs').deleteMany();
          jest.spyOn(Date, 'now').mockReturnValue(updatedTimestamp);
          return ds.collection().bulkWrite([
            {
              updateOne: {
                filter: { _id: id('collection id 1') },
                update: { $set: { data: 'collection id 1 updated' } },
              },
            },
            {
              updateOne: {
                filter: { _id: id('upserted') },
                update: { $set: { data: 'upserted' } },
                upsert: true,
              },
            },
            {
              updateMany: {
                filter: { _id: { $in: [id('data 1'), id('data 2')] } },
                update: { $set: { data: 'data 1 and 2 updated' } },
              },
            },
          ]);
        },
        expectedDBStateOnTransactionError: [],
        expectedDBState: [
          updateLog({ mongoId: id('data 1'), timestamp: updatedTimestamp }),
          updateLog({ mongoId: id('data 2'), timestamp: updatedTimestamp }),
          updateLog({ mongoId: id('upserted'), timestamp: updatedTimestamp }),
          { ...updateLogsBlankState[0], timestamp: updatedTimestamp, _id: expect.any(ObjectId) },
        ],
        expectedResult: {
          matchedCount: 3,
          modifiedCount: 3,
          upsertedCount: 1,
          upsertedIds: { 1: id('upserted') },
        },
      },
    ];

    it.each(casesForUpdates)(
      '$description should log changes to updatelogs if any',
      async ({
        callback,
        expectedDBState,
        expectedResult,
        expectedDBStateOnTransactionError = updateLogsBlankState,
      }) => {
        const transactionManager1 = createTransactionManager();
        const dataSource1 = new DataSource(getConnection(), transactionManager1);

        try {
          await transactionManager1.run(async () => {
            await callback(dataSource1);
            throw new Error('make it fail');
          });
        } catch (e) {
          if (e.message !== 'make it fail') {
            throw e;
          }
          expect(await testingDB.mongodb?.collection('updatelogs').find({}).toArray()).toEqual(
            expectedDBStateOnTransactionError
          );
        }

        const transactionManager2 = createTransactionManager();
        const dataSource2 = new DataSource(getConnection(), transactionManager2);

        const result = await transactionManager2.run(async () => callback(dataSource2));

        expect(result).toMatchObject(expectedResult);
        expect(await testingDB.mongodb?.collection('updatelogs').find({}).toArray()).toEqual(
          expectedDBState
        );
      }
    );
  });
});