jwt.encode = function encode(key, payload, algorithm, cb) {
  //
  // some verifications
  //
  if (!algorithm || typeof algorithm === 'function') {
    cb = algorithm;
    algorithm = 'HS256';
  }

  // verify key & payload
  if (!key || !payload) {
    return utils.fnError(
      new JWTError('The key and payload are mandatory!'), cb
    );
  } else if (!Object.keys(payload).length) {
    return utils.fnError(new JWTError('The payload is empty object!'), cb);
  } else {
    // JWT header
    var header = JSON.stringify({typ: 'JWT', alg: algorithm});

    // get algorithm hash and type and check if is valid
    algorithm = this._search(algorithm);

    if (algorithm) {
      var parts = b64url.encode(header) +
        '.' + b64url.encode(JSON.stringify(payload));
      var res = utils.sign(algorithm, key, parts);
      return utils.fnResult(parts + '.' + res, cb);
    } else {
      return utils.fnError(
        new JWTError('The algorithm is not supported!'), cb
      );
    }
  }
};
Пример #2
0
  describe('.processEmailOpen', () => {
    before(() => sinon.stub(Api, 'processEmailOpen').resolves(true));
    after(() => Api.processEmailOpen.restore());
    const campaignId = 'campaign-id';
    const linkId = 'link-id';
    const recipientId = 'recipient-id';
    const userId = 'user-id';
    const encodedUserId = base64url.encode(userId);
    const listId = 'list-id';
    const segmentId = 'segment-id';
    const httpHeaders = { 'User-Agent': 'Firefox' };
    const emailOpen = { linkId, campaignId, userId, listId, recipientId, httpHeaders };
    const apiGatewayEvent = {
      headers: httpHeaders,
      pathParameters: { campaignId, linkId },
      queryStringParameters: { r: recipientId, u: encodedUserId, l: listId, s: segmentId }
    };

    it('parses the HTTP request and forwards it to Api.processEmailOpen', (done) => {
      handler.processEmailOpen(apiGatewayEvent, {}, (err) => {
        if (err) return done(err);
        expect(Api.processEmailOpen).to.have.been.calledWithMatch(emailOpen);
        return done();
      });
    });
  });
Пример #3
0
      it('gmail', function (done) {
        // 'Message-ID: <*****@*****.**>\n'+
        // 'Date: '+(new Date())+'\n'+
        // 'MIME-Version: 1.0\n'+
        // 'Content-Transfer-Encoding: quoted-printable\n'+
        // 'Content-Disposition: attachment; filename*=utf-8''report%E2%80%93may.pdf\n'+
        // 'Return-Path: <*****@*****.**>\n'+
        var message =
          'From: Purest <*****@*****.**>\n'+
          'To: Mailinator 1 <*****@*****.**>,'+
              'Mailinator 2 <*****@*****.**>\n'+
          'Cc: Mailinator 3 <*****@*****.**>\n'+
          'Bcc: Mailinator 4 <*****@*****.**>\n'+
          'Subject: Purest is awesome! (gmail)\n'+
          'Content-Type: text/html; charset=utf-8\n'+
          '\n'+
          '<h1>True idd!</h1>'

        p.google.post('users/me/messages/send', {
          api:'gmail',
          auth:{bearer:cred.user.google.token},
          json:{raw:base64url.encode(message)}
        }, function (err, res, body) {
          debugger
          if (err) return error(err, done)
          body.id.should.be.type('string')
          should.deepEqual(body.labelIds, ['SENT'])
          done()
        })
      })
Пример #4
0
function calculate(params) {
  const normalizedProperties = automationActionToFootprintAdapter(params);
  const footprintProperties = footprintPropertiesByType[params.type];
  const footprintString = footprintProperties.map(prop => normalizedProperties[prop])
    .concat(params.type).sort().join('');
  return base64url.encode(footprintString);
}
Пример #5
0
let asPayload = code => {
  let grader_payload = {
    payload: b64.encode(JSON.stringify(code))
  };
  grader_payload = JSON.stringify(grader_payload);
  return grader_payload;
};
Пример #6
0
 const recipients = emails.map(email => ({
   userId,
   listId,
   email,
   metadata: {name: faker.name.firstName(), surname: faker.name.lastName()},
   id: base64url.encode(email),
   status: Recipient.statuses.subscribed
 }));
Пример #7
0
function calculate(params, source) {
  const normalizedProperties = footprintPropertiesMapping[source](params);
  const type = params.triggerEventType || params.type;
  const footprintProperties = footprintPropertiesByType[type];
  const footprintString = footprintProperties.map(prop => normalizedProperties[prop])
    .concat(type).sort().join('');
  return base64url.encode(footprintString);
}
Пример #8
0
;/**
 * Return a JWT header base64url encoded. The keyId is stored in the header
 * and used when verifying the signature.
 *
 * @param {keyId} string
 * @returns {string}
 * @private
 */
function jwtHeader(keyId) {
    var data = {
        typ: 'JWT',
        alg: 'CUSTOM-BITCOIN-SIGN',
        kid: keyId
    };

    return base64url.encode(stringify(data));
}
Пример #9
0
 static _buildVerifyUrl(recipient, userId) {
   const verifyPath = `lists/${recipient.listId}/recipients/${recipient.id}/verify`;
   const unsubscribeUrl = {
     protocol: 'https',
     hostname: this.apiHost,
     pathname: verifyPath,
     query: { v: recipient.verificationCode, u: base64url.encode(userId) }
   };
   return url.format(unsubscribeUrl);
 }
Пример #10
0
test('should not decode for the "none" algorithm', function(assert) {
  var encode    = jwt.encode(secret, payload).value
  var badToken  = encode.split('.')
  var badAlg    = b64url.encode(JSON.stringify({typ: 'JWT', alg: 'none'}))
  badToken[0]   = badAlg
  var result    = jwt.decode(secret, badToken.join('.'))
  assert.deepEqual(!!result.error, true)
  assert.equal(result.error.name, 'JWTError')
  assert.equal(result.error.message, 'The algorithm is not supported!')
  assert.end()
})
Пример #11
0
 static _validateUniqueRecipient(listId, email) {
   const recipientId = base64url.encode(email);
   return Recipient.get(listId, recipientId)
     .then((recipient) => {
       if ((recipient || {}).hasOwnProperty('email')) {
         return Promise.reject(new RecipientAlreadyExists(`The recipient ${email} already exists`));
       } else {
         return Promise.resolve(email);
       }
     });
 }
