senecajs/seneca-store-test

View on GitHub
store-test.js

Summary

Maintainability
F
1 mo
Test Coverage
/* Copyright (c) 2014-2021 Richard Rodger, MIT License */
'use strict'

var Util = require('util')

var Assert = require('chai').assert
var Async = require('async')
var Lab = require('@hapi/lab')
const Code = require('@hapi/code')
const Nid = require('nid')

var ExtendedTests = require('./lib/store-test-extended')

const expect = Code.expect

var bartemplate = {
  name$: 'bar',
  base$: 'moon',
  zone$: 'zen',

  str: 'aaa',
  int: 11,
  dec: 33.33,
  bol: false,
  wen: new Date(2020, 1, 1),
  arr: [2, 3],
  obj: {
    a: 1,
    b: [2],
    c: { d: 3 },
  },
}

var barverify = function (bar) {
  Assert.equal(bar.str, 'aaa')
  Assert.equal(bar.int, 11)
  Assert.equal(bar.dec, 33.33)
  Assert.equal(bar.bol, false)

  const base_date = new Date(2020, 1, 1)
  const areDatesEqual = (d1, d2) => d1.getTime() === d2.getTime()

  Assert(
    areDatesEqual(new Date(bar.wen), base_date),
    'Expected bar.wen to be either a Unix timestamp, date ISO string or a Date.',
  )

  const isJsonMaybe = (x) => typeof x === 'string'

  // NOTE: Please consider making this test (entire `barverify`) specific to
  // the store plugin you are testing. The problem here is that SQL databases
  // normally auto-cast to JSON upon insert and return such arrays and objects
  // as, well, JSON,
  //
  // On the other hand, NoSQL databases do not need to cast objects and arrays
  // to JSON, and will both accept and return such objects and arrays as they
  // are - no JSON involved.

  const expected_arr = [2, 3]

  if (isJsonMaybe(bar.arr)) {
    expect(JSON.parse(bar.arr)).to.equal(expected_arr)
  } else {
    expect(bar.arr).to.equal(expected_arr)
  }

  const expected_obj = {
    a: 1,
    b: [2],
    c: { d: 3 },
  }

  if (isJsonMaybe(bar.obj)) {
    expect(JSON.parse(bar.obj)).to.equal(expected_obj)
  } else {
    expect(bar.obj).to.equal(expected_obj)
  }
}

function verify(cb, tests) {
  return function (error, out) {
    if (error) {
      return cb(error)
    }

    try {
      tests(out)
    } catch (ex) {
      return cb(ex)
    }

    cb()
  }
}

function clearDb(si) {
  return () => {
    return new Promise((done) => {
      si.ready(function () {
        Async.series(
          [
            function clearFoo(next) {
              si.make('foo').remove$({ all$: true }, next)
            },

            function clearBar(next) {
              si.make('zen', 'moon', 'bar').remove$({ all$: true }, next)
            },

            function clearPlayers(next) {
              si.make('players').remove$({ all$: true }, next)
            },

            function clearRacers(next) {
              si.make('racers').remove$({ all$: true }, next)
            },

            function clearUsers(next) {
              si.make('users').remove$({ all$: true }, next)
            },

            function clearCustomers(next) {
              si.make('customers').remove$({ all$: true }, next)
            },

            function clearProducts(next) {
              si.make('products').remove$({ all$: true }, next)
            },
          ],
          done,
        )
      })
    })
  }
}

function createEntities(si, name, data) {
  return () => {
    return new Promise((done) => {
      Async.each(
        data,
        function (el, next) {
          si.make$(name, el).save$(next)
        },
        done,
      )
    })
  }
}

function mergetest(settings) {
  const si = settings.senecaMergeFalse

  const script = settings.script || Lab.script()
  const it = make_it(script)
  const { describe, beforeEach } = script

  describe('Testing the merge option', function () {
    describe('merge:false as plugin option', function () {
      beforeEach(clearDb(si))
      beforeEach(
        createEntities(si, 'foo', [
          {
            id$: 'to-be-updated',
            p1: 'v1',
            p2: 'v2',
            p3: 'v3',
          },
        ]),
      )

      it('should update an entity if id provided', function (done) {
        const foo = si.make('foo')
        foo.id = 'to-be-updated'
        foo.p1 = 'z1'
        foo.p2 = 'z2'

        foo.save$(function (err, foo1) {
          Assert.isNull(err)

          expect(foo1).to.contain({
            id: 'to-be-updated',
            p1: 'z1',
            p2: 'z2',
          })

          expect('p3' in foo1).to.equal(false)

          foo1.load$(
            'to-be-updated',
            verify(done, function (foo2) {
              expect(foo2).to.contain({
                id: 'to-be-updated',
                p1: 'z1',
                p2: 'z2',
              })

              expect('p3' in foo2).to.equal(false)
            }),
          )
        })
      })

      it('should allow to merge during update with merge$: true', function (done) {
        const foo = si.make('foo')
        foo.id = 'to-be-updated'
        foo.p1 = 'z1'
        foo.p2 = 'z2'
        foo.p3 = 'v3'

        foo.save$({ merge$: true }, function (err, foo1) {
          Assert.isNull(err)

          expect(foo1).to.contain({
            id: 'to-be-updated',
            p1: 'z1',
            p2: 'z2',
            p3: 'v3',
          })

          expect('merge$' in foo1).to.equal(false)

          foo1.load$(
            'to-be-updated',
            verify(done, function (foo2) {
              expect(foo1).to.contain({
                id: 'to-be-updated',
                p1: 'z1',
                p2: 'z2',
                p3: 'v3',
              })

              expect('merge$' in foo1).to.equal(false)
            }),
          )
        })
      })
    })

    it('should allow to not merge during update with merge$: false', function (done) {
      const foo = si.make('foo')
      foo.id = 'to-be-updated'
      foo.p1 = 'z1'
      foo.p2 = 'z2'

      foo.save$({ merge$: false }, function (err, foo1) {
        Assert.isNull(err)

        expect(foo1).to.contain({
          id: 'to-be-updated',
          p1: 'z1',
          p2: 'z2',
        })

        expect(foo1.p3).to.satisfy((p3) => null == p3)

        foo1.load$(
          'to-be-updated',
          verify(done, function (foo2) {
            expect(foo1).to.contain({
              id: 'to-be-updated',
              p1: 'z1',
              p2: 'z2',
            })

            expect(foo1.p3).to.satisfy((p3) => null == p3)
          }),
        )
      })
    })
  })

  return script
}

