ThirdPartyIdentityManagerTests.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const SandboxedModule = require('sandboxed-module')
  4. const modulePath =
  5. '../../../../app/src/Features/User/ThirdPartyIdentityManager.js'
  6. describe('ThirdPartyIdentityManager', function () {
  7. beforeEach(function () {
  8. this.userId = 'a1b2c3'
  9. this.user = {
  10. _id: this.userId,
  11. email: 'example@overleaf.com',
  12. }
  13. this.externalUserId = 'id789'
  14. this.externalData = {}
  15. this.auditLog = { initiatorId: this.userId, ipAddress: '0:0:0:0' }
  16. this.ThirdPartyIdentityManager = SandboxedModule.require(modulePath, {
  17. requires: {
  18. '../../../../app/src/Features/User/UserAuditLogHandler':
  19. (this.UserAuditLogHandler = {
  20. addEntry: sinon.stub().yields(),
  21. }),
  22. '../../../../app/src/Features/Email/EmailHandler': (this.EmailHandler =
  23. {
  24. sendEmail: sinon.stub().yields(),
  25. }),
  26. '../../../../app/src/models/User': {
  27. User: (this.User = {
  28. findOneAndUpdate: sinon.stub().yields(undefined, this.user),
  29. findOne: sinon.stub().yields(undefined, undefined),
  30. }),
  31. },
  32. '@overleaf/settings': {
  33. oauthProviders: {
  34. google: {
  35. name: 'Google',
  36. },
  37. orcid: {
  38. name: 'ORCID',
  39. },
  40. },
  41. },
  42. },
  43. })
  44. })
  45. describe('getUser', function () {
  46. it('should an error when missing providerId or externalUserId', function (done) {
  47. this.ThirdPartyIdentityManager.getUser(
  48. undefined,
  49. undefined,
  50. (error, user) => {
  51. expect(error).to.exist
  52. expect(error.message).to.equal('invalid SSO arguments')
  53. expect(error.info).to.deep.equal({
  54. externalUserId: undefined,
  55. providerId: undefined,
  56. })
  57. done()
  58. }
  59. )
  60. })
  61. describe('when user linked', function () {
  62. beforeEach(function () {
  63. this.User.findOne.yields(undefined, this.user)
  64. })
  65. it('should return the user', async function () {
  66. this.User.findOne.returns(undefined, this.user)
  67. const user = await this.ThirdPartyIdentityManager.promises.getUser(
  68. 'google',
  69. 'an-id-linked'
  70. )
  71. expect(user).to.deep.equal(this.user)
  72. })
  73. })
  74. it('should return ThirdPartyUserNotFoundError when no user linked', function (done) {
  75. this.ThirdPartyIdentityManager.getUser(
  76. 'google',
  77. 'an-id-not-linked',
  78. (error, user) => {
  79. expect(error).to.exist
  80. expect(error.name).to.equal('ThirdPartyUserNotFoundError')
  81. done()
  82. }
  83. )
  84. })
  85. })
  86. describe('link', function () {
  87. it('should send email alert', async function () {
  88. await this.ThirdPartyIdentityManager.promises.link(
  89. this.userId,
  90. 'google',
  91. this.externalUserId,
  92. this.externalData,
  93. this.auditLog
  94. )
  95. const emailCall = this.EmailHandler.sendEmail.getCall(0)
  96. expect(emailCall.args[0]).to.equal('securityAlert')
  97. expect(emailCall.args[1].actionDescribed).to.contain(
  98. 'a Google account was linked'
  99. )
  100. })
  101. it('should update user audit log', async function () {
  102. await this.ThirdPartyIdentityManager.promises.link(
  103. this.userId,
  104. 'google',
  105. this.externalUserId,
  106. this.externalData,
  107. this.auditLog
  108. )
  109. expect(this.UserAuditLogHandler.addEntry).to.have.been.calledOnceWith(
  110. this.userId,
  111. 'link-sso',
  112. this.auditLog.initiatorId,
  113. this.auditLog.ipAddress,
  114. {
  115. providerId: 'google',
  116. }
  117. )
  118. })
  119. describe('errors', function () {
  120. const anError = new Error('oops')
  121. it('should not unlink if the UserAuditLogHandler throws an error', function (done) {
  122. this.UserAuditLogHandler.addEntry.yields(anError)
  123. this.ThirdPartyIdentityManager.link(
  124. this.userId,
  125. 'google',
  126. this.externalUserId,
  127. this.externalData,
  128. this.auditLog,
  129. error => {
  130. expect(error).to.exist
  131. expect(error).to.equal(anError)
  132. expect(this.User.findOneAndUpdate).to.not.have.been.called
  133. done()
  134. }
  135. )
  136. })
  137. describe('EmailHandler', function () {
  138. beforeEach(function () {
  139. this.EmailHandler.sendEmail.yields(anError)
  140. })
  141. it('should log but not return the error', function (done) {
  142. this.ThirdPartyIdentityManager.link(
  143. this.userId,
  144. 'google',
  145. this.externalUserId,
  146. this.externalData,
  147. this.auditLog,
  148. error => {
  149. expect(error).to.not.exist
  150. expect(this.logger.error.lastCall).to.be.calledWithExactly(
  151. {
  152. err: anError,
  153. userId: this.userId,
  154. },
  155. 'could not send security alert email when new account linked'
  156. )
  157. done()
  158. }
  159. )
  160. })
  161. })
  162. })
  163. })
  164. describe('unlink', function () {
  165. it('should send email alert', async function () {
  166. await this.ThirdPartyIdentityManager.promises.unlink(
  167. this.userId,
  168. 'orcid',
  169. this.auditLog
  170. )
  171. const emailCall = this.EmailHandler.sendEmail.getCall(0)
  172. expect(emailCall.args[0]).to.equal('securityAlert')
  173. expect(emailCall.args[1].actionDescribed).to.contain(
  174. 'an ORCID account was unlinked from'
  175. )
  176. })
  177. it('should update user audit log', async function () {
  178. await this.ThirdPartyIdentityManager.promises.unlink(
  179. this.userId,
  180. 'orcid',
  181. this.auditLog
  182. )
  183. expect(this.UserAuditLogHandler.addEntry).to.have.been.calledOnceWith(
  184. this.userId,
  185. 'unlink-sso',
  186. this.auditLog.initiatorId,
  187. this.auditLog.ipAddress,
  188. {
  189. providerId: 'orcid',
  190. }
  191. )
  192. })
  193. describe('errors', function () {
  194. const anError = new Error('oops')
  195. it('should not unlink if the UserAuditLogHandler throws an error', function (done) {
  196. this.UserAuditLogHandler.addEntry.yields(anError)
  197. this.ThirdPartyIdentityManager.unlink(
  198. this.userId,
  199. 'orcid',
  200. this.auditLog,
  201. error => {
  202. expect(error).to.exist
  203. expect(error).to.equal(anError)
  204. expect(this.User.findOneAndUpdate).to.not.have.been.called
  205. done()
  206. }
  207. )
  208. expect(this.User.findOneAndUpdate).to.not.have.been.called
  209. })
  210. describe('EmailHandler', function () {
  211. beforeEach(function () {
  212. this.EmailHandler.sendEmail.yields(anError)
  213. })
  214. it('should log but not return the error', function (done) {
  215. this.ThirdPartyIdentityManager.unlink(
  216. this.userId,
  217. 'google',
  218. this.auditLog,
  219. error => {
  220. expect(error).to.not.exist
  221. expect(this.logger.error.lastCall).to.be.calledWithExactly(
  222. {
  223. err: anError,
  224. userId: this.userId,
  225. },
  226. 'could not send security alert email when account no longer linked'
  227. )
  228. done()
  229. }
  230. )
  231. })
  232. })
  233. })
  234. })
  235. })