Пример #12
0
/**
 * Return a signed bitcore.message. By default the expiration claim (exp) is
 * set to one hour in the future and the issued at claim (iat) is the current
 * unix timestamp * 1000.
 *
 * @param {string} url - Used as the audience (aud) in the bitcore.message claims.
 * @param {object} payload - Arbitrary data to be added to the bitcore.message payload.
 * @param {object} sign - An object that contains at least "address" and "key".
 * @returns {string}
 */
function signSerialize(url, data, key, expTime) {

    var exp = (new Date().getTime() / 1000) + 3600;
    if (expTime && expTime > 0)
        exp = (new Date().getTime() / 1000) + expTime;

    var payload = {
        aud : url,
        data : data,
        exp : exp,
        iat : new Date().getTime()
    }

    var rawPayload = base64url.encode(stringify(payload));
    var msg = jwtHeader(key.publicKey.toAddress().toString()) + '.' + rawPayload;
    var signature = base64url.encode(new Message(msg).sign(key));

    return msg + '.' + signature;

}
Пример #13
0
 constructor({ fromEmail, to, body, subject, metadata, recipientId, listId, campaignId, userId } = {}, options = { footer: true }) {
   this.from = fromEmail;
   this.to = to;
   this.body = body;
   this.subject = subject;
   this.metadata = metadata;
   this.listId = listId;
   this.userId = base64url.encode(userId || '');
   this.recipientId = recipientId;
   this.campaignId = campaignId;
   this.apiHost = process.env.API_HOST;
   this.unsubscribeApiHost = process.env.UNSUBSCRIBE_API_HOST;
   this.options = options;
   this.opensPath = 'links/open';
 }
Пример #14
0
 static _doCreate(listId, recipient, userId) {
   const subscriptionOrigin = Recipient.subscriptionOrigins.signupForm;
   const recipientParams = Object.assign({}, { listId, subscriptionOrigin }, omitEmpty(recipient));
   recipientParams.id = base64url.encode(recipient.email);
   return List.get(userId, listId).then((list) => {
     if (list.hasOwnProperty('sendEmailVerificationOnSubscribe')) {
       if (JSON.parse(list.sendEmailVerificationOnSubscribe) === false) {
         recipientParams.status = Recipient.statuses.subscribed;
         return Recipient.save(recipientParams).then(() => recipientParams);
       }
     }
     recipientParams.verificationCode = this._generateVerificationCode();
     recipientParams.status = Recipient.statuses.awaitingConfirmation;
     return Recipient.save(recipientParams).then(() => recipientParams);
   });
 }
Пример #15
0
export function respond(event, cb) {
  debug('= subscribeRecipient.action', JSON.stringify(event));
  if (event.listId && event.recipient && event.recipient.email) {
    const recipient = event.recipient;
    recipient.listId = event.listId;
    recipient.id = base64url.encode(recipient.email);
    recipient.status = recipient.status || Recipient.statuses.awaitingConfirmation;
    Recipient.save(recipient).then(() => cb(null, recipient))
    .catch(e => {
      debug(e);
      return cb(e);
    });
  } else {
    return cb('No recipient specified');
  }
}
Пример #16
0
let packet = (anonimized_id, response, payload) => {
    return {
        xqueue_body: $({

            student_info: $({
                anonimized_id
            }),

            student_response: response,

            grader_payload: $({
                payload: b64.encode($(payload))
            })
        })
    };
};
Пример #17
0
  _getQuery (source) {
    if (!source) source = ''
    var query = 'app_id=' + this.appid + '&time=' + Math.floor(Date.now() / 1000) + '&source=' + base64.encode(JSON.stringify(source))

    var hmac = crypto.createHmac('sha1', this.appkey)
    hmac.update(query)
    return query + '&sign=' + hmac.digest('hex')
  }
Пример #18
0
 static nextPage(key) {
   return base64url.encode(JSON.stringify(key));
 }
Пример #19
0
  describe('.processLinkClick', () => {
    before(() => sinon.stub(Api, 'processLinkClick').resolves(true));
    after(() => Api.processLinkClick.restore());
    const campaignId = 'campaign-id';
    const linkId = 'link-id';
    const redirectUrl = 'https://github.com/microapps/moonmail';
    const url = encodeURIComponent(redirectUrl);
    const recipientId = 'recipient-id';
    const userId = 'user-id';
    const encodedUserId = base64url.encode(userId);
    const listId = 'list-id';
    const segmentId = 'segment-id';
    const httpHeaders = { 'User-Agent': 'Firefox' };
    const linkClick = { linkId, campaignId, userId, listId, recipientId, httpHeaders };
    const apiGatewayEvent = {
      headers: httpHeaders,
      pathParameters: { campaignId, linkId },
      queryStringParameters: { r: recipientId, u: encodedUserId, l: listId, s: segmentId, url }
    };

    it('parses the HTTP request and forwards it to Api.processLinkClick', (done) => {
      handler.processLinkClick(apiGatewayEvent, {}, (err) => {
        if (err) return done(err);
        expect(Api.processLinkClick).to.have.been.calledWithMatch(linkClick);
        return done();
      });
    });

    it('redirects to the redirection URL', (done) => {
      handler.processLinkClick(apiGatewayEvent, {}, (err, res) => {
        if (err) return done(err);
        const expected = ApiGatewayUtils.buildRedirectResponse({ url: redirectUrl });
        expect(res).to.deep.equal(expected);
        return done();
      });
    });

    context('when Api.processLinkClick fails', () => {
      before(() => Api.processLinkClick.rejects(new Error('Kaboom!')));

      it('redirects to the redirection URL', (done) => {
        handler.processLinkClick(apiGatewayEvent, {}, (err, res) => {
          if (err) return done(err);
          const expected = ApiGatewayUtils.buildRedirectResponse({ url: redirectUrl });
          expect(res).to.deep.equal(expected);
          return done();
        });
      });
    });

    context('when no redirection URL is specified', () => {
      it('redirects to MoonMail homepage', (done) => {
        const noUrlEvent = R.dissocPath(['queryStringParameters', 'url'], apiGatewayEvent);
        handler.processLinkClick(noUrlEvent, {}, (err, res) => {
          if (err) return done(err);
          const expected = ApiGatewayUtils.buildRedirectResponse({ url: 'https://moonmail.io' });
          expect(res).to.deep.equal(expected);
          return done();
        });
      });
    });

    context('when the redirection URL has no protocol', () => {
      it('adds https', (done) => {
        const noProtocolEvent = R.assocPath(['queryStringParameters', 'url'], 'moonmail.io', apiGatewayEvent);
        handler.processLinkClick(noProtocolEvent, {}, (err, res) => {
          if (err) return done(err);
          const expected = ApiGatewayUtils.buildRedirectResponse({ url: 'https://moonmail.io' });
          expect(res).to.deep.equal(expected);
          return done();
        });
      });
    });
  });