function basictest(settings) {
  var si = settings.seneca

  var script = settings.script || Lab.script()

  var describe = script.describe
  var it = make_it(script)
  var before = script.before
  var beforeEach = script.beforeEach

  describe('Basic Tests', function () {
    describe('Load', function () {
      before(clearDb(si))
      before(
        createEntities(si, 'foo', [
          {
            id$: 'foo1',
            p1: 'v1',
          },
          {
            id$: 'foo2',
            p1: 'v2',
            p2: 'z2',
          },
        ]),
      )
      before(createEntities(si, 'bar', [bartemplate]))

      it('should load an entity qqq', function (done) {
        var foo = si.make('foo')
        foo.load$(
          'foo1',
          verify(done, function (foo1) {
            Assert.isNotNull(foo1)
            Assert.equal(foo1.id, 'foo1')
            Assert.equal(foo1.p1, 'v1')
          }),
        )
      })

      it('should return null for non existing entity', function (done) {
        var foo = si.make('foo')
        foo.load$(
          'does-not-exist-at-all-at-all',
          verify(done, function (out) {
            Assert.isNull(out)
          }),
        )
      })

      it('should support filtering', function (done) {
        var foo = si.make('foo')
        foo.load$(
          { p1: 'v2' },
          verify(done, function (foo1) {
            Assert.isNotNull(foo1)
            Assert.equal(foo1.id, 'foo2')
            Assert.equal(foo1.p1, 'v2')
            Assert.equal(foo1.p2, 'z2')
          }),
        )
      })

      it('should filter with AND', function (done) {
        var foo = si.make('foo')
        foo.load$(
          { p1: 'v2', p2: 'z2' },
          verify(done, function (foo1) {
            Assert.isNotNull(foo1)
            Assert.equal(foo1.id, 'foo2')
            Assert.equal(foo1.p1, 'v2')
            Assert.equal(foo1.p2, 'z2')
          }),
        )
      })

      it('should filter with AND 2', function (done) {
        var foo = si.make('foo')
        foo.load$(
          { p1: 'v2', p2: 'a' },
          verify(done, function (foo1) {
            Assert.isNull(foo1)
          }),
        )
      })

      it('should support different attribute types', function (done) {
        var bar = si.make('zen', 'moon', 'bar')

        bar.load$(
          { str: 'aaa' },
          verify(done, function (bar1) {
            Assert.isNotNull(bar1)
            Assert.isNotNull(bar1.id)
            barverify(bar1)
          }),
        )
      })

      it('should not mix attributes from entity to query for filtering', function (done) {
        var foo = si.make('foo')
        foo.p1 = 'v1'
        foo.load$(
          { p2: 'z2' },
          verify(done, function (foo1) {
            Assert.ok(foo1)
            Assert.equal(foo1.id, 'foo2')
            Assert.equal(foo1.p1, 'v2')
            Assert.equal(foo1.p2, 'z2')
          }),
        )
      })

      it('should reload current entity if no query provided and id present', function (done) {
        var foo = si.make('foo')
        foo.id = 'foo2'
        foo.load$(
          verify(done, function (foo1) {
            Assert.ok(foo1)
            Assert.equal(foo1.id, 'foo2')
            Assert.equal(foo1.p1, 'v2')
            Assert.equal(foo1.p2, 'z2')
          }),
        )
      })

      it('should do nothing if no query provided and id not present', function (done) {
        var foo = si.make('foo')
        foo.p1 = 'v2'
        foo.load$(
          verify(done, function (foo1) {
            Assert.notOk(foo1)
          }),
        )
      })
    })

    describe('Save', function () {
      beforeEach(clearDb(si))

      beforeEach(
        createEntities(si, 'foo', [
          {
            id$: 'to-be-updated',
            p1: 'v1',
            p2: 'v2',
            p3: 'v3',
          },
        ]),
      )

      beforeEach(
        createEntities(si, 'products', [
          {
            id$: 'product-to-be-updated',
            price: '1.95',
          },
        ]),
      )

      it('should save an entity to store (and generate an id)', function (done) {
        var foo = si.make('foo')
        foo.p1 = 'v1'
        foo.p2 = 'v2'

        foo.save$(function (err, foo1) {
          Assert.isNull(err)
          Assert.isNotNull(foo1.id)

          foo1.load$(
            foo1.id,
            verify(done, function (foo2) {
              Assert.isNotNull(foo2)
              Assert.equal(foo2.id, foo1.id)
              Assert.equal(foo2.p1, 'v1')
              Assert.equal(foo2.p2, 'v2')
            }),
          )
        })
      })

      it('should save an entity to store (with provided id)', function (done) {
        var foo = si.make('foo')
        foo.id$ = 'existing'
        foo.p1 = 'v1'
        foo.p2 = 'v2'

        foo.save$(function (err, foo1) {
          Assert.isNull(err)
          Assert.isNotNull(foo1.id)
          Assert.equal(foo1.id, 'existing')

          foo1.load$(
            'existing',
            verify(done, function (foo2) {
              Assert.isNotNull(foo2)
              Assert.equal(foo2.id, 'existing')
              Assert.equal(foo2.p1, 'v1')
              Assert.equal(foo2.p2, 'v2')
            }),
          )
        })
      })

      describe('when the id$ arg passed to #save$ is undefined', function () {
        it('should generate a new id and save the entity', function (done) {
          si.make('foo')
            .data$({ p1: 'v1', p2: 'v2' })
            .save$({ id$: undefined }, function (err, foo1) {
              if (err) {
                return done(err)
              }

              expect(typeof foo1.id).to.equal('string')

              foo1.load$(
                foo1.id,
                verify(done, function (foo2) {
                  expect(foo2).not.to.be.null()
                  expect(foo2).not.to.be.undefined()

                  expect(foo2.p1).to.equal('v1')
                  expect(foo2.p2).to.equal('v2')
                }),
              )
            })
        })
      })

      describe('when the id$ arg passed to #save$ is null', function () {
        it('should generate a new id and save the entity', function (done) {
          si.make('foo')
            .data$({ p1: 'v1', p2: 'v2' })
            .save$({ id$: null }, function (err, foo1) {
              if (err) {
                return done(err)
              }

              expect(typeof foo1.id).to.equal('string')

              foo1.load$(
                foo1.id,
                verify(done, function (foo2) {
                  expect(foo2).not.to.be.null()
                  expect(foo2).not.to.be.undefined()

                  expect(foo2.p1).to.equal('v1')
                  expect(foo2.p2).to.equal('v2')
                }),
              )
            })
        })
      })

      it('should update an entity if id provided', function (done) {
        var foo = si.make('foo')
        foo.id = 'to-be-updated'
        foo.p1 = 'z1'
        foo.p2 = 'z2'
        foo.p3 = 'v3'

        foo.save$(function (err, foo1) {
          Assert.isNull(err)
          Assert.isNotNull(foo1.id)
          Assert.equal(foo1.id, 'to-be-updated')
          Assert.equal(foo1.p1, 'z1')
          Assert.equal(foo1.p2, 'z2')
          Assert.equal(foo1.p3, 'v3')

          foo1.load$(
            'to-be-updated',
            verify(done, function (foo2) {
              Assert.isNotNull(foo2)
              Assert.equal(foo2.id, 'to-be-updated')
              Assert.equal(foo2.p1, 'z1')
              Assert.equal(foo2.p2, 'z2')
              Assert.equal(foo2.p3, 'v3')
            }),
          )
        })
      })

      it('should update an entity if id provided', function (done) {
        var product = si.make('products')
        product.id = 'product-to-be-updated'
        product.label = 'lorem ipsum'

        product.save$(function (err, out) {
          if (err) {
            return done(err)
          }

          expect(out).to.contain({
            id: 'product-to-be-updated',
            price: '1.95',
            label: 'lorem ipsum',
          })

          return out.load$(
            'product-to-be-updated',
            verify(done, function (out) {
              expect(out).to.contain({
                id: 'product-to-be-updated',
                price: '1.95',
                label: 'lorem ipsum',
              })
            }),
          )
        })
      })

      describe('when the provided id is null', function () {
        it('should generate a new id and save the entity', function (done) {
          si.make('foo')
            .data$({ id: null, p1: 'v1', p2: 'v2' })
            .save$(function (err, foo1) {
              if (err) {
                return done(err)
              }

              expect(typeof foo1.id).to.equal('string')

              foo1.load$(
                foo1.id,
                verify(done, function (foo2) {
                  expect(foo2).not.to.be.null()
                  expect(foo2).not.to.be.undefined()

                  expect(foo2.p1).to.equal('v1')
                  expect(foo2.p2).to.equal('v2')
                }),
              )
            })
        })
      })

      describe('when the provided id is undefined', function () {
        it('should generate a new id and save the entity', function (done) {
          si.make('foo')
            .data$({ id: undefined, p1: 'v1', p2: 'v2' })
            .save$(function (err, foo1) {
              if (err) {
                return done(err)
              }

              expect(typeof foo1.id).to.equal('string')

              foo1.load$(
                foo1.id,
                verify(done, function (foo2) {
                  expect(foo2).not.to.be.null()
                  expect(foo2).not.to.be.undefined()

                  expect(foo2.p1).to.equal('v1')
                  expect(foo2.p2).to.equal('v2')
                }),
              )
            })
        })
      })

      it("should save an entity if id provided but original doesn't exist", function (done) {
        var foo = si.make('foo')
        foo.id = 'will-be-inserted'
        foo.p1 = 'z1'
        foo.p2 = 'z2'
        foo.p3 = 'z3'

        foo.save$(function (err, foo1) {
          Assert.isNull(err)
          Assert.isNotNull(foo1.id)
          Assert.equal(foo1.id, 'will-be-inserted')
          Assert.equal(foo1.p1, 'z1')
          Assert.equal(foo1.p2, 'z2')
          Assert.equal(foo1.p3, 'z3')

          foo1.load$(
            'will-be-inserted',
            verify(done, function (foo2) {
              Assert.isNotNull(foo2)
              Assert.equal(foo2.id, 'will-be-inserted')
              Assert.equal(foo2.p1, 'z1')
              Assert.equal(foo2.p2, 'z2')
              Assert.equal(foo2.p3, 'z3')
            }),
          )
        })
      })

      it('should support different attribute types', function (done) {
        var bar = si.make(bartemplate)
        var mark = (bar.mark = Math.random().toString())

        bar.save$(function (err, bar) {
          Assert.isNull(err)
          Assert.isNotNull(bar)
          Assert.isNotNull(bar.id)
          barverify(bar)
          Assert.equal(bar.mark, mark)

          bar.load$(
            bar.id,
            verify(done, function (bar1) {
              Assert.isNotNull(bar1)
              Assert.equal(bar1.id, bar.id)
              barverify(bar1)
              Assert.equal(bar1.mark, mark)
            }),
          )
        })
      })

      it('should allow dublicate attributes', function (done) {
        var foo = si.make('foo')
        foo.p2 = 'v2'

        foo.save$(function (err, foo1) {
          Assert.isNull(err)
          Assert.isNotNull(foo1.id)
          Assert.equal(foo1.p2, 'v2')

          foo.load$(
            foo1.id,
            verify(done, function (foo2) {
              Assert.isNotNull(foo2)
              Assert.equal(foo2.id, foo1.id)
              Assert.equal(foo2.p2, 'v2')
              Assert.notOk(foo2.p1)
              Assert.notOk(foo2.p3)
            }),
          )
        })
      })

      it('should not save modifications to entity after save completes', function (done) {
        var foo = si.make('foo')
        foo.int_arr = [37]
        foo.save$(
          verify(done, function (foo1) {
            Assert.deepEqual(foo1.int_arr, [37])
            // now that foo is in the database, modify the original data
            foo.int_arr.push(43)
            Assert.deepEqual(foo1.int_arr, [37])
          }),
        )
      })

      it('should not backport modification to saved entity to the original one', function (done) {
        var foo = si.make('foo')
        foo.int_arr = [37]
        foo.save$(
          verify(done, function (foo1) {
            Assert.deepEqual(foo1.int_arr, [37])
            // now that foo is in the database, modify the original data
            foo1.int_arr.push(43)
            Assert.deepEqual(foo.int_arr, [37])
          }),
        )
      })

      it('should clear an attribute if = null', function (done) {
        var foo = si.make('foo')
        foo.p1 = 'v1'
        foo.p2 = 'v2'

        foo.save$(function (err, foo1) {
          if (err) {
            return done(err)
          }

          foo1.p1 = null

          // NOTE: undefined has no effect
          foo1.p2 = undefined

          foo1.save$(function (err, foo2) {
            if (err) {
              return done(err)
            }

            expect(foo2.p1).to.equal(null)
            expect(foo2.p2).to.equal('v2')

            foo.load$(
              foo1.id,
              verify(done, function (foo3) {
                expect(foo3).to.be.instanceof(Object)
                expect(foo3.p1).to.equal(null)
                expect(foo3.p2).to.equal('v2')
              }),
            )
          })
        })
      })
    })

    describe('List', function () {
      before(clearDb(si))
      before(
        createEntities(si, 'foo', [
          {
            id$: 'foo1',
            p1: 'v1',
          },
          {
            id$: 'foo2',
            p1: 'v2',
            p2: 'z2',
          },
        ]),
      )
      before(createEntities(si, 'bar', [bartemplate]))

      it('should load all elements if no params', function (done) {
        var bar = si.make('zen', 'moon', 'bar')
        bar.list$(
          {},
          verify(done, function (res) {
            Assert.lengthOf(res, 1)
            barverify(res[0])
          }),
        )
      })

      it('should load all elements if no params 2', function (done) {
        var foo = si.make('foo')
        foo.list$(
          {},
          verify(done, function (res) {
            Assert.lengthOf(res, 2)
          }),
        )
      })

      it('should load all elements if no query provided', function (done) {
        var foo = si.make('foo')
        foo.list$(
          verify(done, function (res) {
            Assert.lengthOf(res, 2)
          }),
        )
      })

      it('should list entities by id', function (done) {
        var foo = si.make('foo')
        foo.list$(
          { id: 'foo1' },
          verify(done, function (res) {
            Assert.lengthOf(res, 1)
            Assert.equal(res[0].p1, 'v1')
            Assert.notOk(res[0].p2)
            Assert.notOk(res[0].p3)
          }),
        )
      })

      it('should list entities by integer property', function (done) {
        var bar = si.make('zen', 'moon', 'bar')
        bar.list$(
          { int: bartemplate.int },
          verify(done, function (res) {
            Assert.lengthOf(res, 1)
            barverify(res[0])
          }),
        )
      })

      it('should list entities by string property', function (done) {
        var foo = si.make('foo')
        foo.list$(
          { p2: 'z2' },
          verify(done, function (res) {
            Assert.lengthOf(res, 1)
            Assert.equal(res[0].p1, 'v2')
            Assert.equal(res[0].p2, 'z2')
          }),
        )
      })

      it('should list entities by two properties', function (done) {
        var foo = si.make('foo')
        foo.list$(
          { p2: 'z2', p1: 'v2' },
          verify(done, function (res) {
            Assert.lengthOf(res, 1)
            Assert.equal(res[0].p1, 'v2')
            Assert.equal(res[0].p2, 'z2')
          }),
        )
      })

      it('should support opaque ids (array)', function (done) {
        var foo = si.make('foo')
        foo.list$(
          ['foo1', 'foo2'],
          verify(done, function (res) {
            Assert.lengthOf(res, 2)

            // Order is not guaranteed.
            if ('v2' === res[0].p1) {
              let res0 = res[0]
              res[0] = res[1]
              res[1] = res0
            }

            Assert.equal(res[0].p1, 'v1')
            Assert.notOk(res[0].p2)
            Assert.notOk(res[0].p3)
            Assert.equal(res[1].p1, 'v2')
            Assert.equal(res[1].p2, 'z2')
            Assert.equal(res[1].p3)
          }),
        )
      })

      it('should support opaque ids (single id)', function (done) {
        var foo = si.make('foo')
        foo.list$(
          ['foo2'],
          verify(done, function (res) {
            Assert.lengthOf(res, 1)
            Assert.equal(res[0].p1, 'v2')
            Assert.equal(res[0].p2, 'z2')
            Assert.equal(res[0].p3)
          }),
        )
      })

      it('should support opaque ids (string)', function (done) {
        var foo = si.make('foo')
        foo.list$(
          'foo2',
          verify(done, function (res) {
            Assert.lengthOf(res, 1)
            Assert.equal(res[0].p1, 'v2')
            Assert.equal(res[0].p2, 'z2')
            Assert.equal(res[0].p3)
          }),
        )
      })

      it('should filter with AND', function (done) {
        var foo = si.make('foo')
        foo.list$(
          { p2: 'z2', p1: 'v1' },
          verify(done, function (res) {
            Assert.lengthOf(res, 0)
          }),
        )
      })

      it('should not mix attributes from entity to query for filtering', function (done) {
        var foo = si.make('foo')
        foo.p1 = 'v1'
        foo.list$(
          { p2: 'z2' },
          verify(done, function (res) {
            Assert.lengthOf(res, 1)
            Assert.equal(res[0].id, 'foo2')
            Assert.equal(res[0].p1, 'v2')
            Assert.equal(res[0].p2, 'z2')
          }),
        )
      })
    })

    describe('Remove', function () {
      beforeEach(clearDb(si))
      beforeEach(
        createEntities(si, 'foo', [
          {
            id$: 'foo1',
            p1: 'v1',
          },
          {
            id$: 'foo2',
            p1: 'v2',
            p2: 'z2',
          },
        ]),
      )
      beforeEach(createEntities(si, 'bar', [bartemplate]))

      it('should delete only an entity', function (done) {
        var foo = si.make('foo')
        foo.remove$('foo1', function (err, res) {
          Assert.isNull(err)

          foo.list$(
            {},
            verify(done, function (res) {
              Assert.lengthOf(res, 1)
            }),
          )
        })
      })

      it('should delete all entities if all$ = true', function (done) {
        var foo = si.make('foo')
        foo.remove$({ all$: true }, function (err, res) {
          Assert.isNull(err)
          Assert.notOk(res)

          foo.list$(
            {},
            verify(done, function (res) {
              Assert.lengthOf(res, 0)
            }),
          )
        })
      })

      it('should delete an entity by property', function (done) {
        var foo = si.make('foo')
        foo.remove$({ p1: 'v1' }, function (err, res) {
          Assert.isNull(err)

          foo.list$(
            {},
            verify(done, function (res) {
              Assert.lengthOf(res, 1)
              Assert.equal('v2', res[0].p1)
            }),
          )
        })
      })

      it('should delete entities filtered by AND', function (done) {
        var foo = si.make('foo')
        foo.remove$({ p1: 'v1', p2: 'z2' }, function (err) {
          Assert.isNull(err)

          foo.list$(
            {},
            verify(done, function (res) {
              Assert.lengthOf(res, 2)
            }),
          )
        })
      })

      it('should return deleted entity if load$: true', function (done) {
        var foo = si.make('foo')
        foo.remove$(
          { p1: 'v2', load$: true },
          verify(done, function (res) {
            Assert.ok(res)
            Assert.equal(res.p1, 'v2')
            Assert.equal(res.p2, 'z2')
          }),
        )
      })

      it('should never return deleted entities if all$: true', function (done) {
        var foo = si.make('foo')
        foo.remove$(
          { all$: true, load$: true },
          verify(done, function (res) {
            Assert.notOk(res)
          }),
        )
      })

      it('should not delete current ent (only uses query)', function (done) {
        var foo = si.make('foo')
        foo.id = 'foo2'
        foo.remove$({ p1: 'v1' }, function (err) {
          if (err) {
            return done(err)
          }

          foo.list$(
            verify(done, function (res) {
              Assert.lengthOf(res, 1)
              Assert.equal(res[0].id, 'foo2')
            }),
          )
        })
      })

      it('should delete current entity if no query present', function (done) {
        var foo = si.make$('foo')
        foo.load$('foo2', function (err, foo2) {
          if (err) {
            return done(err)
          }
          foo2.remove$(function (err) {
            if (err) {
              return done(err)
            }
            foo.list$(
              {},
              verify(done, function (res) {
                Assert.lengthOf(res, 1)
                Assert.equal(res[0].id, 'foo1')
              }),
            )
          })
        })
      })
    })

    describe('Native', function () {
      it('should prived direct access to the driver', function (done) {
        var foo = si.make('foo')
        foo.native$(
          verify(done, function (driver) {
            Assert.ok(driver)
          }),
        )
      })
    })
  })

  return script
}

