const should = require('should'), url = require('url'), sinon = require('sinon'), _ = require('lodash'), schema = require('../../../server/data/schema'), models = require('../../../server/models'), validation = require('../../../server/data/validation'), common = require('../../../server/lib/common'), security = require('../../../server/lib/security'), testUtils = require('../../utils'), sandbox = sinon.sandbox.create(); describe('Unit: models/user', function () { before(function () { models.init(); }); before(testUtils.teardown); before(testUtils.setup('users:roles')); afterEach(function () { sandbox.restore(); }); describe('validation', function () { beforeEach(function () { sandbox.stub(security.password, 'hash').resolves('$2a$10$we16f8rpbrFZ34xWj0/ZC.LTPUux8ler7bcdTs5qIleN6srRHhilG'); }); describe('password', function () { it('no password', function () { return models.User.add({email: 'test1@ghost.org', name: 'Ghosty'}) .then(function (user) { user.get('name').should.eql('Ghosty'); should.exist(user.get('password')); }); }); it('only numbers', function () { return models.User.add({email: 'test2@ghost.org', name: 'Wursti', password: 109674836589}) .then(function (user) { user.get('name').should.eql('Wursti'); should.exist(user.get('password')); }); }); it('can change password', function () { let oldPassword; return models.User.findOne({slug: 'joe-bloggs'}) .then(function (user) { user.get('slug').should.eql('joe-bloggs'); oldPassword = user.get('password'); user.set('password', '12734!!332'); return user.save(); }) .then(function (user) { user.get('slug').should.eql('joe-bloggs'); user.get('password').should.not.eql(oldPassword); }); }); }); describe('blank', function () { it('name cannot be blank', function () { return models.User.add({email: 'test@ghost.org'}) .then(function () { throw new Error('expected ValidationError'); }) .catch(function (err) { (err instanceof common.errors.ValidationError).should.be.true; err.message.should.match(/users\.name/); }); }); it('email cannot be blank', function () { let data = {name: 'name'}; sandbox.stub(models.User, 'findOne').resolves(null); return models.User.add(data) .then(function () { throw new Error('expected ValidationError'); }) .catch(function (err) { err.should.be.an.Array(); (err[0] instanceof common.errors.ValidationError).should.eql.true; err[0].message.should.match(/users\.email/); }); }); }); }); describe('fn: check', function () { beforeEach(function () { sandbox.stub(security.password, 'hash').resolves('$2a$10$we16f8rpbrFZ34xWj0/ZC.LTPUux8ler7bcdTs5qIleN6srRHhilG'); }); it('user status is warn', function () { sandbox.stub(security.password, 'compare').resolves(true); // NOTE: Add a user with a broken field to ensure we only validate changed fields on login sandbox.stub(validation, 'validateSchema').resolves(); const user = testUtils.DataGenerator.forKnex.createUser({ status: 'warn-1', email: 'test-9@example.de', website: '!!!!!this-is-not-a-website!!!!' }); return models.User.add(user) .then(function (model) { validation.validateSchema.restore(); return models.User.check({email: model.get('email'), password: 'test'}); }); }); it('user status is active', function () { sandbox.stub(security.password, 'compare').resolves(true); return models.User.check({email: testUtils.DataGenerator.Content.users[1].email, password: 'test'}); }); it('password is incorrect', function () { sandbox.stub(security.password, 'compare').resolves(false); return models.User.check({email: testUtils.DataGenerator.Content.users[1].email, password: 'test'}) .catch(function (err) { (err instanceof common.errors.ValidationError).should.be.true; }); }); it('user not found', function () { sandbox.stub(security.password, 'compare').resolves(true); return models.User.check({email: 'notfound@example.to', password: 'test'}) .catch(function (err) { (err instanceof common.errors.NotFoundError).should.be.true; }); }); it('user not found', function () { sandbox.stub(security.password, 'compare').resolves(true); return models.User.check({email: null, password: 'test'}) .catch(function (err) { (err instanceof common.errors.NotFoundError).should.be.true; }); }); }); describe('fn: permissible', function () { function getUserModel(id, role) { var hasRole = sandbox.stub(); hasRole.withArgs(role).returns(true); return { hasRole: hasRole, related: sandbox.stub().returns([{name: role}]), get: sandbox.stub().returns(id) }; } it('cannot delete owner', function (done) { var mockUser = getUserModel(1, 'Owner'), context = {user: 1}; models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.owner, true, true).then(() => { done(new Error('Permissible function should have errored')); }).catch((error) => { error.should.be.an.instanceof(common.errors.NoPermissionError); should(mockUser.hasRole.calledOnce).be.true(); done(); }); }); it('can always edit self', function () { var mockUser = getUserModel(3, 'Contributor'), context = {user: 3}; return models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.contributor, false, true).then(() => { should(mockUser.get.calledOnce).be.true(); }); }); describe('as editor', function () { it('can\'t edit another editor', function (done) { var mockUser = getUserModel(3, 'Editor'), context = {user: 2}; models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true).then(() => { done(new Error('Permissible function should have errored')); }).catch((error) => { error.should.be.an.instanceof(common.errors.NoPermissionError); should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); done(); }); }); it('can\'t edit an admin', function (done) { var mockUser = getUserModel(3, 'Administrator'), context = {user: 2}; models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true).then(() => { done(new Error('Permissible function should have errored')); }).catch((error) => { error.should.be.an.instanceof(common.errors.NoPermissionError); should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); done(); }); }); it('can edit author', function () { var mockUser = getUserModel(3, 'Author'), context = {user: 2}; return models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true).then(() => { should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); }); }); it('can edit contributor', function () { var mockUser = getUserModel(3, 'Contributor'), context = {user: 2}; return models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true).then(() => { should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); }); }); it('can destroy self', function () { var mockUser = getUserModel(3, 'Editor'), context = {user: 3}; return models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true).then(() => { should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); }); }); it('can\'t destroy another editor', function (done) { var mockUser = getUserModel(3, 'Editor'), context = {user: 2}; models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true).then(() => { done(new Error('Permissible function should have errored')); }).catch((error) => { error.should.be.an.instanceof(common.errors.NoPermissionError); should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); done(); }); }); it('can\'t destroy an admin', function (done) { var mockUser = getUserModel(3, 'Administrator'), context = {user: 2}; models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true).then(() => { done(new Error('Permissible function should have errored')); }).catch((error) => { error.should.be.an.instanceof(common.errors.NoPermissionError); should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); done(); }); }); it('can destroy an author', function () { var mockUser = getUserModel(3, 'Author'), context = {user: 2}; return models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true).then(() => { should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); }); }); it('can destroy a contributor', function () { var mockUser = getUserModel(3, 'Contributor'), context = {user: 2}; return models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true).then(() => { should(mockUser.hasRole.called).be.true(); should(mockUser.get.calledOnce).be.true(); }); }); }); }); describe('Fetch', function () { before(function () { models.init(); }); after(function () { sandbox.restore(); }); it('ensure data type', function () { return models.User.findOne({slug: 'joe-bloggs'}, testUtils.context.internal) .then((user) => { user.get('updated_by').should.be.a.String(); user.get('created_by').should.be.a.String(); user.get('created_at').should.be.a.Date(); user.get('updated_at').should.be.a.Date(); }); }); }); describe('Edit', function () { before(function () { models.init(); }); after(function () { sandbox.restore(); }); it('resets given empty value to null', function () { return models.User.findOne({slug: 'joe-bloggs'}) .then(function (user) { user.get('slug').should.eql('joe-bloggs'); user.get('profile_image').should.eql('https://example.com/super_photo.jpg'); user.set('profile_image', ''); user.set('bio', ''); return user.save(); }) .then(function (user) { should(user.get('profile_image')).be.null(); user.get('bio').should.eql(''); }); }); }); describe('Add', function () { const events = { user: [] }; before(function () { models.init(); sandbox.stub(models.User.prototype, 'emitChange').callsFake(function (event) { events.user.push({event: event, data: this.toJSON()}); }); }); after(function () { sandbox.restore(); }); it('defaults', function () { return models.User.add({slug: 'joe', name: 'Joe', email: 'joe@test.com'}) .then(function (user) { user.get('name').should.eql('Joe'); user.get('email').should.eql('joe@test.com'); user.get('slug').should.eql('joe'); user.get('visibility').should.eql('public'); user.get('status').should.eql('active'); _.each(_.keys(schema.tables.users), (key) => { should.exist(events.user[0].data.hasOwnProperty(key)); if (['status', 'visibility'].indexOf(key) !== -1) { events.user[0].data[key].should.eql(schema.tables.users[key].defaultTo); } }); }); }); }); });