text: // Copyright IBM Corp. 2016,2019. All Rights Reserved.
  // Node module: loopback-datasource-juggler
  // This file is licensed under the MIT License.
  // License text available at https://opensource.org/licenses/MIT
  
  'use strict';
  
  const should = require('./init.js');
  const DataSource = require('../lib/datasource.js').DataSource;
  
  describe('DataSource', function() {
    it('clones settings to prevent surprising changes in passed args', () => {
      const config = {connector: 'memory'};
  
      const ds = new DataSource(config);
      ds.settings.extra = true;
  
      config.should.eql({connector: 'memory'});
    });
  
    it('reports helpful error when connector init throws', function() {
      const throwingConnector = {
        name: 'loopback-connector-throwing',
        initialize: function(ds, cb) {
          throw new Error('expected test error');
        },
      };
  
      (function() {
        // this is what LoopBack does
        return new DataSource({
          name: 'dsname',
          connector: throwingConnector,
        });
      }).should.throw(/loopback-connector-throwing/);
    });
  
    it('reports helpful error when connector init via short name throws', function() {
      (function() {
        // this is what LoopBack does
        return new DataSource({
          name: 'dsname',
          connector: 'throwing',
        });
      }).should.throw(/expected test error/);
    });
  
    it('reports helpful error when connector init via long name throws', function() {
      (function() {
        // this is what LoopBack does
        return new DataSource({
          name: 'dsname',
          connector: 'loopback-connector-throwing',
        });
      }).should.throw(/expected test error/);
    });
  
    /**
     * new DataSource(dsName, settings) without settings.name
     */
    it('should retain the name assigned to it', function() {
      const dataSource = new DataSource('myDataSource', {
        connector: 'memory',
      });
  
      dataSource.name.should.equal('myDataSource');
    });
  
    /**
     * new DataSource(dsName, settings)
     */
    it('should allow the name assigned to it to take precedence over the settings name', function() {
      const dataSource = new DataSource('myDataSource', {
        name: 'defaultDataSource',
        connector: 'memory',
      });
  
      dataSource.name.should.equal('myDataSource');
    });
  
    /**
     * new DataSource(settings) with settings.name
     */
    it('should retain the name from the settings if no name is assigned', function() {
      const dataSource = new DataSource({
        name: 'defaultDataSource',
        connector: 'memory',
      });
  
      dataSource.name.should.equal('defaultDataSource');
    });
  
    /**
     * new DataSource(undefined, settings)
     */
    it('should retain the name from the settings if name is undefined', function() {
      const dataSource = new DataSource(undefined, {
        name: 'defaultDataSource',
        connector: 'memory',
      });
  
      dataSource.name.should.equal('defaultDataSource');
    });
  
    /**
     * new DataSource(settings) without settings.name
     */
    it('should use the connector name if no name is provided', function() {
      const dataSource = new DataSource({
        connector: 'memory',
      });
  
      dataSource.name.should.equal('memory');
    });
  
    /**
     * new DataSource(connectorInstance)
     */
    it('should accept resolved connector', function() {
      const mockConnector = {
        name: 'loopback-connector-mock',
        initialize: function(ds, cb) {
          ds.connector = mockConnector;
          return cb(null);
        },
      };
      const dataSource = new DataSource(mockConnector);
  
      dataSource.name.should.equal('loopback-connector-mock');
      dataSource.connector.should.equal(mockConnector);
    });
  
    /**
     * new DataSource(dsName, connectorInstance)
     */
    it('should accept dsName and resolved connector', function() {
      const mockConnector = {
        name: 'loopback-connector-mock',
        initialize: function(ds, cb) {
          ds.connector = mockConnector;
          return cb(null);
        },
      };
      const dataSource = new DataSource('myDataSource', mockConnector);
  
      dataSource.name.should.equal('myDataSource');
      dataSource.connector.should.equal(mockConnector);
    });
  
    /**
     * new DataSource(connectorInstance, settings)
     */
    it('should accept resolved connector and settings', function() {
      const mockConnector = {
        name: 'loopback-connector-mock',
        initialize: function(ds, cb) {
          ds.connector = mockConnector;
          return cb(null);
        },
      };
      const dataSource = new DataSource(mockConnector, {name: 'myDataSource'});
  
      dataSource.name.should.equal('myDataSource');
      dataSource.connector.should.equal(mockConnector);
    });
  
    it('should set states correctly with eager connect', function(done) {
      const mockConnector = {
        name: 'loopback-connector-mock',
        initialize: function(ds, cb) {
          ds.connector = mockConnector;
          this.connect(cb);
        },
  
        connect: function(cb) {
          process.nextTick(function() {
            cb(null);
          });
        },
      };
      const dataSource = new DataSource(mockConnector);
      // DataSource is instantiated
      // connected: false, connecting: false, initialized: false
      dataSource.connected.should.be.false();
      dataSource.connecting.should.be.false();
      dataSource.initialized.should.be.false();
  
      dataSource.on('initialized', function() {
        // DataSource is initialized with lazyConnect
        // connected: false, connecting: false, initialized: true
        dataSource.connected.should.be.false();
        dataSource.connecting.should.be.false();
        dataSource.initialized.should.be.true();
      });
  
      dataSource.on('connected', function() {
        // DataSource is now connected
        // connected: true, connecting: false
        dataSource.connected.should.be.true();
        dataSource.connecting.should.be.false();
      });
  
      // Call connect() in next tick so that we'll receive initialized event
      // first
      process.nextTick(function() {
        // At this point, the datasource is already connected by
        // connector's (mockConnector) initialize function
        dataSource.connect(function() {
          // DataSource is now connected
          // connected: true, connecting: false
          dataSource.connected.should.be.true();
          dataSource.connecting.should.be.false();
          done();
        });
        // As the datasource is already connected, no connecting will happen
        // connected: true, connecting: false
        dataSource.connected.should.be.true();
        dataSource.connecting.should.be.false();
      });
    });
  
    it('should set states correctly with deferred connect', function(done) {
      const mockConnector = {
        name: 'loopback-connector-mock',
        initialize: function(ds, cb) {
          ds.connector = mockConnector;
          // Explicitly call back with false to denote connection is not ready
          process.nextTick(function() {
            cb(null, false);
          });
        },
  
        connect: function(cb) {
          process.nextTick(function() {
            cb(null);
          });
        },
      };
      const dataSource = new DataSource(mockConnector);
      // DataSource is instantiated
      // connected: false, connecting: false, initialized: false
      dataSource.connected.should.be.false();
      dataSource.connecting.should.be.false();
      dataSource.initialized.should.be.false();
  
      dataSource.on('initialized', function() {
        // DataSource is initialized with lazyConnect
        // connected: false, connecting: false, initialized: true
        dataSource.connected.should.be.false();
        dataSource.connecting.should.be.false();
        dataSource.initialized.should.be.true();
      });
  
      dataSource.on('connected', function() {
        // DataSource is now connected
        // connected: true, connecting: false
        dataSource.connected.should.be.true();
        dataSource.connecting.should.be.false();
      });
  
      // Call connect() in next tick so that we'll receive initialized event
      // first
      process.nextTick(function() {
        dataSource.connect(function() {
          // DataSource is now connected
          // connected: true, connecting: false
          dataSource.connected.should.be.true();
          dataSource.connecting.should.be.false();
          done();
        });
        // As the datasource is not connected, connecting will happen
        // connected: false, connecting: true
        dataSource.connected.should.be.false();
        dataSource.connecting.should.be.true();
      });
    });
  
    it('should set states correctly with lazyConnect = true', function(done) {
      const mockConnector = {
        name: 'loopback-connector-mock',
        initialize: function(ds, cb) {
          ds.connector = mockConnector;
          process.nextTick(function() {
            cb(null);
          });
        },
  
        connect: function(cb) {
          process.nextTick(function() {
            cb(null);
          });
        },
      };
      const dataSource = new DataSource(mockConnector, {lazyConnect: true});
      // DataSource is instantiated
      // connected: false, connecting: false, initialized: false
      dataSource.connected.should.be.false();
      dataSource.connecting.should.be.false();
      dataSource.initialized.should.be.false();
  
      dataSource.on('initialized', function() {
        // DataSource is initialized with lazyConnect
        // connected: false, connecting: false, initialized: true
        dataSource.connected.should.be.false();
        dataSource.connecting.should.be.false();
        dataSource.initialized.should.be.true();
      });
  
      dataSource.on('connected', function() {
        // DataSource is now connected
        // connected: true, connecting: false
        dataSource.connected.should.be.true();
        dataSource.connecting.should.be.false();
      });
  
      // Call connect() in next tick so that we'll receive initialized event
      // first
      process.nextTick(function() {
        dataSource.connect(function() {
          // DataSource is now connected
          // connected: true, connecting: false
          dataSource.connected.should.be.true();
          dataSource.connecting.should.be.false();
          done();
        });
        // DataSource is now connecting
        // connected: false, connecting: true
        dataSource.connected.should.be.false();
        dataSource.connecting.should.be.true();
      });
    });
  
    it('provides stop() API calling disconnect', function(done) {
      const mockConnector = {
        name: 'loopback-connector-mock',
        initialize: function(ds, cb) {
          ds.connector = mockConnector;
          process.nextTick(function() {
            cb(null);
          });
        },
      };
  
      const dataSource = new DataSource(mockConnector);
      dataSource.on('connected', function() {
        // DataSource is now connected
        // connected: true, connecting: false
        dataSource.connected.should.be.true();
        dataSource.connecting.should.be.false();
  
        dataSource.stop(() => {
          dataSource.connected.should.be.false();
          done();
        });
      });
    });
  
    describe('deleteModelByName()', () => {
      it('removes the model from ModelBuilder registry', () => {
        const ds = new DataSource('ds', {connector: 'memory'});
  
        ds.createModel('TestModel');
        Object.keys(ds.modelBuilder.models)
          .should.containEql('TestModel');
        Object.keys(ds.modelBuilder.definitions)
          .should.containEql('TestModel');
  
        ds.deleteModelByName('TestModel');
  
        Object.keys(ds.modelBuilder.models)
          .should.not.containEql('TestModel');
        Object.keys(ds.modelBuilder.definitions)
          .should.not.containEql('TestModel');
      });
  
      it('removes the model from connector registry', () => {
        const ds = new DataSource('ds', {connector: 'memory'});
  
        ds.createModel('TestModel');
        Object.keys(ds.connector._models)
          .should.containEql('TestModel');
  
        ds.deleteModelByName('TestModel');
  
        Object.keys(ds.connector._models)
          .should.not.containEql('TestModel');
      });
    });
  
    describe('execute', () => {
      let ds;
      beforeEach(() => ds = new DataSource('ds', {connector: 'memory'}));
  
      it('calls connnector to execute the command', async () => {
        let called = 'not called';
        ds.connector.execute = function(command, args, options, callback) {
          called = {command, args, options};
          callback(null, 'a-result');
        };
  
        const result = await ds.execute(
          'command',
          ['arg1', 'arg2'],
          {'a-flag': 'a-value'},
        );
  
        result.should.be.equal('a-result');
        called.should.be.eql({
          command: 'command',
          args: ['arg1', 'arg2'],
          options: {'a-flag': 'a-value'},
        });
      });
  
      it('supports shorthand version (cmd)', async () => {
        let called = 'not called';
        ds.connector.execute = function(command, args, options, callback) {
          // copied from loopback-connector/lib/sql.js
          if (typeof args === 'function' && options === undefined && callback === undefined) {
            // execute(sql, callback)
            options = {};
            callback = args;
            args = [];
          }
  
          called = {command, args, options};
          callback(null, 'a-result');
        };
  
        const result = await ds.execute('command');
        result.should.be.equal('a-result');
        called.should.be.eql({
          command: 'command',
          args: [],
          options: {},
        });
      });
  
      it('supports shorthand version (cmd, args)', async () => {
        let called = 'not called';
        ds.connector.execute = function(command, args, options, callback) {
          // copied from loopback-connector/lib/sql.js
          if (typeof options === 'function' && callback === undefined) {
            // execute(sql, params, callback)
            callback = options;
            options = {};
          }
  
          called = {command, args, options};
          callback(null, 'a-result');
        };
  
        await ds.execute('command', ['arg1', 'arg2']);
        called.should.be.eql({
          command: 'command',
          args: ['arg1', 'arg2'],
          options: {},
        });
      });
  
      it('converts multiple callbacks arguments into a promise resolved with an array', async () => {
        ds.connector.execute = function() {
          const callback = arguments[arguments.length - 1];
          callback(null, 'result1', 'result2');
        };
        const result = await ds.execute('command');
        result.should.eql(['result1', 'result2']);
      });
  
      it('allows args as object', async () => {
        let called = 'not called';
        ds.connector.execute = function(command, args, options, callback) {
          called = {command, args, options};
          callback();
        };
  
        // See https://www.npmjs.com/package/loopback-connector-neo4j-graph
        const command = 'MATCH (u:User {email: {email}}) RETURN u';
        await ds.execute(command, {email: 'alice@example.com'}, {options: true});
        called.should.be.eql({
          command,
          args: {email: 'alice@example.com'},
          options: {options: true},
        });
      });
  
      it('supports MongoDB version (collection, cmd, args, options)', async () => {
        let called = 'not called';
        ds.connector.execute = function(...params) {
          const callback = params.pop();
          called = params;
          callback(null, 'a-result');
        };
  
        const result = await ds.execute(
          'collection',
          'command',
          ['arg1', 'arg2'],
          {options: true},
        );
  
        result.should.equal('a-result');
        called.should.be.eql([
          'collection',
          'command',
          ['arg1', 'arg2'],
          {options: true},
        ]);
      });
  
      it('supports free-form version (...params)', async () => {
        let called = 'not called';
        ds.connector.execute = function(...params) {
          const callback = params.pop();
          called = params;
          callback(null, 'a-result');
        };
  
        const result = await ds.execute(
          'arg1',
          'arg2',
          'arg3',
          'arg4',
          {options: true},
        );
  
        result.should.equal('a-result');
        called.should.be.eql([
          'arg1',
          'arg2',
          'arg3',
          'arg4',
          {options: true},
        ]);
      });
  
      it('throws NOT_IMPLEMENTED when no connector is provided', () => {
        ds.connector = undefined;
        return ds.execute('command').should.be.rejectedWith({
          code: 'NOT_IMPLEMENTED',
        });
      });
  
      it('throws NOT_IMPLEMENTED for connectors not implementing execute', () => {
        ds.connector.execute = undefined;
        return ds.execute('command').should.be.rejectedWith({
          code: 'NOT_IMPLEMENTED',
        });
      });
    });
  
    describe('automigrate', () => {
      it('reports connection errors (immediate connect)', async () => {
        const dataSource = new DataSource({
          connector: givenConnectorFailingOnConnect(),
        });
        dataSource.define('MyModel');
        await dataSource.automigrate().should.be.rejectedWith(/test failure/);
      });
  
      it('reports connection errors (lazy connect)', () => {
        const dataSource = new DataSource({
          connector: givenConnectorFailingOnConnect(),
          lazyConnect: true,
        });
        dataSource.define('MyModel');
        return dataSource.automigrate().should.be.rejectedWith(/test failure/);
      });
  
      function givenConnectorFailingOnConnect() {
        return givenMockConnector({
          connect: function(cb) {
            process.nextTick(() => cb(new Error('test failure')));
          },
          automigrate: function(models, cb) {
            cb(new Error('automigrate should not have been called'));
          },
        });
      }
    });
  
    describe('autoupdate', () => {
      it('reports connection errors (immediate connect)', async () => {
        const dataSource = new DataSource({
          connector: givenConnectorFailingOnConnect(),
        });
        dataSource.define('MyModel');
        await dataSource.autoupdate().should.be.rejectedWith(/test failure/);
      });
  
      it('reports connection errors (lazy connect)', () => {
        const dataSource = new DataSource({
          connector: givenConnectorFailingOnConnect(),
          lazyConnect: true,
        });
        dataSource.define('MyModel');
        return dataSource.autoupdate().should.be.rejectedWith(/test failure/);
      });
  
      function givenConnectorFailingOnConnect() {
        return givenMockConnector({
          connect: function(cb) {
            process.nextTick(() => cb(new Error('test failure')));
          },
          autoupdate: function(models, cb) {
            cb(new Error('autoupdate should not have been called'));
          },
        });
      }
    });
  
    describe('deleteAllModels', () => {
      it('removes all model definitions', () => {
        const ds = new DataSource({connector: 'memory'});
        ds.define('Category');
        ds.define('Product');
  
        Object.keys(ds.modelBuilder.definitions)
          .should.deepEqual(['Category', 'Product']);
        Object.keys(ds.modelBuilder.models)
          .should.deepEqual(['Category', 'Product']);
        Object.keys(ds.connector._models)
          .should.deepEqual(['Category', 'Product']);
  
        ds.deleteAllModels();
  
        Object.keys(ds.modelBuilder.definitions).should.be.empty();
        Object.keys(ds.modelBuilder.models).should.be.empty();
        Object.keys(ds.connector._models).should.be.empty();
      });
  
      it('preserves the connector instance', () => {
        const ds = new DataSource({connector: 'memory'});
        const connector = ds.connector;
        ds.deleteAllModels();
        ds.connector.should.equal(connector);
      });
    });
  
    describe('getMaxOfflineRequests', () => {
      let ds;
      beforeEach(() => ds = new DataSource('ds', {connector: 'memory'}));
  
      it('sets the default maximum number of event listeners to 16', () => {
        ds.getMaxOfflineRequests().should.be.eql(16);
      });
  
      it('uses provided number of listeners', () => {
        ds.settings.maxOfflineRequests = 17;
        ds.getMaxOfflineRequests().should.be.eql(17);
      });
  
      it('throws an error if a non-number is provided for the max number of listeners', () => {
        ds.settings.maxOfflineRequests = '17';
  
        (function() {
          return ds.getMaxOfflineRequests();
        }).should.throw('maxOfflineRequests must be a number');
      });
    });
  });
  
  function givenMockConnector(props) {
    const connector = {
      name: 'loopback-connector-mock',
      initialize: function(ds, cb) {
        ds.connector = connector;
        if (ds.settings.lazyConnect) {
          cb(null, false);
        } else {
          connector.connect(cb);
        }
      },
      ...props,
    };
    return connector;
  }
  