function sorttest(settings) {
  var si = settings.seneca
  var script = settings.script || Lab.script()

  var describe = script.describe
  var it = make_it(script)
  var beforeEach = script.beforeEach

  describe('Sorting', function () {
    beforeEach(clearDb(si))
    beforeEach(
      createEntities(si, 'foo', [
        { p1: 'v1', p2: 'v1' },
        // make sure this is not in alphabetical order,
        // otherwise insertion order will be similar to the order we use for tests
        // possibly leading to false positives
        { p1: 'v2', p2: 'v3' },
        { p1: 'v3', p2: 'v2' },
      ]),
    )

    describe('Load', function () {
      it('should support ascending order', function (done) {
        var cl = si.make('foo')
        cl.load$(
          { sort$: { p1: 1 } },
          verify(done, function (foo) {
            Assert.ok(foo)
            Assert.equal(foo.p1, 'v1')
          }),
        )
      })

      it('should support descending order', function (done) {
        var cl = si.make('foo')
        cl.load$(
          { sort$: { p1: -1 } },
          verify(done, function (foo) {
            Assert.ok(foo)
            Assert.equal(foo.p1, 'v3')
          }),
        )
      })
    })

    describe('List', function () {
      it('should support ascending order', function (done) {
        var cl = si.make('foo')
        cl.list$(
          { sort$: { p1: 1 } },
          verify(done, function (lst) {
            Assert.lengthOf(lst, 3)
            Assert.equal(lst[0].p1, 'v1')
            Assert.equal(lst[1].p1, 'v2')
            Assert.equal(lst[2].p1, 'v3')
          }),
        )
      })

      it('should support descending order', function (done) {
        var cl = si.make('foo')
        cl.list$(
          { sort$: { p1: -1 } },
          verify(done, function (lst) {
            Assert.lengthOf(lst, 3)
            Assert.equal(lst[0].p1, 'v3')
            Assert.equal(lst[1].p1, 'v2')
            Assert.equal(lst[2].p1, 'v1')
          }),
        )
      })
    })

    describe('Remove', function () {
      it('should support ascending order', function (done) {
        var cl = si.make('foo')
        cl.remove$({ sort$: { p1: 1 } }, function (err) {
          if (err) {
            return done(err)
          }

          cl.list$(
            { sort$: { p1: 1 } },
            verify(done, function (lst) {
              Assert.equal(lst.length, 2)
              Assert.equal(lst[0].p1, 'v2')
              Assert.equal(lst[1].p1, 'v3')
            }),
          )
        })
      })

      it('should support descending order', function (done) {
        var cl = si.make('foo')
        cl.remove$({ sort$: { p1: -1 } }, function (err) {
          if (err) {
            return done(err)
          }

          cl.list$(
            { sort$: { p1: 1 } },
            verify(done, function (lst) {
              Assert.equal(lst.length, 2)
              Assert.equal(lst[0].p1, 'v1')
              Assert.equal(lst[1].p1, 'v2')
            }),
          )
        })
      })
    })
  })

  return script
}