Пример #20
0
  context('stub client', () => {
    const tableName = 'my-table';
    const hashKey = 'myKey';
    const rangeKey = 'myRange';
    const hashValue = 'some hash value';
    const rangeValue = 'some range value';
    const item = { myKey: 1, myRange: 2, anAttribute: 'its value', someAttribute: 'some_value', anotherAttribute: 'value', another: 'value' };
    const lastEvaluatedKey = { myKey: 1, myRange: 2 };
    const nextPage = base64url.encode(JSON.stringify(lastEvaluatedKey));
    const items = Array(5).fill().map(() => item);
    let tNameStub;
    let hashStub;
    let rangeStub;
    let clientStub;
    let schemaStub;

    before(() => {
      clientStub = sinon.stub(Model, '_client');
      clientStub.resolves('ok');
      clientStub.withArgs('query').resolves({ Items: items, LastEvaluatedKey: lastEvaluatedKey });
      clientStub.withArgs('get').resolves({ Item: item });
      tNameStub = sinon.stub(Model, 'tableName', { get: () => tableName });
      hashStub = sinon.stub(Model, 'hashKey', { get: () => hashKey });
      rangeStub = sinon.stub(Model, 'rangeKey', { get: () => rangeKey });
    });

    describe('#get', () => {
      context('only hash key was provided', () => {
        it('calls the DynamoDB get method with correct params', (done) => {
          Model.get(hashValue).then((result) => {
            const args = Model._client.lastCall.args;
            expect(args[0]).to.equal('get');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.deep.property(`Key.${hashKey}`, hashValue);
            expect(result).to.deep.equal(item);
            done();
          });
        });
      });

      context('range key was provided', () => {
        it('calls the DynamoDB get method with correct params', (done) => {
          Model.get(hashValue, rangeValue).then(() => {
            const args = Model._client.lastCall.args;
            expect(args[0]).to.equal('get');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.deep.property(`Key.${hashKey}`, hashValue);
            expect(args[1]).to.have.deep.property(`Key.${rangeKey}`, rangeValue);
            done();
          });
        });
      });

      context('fields filter was provided and include_fields is true', () => {
        it('calls the DynamoDB get method with correct params', (done) => {
          const fields = ['attr1', 'attr2'];
          const options = { fields: fields.join(','), include_fields: true };
          Model.get(hashValue, rangeValue, options).then(() => {
            const args = Model._client.lastCall.args;
            expect(args[0]).to.equal('get');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.deep.property(`Key.${hashKey}`, hashValue);
            expect(args[1]).to.have.deep.property(`Key.${rangeKey}`, rangeValue);
            const dbOptions = Model._buildOptions(options);
            for (let key in dbOptions) {
              expect(args[1][key]).to.deep.equal(dbOptions[key]);
            }
            done();
          });
        });
      });

      context('fields filter was provided and include_fields is false', () => {
        it('calls the DynamoDB get method with correct params', done => {
          const field = 'someAttribute';
          const options = { fields: field, include_fields: false };
          Model.get(hashValue, rangeValue, options).then(result => {
            const args = Model._client.lastCall.args;
            expect(args[0]).to.equal('get');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.deep.property(`Key.${hashKey}`, hashValue);
            expect(args[1]).to.have.deep.property(`Key.${rangeKey}`, rangeValue);
            expect(result).not.to.have.property(field);
            done();
          });
        });
      });
    });

    describe('#allBy', () => {
      const value = 'value';

      it('calls the DynamoDB query method with correct params', (done) => {
        Model.allBy(null, value).then((result) => {
          const args = Model._client.lastCall.args;
          expect(args[0]).to.equal('query');
          expect(args[1]).to.have.property('TableName', tableName);
          expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
          expect(args[1]).not.to.have.property('IndexName');
          expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', Model.hashKey);
          expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:hvalue', value);
          expect(result).to.have.property('items');
          expect(result).to.have.property('nextPage', nextPage);
          done();
        }).catch(done);
      });

      context('when the nexPage param was provided', () => {
        it('includes the ExclusiveStartKey in the query', (done) => {
          const page = nextPage;
          Model.allBy(null, value, { page }).then(() => {
            const args = Model._client.lastCall.args;
            expect(args[1].ExclusiveStartKey).to.deep.equal(lastEvaluatedKey);
            done();
          });
        });
      });

      context('fields filter was provided and include_fields is true', () => {
        it('calls the DynamoDB get method with correct params', (done) => {
          const attributes = ['attr1', 'attr2'];
          const options = { fields: attributes.join(','), include_fields: true };
          Model.allBy(null, value, options).then(result => {
            const args = Model._client.lastCall.args;
            expect(args[0]).to.equal('query');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
            expect(args[1]).not.to.have.property('IndexName');
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', Model.hashKey);
            expect(result).to.have.property('items');
            const dbOptions = Model._buildOptions(options);
            for (let key in dbOptions) {
              expect(args[1][key]).to.deep.contain(dbOptions[key]);
            }
            done();
          });
        });
      });

      context('fields filter was provided and include_fields is false', () => {
        it('filters the result', done => {
          const fields = ['anAttribute', 'anotherAttribute'];
          const options = { fields: fields.join(','), include_fields: false };
          Model.allBy(null, value, options).then(result => {
            const args = Model._client.lastCall.args;
            expect(args[0]).to.equal('query');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
            expect(args[1]).not.to.have.property('IndexName');
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', Model.hashKey);
            expect(result).to.have.property('items');
            result.items.forEach(item => {
              fields.forEach(field => expect(item).not.to.have.property(field));
            });
            done();
          })
            .catch(err => done(err));
        });
      });

      context('when range key filter was provided', () => {
        const rkey = 'anotherAttribute';
        const rvalue = 'value';
        const options = { range: { gt: {} } };
        options.range.gt[rkey] = rvalue;

        it('calls the DynamoDB query method with correct params', (done) => {
          Model.allBy(null, value, options).then((result) => {
            const args = Model._client.lastCall.args;
            expect(args[0]).to.equal('query');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue AND #rkey > :rvalue');
            expect(args[1]).not.to.have.property('IndexName');
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', Model.hashKey);
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#rkey', rkey);
            expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:hvalue', value);
            expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:rvalue', rvalue);
            expect(result).to.have.property('items');
            done();
          }).catch(done);
        });

        it('should build next key for the given range key', done => {
          Model.allBy(null, value, options).then((result) => {
            const lastKey = Model.lastEvaluatedKey(result.nextPage);
            expect(lastKey).to.have.property(rkey, item[rkey])
            expect(lastKey).to.have.property(Model.hashKey)
            expect(lastKey).to.have.property(Model.rangeKey)
            done();
          }).catch(done);
        });
      });

      context('when index name was provided', () => {
        it('calls the DynamoDB query method with correct params', (done) => {
          const indexName = 'my-index';
          const options = { indexName };
          Model.allBy(null, value, options).then(() => {
            const args = Model._client.lastCall.args;
            expect(args[0]).to.equal('query');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
            expect(args[1]).to.have.property('IndexName', indexName);
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', Model.hashKey);
            expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:hvalue', value);
            done();
          }).catch(done);
        });
      });

      context('when recusrive option was provided', () => {
          const firstResult = {
            items: [{ id: 'myKey1', attr: 1 }, { id: 'myKey2', attr: 2 }],
            nextPage: '2'
          };

          const secondResult = {
            items: [{ id: 'myKey3', attr: 3 }, { id: 'myKey4', attr: 4 }],
            nextPage: '3'
          };

          const lastResult = {
            items: [{ id: 'myKey5', attr: 5 }, { id: 'myKey6', attr: 6 }]
          };

          beforeEach(() => {
            sinon.stub(Model, '_allBy')
              .onFirstCall()
              .resolves(firstResult)
              .onSecondCall()
              .resolves(secondResult)
              .onThirdCall()
              .resolves(lastResult);
          });

          it('iterates recursively over the pages', (done) => {
            Model.allBy(null, value, { recursive: true }).then((results) => {
              expect(Model._allBy).to.have.been.calledThrice;
              expect(results).to.have.property('items');
              expect(results.items.length).to.equal(6);
              done();
            }).catch(err => done(err));
          });

          it('iterates recursively over the pages respecting the limit', (done) => {
            Model.allBy(null, value, { recursive: true, limit: 5 }).then((results) => {
              expect(Model._allBy).to.have.been.calledThrice;
              expect(results).to.have.property('items');
              expect(results.items.length).to.equal(5);
              done();
            }).catch(err => done(err));
          });

          afterEach(() => {
            Model._allBy.restore();
          });
      });
    });

    describe('#allBetween', () => {
      it('calls the DynamoDB query method with correct params', (done) => {
        const keyValue = 'id';
        const start = 1;
        const end = 2;
        Model.allBetween(keyValue, start, end).then((result) => {
          const args = Model._client.lastCall.args;
          expect(args[0]).to.equal('query');
          expect(args[1]).to.have.property('TableName', tableName);
          expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue AND #rkey BETWEEN :start AND :end');
          expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', Model.hashKey);
          expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#rkey', Model.rangeKey);
          expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:hvalue', keyValue);
          expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:start', start);
          expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:end', end);
          expect(result).to.have.property('items');
          done();
        }).catch(done);
      });
    });

    describe('#countBy', () => {
      const key = 'key';
      const value = 'value';
      it('calls the DynamoDB query method with correct params', (done) => {
        Model.countBy(key, value).then((result) => {
          const args = Model._client.lastCall.args;
          expect(args[0]).to.equal('query');
          expect(args[1]).to.have.property('TableName', tableName);
          expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
          expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', key);
          expect(args[1]).to.have.property('Select', 'COUNT');
          done();
        });
      });
    });

    describe('#save', () => {
      it('calls the DynamoDB put method with correct params', (done) => {
        let params = { id: 'key' };
        Model.save(params).then(() => {
          const args = Model._client.lastCall.args;
          expect(args[0]).to.equal('put');
          expect(args[1]).to.have.property('TableName', tableName);
          expect(args[1].Item).to.deep.contain(params);
          expect(args[1].Item).to.have.property('createdAt');
          done();
        });
      });
    });

    describe('#saveAll', () => {
      it('calls the DynamoDB batchWrite method with correct params', (done) => {
        const items = [{ id: 'key', some: { nonEmpty: 1 } }, { id: 'key2', some: { attribute: '', nonEmpty: 1 } }];
        const nItems = items.map((i) => omitEmpty(i));
        Model.saveAll(items).then(() => {
          const args = Model._client.lastCall.args;
          const method = args[0];
          const params = args[1];
          expect(method).to.equal('batchWrite');
          expect(params).to.have.deep.property(`RequestItems.${tableName}`);

          for (let item of params.RequestItems[tableName]) {
            expect(item).to.have.deep.property('PutRequest.Item');
            expect(nItems).to.include.something.that.deep.equals(item.PutRequest.Item);
          }
          done();
        });
      });
    });

    describe('#deleteAll', () => {
      it('calls the DynamoDB batchWrite method with correct params', (done) => {
        const itemsKeys = [['key1'], ['key2']];
        Model.deleteAll(itemsKeys).then(() => {
          const args = Model._client.lastCall.args;
          const method = args[0];
          const params = args[1];
          expect(method).to.equal('batchWrite');
          expect(params).to.have.deep.property(`RequestItems.${tableName}`);
          for (let request of params.RequestItems[tableName]) {
            expect(request).to.have.deep.property(`DeleteRequest.Key.${Model.hashKey}`);
          }
          done();
        });
      });
    });

    describe('#update', () => {
      it('calls the DynamoDB update method with correct params', (done) => {
        const params = { att: 'value', att2: 'value 2' };
        Model.update(params, hashValue, rangeValue).then(() => {
          const args = Model._client.lastCall.args;
          expect(args[0]).to.equal('update');
          expect(args[1]).to.have.property('TableName');
          expect(args[1]).to.have.property('Key');
          expect(args[1].AttributeUpdates).to.deep.equal(Model._buildAttributeUpdates(params));
          done();
        });
      });
    });

    describe('#increment', () => {
      it('calls the DynamoDB update method with correct params', (done) => {
        const count = 2;
        const countAttribute = 'someCount';
        Model.increment(countAttribute, count, hashValue, rangeValue).then(() => {
          const args = Model._client.lastCall.args;
          expect(args[0]).to.equal('update');
          expect(args[1]).to.have.property('TableName');
          expect(args[1]).to.have.property('Key');
          expect(args[1]).to.have.deep.property(`AttributeUpdates.${countAttribute}.Action`, 'ADD');
          expect(args[1]).to.have.deep.property(`AttributeUpdates.${countAttribute}.Value`, count);
          done();
        });
      });
    });

    describe('#incrementAll', () => {
      it('calls the DynamoDB update method with correct params', (done) => {
        const attrValue = {
          attr1: 1,
          attr2: -2
        };
        Model.incrementAll(hashValue, rangeValue, attrValue).then(() => {
          const args = Model._client.lastCall.args;
          expect(args[0]).to.equal('update');
          expect(args[1]).to.have.property('TableName');
          expect(args[1]).to.have.property('Key');
          expect(args[1]).to.have.deep.property('AttributeUpdates.attr1.Action', 'ADD');
          expect(args[1]).to.have.deep.property('AttributeUpdates.attr1.Value', 1);
          expect(args[1]).to.have.deep.property('AttributeUpdates.attr2.Action', 'ADD');
          expect(args[1]).to.have.deep.property('AttributeUpdates.attr2.Value', -2);
          done();
        });
      });
    });

    describe('#delete', () => {
      it('calls the DynamoDB delete method with correct params', (done) => {
        Model.delete(hashValue, rangeValue).then(() => {
          const args = Model._client.lastCall.args;
          expect(args[0]).to.equal('delete');
          expect(args[1]).to.have.property('TableName');
          expect(args[1].Key).to.deep.equal(Model._buildKey(hashValue, rangeValue));
          done();
        });
      });
    });

    describe('#isValid', () => {
      context('when using validation', () => {
        before(() => {
          const schema = Joi.object({
            attr1: Joi.string().required(),
            attr2: Joi.string().required()
          });
          schemaStub = sinon.stub(Model, 'schema', { get: () => schema });
        });

        it('succeeds if all required fields are valid', () => {
          expect(Model.isValid(validModel)).to.be.true;
        });

        it('fails if required fields are missing', () => {
          expect(Model.isValid(invalidModel)).to.be.false;
        });

        after(() => {
          schemaStub.restore();
        });
      });

      context('when not using validation', () => {
        before(() => {
          schemaStub = sinon.stub(Model, 'schema', { get: () => null });
        });

        it('succeeds if no validation schema is defined', () => {
          expect(Model.isValid({ some: 'object' })).to.be.true;
        });

        after(() => {
          schemaStub.restore();
        });
      });
    });

    after(() => {
      Model._client.restore();
      tNameStub.restore();
      hashStub.restore();
      rangeStub.restore();
    });
  });
