UNPKG

@quarks/quarks-iam

Version:

A modern authorization server built to authenticate your users and protect your APIs

932 lines (635 loc) 23.1 kB
# Test dependencies cwd = process.cwd() path = require 'path' faker = require 'faker' chai = require 'chai' sinon = require 'sinon' sinonChai = require 'sinon-chai' mockMulti = require '../lib/multi' qs = require 'qs' expect = chai.expect # Configure Chai and Sinon chai.use sinonChai chai.should() # Code under test Modinha = require 'modinha' User = require path.join(cwd, 'models/User') settings = require path.join(cwd, 'boot/settings') Role = require path.join(cwd, 'models/Role') # Redis lib for spying and stubbing Redis = require('ioredis') rclient = Redis.prototype {client,multi} = {} describe 'User', -> before -> client = new Redis(12345) multi = mockMulti(rclient) User.__client = client Role.__client = client after -> rclient.multi.restore() {data,user,users,role,roles,jsonUsers} = {} {err,validation,instance,instances,update,deleted,original,ids} = {} {stat,info,options,userInfo} = {} before -> # Mock data data = [] for i in [0..9] data.push name: "#{faker.name.firstName()} #{faker.name.lastName()}" email: faker.internet.email() hash: 'private' password: 'secret1337' users = User.initialize(data, { private: true }) jsonUsers = users.map (d) -> User.serialize(d) ids = users.map (d) -> d._id describe 'schema', -> beforeEach -> user = new User validation = user.validate() it 'should have unique identifier', -> User.schema[User.uniqueId].should.be.a('object') # STANDARD CLAIMS it 'should have name', -> User.schema.name.type.should.equal 'string' it 'should have given name', -> User.schema.givenName.type.should.equal 'string' it 'should have family name', -> User.schema.familyName.type.should.equal 'string' it 'should have middle name', -> User.schema.middleName.type.should.equal 'string' it 'should have nickname', -> User.schema.nickname.type.should.equal 'string' it 'should have perferredUsername', -> User.schema.preferredUsername.type.should.equal 'string' it 'should have profile', -> User.schema.profile.type.should.equal 'string' it 'should have picture', -> User.schema.picture.type.should.equal 'string' it 'should have website', -> User.schema.website.type.should.equal 'string' it 'should have email', -> User.schema.email.type.should.equal 'string' # Currently email is not required it 'should not require email', -> validation = (new User email: undefined).validate() validation.valid.should.be.true ### # If email is required it 'should require email', -> validation.errors.email.attribute.should.equal 'required' ### it 'should require email to be valid', -> validation = (new User email: 'not-valid').validate() validation.errors.email.attribute.should.equal 'format' it 'should have email verified', -> User.schema.emailVerified.type.should.equal 'boolean' it 'should have gender', -> User.schema.gender.type.should.equal 'string' it 'should have birthdate', -> User.schema.birthdate.type.should.equal 'string' it 'should have zoneinfo', -> User.schema.zoneinfo.type.should.equal 'string' it 'should have locale', -> User.schema.locale.type.should.equal 'string' it 'should have phone number', -> User.schema.phoneNumber.type.should.equal 'string' it 'should have phone number verified', -> User.schema.phoneNumberVerified.type.should.equal 'boolean' it 'should have address', -> User.schema.address.type.should.equal 'object' # HASHED PASSWORD it 'should have hash', -> User.schema.hash.type.should.equal 'string' it 'should hash a password', -> user = new User { password: 'secret1337' }, { private: true } expect(typeof user.hash).equals 'string' it 'should protect hash', -> User.schema.hash.private.should.equal true # TIMESTAMPS it 'should have "created" timestamp', -> User.schema.created.default.should.equal Modinha.defaults.timestamp it 'should have "modified" timestamp', -> User.schema.modified.default.should.equal Modinha.defaults.timestamp describe 'insert', -> describe 'with valid data', -> beforeEach (done) -> sinon.spy multi, 'hset' sinon.spy multi, 'zadd' sinon.spy User, 'index' sinon.stub(User, 'enforceUnique').callsArgWith(1, null) sinon.stub(multi, 'exec').callsArgWith 0, null User.insert data[0], (error, result) -> err = error instance = result done() afterEach -> multi.hset.restore() multi.zadd.restore() User.index.restore() User.enforceUnique.restore() multi.exec.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide the inserted instance', -> expect(instance).to.be.instanceof User it 'should not provide private properties', -> expect(instance.hash).to.be.undefined it 'should store the hashed password', -> multi.hset.should.have.been.calledWith 'users', instance._id, sinon.match('"hash":"') it 'should discard the password', -> expect(instance.password).to.be.undefined multi.hset.should.not.have.been.calledWith 'users', instance._id, sinon.match('password') it 'should store the serialized instance by unique id', -> multi.hset.should.have.been.calledWith 'users', instance._id, sinon.match('"name":"' + instance.name + '"') it 'should index the instance', -> User.index.should.have.been.calledWith sinon.match.object, sinon.match(instance) describe 'with invalid data', -> before (done) -> sinon.spy multi, 'hset' sinon.spy multi, 'zadd' sinon.spy User, 'index' User.insert { email: 'not-valid', password: 'secret1337' }, (error, result) -> err = error instance = result done() after -> multi.hset.restore() multi.zadd.restore() User.index.restore() it 'should provide a validation error', -> err.should.be.instanceof Modinha.ValidationError it 'should not provide an instance', -> expect(instance).to.be.undefined it 'should not store the data', -> multi.hset.should.not.have.been.called it 'should not index the data', -> User.index.should.not.have.been.called describe 'with a weak password', -> before (done) -> User.insert { email: 'valid@example.com', password: 'secret' }, (error, result) -> err = error instance = result done() it 'should provide an error', -> err.name.should.equal 'InsecurePasswordError' it 'should not provide an instance', -> expect(instance).to.be.undefined describe 'without a password', -> before (done) -> User.insert { email: 'valid@example.com' }, (error, instance) -> err = error user = instance done() it 'should provide an error', -> err.name.should.equal 'PasswordRequiredError' it 'should not provide an instance', -> expect(user).to.be.undefined describe 'with private values option', -> beforeEach (done) -> sinon.spy multi, 'hset' sinon.spy multi, 'zadd' sinon.spy User, 'index' sinon.stub(User, 'enforceUnique').callsArgWith(1, null) sinon.stub(multi, 'exec').callsArgWith 0, null, [] User.insert data[0], { private: true }, (error, result) -> err = error instance = result done() afterEach -> multi.hset.restore() multi.zadd.restore() multi.exec.restore() User.index.restore() User.enforceUnique.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide the inserted instance', -> expect(instance).to.be.instanceof User it 'should provide private properties', -> expect(instance.hash).to.be.a.string describe 'with duplicate email', -> beforeEach (done) -> sinon.spy multi, 'hset' sinon.spy multi, 'zadd' sinon.spy User, 'index' sinon.stub(User, 'getByEmail') .callsArgWith 1, null, users[0] User.insert data[0], (error, result) -> err = error instance = result done() afterEach -> multi.hset.restore() multi.zadd.restore() User.index.restore() User.getByEmail.restore() it 'should provide a unique value error', -> expect(err).to.be.instanceof User.UniqueValueError it 'should not provide an instance', -> expect(instance).to.be.undefined describe 'password verification', -> before -> src = email: faker.internet.email(), password: 'secret1337' user = new User src, { private: true } it 'should verify a correct password', (done) -> user.verifyPassword 'secret1337', (err, match) -> match.should.be.true done() it 'should not verify an incorrect password', (done) -> user.verifyPassword 'wrong', (err, match) -> match.should.be.false done() it 'should not verify against an undefined hash', (done) -> user = new User expect(user.hash).to.be.undefined user.verifyPassword 'secret', (err, match) -> match.should.be.false done() describe 'password strength validation', -> describe 'with a weak password', -> it 'should return false', -> User.verifyPasswordStrength('password').should.equal false describe 'with a strong password', -> it 'should return true', -> User.verifyPasswordStrength('_u247c^c5u4@$324v23').should.equal true describe 'change password', -> before -> sinon.stub(User, 'patch').callsFake((id, data, opts, cb) -> cb null, { _id: id, hash: 'fakehash' }) after -> User.patch.restore() describe 'without a password', -> {err,user} = {} before -> User.changePassword 'someid', null, (error, userObj) -> err = error user = userObj it 'should provide a PasswordRequiredError', -> err.name.should.equal 'PasswordRequiredError' it 'should not return a user', -> expect(user).to.not.be.ok describe 'with a weak password', -> {err,user} = {} before -> User.changePassword 'someid', 'password', (error, userObj) -> err = error user = userObj it 'should provide a InsecurePasswordError', -> err.name.should.equal 'InsecurePasswordError' it 'should not return a user', -> expect(user).to.not.be.ok describe 'with a strong password', -> {err,user} = {} before -> User.changePassword 'someid', '91385m%@%##$G.', (error, userObj) -> err = error user = userObj it 'should provide a null error', -> expect(err).to.not.be.ok it 'should return a user instance', -> expect(user).to.be.an 'object' user._id.should.equal 'someid' user.hash.should.be.a 'string' describe 'authentication', -> describe 'with valid email and password credentials', -> before (done) -> {email,password} = data[0] sinon.stub(User, 'getByEmail').callsArgWith(2, null, users[0]) sinon.stub(User.prototype, 'verifyPassword').callsArgWith(1, null, true) User.authenticate email, password, (error, instance, information) -> err = error user = instance info = information done() after -> User.getByEmail.restore() User.prototype.verifyPassword.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide an User instance', -> expect(user).to.be.instanceof User it 'should provide a message', -> info.message.should.equal 'Authenticated successfully!' describe 'with unknown user', -> before (done) -> {email,password} = data[0] sinon.stub(User, 'getByEmail').callsArgWith(2, null, null) User.authenticate email, password, (error, instance, information) -> err = error user = instance info = information done() after -> User.getByEmail.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide a false user', -> expect(user).to.be.false it 'should provide a message', -> info.message.should.equal 'Unknown user.' describe 'with incorrect password', -> before (done) -> {email} = data[0] sinon.stub(User, 'getByEmail').callsArgWith(2, null, users[0]) sinon.stub(User.prototype, 'verifyPassword').callsArgWith(1, null, false) User.authenticate email, 'wrong', (error, instance, information) -> err = error user = instance info = information done() after -> User.getByEmail.restore() User.prototype.verifyPassword.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide a false user', -> expect(user).to.be.false it 'should provide a message', -> info.message.should.equal 'Invalid password.' describe 'password reset', -> describe 'user verification', -> # describe 'add roles', -> before (done) -> user = users[0] role = new Role sinon.stub(multi, 'exec').callsArgWith 0, null, [] sinon.spy multi, 'zadd' User.addRoles user, role, done after -> multi.exec.restore() multi.zadd.restore() it 'should index the role by the user', -> multi.zadd.should.have.been.calledWith "users:#{user._id}:roles", role.created, role._id it 'should index the user by the role', -> multi.zadd.should.have.been.calledWith "roles:#{role._id}:users", user.created, user._id describe 'remove roles', -> before (done) -> user = users[1] role = new Role sinon.stub(multi, 'exec').callsArgWith 0, null, [] sinon.spy multi, 'zrem' User.removeRoles user, role, done after -> multi.exec.restore() multi.zrem.restore() it 'should deindex the role by the user', -> multi.zrem.should.have.been.calledWith "users:#{user._id}:roles", role._id it 'should deindex the user by the role', -> multi.zrem.should.have.been.calledWith "roles:#{role._id}:users", user._id describe 'list by roles', -> before (done) -> role = new Role name: 'authority' sinon.stub(User, 'list').callsArgWith 1, null, [] User.listByRoles role.name, done after -> User.list.restore() it 'should look in the users index', -> User.list.should.have.been.calledWith( sinon.match({ index: "roles:#{role.name}:users" }) ) describe 'lookup with authenticated user', -> {authenticated} = {} before (done) -> authenticated = new User _id: 'r4nd0m' req = user: authenticated info = id: '1234' User.lookup req, info, (error, instance) -> err = error user = instance done() it 'should provide a null error', -> expect(err).to.be.null it 'should provide the authenticated user', -> user.should.equal authenticated describe 'lookup with unauthenticated known user', -> it 'should provide a null error' it 'should provide the user' describe 'lookup with unknown user', -> it 'should provide a null error' it 'should provide a null user' describe 'connect with authenticated user', -> before (done) -> user = new User() req = params: provider: 'google' user: user auth = access_token: 'b34r3r' info = id: 'g00gl3' sinon.stub(User, 'patch').callsArgWith(2, null, user) User.connect req, auth, info, (error, instance) -> err = error user = instance done() after -> User.patch.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide a user', -> user.should.be.instanceof User it 'should update the provider id', -> User.patch.should.have.been.calledWith user._id, { lastProvider: 'google', providers: { google: { provider: 'google', protocol: 'OAuth2', auth: { access_token: 'b34r3r' }, info: { id: 'g00gl3' } } } } describe 'connect with unauthenticated existing user', -> before (done) -> user = new User() req = params: provider: 'google' auth = access_token: 'b34r3r' info = id: 'g00gl3_2' sinon.stub(User, 'lookup').callsArgWith(2, null, user) sinon.stub(User, 'patch').callsArgWith(2, null, user) User.connect req, auth, info, (error, instance) -> err = error user = instance done() after -> User.lookup.restore() User.patch.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide a user', -> user.should.be.instanceof User it 'should update the provider id', -> User.patch.should.have.been.calledWith user._id, { lastProvider: 'google', providers: { google: { provider: 'google', protocol: 'OAuth2', auth: { access_token: 'b34r3r' }, info: { id: 'g00gl3_2' } } } } describe 'connect with new user and existing email', -> before (done) -> user = new User() req = params: provider: 'google' auth = access_token: 'b34r3r' userInfo = id: 'g00gl3_3' email: 'john@smith.com' given_name: 'John' family_name: 'Smith' user = _id: 'uuid' email: 'john@smith.com' providers: github: {} uniqueError = new Error 'email must be unique' sinon.stub(User, 'lookup').callsArgWith(2, null, null) sinon.stub(User, 'insert').callsArgWith(2, uniqueError) sinon.stub(User, 'getByEmail').callsArgWith(1, null, user) User.connect req, auth, userInfo, (error, _instance, information) -> err = error instance = _instance info = information done() after -> User.lookup.restore() User.insert.restore() User.getByEmail.restore() it 'should not provide an error', -> expect(err).to.be.null it 'should not provide a user', -> expect(instance).to.be.false it 'should provide a message', -> info.message.should.equal 'email must be unique' it 'should provide providers', -> info.providers.should.equal user.providers describe 'connect with new user', -> before (done) -> user = new User() req = params: provider: 'google' connectParams: redirect_uri: 'https://app.example.com/callback' client_id: 'uuid' response_type: 'id_token token' scope: 'openid profile' flash: sinon.spy() provider: emailVerification: enable: false auth = access_token: 'b34r3r' info = id: 'g00gl3_3' given_name: 'John' family_name: 'Smith' sinon.stub(User, 'lookup').callsArgWith(2, null, null) sinon.stub(User, 'insert').callsArgWith(2, null, user) User.connect req, auth, info, (error, instance) -> err = error user = instance done() after -> User.lookup.restore() User.insert.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide a user', -> user.should.be.instanceof User it 'should insert the user profile', -> User.insert.should.have.been.calledWith sinon.match({ givenName: 'John' familyName: 'Smith' providers: google: provider: 'google' protocol: 'OAuth2' auth: { access_token: 'b34r3r' } info: id: 'g00gl3_3' given_name: 'John' family_name: 'Smith' }) #it 'should include a mapping in the options', -> # User.insert.should.have.been.calledWith sinon.match.object, sinon.match({ # mapping: 'google' # }) it 'should disable the password requirement', -> User.insert.should.have.been.calledWith sinon.match.object, sinon.match({ password: false }) describe 'connect existing user with refreshed userinfo', -> before (done) -> settings.refresh_userinfo = true user = new User email: 'initial@example.com' req = params: provider: 'google' auth = access_token: 'b34r3r' info = id: 'g00gl3_2' email: 'updated@example.com' sinon.stub(User, 'lookup').callsArgWith(2, null, user) sinon.stub(User, 'patch').callsArgWith(2, null, user) sinon.spy(Modinha, 'map') User.connect req, auth, info, (error, instance) -> err = error user = instance done() after -> delete settings.refresh_userinfo User.lookup.restore() User.patch.restore() Modinha.map.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide a user', -> user.should.be.instanceof User it 'should update user claims from provider info', -> Modinha.map.should.have.been.called describe 'connect existing user without refreshed userinfo', -> before (done) -> user = new User email: 'initial@example.com' req = params: provider: 'google' auth = access_token: 'b34r3r' info = id: 'g00gl3_2' email: 'updated@example.com' sinon.stub(User, 'lookup').callsArgWith(2, null, user) sinon.stub(User, 'patch').callsArgWith(2, null, user) sinon.spy(Modinha, 'map') User.connect req, auth, info, (error, instance) -> err = error user = instance done() after -> User.lookup.restore() User.patch.restore() Modinha.map.restore() it 'should provide a null error', -> expect(err).to.be.null it 'should provide a user', -> user.should.be.instanceof User it 'should update user claims from provider info', -> Modinha.map.should.not.have.been.called