function limitstest(settings) {
  var si = settings.seneca
  var script = settings.script || Lab.script()

  var describe = script.describe
  var it = make_it(script)
  var beforeEach = script.beforeEach

  describe('Limits', function () {
    beforeEach(clearDb(si))
    beforeEach(
      createEntities(si, 'foo', [
        { p1: 'v1' },
        // make sure this is not in alphabetical order,
        // otherwise insertion order will be similar to the order we use for tests
        // possibly leading to false positives
        { p1: 'v3' },
        { p1: 'v2' },
      ]),
    )

    it('check setup correctly', function (done) {
      var cl = si.make('foo')
      cl.list$(
        {},
        verify(done, function (lst) {
          Assert.lengthOf(lst, 3)
        }),
      )
    })

    describe('Load', function () {
      it('should support skip and sort', function (done) {
        var cl = si.make('foo')
        cl.load$(
          { skip$: 1, sort$: { p1: 1 } },
          verify(done, function (foo) {
            Assert.ok(foo)
            Assert.equal(foo.p1, 'v2')
          }),
        )
      })

      it('should return empty array when skipping all the records', function (done) {
        var cl = si.make('foo')
        cl.load$(
          { skip$: 3 },
          verify(done, function (foo) {
            Assert.notOk(foo)
          }),
        )
      })

      it('should not be influenced by limit', function (done) {
        var cl = si.make('foo')
        cl.load$(
          { limit$: 2, sort$: { p1: 1 } },
          verify(done, function (foo) {
            Assert.ok(foo)
            Assert.equal(foo.p1, 'v1')
          }),
        )
      })

      it('should ignore skip < 0', function (done) {
        var cl = si.make('foo')
        cl.load$(
          { skip$: -1, sort$: { p1: 1 } },
          verify(done, function (foo) {
            Assert.ok(foo)
            Assert.equal(foo.p1, 'v1')
          }),
        )
      })

      it('should ignore limit < 0', function (done) {
        var cl = si.make('foo')
        cl.load$(
          { limit$: -1, sort$: { p1: 1 } },
          verify(done, function (foo) {
            Assert.ok(foo)
            Assert.equal(foo.p1, 'v1')
          }),
        )
      })

      it('should ignore invalid qualifier values', function (done) {
        var cl = si.make('foo')
        cl.load$(
          { limit$: 'A', skip$: 'B', sort$: { p1: 1 } },
          verify(done, function (foo) {
            Assert.ok(foo)
            Assert.equal(foo.p1, 'v1')
          }),
        )
      })
    })

    describe('List', function () {
      it('should support limit, skip and sort', function (done) {
        var cl = si.make('foo')
        cl.list$(
          { limit$: 1, skip$: 1, sort$: { p1: 1 } },
          verify(done, function (lst) {
            Assert.lengthOf(lst, 1)
            Assert.equal(lst[0].p1, 'v2')
          }),
        )
      })

      it('should return empty array when skipping all the records', function (done) {
        var cl = si.make('foo')
        cl.list$(
          { limit$: 2, skip$: 3 },
          verify(done, function (lst) {
            Assert.lengthOf(lst, 0)
          }),
        )
      })

      it('should return correct number of records if limit is too high', function (done) {
        var cl = si.make('foo')
        cl.list$(
          { limit$: 5, skip$: 2, sort$: { p1: 1 } },
          verify(done, function (lst) {
            Assert.lengthOf(lst, 1)
            Assert.equal(lst[0].p1, 'v3')
          }),
        )
      })

      it('should ignore skip < 0', function (done) {
        var cl = si.make('foo')
        cl.list$(
          { skip$: -1, sort$: { p1: 1 } },
          verify(done, function (lst) {
            Assert.lengthOf(lst, 3)
            Assert.equal(lst[0].p1, 'v1')
            Assert.equal(lst[1].p1, 'v2')
            Assert.equal(lst[2].p1, 'v3')
          }),
        )
      })

      it('should ignore limit < 0', function (done) {
        var cl = si.make('foo')
        cl.list$(
          { limit$: -1, sort$: { p1: 1 } },
          verify(done, function (lst) {
            Assert.lengthOf(lst, 3)
            Assert.equal(lst[0].p1, 'v1')
            Assert.equal(lst[1].p1, 'v2')
            Assert.equal(lst[2].p1, 'v3')
          }),
        )
      })

      it('should ignore invalid qualifier values', function (done) {
        var cl = si.make('foo')
        cl.list$(
          { limit$: 'A', skip$: 'B', sort$: { p1: 1 } },
          verify(done, function (lst) {
            Assert.lengthOf(lst, 3)
            Assert.equal(lst[0].p1, 'v1')
            Assert.equal(lst[1].p1, 'v2')
            Assert.equal(lst[2].p1, 'v3')
          }),
        )
      })
    })

    describe('Remove', function () {
      it('should support limit, skip and sort', function (done) {
        var cl = si.make('foo')
        cl.remove$({ limit$: 1, skip$: 1, sort$: { p1: 1 } }, function (err) {
          if (err) {
            return done(err)
          }

          cl.list$(
            { sort$: { p1: 1 } },
            verify(done, function (lst) {
              Assert.lengthOf(lst, 2)
              Assert.equal(lst[0].p1, 'v1')
              Assert.equal(lst[1].p1, 'v3')
            }),
          )
        })
      })

      it('should not be impacted by limit > 1', function (done) {
        var cl = si.make('foo')
        cl.remove$({ limit$: 2, sort$: { p1: 1 } }, function (err) {
          if (err) {
            return done(err)
          }

          cl.list$(
            { sort$: { p1: 1 } },
            verify(done, function (lst) {
              Assert.lengthOf(lst, 2)
              Assert.equal(lst[0].p1, 'v2')
              Assert.equal(lst[1].p1, 'v3')
            }),
          )
        })
      })

      it('should work with all$: true', function (done) {
        var cl = si.make('foo')
        cl.remove$(
          { all$: true, limit$: 2, skip$: 1, sort$: { p1: 1 } },
          function (err) {
            if (err) {
              return done(err)
            }

            cl.list$(
              { sort$: { p1: 1 } },
              verify(done, function (lst) {
                Assert.lengthOf(lst, 1)
                Assert.equal(lst[0].p1, 'v1')
              }),
            )
          },
        )
      })

      it('should not delete anyithing when skipping all the records', function (done) {
        var cl = si.make('foo')
        cl.remove$({ all$: true, limit$: 2, skip$: 3 }, function (err) {
          if (err) {
            return done(err)
          }

          cl.list$(
            { sort$: { p1: 1 } },
            verify(done, function (lst) {
              Assert.lengthOf(lst, 3)
            }),
          )
        })
      })

      it('should delete correct number of records if limit is too high', function (done) {
        var cl = si.make('foo')
        cl.remove$(
          { all$: true, limit$: 5, skip$: 2, sort$: { p1: 1 } },
          function (err) {
            if (err) {
              return done(err)
            }

            cl.list$(
              { sort$: { p1: 1 } },
              verify(done, function (lst) {
                Assert.lengthOf(lst, 2)
                Assert.equal(lst[0].p1, 'v1')
                Assert.equal(lst[1].p1, 'v2')
              }),
            )
          },
        )
      })

      it('should ignore skip < 0', function (done) {
        var cl = si.make('foo')
        cl.remove$({ skip$: -1, sort$: { p1: 1 } }, function (err) {
          if (err) {
            return done(err)
          }

          cl.list$(
            { sort$: { p1: 1 } },
            verify(done, function (lst) {
              Assert.lengthOf(lst, 2)
              Assert.equal(lst[0].p1, 'v2')
              Assert.equal(lst[1].p1, 'v3')
            }),
          )
        })
      })

      it('should ignore limit < 0', function (done) {
        var cl = si.make('foo')
        cl.remove$(
          { all$: true, limit$: -1, sort$: { p1: 1 } },
          function (err) {
            if (err) {
              return done(err)
            }

            cl.list$(
              { sort$: { p1: 1 } },
              verify(done, function (lst) {
                Assert.lengthOf(lst, 0)
              }),
            )
          },
        )
      })

      it('should ignore invalid qualifier values', function (done) {
        var cl = si.make('foo')
        cl.remove$(
          { limit$: 'A', skip$: 'B', sort$: { p1: 1 } },
          function (err) {
            if (err) {
              return done(err)
            }

            cl.list$(
              { sort$: { p1: 1 } },
              verify(done, function (lst) {
                Assert.lengthOf(lst, 2)
                Assert.equal(lst[0].p1, 'v2')
                Assert.equal(lst[1].p1, 'v3')
              }),
            )
          },
        )
      })
    })
  })

  return script
}