Пример #21
0
  context('stub client', () => {
    const tableName = 'my-table';
    const hashKey = 'myKey';
    const rangeKey = 'myRange';
    const hashValue = 'some hash value';
    const rangeValue = 'some range value';
    const item = { myKey: 1, myRange: 2, anAttribute: 'its value', someAttribute: 'some_value', anotherAttribute: 'value', another: 'value' };
    const lastEvaluatedKey = { myKey: 1, myRange: 2 };
    const nextPage = base64url.encode(JSON.stringify(lastEvaluatedKey));
    const items = Array(5).fill().map(() => item);
    let tNameStub;
    let hashStub;
    let rangeStub;
    let clientStub;
    let schemaStub;

    before(() => {
      clientStub = sinon.stub(BaseModel, '_client');
      clientStub.resolves('ok');
      clientStub.withArgs('query').resolves({ Items: items, LastEvaluatedKey: lastEvaluatedKey });
      clientStub.withArgs('get').resolves({ Item: item });
      tNameStub = sinon.stub(BaseModel, 'tableName', { get: () => tableName });
      hashStub = sinon.stub(BaseModel, 'hashKey', { get: () => hashKey });
      rangeStub = sinon.stub(BaseModel, 'rangeKey', { get: () => rangeKey });
    });

    describe('#get', () => {
      context('only hash key was provided', () => {
        it('calls the DynamoDB get method with correct params', (done) => {
          BaseModel.get(hashValue).then((result) => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('get');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.deep.property(`Key.${hashKey}`, hashValue);
            expect(result).to.deep.equal(item);
            done();
          });
        });
      });

      context('range key was provided', () => {
        it('calls the DynamoDB get method with correct params', (done) => {
          BaseModel.get(hashValue, rangeValue).then(() => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('get');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.deep.property(`Key.${hashKey}`, hashValue);
            expect(args[1]).to.have.deep.property(`Key.${rangeKey}`, rangeValue);
            done();
          });
        });
      });

      context('fields filter was provided and include_fields is true', () => {
        it('calls the DynamoDB get method with correct params', (done) => {
          const fields = ['attr1', 'attr2'];
          const options = { fields: fields.join(','), include_fields: true };
          BaseModel.get(hashValue, rangeValue, options).then(() => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('get');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.deep.property(`Key.${hashKey}`, hashValue);
            expect(args[1]).to.have.deep.property(`Key.${rangeKey}`, rangeValue);
            const dbOptions = BaseModel._buildOptions(options);
            for (let key in dbOptions) {
              expect(args[1][key]).to.deep.equal(dbOptions[key]);
            }
            done();
          });
        });
      });

      context('fields filter was provided and include_fields is false', () => {
        it('calls the DynamoDB get method with correct params', done => {
          const field = 'someAttribute';
          const options = { fields: field, include_fields: false };
          BaseModel.get(hashValue, rangeValue, options).then(result => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('get');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.deep.property(`Key.${hashKey}`, hashValue);
            expect(args[1]).to.have.deep.property(`Key.${rangeKey}`, rangeValue);
            expect(result).not.to.have.property(field);
            done();
          });
        });
      });
    });

    describe('#allBy', () => {
      const value = 'value';

      it('calls the DynamoDB query method with correct params', (done) => {
        BaseModel.allBy(null, value).then((result) => {
          const args = BaseModel._client.lastCall.args;
          expect(args[0]).to.equal('query');
          expect(args[1]).to.have.property('TableName', tableName);
          expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
          expect(args[1]).not.to.have.property('IndexName');
          expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', BaseModel.hashKey);
          expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:hvalue', value);
          expect(result).to.have.property('items');
          expect(result).to.have.property('nextPage', nextPage);
          done();
        }).catch(done);
      });

      context('when the nexPage param was provided', () => {
        it('includes the ExclusiveStartKey in the query', (done) => {
          const page = nextPage;
          BaseModel.allBy(null, value, { page }).then(() => {
            const args = BaseModel._client.lastCall.args;
            expect(args[1].ExclusiveStartKey).to.deep.equal(lastEvaluatedKey);
            done();
          });
        });
      });

      context('fields filter was provided and include_fields is true', () => {
        it('calls the DynamoDB get method with correct params', (done) => {
          const attributes = ['attr1', 'attr2'];
          const options = { fields: attributes.join(','), include_fields: true };
          BaseModel.allBy(null, value, options).then(result => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('query');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
            expect(args[1]).not.to.have.property('IndexName');
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', BaseModel.hashKey);
            expect(result).to.have.property('items');
            const dbOptions = BaseModel._buildOptions(options);
            for (let key in dbOptions) {
              expect(args[1][key]).to.deep.contain(dbOptions[key]);
            }
            done();
          });
        });
      });

      context('fields filter was provided and include_fields is false', () => {
        it('filters the result', done => {
          const fields = ['anAttribute', 'anotherAttribute'];
          const options = { fields: fields.join(','), include_fields: false };
          BaseModel.allBy(null, value, options).then(result => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('query');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
            expect(args[1]).not.to.have.property('IndexName');
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', BaseModel.hashKey);
            expect(result).to.have.property('items');
            result.items.forEach(item => {
              fields.forEach(field => expect(item).not.to.have.property(field));
            });
            done();
          })
            .catch(err => done(err));
        });
      });

      context('when range key filter was provided', () => {
        const rkey = 'anotherAttribute';
        const rvalue = 'value';
        const options = { range: { gt: {} } };
        options.range.gt[rkey] = rvalue;

        it('calls the DynamoDB query method with correct params', (done) => {
          BaseModel.allBy(null, value, options).then((result) => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('query');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue AND #rkey > :rvalue');
            expect(args[1]).not.to.have.property('IndexName');
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', BaseModel.hashKey);
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#rkey', rkey);
            expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:hvalue', value);
            expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:rvalue', rvalue);
            expect(result).to.have.property('items');
            done();
          }).catch(done);
        });

        it('should build next key for the given range key', done => {
          BaseModel.allBy(null, value, options).then((result) => {
            const lastKey = BaseModel.lastEvaluatedKey(result.nextPage);
            expect(lastKey).to.have.property(rkey, item[rkey])
            expect(lastKey).to.have.property(BaseModel.hashKey)
            expect(lastKey).to.have.property(BaseModel.rangeKey)
            done();
          }).catch(done);
        });
      });

      context('when index name was provided', () => {
        it('calls the DynamoDB query method with correct params', (done) => {
          const indexName = 'my-index';
          const options = { indexName };
          BaseModel.allBy(null, value, options).then(() => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('query');
            expect(args[1]).to.have.property('TableName', tableName);
            expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
            expect(args[1]).to.have.property('IndexName', indexName);
            expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', BaseModel.hashKey);
            expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:hvalue', value);
            done();
          }).catch(done);
        });
      });

      context('when recusrive option was provided', () => {
        const firstResult = {
          items: [{ id: 'myKey1', attr: 1 }, { id: 'myKey2', attr: 2 }],
          nextPage: '2'
        };

        const secondResult = {
          items: [{ id: 'myKey3', attr: 3 }, { id: 'myKey4', attr: 4 }],
          nextPage: '3'
        };

        const lastResult = {
          items: [{ id: 'myKey5', attr: 5 }, { id: 'myKey6', attr: 6 }]
        };

        beforeEach(() => {
          sinon.stub(BaseModel, '_allBy')
            .onFirstCall()
            .resolves(firstResult)
            .onSecondCall()
            .resolves(secondResult)
            .onThirdCall()
            .resolves(lastResult);
        });

        it('iterates recursively over the pages', (done) => {
          BaseModel.allBy(null, value, { recursive: true }).then((results) => {
            expect(BaseModel._allBy).to.have.been.calledThrice;
            expect(results).to.have.property('items');
            expect(results.items.length).to.equal(6);
            done();
          }).catch(err => done(err));
        });

        it('iterates recursively over the pages respecting the limit', (done) => {
          BaseModel.allBy(null, value, { recursive: true, limit: 5 }).then((results) => {
            expect(BaseModel._allBy).to.have.been.calledThrice;
            expect(results).to.have.property('items');
            expect(results.items.length).to.equal(5);
            done();
          }).catch(err => done(err));
        });

        afterEach(() => {
          BaseModel._allBy.restore();
        });
      });
    });

    describe('#allBetween', () => {
      it('calls the DynamoDB query method with correct params', (done) => {
        const keyValue = 'id';
        const start = 1;
        const end = 2;
        BaseModel.allBetween(keyValue, start, end).then((result) => {
          const args = BaseModel._client.lastCall.args;
          expect(args[0]).to.equal('query');
          expect(args[1]).to.have.property('TableName', tableName);
          expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue AND #rkey BETWEEN :start AND :end');
          expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', BaseModel.hashKey);
          expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#rkey', BaseModel.rangeKey);
          expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:hvalue', keyValue);
          expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:start', start);
          expect(args[1]).to.have.deep.property('ExpressionAttributeValues.:end', end);
          expect(result).to.have.property('items');
          done();
        }).catch(done);
      });
    });

    describe('#countBy', () => {
      const key = 'key';
      const value = 'value';
      it('calls the DynamoDB query method with correct params', (done) => {
        BaseModel.countBy(key, value).then((result) => {
          const args = BaseModel._client.lastCall.args;
          expect(args[0]).to.equal('query');
          expect(args[1]).to.have.property('TableName', tableName);
          expect(args[1]).to.have.property('KeyConditionExpression', '#hkey = :hvalue');
          expect(args[1]).to.have.deep.property('ExpressionAttributeNames.#hkey', key);
          expect(args[1]).to.have.property('Select', 'COUNT');
          done();
        });
      });
    });

    describe('#save', () => {
      it('calls the DynamoDB put method with correct params', (done) => {
        let params = { id: 'key' };
        BaseModel.save(params).then(() => {
          const args = BaseModel._client.lastCall.args;
          expect(args[0]).to.equal('put');
          expect(args[1]).to.have.property('TableName', tableName);
          expect(args[1].Item).to.deep.contain(params);
          expect(args[1].Item).to.have.property('createdAt');
          done();
        });
      });
    });

    describe('.create', () => {
      const payload = { email: '*****@*****.**' };
      before(() => {
        // BaseModel don't return the data after saving
        sinon.stub(BaseModel, 'save').resolves({});
      });
      after(() => {
        BaseModel.save.restore();
      });
      it('validates and resolves accordingly', (done) => {
        ValidModel.create(payload)
          .then((item) => {
            expect(item.email).to.equals(payload.email);
            expect(item).to.have.property('createdAt');
            expect(item).to.have.property('id');
            done();
          }).catch(done);
      });
    });

    describe('#saveAll', () => {
      it('calls the DynamoDB batchWrite method with correct params', (done) => {
        const items = [{ id: 'key', some: { nonEmpty: 1 } }, { id: 'key2', some: { attribute: '', nonEmpty: 1 } }];
        const nItems = items.map((i) => omitEmpty(i));
        BaseModel.saveAll(items).then(() => {
          const args = BaseModel._client.lastCall.args;
          const method = args[0];
          const params = args[1];
          expect(method).to.equal('batchWrite');
          expect(params).to.have.deep.property(`RequestItems.${tableName}`);

          for (let item of params.RequestItems[tableName]) {
            expect(item).to.have.deep.property('PutRequest.Item');
            expect(nItems).to.include.something.that.deep.equals(item.PutRequest.Item);
          }
          done();
        });
      });
      context('when all the items are valid', () => {
        const item1 = { id: '1', email: '*****@*****.**' };
        const item2 = { id: '2', email: '*****@*****.**' };
        const item3 = { id: '3', email: '*****@*****.**' };
        it('validates and resolves accordingly', (done) => {
          ValidModel.saveAll([item1, item2, item3]).then((items) => {
            const args = BaseModel._client.lastCall.args;
            expect(args[0]).to.equal('batchWrite');
            const requestItems = args[1].RequestItems['my-table'];
            expect(requestItems.length).to.equals(3);
            const item = requestItems[0].PutRequest.Item;
            expect(item).to.have.property('createdAt');
            expect(item).to.have.property('id');
            done();
          }).catch(done);
        });
      });

      context('when some item doesnt validate', () => {
        it('raises an error', (done) => {
          ValidModel.saveAll([{ email: 'ups1@' }, { id: '1', email: '*****@*****.**' }])
            .catch((err) => {
              expect(err.name).to.equals('ValidationError');
              done();
            });
        });
      });
    });

    describe('.batchCreate', () => {
      const items = Array.apply(null, { length: 50 }).map(Number.call, Number).map(i => ({ email: `example${i}@email.com` }));
      before(() => {
        sinon.stub(BaseModel, 'saveAll').resolves(items.slice(0, 25));
      });
      after(() => {
        BaseModel.saveAll.restore();
      });
      it('flattens inputs and outputs and splits in batches if the length of the input is more than 25', (done) => {
        ValidModel.batchCreate(items)
          .then((itms) => {
            itms.forEach(item => expect(item).to.be.a('object'));
            expect(itms.length).to.equals(50);
            done();
          }).catch(done);
      });
    });

    describe('#deleteAll', () => {
      it('calls the DynamoDB batchWrite method with correct params', (done) => {
        const itemsKeys = [['key1'], ['key2']];
        BaseModel.deleteAll(itemsKeys).then(() => {
          const args = BaseModel._client.lastCall.args;
          const method = args[0];
          const params = args[1];
          expect(method).to.equal('batchWrite');
          expect(params).to.have.deep.property(`RequestItems.${tableName}`);
          for (let request of params.RequestItems[tableName]) {
            expect(request).to.have.deep.property(`DeleteRequest.Key.${BaseModel.hashKey}`);
          }
          done();
        });
      });
    });

    describe('#update', () => {
      it('calls the DynamoDB update method with correct params', (done) => {
        const params = { att: 'value', att2: 'value 2' };
        BaseModel.update(params, hashValue, rangeValue).then(() => {
          const args = BaseModel._client.lastCall.args;
          expect(args[0]).to.equal('update');
          expect(args[1]).to.have.property('TableName');
          expect(args[1]).to.have.property('Key');
          expect(args[1].AttributeUpdates).to.deep.equal(BaseModel._buildAttributeUpdates(params));
          done();
        });
      });
      context('when the update payload is empty', () => {
        it('raises an error', (done) => {
          ValidModel.update({}, '1')
            .catch((err) => {
              expect(err).to.match(/EmptyPayload/);
              done();
            });
        });
      });

      context('when the update payload exists', () => {
        const payload = { id: '1', email: '*****@*****.**' };
        it('validates and resolves accordingly', (done) => {
          const params = { email: payload.email };
          ValidModel.update(params, '1')
            .then((item) => {
              const args = BaseModel._client.lastCall.args;
              expect(args[0]).to.equal('update');
              expect(args[1]).to.have.property('TableName');
              expect(args[1]).to.have.property('Key');
              expect(args[1].AttributeUpdates).to.deep.equal(BaseModel._buildAttributeUpdates(Object.assign({}, params, { updatedAt: 1111111 })));
              done();
            }).catch(done);
        });
      });
    });

    describe('#increment', () => {
      it('calls the DynamoDB update method with correct params', (done) => {
        const count = 2;
        const countAttribute = 'someCount';
        BaseModel.increment(countAttribute, count, hashValue, rangeValue).then(() => {
          const args = BaseModel._client.lastCall.args;
          expect(args[0]).to.equal('update');
          expect(args[1]).to.have.property('TableName');
          expect(args[1]).to.have.property('Key');
          expect(args[1]).to.have.deep.property(`AttributeUpdates.${countAttribute}.Action`, 'ADD');
          expect(args[1]).to.have.deep.property(`AttributeUpdates.${countAttribute}.Value`, count);
          done();
        });
      });
    });

    describe('#incrementAll', () => {
      it('calls the DynamoDB update method with correct params', (done) => {
        const attrValue = {
          attr1: 1,
          attr2: -2
        };
        BaseModel.incrementAll(hashValue, rangeValue, attrValue).then(() => {
          const args = BaseModel._client.lastCall.args;
          expect(args[0]).to.equal('update');
          expect(args[1]).to.have.property('TableName');
          expect(args[1]).to.have.property('Key');
          expect(args[1]).to.have.deep.property('AttributeUpdates.attr1.Action', 'ADD');
          expect(args[1]).to.have.deep.property('AttributeUpdates.attr1.Value', 1);
          expect(args[1]).to.have.deep.property('AttributeUpdates.attr2.Action', 'ADD');
          expect(args[1]).to.have.deep.property('AttributeUpdates.attr2.Value', -2);
          done();
        });
      });
    });

    describe('#delete', () => {
      it('calls the DynamoDB delete method with correct params', (done) => {
        BaseModel.delete(hashValue, rangeValue).then(() => {
          const args = BaseModel._client.lastCall.args;
          expect(args[0]).to.equal('delete');
          expect(args[1]).to.have.property('TableName');
          expect(args[1].Key).to.deep.equal(BaseModel._buildKey(hashValue, rangeValue));
          done();
        });
      });
    });

    describe('#validate', () => {
      it('skips the validation if schema is null', (done) => {
        const emptyModel = { name: 'Some name' };
        EmptyModel.validate(null, emptyModel)
          .then((emptyModelResult) => {
            expect(emptyModelResult).to.deep.equals(emptyModel);
            done();
          }).catch(done);
      });
      it('delegates to Joi.validate if the schema is not null', (done) => {
        const validModel = { email: '*****@*****.**' };
        ValidModel.validate(Joi.object({
          email: Joi.string().required().email()
        }), validModel).then((validModelResult) => {
          expect(validModelResult).to.deep.equals(validModel);
          done();
        });
      });
    });

    describe('#isValid', () => {
      context('when using validation', () => {
        before(() => {
          const schema = Joi.object({
            attr1: Joi.string().required(),
            attr2: Joi.string().required()
          });
          schemaStub = sinon.stub(BaseModel, 'schema', { get: () => schema });
        });

        it('succeeds if all required fields are valid', () => {
          expect(BaseModel.isValid(validModel)).to.be.true;
        });

        it('fails if required fields are missing', () => {
          expect(BaseModel.isValid(invalidModel)).to.be.false;
        });

        after(() => {
          schemaStub.restore();
        });
      });

      context('when not using validation', () => {
        before(() => {
          schemaStub = sinon.stub(BaseModel, 'schema', { get: () => null });
        });

        it('succeeds if no validation schema is defined', () => {
          expect(BaseModel.isValid({ some: 'object' })).to.be.true;
        });

        after(() => {
          schemaStub.restore();
        });
      });
    });

    describe('.find', () => {
      context('when the searched item exists', () => {
        before(() => {
          sinon.stub(BaseModel, 'get').resolves({ id: '1' });
        });
        after(() => {
          BaseModel.get.restore();
        });
        it('returns it', (done) => {
          ValidModel.find('1').then((item) => {
            expect(item).to.deep.equals({ id: '1' });
            done();
          }).catch(done);
        });
      });
      context('when the searched item doesnt exist', () => {
        before(() => {
          sinon.stub(BaseModel, 'get').resolves({});
        });
        after(() => {
          BaseModel.get.restore();
        });
        it('raises an error', (done) => {
          ValidModel.find(1).catch((err) => {
            expect(err).to.match(/ItemNotFound/);
            done();
          });
        });
      });
    });

    after(() => {
      BaseModel._client.restore();
      tNameStub.restore();
      hashStub.restore();
      rangeStub.restore();
    });
  });