const UsersService = require('../../../services/users'); const SettingsService = require('../../../services/settings'); const mailer = require('../../../services/mailer'); const Context = require('../../../graph/context'); const timekeeper = require('timekeeper'); const moment = require('moment'); const chai = require('chai'); chai.use(require('chai-as-promised')); const sinon = require('sinon'); chai.use(require('sinon-chai')); const expect = chai.expect; describe('services.UsersService', () => { let mockUsers; beforeEach(async () => { const settings = { id: '1', moderation: 'PRE', wordlist: { banned: ['bad words'], suspect: ['suspect words'] }, }; await SettingsService.init(settings); const ctx = Context.forSystem(); mockUsers = await Promise.all( [ { email: 'stampi@gmail.com', username: 'Stampi', password: '1Coral!-', }, { email: 'sockmonster@gmail.com', username: 'Sockmonster', password: '2Coral!2', }, { email: 'marvel@gmail.com', username: 'Marvel', password: '3Coral!3', }, ].map(({ email, username, password }) => UsersService.createLocalUser(ctx, email, password, username) ) ); sinon.spy(mailer, 'send'); }); afterEach(() => { mailer.send.restore(); }); describe('#findById()', () => { it('should find a user by id', async () => { const user = await UsersService.findById(mockUsers[0].id); expect(user).to.have.property('username', 'Stampi'); }); }); describe('#findByIdArray()', () => { it('should find an array of users from an array of ids', async () => { const ids = mockUsers.map(user => user.id); const users = await UsersService.findByIdArray(ids); expect(users).to.have.length(3); }); }); describe('#getInitialUsername', () => { it('should find the first result when there is no conflict', async () => { const username = await UsersService.getInitialUsername( 'TheGreatSockmonster' ); expect(username).to.equal('TheGreatSockmonster'); }); it('should find a first result when there is a conflict', async () => { const username = await UsersService.getInitialUsername('Sockmonster'); expect(username).to.match(/Sockmonster_[0-9]+/); }); }); describe('#findPublicByIdArray()', () => { it('should find an array of users from an array of ids', async () => { const ids = mockUsers.map(user => user.id); const users = await UsersService.findPublicByIdArray(ids); expect(users).to.have.length(3); const sorted = users.sort((a, b) => { if (a.username < b.username) { return -1; } if (a.username > b.username) { return 1; } return 0; }); expect(sorted[0]).to.have.property('username', 'Marvel'); }); }); describe('#findLocalUser', () => { it('should find a user', () => { return UsersService.findLocalUser(mockUsers[0].profiles[0].id).then( user => { expect(user).to.have.property('username', mockUsers[0].username); } ); }); }); describe('#createLocalUser', () => { it('should not create a user with duplicate username', () => { const ctx = Context.forSystem(); return expect( UsersService.createLocalUser( ctx, 'otrostampi@gmail.com', '1Coralito!', 'Stampi' ) ).be.rejected; }); }); describe('#createEmailConfirmToken', () => { it('should create a token for a valid user', async () => { const token = await UsersService.createEmailConfirmToken( mockUsers[0], mockUsers[0].profiles[0].id ); expect(token).to.not.be.null; }); it('should not create a token for a user already verified', async () => { const token = await UsersService.createEmailConfirmToken( mockUsers[0], mockUsers[0].profiles[0].id ); expect(token).to.not.be.null; await UsersService.verifyEmailConfirmation(token); const user = await UsersService.findById(mockUsers[0].id); return expect( UsersService.createEmailConfirmToken(user, mockUsers[0].profiles[0].id) ).to.eventually.be.rejected; }); }); describe('#verifyEmailConfirmation', () => { it('should correctly validate a valid token', async () => { const token = await UsersService.createEmailConfirmToken( mockUsers[0], mockUsers[0].profiles[0].id ); expect(token).to.not.be.null; return expect(UsersService.verifyEmailConfirmation(token)).to.eventually .not.be.rejected; }); it('should correctly reject an invalid token', async () => { return UsersService.verifyEmailConfirmation('cats').catch(err => { expect(err).to.not.be.null; }); }); it('should update the user model when verification is complete', () => { return UsersService.createEmailConfirmToken( mockUsers[0], mockUsers[0].profiles[0].id ) .then(token => { expect(token).to.not.be.null; return UsersService.verifyEmailConfirmation(token); }) .then(() => { return UsersService.findById(mockUsers[0].id); }) .then(user => { expect(user.profiles[0]).to.have.property('metadata'); expect(user.profiles[0].metadata).to.have.property('confirmed_at'); expect(user.profiles[0].metadata.confirmed_at).to.not.be.null; }); }); }); describe('#ignoreUser', () => { it('should add user id to ignoredUsers set', async () => { const user = mockUsers[0]; const usersToIgnore = [mockUsers[1], mockUsers[2]]; await UsersService.ignoreUsers(user.id, usersToIgnore.map(u => u.id)); const userAfterIgnoring = await UsersService.findById(user.id); expect(userAfterIgnoring.ignoresUsers.length).to.equal(2); // ignore same user another time, make sure it's not added to the list. await UsersService.ignoreUsers( user.id, usersToIgnore.slice(0, 1).map(u => u.id) ); const userAfterIgnoring2 = await UsersService.findById(user.id); expect(userAfterIgnoring2.ignoresUsers.length).to.equal(2); }); it('should not ignore a staff member', async () => { const user = mockUsers[0]; const usersToIgnore = [mockUsers[1]]; await UsersService.setRole(usersToIgnore[0].id, 'STAFF'); try { await UsersService.ignoreUsers(user.id, usersToIgnore.map(u => u.id)); } catch (err) { expect(err.status).to.equal(400); expect(err.translation_key).to.equal('CANNOT_IGNORE_STAFF'); } }); }); [ { func: 'changeUsername', okStatus: 'REJECTED', notOKStatus: 'UNSET', newStatus: 'CHANGED', }, { func: 'setUsername', okStatus: 'UNSET', notOKStatus: 'REJECTED', newStatus: 'SET', }, ].forEach(({ func, okStatus, notOKStatus, newStatus }) => { describe(`#${func}`, () => { [ { status: okStatus }, { error: 'EDIT_USERNAME_NOT_AUTHORIZED', status: notOKStatus }, { error: 'EDIT_USERNAME_NOT_AUTHORIZED', status: 'SET' }, { error: 'EDIT_USERNAME_NOT_AUTHORIZED', status: 'APPROVED' }, { error: 'EDIT_USERNAME_NOT_AUTHORIZED', status: 'CHANGED' }, ].forEach(({ status, error }) => { it(`${ error ? 'should not' : 'should' } let them change the username if they have the status of ${status}`, async () => { const user = mockUsers[0]; // Set the user to the desired status. await UsersService.setUsernameStatus(user.id, status); try { await UsersService[func](user.id, 'spock'); } catch (err) { if (error) { expect(err).have.property('translation_key', error); } else { throw err; } } }); }); it(`should change the status to ${newStatus} when changed`, async () => { const user = mockUsers[0]; // Set the user to the desired status. await UsersService.setUsernameStatus(user.id, okStatus); const editedUser = await UsersService[func](user.id, 'spock'); expect(editedUser.status.username.status).to.equal(newStatus); try { await UsersService[func](user.id, 'spock'); throw new Error('edit was processed successfully'); } catch (err) { expect(err).have.property( 'translation_key', 'EDIT_USERNAME_NOT_AUTHORIZED' ); } }); it(`${ func === 'changeUsername' ? 'should' : 'should not' } refuse changing the username to the same username`, async () => { const user = mockUsers[0]; // Set the user to the desired status. await UsersService.setUsernameStatus(user.id, okStatus); if (func === 'changeUsername') { try { await UsersService[func](user.id, user.username); throw new Error('edit was processed successfully'); } catch (err) { expect(err).have.property( 'translation_key', 'SAME_USERNAME_PROVIDED' ); } } else { await UsersService[func](user.id, user.username); } }); if (func === 'setUsername') { it('should let a user set their username from UNSET', async () => { const user = mockUsers[0]; // Set the user to the desired status. await UsersService.setUsernameStatus(user.id, 'UNSET'); await UsersService.setUsername(user.id, 'new_username', null); }); describe('time based', () => { afterEach(() => { timekeeper.reset(); }); ['SET', 'APPROVED'].forEach(status => { it(`should not allow users to change their username if it was changed within 14 of today from ${status}`, async () => { const user = mockUsers[0]; // Set the user to the desired status. await UsersService.setUsernameStatus(user.id, status); timekeeper.travel( moment() .add(5, 'days') .toDate() ); try { await UsersService.setUsername(user.id, 'new_username', null); throw new Error('edit was processed successfully'); } catch (err) { expect(err).have.property( 'translation_key', 'EDIT_USERNAME_NOT_AUTHORIZED' ); } }); it(`allows users to change their username if it was changed 14 days before today from ${status}`, async () => { const user = mockUsers[0]; // Set the user to the desired status. await UsersService.setUsernameStatus(user.id, status); timekeeper.travel( moment() .add(15, 'days') .toDate() ); await UsersService.setUsername(user.id, 'new_username', null); }); }); }); } }); }); describe('#isValidUsername', () => { it('should not allow non-alphanumeric characters in usernames', () => { return UsersService.isValidUsername('hi🖕') .then(() => { expect(false).to.be.true; }) .catch(err => { expect(err).to.be.ok; }); }); }); });