function upserttest(settings) {
  Assert('seneca' in settings, 'settings.seneca')
  const si = settings.seneca

  // NOTE: WARNING: Side-effect - the original seneca instance will be mutated.
  //
  si.use('promisify')

  const script = settings.script || Lab.script()
  const { describe, beforeEach, afterEach } = script
  const it = make_it(script)

  describe('Upserts', () => {
    beforeEach(
      () =>
        new Promise((resolve, _reject) => {
          si.ready(resolve)
        }),
    )

    beforeEach(clearDb(si))

    afterEach(clearDb(si))

    describe('matches on 1 upsert$ field', () => {
      let id_of_richard

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('players')
              .data$({ username: 'richard', points: 0 })
              .save$((err, user) => {
                if (err) {
                  return fin(err)
                }

                id_of_richard = user.id

                return fin()
              })
          }),
      )

      let id_of_bob

      beforeEach(
        (fin) =>
          new Promise((fin) => {
            si.make('players')
              .data$({ username: 'bob', points: 0 })
              .save$((err, user) => {
                if (err) {
                  return fin(err)
                }

                id_of_bob = user.id

                return fin()
              })
          }),
      )

      it('updates the entity', (fin) => {
        si.test(fin)

        si.make('players')
          .data$({ username: 'richard', points: 9999 })
          .save$({ upsert$: ['username'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('players').list$({}, (err, players) => {
              if (err) {
                return fin(err)
              }

              players = sortBy(players, (x) => x.points)

              expect(players.length).to.equal(2)

              expect(players[0]).to.contain({
                id: id_of_bob,
                username: 'bob',
                points: 0,
              })

              expect(players[1]).to.contain({
                id: id_of_richard,
                username: 'richard',
                points: 9999,
              })

              return fin()
            })
          })
      })

      it("shouldn't mutate the original entity after save completes", (fin) => {
        si.test(fin)

        const player = si
          .make('players')
          .data$({ username: 'richard', points_history: [37] })

        player.save$(
          { upsert$: ['username'] },
          verify(fin, function (out) {
            expect(out.id).to.equal(id_of_richard)
            expect(out.points_history).to.equal([37])

            // Now that the player is in the database, we modify
            // the original data.
            //
            out.points_history.push(43)

            expect(player.points_history).to.equal([37])
          }),
        )
      })
    })

    describe('matches on 1 upsert$ field, some data$ fields missing', () => {
      let id_of_richard

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('racers')
              .data$({
                username: 'richard',
                points: 37,
                favorite_car: 'land rover',
              })
              .save$((err, racer) => {
                if (err) {
                  return fin(err)
                }

                try {
                  id_of_richard = racer.id

                  return fin()
                } catch (err) {
                  return fin(err)
                }
              })
          }),
      )

      let id_of_bob

      beforeEach(
        (fin) =>
          new Promise((fin) => {
            si.make('racers')
              .data$({
                username: 'bob',
                points: 20,
                favorite_car: 'peugeot 307',
              })
              .save$((err, racer) => {
                if (err) {
                  return fin(err)
                }

                try {
                  id_of_bob = racer.id

                  return fin()
                } catch (err) {
                  return fin(err)
                }
              })
          }),
      )

      it('retains the entity fields missing from data$', (fin) => {
        si.test(fin)

        si.make('racers')
          .data$({ username: 'richard', favorite_car: 'bmw m3 e46' })
          .save$({ upsert$: ['username'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('racers').list$({}, (err, racers) => {
              if (err) {
                return fin(err)
              }

              racers = sortBy(racers, (x) => x.points)

              expect(racers.length).to.equal(2)

              expect(racers[0]).to.contain({
                id: id_of_bob,
                username: 'bob',
                points: 20,
                favorite_car: 'peugeot 307',
              })

              expect(racers[1]).to.contain({
                id: id_of_richard,
                username: 'richard',
                points: 37,
                favorite_car: 'bmw m3 e46',
              })

              return fin()
            })
          })
      })
    })

    describe('matches on 1 upsert$ field, save$ includes id$ the field', () => {
      beforeEach(clearDb)

      let target_user_id

      beforeEach(
        () =>
          new Promise((resolve, reject) => {
            si.make('users')
              .data$({ email: 'elvis@no1.com', username: 'elvispresley' })
              .save$((err, user) => {
                if (err) {
                  return reject(err)
                }

                Assert.ok(user, 'user')
                target_user_id = user.id

                return resolve()
              })
          }),
      )

      beforeEach(
        () =>
          new Promise((resolve, reject) => {
            // Do a fresh fetch from the db.
            //
            si.make('users').load$(target_user_id, (err, user) => {
              if (err) {
                return reject(err)
              }

              Assert.ok(user, 'user')

              return resolve()
            })
          }),
      )

      it('updates the fields and ignores the id$ qualifier', (fin) => {
        si.test(fin)

        const new_id = 'bbbba6f73a861890cc1f4e23'

        si.make('users')
          .data$({ email: 'elvis@no1.com', username: 'theking' })
          .save$({ id$: new_id, upsert$: ['email'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('users').list$({}, (err, users) => {
              if (err) {
                return fin(err)
              }

              expect(users.length).to.equal(1)

              expect(users[0]).to.contain({
                email: 'elvis@no1.com',
                username: 'theking',
              })

              expect(users[0].id).not.to.equal(new_id)

              return fin()
            })
          })
      })
    })

    describe('matches on 2 upsert$ fields', () => {
      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('customers')
              .data$({
                first_name: 'richard',
                last_name: 'gear',
                credits: 0,
              })
              .save$(fin)
          }),
      )

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('customers')
              .data$({
                first_name: 'richard',
                last_name: 'sinatra',
                credits: 0,
              })
              .save$(fin)
          }),
      )

      it('updates the entity', (fin) => {
        si.test(fin)

        si.make('customers')
          .data$({
            first_name: 'richard',
            last_name: 'gear',
            credits: 1234,
          })
          .save$({ upsert$: ['first_name', 'last_name'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('customers').list$({}, (err, customers) => {
              if (err) {
                return fin(err)
              }

              customers = sortBy(customers, (x) => x.credits)

              expect(customers.length).to.equal(2)

              expect(customers[0]).to.contain({
                first_name: 'richard',
                last_name: 'sinatra',
                credits: 0,
              })

              expect(customers[1]).to.contain({
                first_name: 'richard',
                last_name: 'gear',
                credits: 1234,
              })

              return fin()
            })
          })
      })
    })

    describe('no match, 1 upsert$ field', () => {
      let id_of_macchiato

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products')
              .data$({
                label: 'a macchiato espressionado',
                price: '3.40',
              })
              .save$((err, product) => {
                if (err) {
                  return fin(err)
                }

                try {
                  id_of_macchiato = product.id

                  return fin()
                } catch (err) {
                  return fin(err)
                }
              })
          }),
      )

      it('creates a new entity', (fin) => {
        si.test(fin)

        si.make('products')
          .data$({ label: 'b toothbrush', price: '3.40' })
          .save$({ upsert$: ['label'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('products').list$({}, (err, products) => {
              if (err) {
                return fin(err)
              }

              products = sortBy(products, (x) => x.label)

              expect(products.length).to.equal(2)

              expect(products[0]).to.contain({
                id: id_of_macchiato,
                label: 'a macchiato espressionado',
                price: '3.40',
              })

              expect(products[1]).to.contain({
                label: 'b toothbrush',
                price: '3.40',
              })

              expect(products[1].id).not.to.equal(id_of_macchiato)

              return fin()
            })
          })
      })
    })

    describe('no match, 1 upsert$ field, save$ includes the id$ field', () => {
      it('creates a new entity with the given id', (fin) => {
        si.test(fin)

        const new_id = '6095a6f73a861890cc1f4e23'

        si.make('users')
          .data$({
            email: 'frank.sinatra@gmail.com',
            username: 'ididitmyway',
          })
          .save$({ id$: new_id, upsert$: ['email'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('users').list$({}, (err, users) => {
              if (err) {
                return fin(err)
              }

              expect(users.length).to.equal(1)

              const user = users[0]

              expect(user.id).to.equal(new_id)

              return fin()
            })
          })
      })
    })

    describe('no match, 2 upsert$ fields', () => {
      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('customers')
              .data$({
                first_name: 'frank',
                last_name: 'sinatra',
                credits: 5,
              })
              .save$(fin)
          }),
      )

      it('creates a new entity', (fin) => {
        si.test(fin)

        si.make('customers')
          .data$({ first_name: 'frank', last_name: 'nixon', credits: 7 })
          .save$({ upsert$: ['first_name', 'last_name'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('customers').list$({}, (err, customers) => {
              if (err) {
                return fin(err)
              }

              customers = sortBy(customers, (x) => x.credits)

              expect(customers.length).to.equal(2)

              expect(customers[0]).to.contain({
                first_name: 'frank',
                last_name: 'sinatra',
                credits: 5,
              })

              expect(customers[1]).to.contain({
                first_name: 'frank',
                last_name: 'nixon',
                credits: 7,
              })

              return fin()
            })
          })
      })
    })

    describe('bombarding the store with near-parallel upserts', () => {
      //
      // NOTE: WARNING: Chances are, in order to pass this test, you
      // need to create a unique index on the users.email column/field,
      // - whether you are testing a plugin meant for a SQL or a NoSQL
      // database/store.
      //
      // That's due to the way how upserts are normally implemented in
      // databases.
      //
      // For example, in case of MongoDb, in order for the database to
      // be able to avert race conditions, a field you upsert on must
      // have a unique index created on it. Without the index, your
      // upserts will not be atomic, and as a result your plugin will
      // fail the race condition tests.
      //
      // It is a case of a leaky abstraction that test suites of client
      // store plugins must "know" what collection and what field is
      // being used in a race condition test in seneca-store-test. We
      // may want to come up with a better alternative in the future.
      //
      it('has no race condition - creates a single new entity', (fin) => {
        si.test(fin)

        const user_entity = si.entity('users')

        const upsertUser = (cb) =>
          user_entity
            .data$({
              username: 'jimihendrix',
              email: 'jimi@experience.com',
            })
            .save$({ upsert$: ['email'] }, cb)

        Async.parallel([upsertUser, upsertUser, upsertUser], (err) => {
          if (err) {
            return fin(err)
          }

          user_entity.list$((err, users) => {
            if (err) {
              return fin(err)
            }

            expect(users.length).to.equal(1)

            expect(users[0]).to.contain({
              username: 'jimihendrix',
              email: 'jimi@experience.com',
            })

            return fin()
          })
        })

        return
      })
    })

    describe('entity matches on a private field', () => {
      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products')
              .data$({
                label: 'toothbrush',
                price: '3.95',
                psst$: 'private',
              })
              .save$(fin)
          }),
      )

      it('creates a new entity', (fin) => {
        si.test(fin)

        si.make('products')
          .data$({
            label: 'a new toothbrush',
            price: '5.95',
            psst$: 'private',
          })
          .save$({ upsert$: ['psst$'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('products').list$({}, (err, products) => {
              if (err) {
                return fin(err)
              }

              products = sortBy(products, (x) => x.label)

              expect(products.length).to.equal(2)

              expect(products[0]).to.contain({
                label: 'a new toothbrush',
                price: '5.95',
              })

              expect(products[1]).to.contain({
                label: 'toothbrush',
                price: '3.95',
              })

              return fin()
            })
          })
      })
    })

    describe('entity matches on a private and a public field', () => {
      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products')
              .data$({
                label: 'toothbrush',
                price: '3.95',
                psst$: 'private',
              })
              .save$(fin)
          }),
      )

      it('matches by the public field and updates the entity', (fin) => {
        si.test(fin)

        si.make('products')
          .data$({
            label: 'toothbrush',
            price: '5.95',
            psst$: 'private',
          })
          .save$({ upsert$: ['psst$', 'label'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('products').list$({}, (err, products) => {
              if (err) {
                return fin(err)
              }

              expect(products.length).to.equal(1)

              expect(products[0]).to.contain({
                label: 'toothbrush',
                price: '5.95',
              })

              return fin()
            })
          })
      })
    })

    describe('empty upsert$ array', () => {
      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products')
              .data$({ label: 'toothbrush', price: '3.95' })
              .save$(fin)
          }),
      )

      it('creates a new document', (fin) => {
        si.test(fin)

        si.make('products')
          .data$({ label: 'banana', price: '3.95' })
          .save$({ upsert$: [] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('products').list$({}, (err, products) => {
              if (err) {
                return fin(err)
              }

              products = sortBy(products, (x) => x.label)

              expect(products.length).to.equal(2)

              expect(products[0]).to.contain({
                label: 'banana',
                price: '3.95',
              })

              expect(products[1]).to.contain({
                label: 'toothbrush',
                price: '3.95',
              })

              return fin()
            })
          })
      })
    })

    describe('entity matches on a field with the `undefined` value', () => {
      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products')
              .data$({ label: undefined, price: '3.95' })
              .save$(fin)
          }),
      )

      it('creates a new document', (fin) => {
        si.test(fin)

        si.make('products')
          .data$({ label: undefined, price: '5.95' })
          .save$({ upsert$: ['label'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('products').list$({}, (err, products) => {
              if (err) {
                return fin(err)
              }

              products = sortBy(products, (x) => x.price)

              expect(products.length).to.equal(2)

              expect(products[0]).to.contain({
                // NOTE: Seneca is stripping out fields
                // with a value of `undefined` in a document.
                //
                // label: undefined,

                price: '3.95',
              })

              expect(products[1]).to.contain({
                // NOTE: Seneca is stripping out fields
                // with a value of `undefined` in a document.
                //
                // label: undefined,

                price: '5.95',
              })

              return fin()
            })
          })
      })
    })

    describe('some upsert$ fields are blank in existing entities', () => {
      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products').data$({ price: '3.40', label: null }).save$(fin)
          }),
      )

      it('creates a new entity', (fin) => {
        si.test(fin)

        si.make('products')
          .data$({ price: '3.40', label: 'a toothbrush' })
          .save$({ upsert$: ['price', 'label'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('products').list$({}, (err, products) => {
              if (err) {
                return fin(err)
              }

              products = sortBy(products, (x) => x.label || '')

              expect(products.length).to.equal(2)

              expect(products[0]).to.contain({
                label: null,
                price: '3.40',
              })

              expect(products[1]).to.contain({
                label: 'a toothbrush',
                price: '3.40',
              })

              return fin()
            })
          })
      })
    })

    describe('fields in upsert$ are not present in the data$ object', () => {
      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products')
              .data$({ label: 'a toothbrush', price: '3.40' })
              .save$(fin)
          }),
      )

      it('creates a new entity because it can never match', (fin) => {
        si.test(fin)

        si.make('products')
          .data$({ price: '2.95', label: null })
          .save$({ upsert$: ['label'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('products').list$({}, (err, products) => {
              if (err) {
                return fin(err)
              }

              products = sortBy(products, (x) => x.price)

              expect(products.length).to.equal(2)

              expect(products[0]).to.contain({
                label: null,
                price: '2.95',
              })

              expect(products[1]).to.contain({
                label: 'a toothbrush',
                price: '3.40',
              })

              return fin()
            })
          })
      })
    })

    describe('upserting on the id field, match exists', () => {
      const id_of_richard = 'some_id'

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('players')
              .data$({
                id: id_of_richard,
                username: 'richard',
                points: 8000,
              })
              .save$(fin)
          }),
      )

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('players')
              .data$({ username: 'bob', points: 1000 })
              .save$(fin)
          }),
      )

      it('updates the matching entity', (fin) => {
        si.test(fin)

        si.make('players')
          .data$({ id: id_of_richard, username: 'richard', points: 9999 })
          .save$({ upsert$: ['id'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('players').list$({}, (err, players) => {
              if (err) {
                return fin(err)
              }

              players = sortBy(players, (x) => x.points)

              expect(players.length).to.equal(2)

              expect(players[0]).to.contain({
                username: 'bob',
                points: 1000,
              })

              expect(players[1]).to.contain({
                id: id_of_richard,
                username: 'richard',
                points: 9999,
              })

              return fin()
            })
          })
      })

      it('works with load$ after the update', (fin) => {
        si.test(fin)

        si.make('players')
          .data$({ id: id_of_richard, username: 'richard', points: 9999 })
          .save$({ upsert$: ['id'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('players').load$(id_of_richard, (err, player) => {
              if (err) {
                return fin(err)
              }

              expect(player).to.contain({
                id: id_of_richard,
                username: 'richard',
                points: 9999,
              })

              return fin()
            })
          })
      })
    })

    describe('upserting on the id field, no match', () => {
      const some_id = 'some_id'

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('users')
              .data$({ username: 'richard', email: 'rr@example.com' })
              .save$(fin)
          }),
      )

      it('creates a new document with that id', (fin) => {
        si.test(fin)

        si.make('users')
          .data$({
            id: some_id,
            username: 'jim',
            email: 'jhendrix@example.com',
          })
          .save$({ upsert$: ['id'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('users').list$({}, (err, users) => {
              if (err) {
                return fin(err)
              }

              users = sortBy(users, (x) => x.username)

              expect(users.length).to.equal(2)

              expect(users[0]).to.contain({
                id: some_id,
                username: 'jim',
                email: 'jhendrix@example.com',
              })

              expect(users[1].id).not.to.equal(some_id)

              expect(users[1]).to.contain({
                username: 'richard',
                email: 'rr@example.com',
              })

              return fin()
            })
          })
      })

      it('works with load$ after the creation', (fin) => {
        si.test(fin)

        si.make('users')
          .data$({
            id: some_id,
            username: 'jim',
            email: 'jhendrix@example.com',
          })
          .save$({ upsert$: ['id'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('users').load$(some_id, (err, user) => {
              if (err) {
                return fin(err)
              }

              expect(user).to.contain({
                id: some_id,
                username: 'jim',
                email: 'jhendrix@example.com',
              })

              return fin()
            })
          })
      })
    })

    describe('upserting on the id and some field, match exists', () => {
      const id_of_richard = 'some_id'

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('players')
              .data$({
                id: id_of_richard,
                username: 'richard',
                points: 8000,
              })
              .save$(fin)
          }),
      )

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('players')
              .data$({ username: 'bob', points: 1000 })
              .save$(fin)
          }),
      )

      it('updates the matching entity', (fin) => {
        si.test(fin)

        si.make('players')
          .data$({ id: id_of_richard, username: 'richard', points: 9999 })
          .save$({ upsert$: ['id', 'username'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('players').list$({}, (err, players) => {
              if (err) {
                return fin(err)
              }

              players = sortBy(players, (x) => x.points)

              expect(players.length).to.equal(2)

              expect(players[0]).to.contain({
                username: 'bob',
                points: 1000,
              })

              expect(players[1]).to.contain({
                id: id_of_richard,
                username: 'richard',
                points: 9999,
              })

              return fin()
            })
          })
      })

      it('works with load$ after the update', (fin) => {
        si.test(fin)

        si.make('players')
          .data$({ id: id_of_richard, username: 'richard', points: 9999 })
          .save$({ upsert$: ['id', 'username'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('players').load$(id_of_richard, (err, player) => {
              if (err) {
                return fin(err)
              }

              expect(player).to.contain({
                id: id_of_richard,
                username: 'richard',
                points: 9999,
              })

              return fin()
            })
          })
      })
    })

    describe('upserting on the id and some field, match does not exist', () => {
      const some_id = 'some_id'

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('users')
              .data$({ username: 'richard', email: 'rr@example.com' })
              .save$(fin)
          }),
      )

      it('creates a new document with that id', (fin) => {
        si.test(fin)

        si.make('users')
          .data$({
            id: some_id,
            username: 'richard',
            email: 'rr@voxgig.com',
          })
          .save$({ upsert$: ['id', 'username'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('users').list$({}, (err, users) => {
              if (err) {
                return fin(err)
              }

              users = sortBy(users, (x) => x.email)

              expect(users.length).to.equal(2)

              expect(users[0].id).not.to.equal(some_id)

              expect(users[0]).to.contain({
                username: 'richard',
                email: 'rr@example.com',
              })

              expect(users[1]).to.contain({
                id: some_id,
                username: 'richard',
                email: 'rr@voxgig.com',
              })

              return fin()
            })
          })
      })

      it('works with load$ after the creation', (fin) => {
        si.test(fin)

        si.make('users')
          .data$({
            id: some_id,
            username: 'richard',
            email: 'rr@voxgig.com',
          })
          .save$({ upsert$: ['id', 'username'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('users').load$(some_id, (err, user) => {
              if (err) {
                return fin(err)
              }

              expect(user).to.contain({
                id: some_id,
                username: 'richard',
                email: 'rr@voxgig.com',
              })

              return fin()
            })
          })
      })
    })

    describe('save$ invoked on a saved entity instance, match exists', () => {
      let existing_product

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products')
              .data$({ label: 'a macchiato espressionado', price: '3.40' })
              .save$((err, new_product) => {
                if (err) {
                  return fin(err)
                }

                existing_product = new_product

                return fin()
              })
          }),
      )

      beforeEach(
        () =>
          new Promise((fin) => {
            si.make('products')
              .data$({ label: 'capuccino', price: '7.99' })
              .save$(fin)
          }),
      )

      it('completely ignores the upsert$ directive', (fin) => {
        si.test(fin)

        existing_product
          .data$({ label: 'a macchiato espressionado', price: '3.95' })
          .save$({ upsert$: ['label'] }, (err) => {
            if (err) {
              return fin(err)
            }

            si.make('products').list$({}, (err, products) => {
              if (err) {
                return fin(err)
              }

              products = sortBy(products, (x) => x.price)

              expect(products.length).to.equal(2)

              expect(products[0]).to.contain({
                label: 'a macchiato espressionado',
                price: '3.95',
              })

              expect(products[1]).to.contain({
                label: 'capuccino',
                price: '7.99',
              })

              return fin()
            })
          })
      })
    })

    describe('happy path', () => {
      it('is happy', async (fin) => {
        const foo_ent = si.entity('foo')

        const f01c = await foo_ent.clone$()
        const f02c = await foo_ent.clone$()

        const f01 = await f01c.data$({ x: 1, y: 22 }).save$()
        const f02 = await f02c.data$({ x: 2, y: 33 }).save$()

        const f03 = await foo_ent
          .data$({ x: 1, y: 55 })
          .save$({ upsert$: ['x'] })

        const foos = sortBy(await foo_ent.list$(), (foo) => foo.x)

        expect(f01.id).not.to.equal(f02.id)
        expect(f01.id).to.equal(f03.id)

        expect(foos.length).to.equal(2)

        expect(foos[0]).to.contain({ x: 1, y: 55 })
        expect(foos[1]).to.contain({ x: 2, y: 33 })

        return fin()
      })
    })
  })

  return script
}

