OneTimeTokenHandlerTests.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. const SandboxedModule = require('sandboxed-module')
  2. const path = require('path')
  3. const sinon = require('sinon')
  4. const modulePath = path.join(
  5. __dirname,
  6. '../../../../app/src/Features/Security/OneTimeTokenHandler'
  7. )
  8. const Errors = require('../../../../app/src/Features/Errors/Errors')
  9. const tk = require('timekeeper')
  10. const { expect } = require('chai')
  11. describe('OneTimeTokenHandler', function () {
  12. beforeEach(function () {
  13. tk.freeze(Date.now()) // freeze the time for these tests
  14. this.stubbedToken = 'mock-token'
  15. this.callback = sinon.stub()
  16. this.OneTimeTokenHandler = SandboxedModule.require(modulePath, {
  17. requires: {
  18. '@overleaf/settings': this.settings,
  19. crypto: {
  20. randomBytes: () => this.stubbedToken,
  21. },
  22. '../../infrastructure/mongodb': {
  23. db: (this.db = { tokens: {} }),
  24. },
  25. },
  26. })
  27. })
  28. afterEach(function () {
  29. tk.reset()
  30. })
  31. describe('getNewToken', function () {
  32. beforeEach(function () {
  33. this.db.tokens.insertOne = sinon.stub().yields()
  34. })
  35. describe('normally', function () {
  36. beforeEach(function () {
  37. this.OneTimeTokenHandler.getNewToken(
  38. 'password',
  39. 'mock-data-to-store',
  40. this.callback
  41. )
  42. })
  43. it('should insert a generated token with a 1 hour expiry', function () {
  44. this.db.tokens.insertOne
  45. .calledWith({
  46. use: 'password',
  47. token: this.stubbedToken,
  48. createdAt: new Date(),
  49. expiresAt: new Date(Date.now() + 60 * 60 * 1000),
  50. data: 'mock-data-to-store',
  51. })
  52. .should.equal(true)
  53. })
  54. it('should call the callback with the token', function () {
  55. this.callback.calledWith(null, this.stubbedToken).should.equal(true)
  56. })
  57. })
  58. describe('with an optional expiresIn parameter', function () {
  59. beforeEach(function () {
  60. this.OneTimeTokenHandler.getNewToken(
  61. 'password',
  62. 'mock-data-to-store',
  63. { expiresIn: 42 },
  64. this.callback
  65. )
  66. })
  67. it('should insert a generated token with a custom expiry', function () {
  68. this.db.tokens.insertOne
  69. .calledWith({
  70. use: 'password',
  71. token: this.stubbedToken,
  72. createdAt: new Date(),
  73. expiresAt: new Date(Date.now() + 42 * 1000),
  74. data: 'mock-data-to-store',
  75. })
  76. .should.equal(true)
  77. })
  78. it('should call the callback with the token', function () {
  79. this.callback.calledWith(null, this.stubbedToken).should.equal(true)
  80. })
  81. })
  82. })
  83. describe('peekValueFromToken', function () {
  84. describe('successfully', function () {
  85. const data = { email: 'some-mock-data' }
  86. let result
  87. beforeEach(async function () {
  88. this.db.tokens.findOneAndUpdate = sinon
  89. .stub()
  90. .resolves({ data, peekCount: 1 })
  91. result = await this.OneTimeTokenHandler.promises.peekValueFromToken(
  92. 'password',
  93. 'mock-token'
  94. )
  95. })
  96. it('should increment the peekCount', function () {
  97. this.db.tokens.findOneAndUpdate
  98. .calledWith(
  99. {
  100. use: 'password',
  101. token: 'mock-token',
  102. expiresAt: { $gt: new Date() },
  103. usedAt: { $exists: false },
  104. peekCount: { $not: { $gte: this.OneTimeTokenHandler.MAX_PEEKS } },
  105. },
  106. {
  107. $inc: { peekCount: 1 },
  108. }
  109. )
  110. .should.equal(true)
  111. })
  112. it('should return the data', function () {
  113. expect(result).to.deep.equal({ data, remainingPeeks: 3 })
  114. })
  115. })
  116. describe('when a valid token is not found', function () {
  117. beforeEach(function () {
  118. this.db.tokens.findOneAndUpdate = sinon.stub().resolves(null)
  119. })
  120. it('should return a NotFoundError', async function () {
  121. await expect(
  122. this.OneTimeTokenHandler.promises.peekValueFromToken(
  123. 'password',
  124. 'mock-token'
  125. )
  126. ).to.be.rejectedWith(Errors.NotFoundError)
  127. })
  128. })
  129. })
  130. describe('expireToken', function () {
  131. beforeEach(function () {
  132. this.db.tokens.updateOne = sinon.stub().yields(null)
  133. this.OneTimeTokenHandler.expireToken(
  134. 'password',
  135. 'mock-token',
  136. this.callback
  137. )
  138. })
  139. it('should expire the token', function () {
  140. this.db.tokens.updateOne
  141. .calledWith(
  142. {
  143. use: 'password',
  144. token: 'mock-token',
  145. },
  146. {
  147. $set: {
  148. usedAt: new Date(),
  149. },
  150. }
  151. )
  152. .should.equal(true)
  153. this.callback.calledWith(null).should.equal(true)
  154. })
  155. })
  156. describe('getValueFromTokenAndExpire', function () {
  157. describe('successfully', function () {
  158. beforeEach(function () {
  159. this.db.tokens.findOneAndUpdate = sinon
  160. .stub()
  161. .yields(null, { data: 'mock-data' })
  162. this.OneTimeTokenHandler.getValueFromTokenAndExpire(
  163. 'password',
  164. 'mock-token',
  165. this.callback
  166. )
  167. })
  168. it('should expire the token', function () {
  169. this.db.tokens.findOneAndUpdate
  170. .calledWith(
  171. {
  172. use: 'password',
  173. token: 'mock-token',
  174. expiresAt: { $gt: new Date() },
  175. usedAt: { $exists: false },
  176. peekCount: { $not: { $gte: this.OneTimeTokenHandler.MAX_PEEKS } },
  177. },
  178. {
  179. $set: { usedAt: new Date() },
  180. }
  181. )
  182. .should.equal(true)
  183. })
  184. it('should return the data', function () {
  185. this.callback.calledWith(null, 'mock-data').should.equal(true)
  186. })
  187. })
  188. describe('when a valid token is not found', function () {
  189. beforeEach(function () {
  190. this.db.tokens.findOneAndUpdate = sinon.stub().yields(null, null)
  191. this.OneTimeTokenHandler.getValueFromTokenAndExpire(
  192. 'password',
  193. 'mock-token',
  194. this.callback
  195. )
  196. })
  197. it('should return a NotFoundError', function () {
  198. this.callback
  199. .calledWith(sinon.match.instanceOf(Errors.NotFoundError))
  200. .should.equal(true)
  201. })
  202. })
  203. })
  204. })