AuthenticationManagerTests.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const SandboxedModule = require('sandboxed-module')
  4. const { ObjectId } = require('mongodb')
  5. const AuthenticationErrors = require('../../../../app/src/Features/Authentication/AuthenticationErrors')
  6. const tk = require('timekeeper')
  7. const modulePath =
  8. '../../../../app/src/Features/Authentication/AuthenticationManager.js'
  9. describe('AuthenticationManager', function () {
  10. beforeEach(function () {
  11. tk.freeze(Date.now())
  12. this.settings = { security: { bcryptRounds: 4 } }
  13. this.AuthenticationManager = SandboxedModule.require(modulePath, {
  14. requires: {
  15. '../../models/User': {
  16. User: (this.User = {
  17. updateOne: sinon.stub().callsArgWith(3, null, { nModified: 1 }),
  18. }),
  19. },
  20. '../../infrastructure/mongodb': {
  21. db: (this.db = { users: {} }),
  22. ObjectId,
  23. },
  24. bcrypt: (this.bcrypt = {}),
  25. '@overleaf/settings': this.settings,
  26. '../User/UserGetter': (this.UserGetter = {}),
  27. './AuthenticationErrors': AuthenticationErrors,
  28. './HaveIBeenPwned': {
  29. checkPasswordForReuseInBackground: sinon.stub(),
  30. },
  31. },
  32. })
  33. this.callback = sinon.stub()
  34. })
  35. afterEach(function () {
  36. tk.reset()
  37. })
  38. describe('with real bcrypt', function () {
  39. beforeEach(function () {
  40. const bcrypt = require('bcrypt')
  41. this.bcrypt.compare = bcrypt.compare
  42. this.bcrypt.getRounds = bcrypt.getRounds
  43. this.bcrypt.genSalt = bcrypt.genSalt
  44. this.bcrypt.hash = bcrypt.hash
  45. // Hash of 'testpassword'
  46. this.testPassword =
  47. '$2a$04$DcU/3UeJf1PfsWlQL./5H.rGTQL1Z1iyz6r7bN9Do8cy6pVWxpKpK'
  48. })
  49. describe('authenticate', function () {
  50. beforeEach(function () {
  51. this.user = {
  52. _id: 'user-id',
  53. email: (this.email = 'USER@sharelatex.com'),
  54. }
  55. this.user.hashedPassword = this.testPassword
  56. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  57. })
  58. describe('when the hashed password matches', function () {
  59. beforeEach(function (done) {
  60. this.unencryptedPassword = 'testpassword'
  61. this.AuthenticationManager.authenticate(
  62. { email: this.email },
  63. this.unencryptedPassword,
  64. (error, user) => {
  65. this.callback(error, user)
  66. done()
  67. }
  68. )
  69. })
  70. it('should look up the correct user in the database', function () {
  71. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  72. })
  73. it('should bump epoch', function () {
  74. this.User.updateOne.should.have.been.calledWith(
  75. {
  76. _id: this.user._id,
  77. loginEpoch: this.user.loginEpoch,
  78. },
  79. {
  80. $inc: { loginEpoch: 1 },
  81. },
  82. {}
  83. )
  84. })
  85. it('should return the user', function () {
  86. this.callback.calledWith(null, this.user).should.equal(true)
  87. })
  88. })
  89. describe('when the encrypted passwords do not match', function () {
  90. beforeEach(function (done) {
  91. this.AuthenticationManager.authenticate(
  92. { email: this.email },
  93. 'notthecorrectpassword',
  94. (...args) => {
  95. this.callback(...args)
  96. done()
  97. }
  98. )
  99. })
  100. it('should persist the login failure and bump epoch', function () {
  101. this.User.updateOne.should.have.been.calledWith(
  102. {
  103. _id: this.user._id,
  104. loginEpoch: this.user.loginEpoch,
  105. },
  106. {
  107. $inc: { loginEpoch: 1 },
  108. $set: { lastFailedLogin: new Date() },
  109. }
  110. )
  111. })
  112. it('should not return the user', function () {
  113. this.callback.calledWith(null, null).should.equal(true)
  114. })
  115. })
  116. describe('when another request runs in parallel', function () {
  117. beforeEach(function () {
  118. this.User.updateOne = sinon
  119. .stub()
  120. .callsArgWith(3, null, { nModified: 0 })
  121. })
  122. describe('correct password', function () {
  123. beforeEach(function (done) {
  124. this.AuthenticationManager.authenticate(
  125. { email: this.email },
  126. 'testpassword',
  127. (...args) => {
  128. this.callback(...args)
  129. done()
  130. }
  131. )
  132. })
  133. it('should return an error', function () {
  134. this.callback.should.have.been.calledWith(
  135. sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
  136. )
  137. })
  138. })
  139. describe('bad password', function () {
  140. beforeEach(function (done) {
  141. this.User.updateOne = sinon.stub().yields(null, { nModified: 0 })
  142. this.AuthenticationManager.authenticate(
  143. { email: this.email },
  144. 'notthecorrectpassword',
  145. (...args) => {
  146. this.callback(...args)
  147. done()
  148. }
  149. )
  150. })
  151. it('should return an error', function () {
  152. this.callback.should.have.been.calledWith(
  153. sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
  154. )
  155. })
  156. })
  157. })
  158. })
  159. describe('setUserPasswordInV2', function () {
  160. beforeEach(function () {
  161. this.user = {
  162. _id: '5c8791477192a80b5e76ca7e',
  163. email: (this.email = 'USER@sharelatex.com'),
  164. }
  165. this.db.users.updateOne = sinon
  166. this.User.findOne = sinon.stub().callsArgWith(2, null, this.user)
  167. this.db.users.updateOne = sinon
  168. .stub()
  169. .callsArgWith(2, null, { modifiedCount: 1 })
  170. })
  171. it('should not produce an error', function (done) {
  172. this.AuthenticationManager.setUserPasswordInV2(
  173. this.user,
  174. 'testpassword',
  175. (err, updated) => {
  176. expect(err).to.not.exist
  177. expect(updated).to.equal(true)
  178. done()
  179. }
  180. )
  181. })
  182. it('should set the hashed password', function (done) {
  183. this.AuthenticationManager.setUserPasswordInV2(
  184. this.user,
  185. 'testpassword',
  186. err => {
  187. expect(err).to.not.exist
  188. const { hashedPassword } =
  189. this.db.users.updateOne.lastCall.args[1].$set
  190. expect(hashedPassword).to.exist
  191. expect(hashedPassword.length).to.equal(60)
  192. expect(hashedPassword).to.match(/^\$2a\$04\$[a-zA-Z0-9/.]{53}$/)
  193. done()
  194. }
  195. )
  196. })
  197. })
  198. })
  199. describe('authenticate', function () {
  200. describe('when the user exists in the database', function () {
  201. beforeEach(function () {
  202. this.user = {
  203. _id: 'user-id',
  204. email: (this.email = 'USER@sharelatex.com'),
  205. }
  206. this.unencryptedPassword = 'banana'
  207. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  208. })
  209. describe('when the hashed password matches', function () {
  210. beforeEach(function (done) {
  211. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  212. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  213. this.bcrypt.getRounds = sinon.stub().returns(4)
  214. this.AuthenticationManager.authenticate(
  215. { email: this.email },
  216. this.unencryptedPassword,
  217. (error, user) => {
  218. this.callback(error, user)
  219. done()
  220. }
  221. )
  222. })
  223. it('should look up the correct user in the database', function () {
  224. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  225. })
  226. it('should check that the passwords match', function () {
  227. this.bcrypt.compare
  228. .calledWith(this.unencryptedPassword, this.hashedPassword)
  229. .should.equal(true)
  230. })
  231. it('should return the user', function () {
  232. this.callback.calledWith(null, this.user).should.equal(true)
  233. })
  234. })
  235. describe('when the encrypted passwords do not match', function () {
  236. beforeEach(function () {
  237. this.AuthenticationManager.authenticate(
  238. { email: this.email },
  239. this.unencryptedPassword,
  240. this.callback
  241. )
  242. })
  243. it('should not return the user', function () {
  244. this.callback.calledWith(null, null).should.equal(true)
  245. })
  246. })
  247. describe('when the hashed password matches but the number of rounds is too low', function () {
  248. beforeEach(function (done) {
  249. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  250. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  251. this.bcrypt.getRounds = sinon.stub().returns(1)
  252. this.AuthenticationManager.setUserPassword = sinon
  253. .stub()
  254. .callsArgWith(2, null)
  255. this.AuthenticationManager.authenticate(
  256. { email: this.email },
  257. this.unencryptedPassword,
  258. (error, user) => {
  259. this.callback(error, user)
  260. done()
  261. }
  262. )
  263. })
  264. it('should look up the correct user in the database', function () {
  265. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  266. })
  267. it('should check that the passwords match', function () {
  268. this.bcrypt.compare
  269. .calledWith(this.unencryptedPassword, this.hashedPassword)
  270. .should.equal(true)
  271. })
  272. it('should check the number of rounds', function () {
  273. this.bcrypt.getRounds.called.should.equal(true)
  274. })
  275. it('should set the users password (with a higher number of rounds)', function () {
  276. this.AuthenticationManager.setUserPassword
  277. .calledWith(this.user, this.unencryptedPassword)
  278. .should.equal(true)
  279. })
  280. it('should return the user', function () {
  281. this.callback.calledWith(null, this.user).should.equal(true)
  282. })
  283. })
  284. describe('when the hashed password matches but the number of rounds is too low, but upgrades disabled', function () {
  285. beforeEach(function (done) {
  286. this.settings.security.disableBcryptRoundsUpgrades = true
  287. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  288. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  289. this.bcrypt.getRounds = sinon.stub().returns(1)
  290. this.AuthenticationManager.setUserPassword = sinon
  291. .stub()
  292. .callsArgWith(2, null)
  293. this.AuthenticationManager.authenticate(
  294. { email: this.email },
  295. this.unencryptedPassword,
  296. (error, user) => {
  297. this.callback(error, user)
  298. done()
  299. }
  300. )
  301. })
  302. it('should not check the number of rounds', function () {
  303. this.bcrypt.getRounds.called.should.equal(false)
  304. })
  305. it('should not set the users password (with a higher number of rounds)', function () {
  306. this.AuthenticationManager.setUserPassword
  307. .calledWith(this.user, this.unencryptedPassword)
  308. .should.equal(false)
  309. })
  310. it('should return the user', function () {
  311. this.callback.calledWith(null, this.user).should.equal(true)
  312. })
  313. })
  314. })
  315. describe('when the user does not exist in the database', function () {
  316. beforeEach(function () {
  317. this.User.findOne = sinon.stub().callsArgWith(1, null, null)
  318. this.AuthenticationManager.authenticate(
  319. { email: this.email },
  320. this.unencrpytedPassword,
  321. this.callback
  322. )
  323. })
  324. it('should not return a user', function () {
  325. this.callback.calledWith(null, null).should.equal(true)
  326. })
  327. })
  328. })
  329. describe('validateEmail', function () {
  330. describe('valid', function () {
  331. it('should return null', function () {
  332. const result =
  333. this.AuthenticationManager.validateEmail('foo@example.com')
  334. expect(result).to.equal(null)
  335. })
  336. })
  337. describe('invalid', function () {
  338. it('should return validation error object for no email', function () {
  339. const result = this.AuthenticationManager.validateEmail('')
  340. expect(result).to.an.instanceOf(AuthenticationErrors.InvalidEmailError)
  341. expect(result.message).to.equal('email not valid')
  342. })
  343. it('should return validation error object for invalid', function () {
  344. const result = this.AuthenticationManager.validateEmail('notanemail')
  345. expect(result).to.be.an.instanceOf(
  346. AuthenticationErrors.InvalidEmailError
  347. )
  348. expect(result.message).to.equal('email not valid')
  349. })
  350. })
  351. })
  352. describe('validatePassword', function () {
  353. beforeEach(function () {
  354. // 73 characters:
  355. this.longPassword =
  356. '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345678'
  357. })
  358. describe('with a null password', function () {
  359. it('should return an error', function () {
  360. const result = this.AuthenticationManager.validatePassword()
  361. expect(result).to.be.an.instanceOf(
  362. AuthenticationErrors.InvalidPasswordError
  363. )
  364. expect(result.message).to.equal('password not set')
  365. expect(result.info.code).to.equal('not_set')
  366. })
  367. })
  368. describe('password length', function () {
  369. describe('with the default password length options', function () {
  370. it('should reject passwords that are too short', function () {
  371. const result1 = this.AuthenticationManager.validatePassword('')
  372. expect(result1).to.be.an.instanceOf(
  373. AuthenticationErrors.InvalidPasswordError
  374. )
  375. expect(result1.message).to.equal('password is too short')
  376. expect(result1.info.code).to.equal('too_short')
  377. const result2 = this.AuthenticationManager.validatePassword('foo')
  378. expect(result2).to.be.an.instanceOf(
  379. AuthenticationErrors.InvalidPasswordError
  380. )
  381. expect(result2.message).to.equal('password is too short')
  382. expect(result2.info.code).to.equal('too_short')
  383. })
  384. it('should reject passwords that are too long', function () {
  385. const result = this.AuthenticationManager.validatePassword(
  386. this.longPassword
  387. )
  388. expect(result).to.be.an.instanceOf(
  389. AuthenticationErrors.InvalidPasswordError
  390. )
  391. expect(result.message).to.equal('password is too long')
  392. expect(result.info.code).to.equal('too_long')
  393. })
  394. it('should accept passwords that are a good length', function () {
  395. expect(
  396. this.AuthenticationManager.validatePassword('l337h4x0r')
  397. ).to.equal(null)
  398. })
  399. })
  400. describe('when the password length is specified in settings', function () {
  401. beforeEach(function () {
  402. this.settings.passwordStrengthOptions = {
  403. length: {
  404. min: 10,
  405. max: 12,
  406. },
  407. }
  408. })
  409. it('should reject passwords that are too short', function () {
  410. const result =
  411. this.AuthenticationManager.validatePassword('012345678')
  412. expect(result).to.be.an.instanceOf(
  413. AuthenticationErrors.InvalidPasswordError
  414. )
  415. expect(result.message).to.equal('password is too short')
  416. expect(result.info.code).to.equal('too_short')
  417. })
  418. it('should accept passwords of exactly minimum length', function () {
  419. expect(
  420. this.AuthenticationManager.validatePassword('0123456789')
  421. ).to.equal(null)
  422. })
  423. it('should reject passwords that are too long', function () {
  424. const result =
  425. this.AuthenticationManager.validatePassword('0123456789abc')
  426. expect(result).to.be.an.instanceOf(
  427. AuthenticationErrors.InvalidPasswordError
  428. )
  429. expect(result.message).to.equal('password is too long')
  430. expect(result.info.code).to.equal('too_long')
  431. })
  432. it('should accept passwords of exactly maximum length', function () {
  433. expect(
  434. this.AuthenticationManager.validatePassword('0123456789ab')
  435. ).to.equal(null)
  436. })
  437. })
  438. describe('when the maximum password length is set to >72 characters in settings', function () {
  439. beforeEach(function () {
  440. this.settings.passwordStrengthOptions = {
  441. length: {
  442. max: 128,
  443. },
  444. }
  445. })
  446. it('should still reject passwords > 72 characters in length', function () {
  447. const result = this.AuthenticationManager.validatePassword(
  448. this.longPassword
  449. )
  450. expect(result).to.be.an.instanceOf(
  451. AuthenticationErrors.InvalidPasswordError
  452. )
  453. expect(result.message).to.equal('password is too long')
  454. expect(result.info.code).to.equal('too_long')
  455. })
  456. })
  457. })
  458. describe('allowed characters', function () {
  459. describe('with the default settings for allowed characters', function () {
  460. it('should allow passwords with valid characters', function () {
  461. expect(
  462. this.AuthenticationManager.validatePassword(
  463. 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  464. )
  465. ).to.equal(null)
  466. expect(
  467. this.AuthenticationManager.validatePassword(
  468. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  469. )
  470. ).to.equal(null)
  471. })
  472. it('should not allow passwords with invalid characters', function () {
  473. const result = this.AuthenticationManager.validatePassword(
  474. 'correct horse battery staple'
  475. )
  476. expect(result).to.be.an.instanceOf(
  477. AuthenticationErrors.InvalidPasswordError
  478. )
  479. expect(result.message).to.equal(
  480. 'password contains an invalid character'
  481. )
  482. expect(result.info.code).to.equal('invalid_character')
  483. })
  484. })
  485. describe('when valid characters are overridden in settings', function () {
  486. beforeEach(function () {
  487. this.settings.passwordStrengthOptions = {
  488. chars: {
  489. symbols: ' ',
  490. },
  491. }
  492. })
  493. it('should allow passwords with valid characters', function () {
  494. expect(
  495. this.AuthenticationManager.validatePassword(
  496. 'correct horse battery staple'
  497. )
  498. ).to.equal(null)
  499. })
  500. it('should disallow passwords with invalid characters', function () {
  501. const result = this.AuthenticationManager.validatePassword(
  502. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  503. )
  504. expect(result).to.be.an.instanceOf(
  505. AuthenticationErrors.InvalidPasswordError
  506. )
  507. expect(result.message).to.equal(
  508. 'password contains an invalid character'
  509. )
  510. expect(result.info.code).to.equal('invalid_character')
  511. })
  512. })
  513. describe('when allowAnyChars is set', function () {
  514. beforeEach(function () {
  515. this.settings.passwordStrengthOptions = {
  516. allowAnyChars: true,
  517. }
  518. })
  519. it('should allow any characters', function () {
  520. expect(
  521. this.AuthenticationManager.validatePassword(
  522. 'correct horse battery staple'
  523. )
  524. ).to.equal(null)
  525. expect(
  526. this.AuthenticationManager.validatePassword(
  527. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  528. )
  529. ).to.equal(null)
  530. })
  531. })
  532. })
  533. })
  534. describe('setUserPassword', function () {
  535. beforeEach(function () {
  536. this.user_id = ObjectId()
  537. this.user = {
  538. _id: this.user_id,
  539. email: 'user@example.com',
  540. }
  541. this.password = 'banana'
  542. this.hashedPassword = 'asdkjfa;osiuvandf'
  543. this.salt = 'saltaasdfasdfasdf'
  544. this.bcrypt.genSalt = sinon.stub().callsArgWith(2, null, this.salt)
  545. this.bcrypt.hash = sinon.stub().callsArgWith(2, null, this.hashedPassword)
  546. this.User.findOne = sinon.stub().callsArgWith(2, null, this.user)
  547. this.db.users.updateOne = sinon.stub().callsArg(2)
  548. })
  549. describe('too long', function () {
  550. beforeEach(function () {
  551. this.settings.passwordStrengthOptions = {
  552. length: {
  553. max: 10,
  554. },
  555. }
  556. this.password = 'dsdsadsadsadsadsadkjsadjsadjsadljs'
  557. })
  558. it('should return and error', function (done) {
  559. this.AuthenticationManager.setUserPassword(
  560. this.user,
  561. this.password,
  562. err => {
  563. expect(err).to.exist
  564. done()
  565. }
  566. )
  567. })
  568. it('should not start the bcrypt process', function (done) {
  569. this.AuthenticationManager.setUserPassword(
  570. this.user,
  571. this.password,
  572. () => {
  573. this.bcrypt.genSalt.called.should.equal(false)
  574. this.bcrypt.hash.called.should.equal(false)
  575. done()
  576. }
  577. )
  578. })
  579. })
  580. describe('contains full email', function () {
  581. beforeEach(function () {
  582. this.password = `some${this.user.email}password`
  583. })
  584. it('should reject the password', function (done) {
  585. this.AuthenticationManager.setUserPassword(
  586. this.user,
  587. this.password,
  588. err => {
  589. expect(err).to.exist
  590. expect(err.name).to.equal('InvalidPasswordError')
  591. done()
  592. }
  593. )
  594. })
  595. })
  596. describe('contains first part of email', function () {
  597. beforeEach(function () {
  598. this.password = `some${this.user.email.split('@')[0]}password`
  599. })
  600. it('should reject the password', function (done) {
  601. this.AuthenticationManager.setUserPassword(
  602. this.user,
  603. this.password,
  604. err => {
  605. expect(err).to.exist
  606. expect(err.name).to.equal('InvalidPasswordError')
  607. done()
  608. }
  609. )
  610. })
  611. })
  612. describe('too short', function () {
  613. beforeEach(function () {
  614. this.settings.passwordStrengthOptions = {
  615. length: {
  616. max: 10,
  617. min: 6,
  618. },
  619. }
  620. this.password = 'dsd'
  621. })
  622. it('should return and error', function (done) {
  623. this.AuthenticationManager.setUserPassword(
  624. this.user,
  625. this.password,
  626. err => {
  627. expect(err).to.exist
  628. done()
  629. }
  630. )
  631. })
  632. it('should not start the bcrypt process', function (done) {
  633. this.AuthenticationManager.setUserPassword(
  634. this.user,
  635. this.password,
  636. () => {
  637. this.bcrypt.genSalt.called.should.equal(false)
  638. this.bcrypt.hash.called.should.equal(false)
  639. done()
  640. }
  641. )
  642. })
  643. })
  644. describe('successful password set attempt', function () {
  645. beforeEach(function () {
  646. this.UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
  647. this.AuthenticationManager.setUserPassword(
  648. this.user,
  649. this.password,
  650. this.callback
  651. )
  652. })
  653. it("should update the user's password in the database", function () {
  654. const { args } = this.db.users.updateOne.lastCall
  655. expect(args[0]).to.deep.equal({
  656. _id: ObjectId(this.user_id.toString()),
  657. })
  658. expect(args[1]).to.deep.equal({
  659. $set: {
  660. hashedPassword: this.hashedPassword,
  661. },
  662. $unset: {
  663. password: true,
  664. },
  665. })
  666. })
  667. it('should hash the password', function () {
  668. this.bcrypt.genSalt.calledWith(4).should.equal(true)
  669. this.bcrypt.hash.calledWith(this.password, this.salt).should.equal(true)
  670. })
  671. it('should call the callback', function () {
  672. this.callback.called.should.equal(true)
  673. })
  674. })
  675. })
  676. })