function sqltest(settings) {
  var si = settings.seneca
  var script = settings.script || Lab.script()

  var describe = script.describe
  var before = script.before
  var it = make_it(script)

  var Product = si.make('products')

  describe('Sql support', function () {
    before(clearDb(si))

    before(
      createEntities(si, 'products', [
        { label: 'apple', price: 200 },
        { label: 'pear', price: 100 },
      ]),
    )

    it('should accept a string query', function (done) {
      Product.list$(
        { native$: 'SELECT * FROM products ORDER BY price' },
        verify(done, function (list) {
          Assert.lengthOf(list, 2)

          Assert.equal(list[0].entity$, '-/-/products')
          Assert.equal(list[0].label, 'pear')
          Assert.equal(list[0].price, 100)

          Assert.equal(list[1].entity$, '-/-/products')
          Assert.equal(list[1].label, 'apple')
          Assert.equal(list[1].price, 200)
        }),
      )
    })

    it('should accept and array with query and parameters', function (done) {
      Product.list$(
        {
          native$: [
            'SELECT * FROM products WHERE price >= ? AND price <= ?',
            0,
            150,
          ],
        },
        verify(done, function (list) {
          Assert.lengthOf(list, 1)

          Assert.equal(list[0].entity$, '-/-/products')
          Assert.equal(list[0].label, 'pear')
          Assert.equal(list[0].price, 100)
        }),
      )
    })
  })

  return script
}

module.exports = {
  basictest: basictest,
  mergetest: mergetest,
  sorttest: sorttest,
  limitstest: limitstest,
  upserttest: upserttest,
  sqltest: sqltest,
  extended: ExtendedTests,
  verify: verify,

  test: {
    init: (lab, opts) => {
      opts.ent0 = opts.ent0 || 'test0'

      // NOTE: pass require to Seneca instance
      lab.describe('store-init', () => {
        lab.it('load-store-plugin', async () => {
          expect(opts.name).string()

          let seneca = opts.seneca
          seneca.use('..', opts.options)
          await seneca.ready()

          expect(seneca.has_plugin(opts.name), 'has_plugin').true()
        })

        lab.it('clear-data', async () => {
          let seneca = opts.seneca

          let ent0 = seneca.entity(opts.ent0)
          await ent0.remove$({ all$: true })

          let list = await ent0.list$()
          expect(list.length, 'empty-list').equal(0)
        })
      })
    },

    keyvalue: (lab, opts) => {
      lab.describe('store-keyvalue', () => {
        lab.it('save-load-auto-id', async () => {
          let seneca = opts.seneca
          let ent0 = seneca.entity(opts.ent0)

          // load non-existent
          let n0 = await ent0.make$().load$('not-an-id')
          expect(n0, 'not-exists').not.exists()

          // basic save and load for single entity
          let a0 = await ent0.make$({ a: 0 }).save$()
          let a0o = await ent0.make$().load$(a0.id)
          expect(a0 === ent0, 'new object').false()
          expect(a0o === ent0, 'new object').false()
          expect(a0o === a0, 'new object').false()
          expect(a0.id, 'same id').equal(a0o.id)
          expect(a0.a, 'same field').equal(a0o.a)

          // basic save and load for another entity
          let a1 = await ent0.make$({ a: 1 }).save$()
          let a1o = await ent0.make$().load$(a1.id)
          expect(a1 === ent0, 'new object').false()
          expect(a1o === ent0, 'new object').false()
          expect(a1o === a1, 'new object').false()
          expect(a1o !== a0o, 'different object').true()
          expect(a1.id, 'same id').equal(a1o.id)
          expect(a1.a, 'same field').equal(a1o.a)
          expect(a1.id, 'different ids').not.equal(a0.id)

          // basic update
          let a0u = await a0o.data$({ a: 10, b: 'b0' }).save$()
          expect(a0u.data$(), 'update data').includes({ a: 10, b: 'b0' })
          expect(a0u.id, 'same id').equal(a0.id)
          let a0uo = await ent0.make$().load$(a0.id)
          expect(a0uo.data$(), 'update data').includes({ a: 10, b: 'b0' })
          expect(a0uo.id, 'same id').equal(a0.id)

          // basic update on another entity
          let a1u = await a1o.data$({ a: 11, b: 'b1' }).save$()
          expect(a1u.data$(), 'update data').includes({ a: 11, b: 'b1' })
          expect(a1u.id, 'same id').equal(a1.id)
          let a1uo = await ent0.make$().load$(a1.id)
          expect(a1uo.data$(), 'update data').includes({ a: 11, b: 'b1' })
          expect(a1uo.id, 'same id').equal(a1.id)

          // sanity
          n0 = await ent0.make$().load$('not-an-id')
          expect(n0, 'not-exists').not.exists()
          let a0s = await ent0.make$().load$(a0.id)
          expect(a0s.data$(), 'expected data').contains({ a: 10, b: 'b0' })
          let a1s = await ent0.make$().load$(a1.id)
          expect(a1s.data$(), 'expected data').contains({ a: 11, b: 'b1' })
        })

        lab.it('save-load-given-id', async () => {
          let seneca = opts.seneca
          let ent0 = seneca.entity(opts.ent0)

          // load non-existent
          let n0 = await ent0.make$().load$('not-an-id')
          expect(n0, 'not-exists').not.exists()

          let id0 = Nid()
          let id1 = Nid()

          let b0 = await ent0.make$({ id$: id0, b: 0 }).save$()
          let b0o = await ent0.make$().load$(id0)
          expect(b0 === ent0, 'new object').false()
          expect(b0o === ent0, 'new object').false()
          expect(b0o === b0, 'new object').false()
          expect(b0.b, 'same field').equal(b0o.b)

          let b1 = await ent0.make$({ id$: id1, b: 1 }).save$()
          let b1o = await ent0.make$().load$(id1)
          expect(b1 === ent0, 'new object').false()
          expect(b1o === ent0, 'new object').false()
          expect(b1o === b1, 'new object').false()
          expect(b1o !== b0o, 'different object').true()
          expect(b1.b, 'same field').equal(b1o.b)

          // basic update
          let b0u = await b0o.data$({ b: 10, c: 'c0' }).save$()
          expect(b0u.data$(), 'update data').includes({ b: 10, c: 'c0' })
          expect(b0u.id, 'same id').equal(b0.id)
          let b0uo = await ent0.make$().load$(b0.id)
          expect(b0uo.data$(), 'update data').includes({ b: 10, c: 'c0' })
          expect(b0uo.id, 'same id').equal(b0.id)

          // basic update on another entity
          let b1u = await b1o.data$({ b: 11, c: 'c1' }).save$()
          expect(b1u.data$(), 'update data').includes({ b: 11, c: 'c1' })
          expect(b1u.id, 'same id').equal(b1.id)
          let b1uo = await ent0.make$().load$(b1.id)
          expect(b1uo.data$(), 'update data').includes({ b: 11, c: 'c1' })
          expect(b1uo.id, 'same id').equal(b1.id)

          // sanity
          n0 = await ent0.make$().load$('not-an-id')
          expect(n0, 'not-exists').not.exists()
          let b0s = await ent0.make$().load$(b0.id)
          expect(b0s.data$(), 'expected data').contains({ b: 10, c: 'c0' })
          let b1s = await ent0.make$().load$(b1.id)
          expect(b1s.data$(), 'expected data').contains({ b: 11, c: 'c1' })
        })

        lab.it('remove', async () => {
          let seneca = opts.seneca
          let ent0 = seneca.entity(opts.ent0)

          let c0 = await ent0.make$({ c: 0 }).save$()
          let c0o = await ent0.make$().load$(c0.id)
          expect(c0o.id).equals(c0.id)

          let c1 = await ent0.make$({ c: 1 }).save$()
          let c1o = await ent0.make$().load$(c1.id)
          expect(c1o.id).equals(c1.id)

          // should be idempotent
          await ent0.make$().remove$(c0o.id)
          await ent0.make$().remove$(c0o.id)

          let c0r = await ent0.make$().load$(c0.id)
          expect(c0r).not.exist()

          let c1r = await ent0.make$().load$(c1.id)
          expect(c1r).exist()
          expect(c1r.id).equal(c1o.id)
          expect(c1r.c).equal(c1o.c)
          expect(c1r.c).equal(c1.c)
        })
      })
    },
  },
}

function make_it(lab) {
  return function it(name, opts, func) {
    if ('function' === typeof opts) {
      func = opts
      opts = {}
    }

    lab.it(
      name,
      opts,
      Util.promisify(function (x, fin) {
        func(fin)
      }),
    )
  }
}

function sortBy(ary, f) {
  return [...ary].sort((a, b) => {
    const x = f(a)
    const y = f(b)

    if (x < y) {
      return -1
    }

    if (x > y) {
      return 1
    }

    return 0
  })
}