SAMLIdentityManagerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  1. const { ObjectId } = require('mongodb')
  2. const sinon = require('sinon')
  3. const { expect } = require('chai')
  4. const SandboxedModule = require('sandboxed-module')
  5. const Errors = require('../../../../app/src/Features/Errors/Errors')
  6. const modulePath = '../../../../app/src/Features/User/SAMLIdentityManager.js'
  7. describe('SAMLIdentityManager', function () {
  8. const linkedEmail = 'another@example.com'
  9. beforeEach(function () {
  10. this.userId = '6005c75b12cbcaf771f4a105'
  11. this.user = {
  12. _id: this.userId,
  13. email: 'not-linked@overleaf.com',
  14. emails: [{ email: 'not-linked@overleaf.com' }],
  15. samlIdentifiers: [],
  16. }
  17. this.auditLog = {
  18. initiatorId: this.userId,
  19. ipAddress: '0:0:0:0',
  20. }
  21. this.userAlreadyLinked = {
  22. _id: '6005c7a012cbcaf771f4a106',
  23. email: 'linked@overleaf.com',
  24. emails: [{ email: 'linked@overleaf.com', samlProviderId: '1' }],
  25. samlIdentifiers: [{ externalUserId: 'linked-id', providerId: '1' }],
  26. }
  27. this.userEmailExists = {
  28. _id: '6005c7a012cbcaf771f4a107',
  29. email: 'exists@overleaf.com',
  30. emails: [{ email: 'exists@overleaf.com' }],
  31. samlIdentifiers: [],
  32. }
  33. this.institution = {
  34. name: 'Overleaf University',
  35. }
  36. this.InstitutionsAPI = {
  37. promises: {
  38. addEntitlement: sinon.stub().resolves(),
  39. removeEntitlement: sinon.stub().resolves(),
  40. },
  41. }
  42. this.SAMLIdentityManager = SandboxedModule.require(modulePath, {
  43. requires: {
  44. '../Email/EmailHandler': (this.EmailHandler = {
  45. sendEmail: sinon.stub().yields(),
  46. }),
  47. '../Notifications/NotificationsBuilder': (this.NotificationsBuilder = {
  48. promises: {
  49. redundantPersonalSubscription: sinon
  50. .stub()
  51. .returns({ create: sinon.stub().resolves() }),
  52. },
  53. }),
  54. '../Subscription/SubscriptionLocator': (this.SubscriptionLocator = {
  55. promises: {
  56. getUserIndividualSubscription: sinon.stub().resolves(),
  57. },
  58. }),
  59. '../../models/User': {
  60. User: (this.User = {
  61. findOneAndUpdate: sinon.stub().returns({
  62. exec: sinon.stub().resolves(this.user),
  63. }),
  64. findOne: sinon.stub().returns({
  65. exec: sinon.stub().resolves(),
  66. }),
  67. updateOne: sinon.stub().returns({
  68. exec: sinon.stub().resolves(),
  69. }),
  70. }),
  71. },
  72. '../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
  73. promises: {
  74. addEntry: sinon.stub().resolves(),
  75. },
  76. }),
  77. '../User/UserGetter': (this.UserGetter = {
  78. getUser: sinon.stub(),
  79. promises: {
  80. getUser: sinon.stub().resolves(this.user),
  81. getUserByAnyEmail: sinon.stub().resolves(),
  82. getUserFullEmails: sinon.stub().resolves(),
  83. },
  84. }),
  85. '../User/UserUpdater': (this.UserUpdater = {
  86. addEmailAddress: sinon.stub(),
  87. promises: {
  88. addEmailAddress: sinon.stub().resolves(),
  89. confirmEmail: sinon.stub().resolves(),
  90. updateUser: sinon.stub().resolves(),
  91. },
  92. }),
  93. '../Institutions/InstitutionsAPI': this.InstitutionsAPI,
  94. },
  95. })
  96. })
  97. describe('getUser', function () {
  98. it('should throw an error if missing all of: provider ID, external user ID, attribute', async function () {
  99. let error
  100. try {
  101. await this.SAMLIdentityManager.getUser(undefined, undefined, undefined)
  102. } catch (e) {
  103. error = e
  104. } finally {
  105. expect(error).to.exist
  106. expect(error.message).to.equal(
  107. 'invalid arguments: providerId: undefined, externalUserId: undefined, userIdAttribute: undefined'
  108. )
  109. }
  110. })
  111. it('should throw an error if missing provider ID', async function () {
  112. let error
  113. try {
  114. await this.SAMLIdentityManager.getUser(undefined, 'id123', 'someAttr')
  115. } catch (e) {
  116. error = e
  117. } finally {
  118. expect(error).to.exist
  119. expect(error.message).to.equal(
  120. 'invalid arguments: providerId: undefined, externalUserId: id123, userIdAttribute: someAttr'
  121. )
  122. }
  123. })
  124. it('should throw an error if missing external user ID', async function () {
  125. let error
  126. try {
  127. await this.SAMLIdentityManager.getUser('123', null, 'someAttr')
  128. } catch (e) {
  129. error = e
  130. } finally {
  131. expect(error).to.exist
  132. }
  133. })
  134. it('should throw an error if missing attribute', async function () {
  135. let error
  136. try {
  137. await this.SAMLIdentityManager.getUser('123', 'id123', undefined)
  138. } catch (e) {
  139. error = e
  140. } finally {
  141. expect(error).to.exist
  142. expect(error.message).to.equal(
  143. 'invalid arguments: providerId: 123, externalUserId: id123, userIdAttribute: undefined'
  144. )
  145. }
  146. })
  147. })
  148. describe('linkAccounts', function () {
  149. describe('errors', function () {
  150. beforeEach(function () {
  151. // first call is to get userWithProvider; should be falsy
  152. this.UserGetter.promises.getUser.onFirstCall().resolves()
  153. this.UserGetter.promises.getUser.onSecondCall().resolves(this.user)
  154. })
  155. it('should throw an error if missing all data', async function () {
  156. let error
  157. try {
  158. await this.SAMLIdentityManager.linkAccounts(null, null, null)
  159. } catch (e) {
  160. error = e
  161. } finally {
  162. expect(error).to.exist
  163. }
  164. })
  165. describe('linking data', function () {
  166. const requiredData = {
  167. externalUserId: 'someUniqueId',
  168. institutionEmail: 'user@example.com',
  169. providerId: '123',
  170. userIdAttribute: 'attribute',
  171. }
  172. for (const [data] of Object.entries(requiredData)) {
  173. const testData = { ...requiredData }
  174. delete testData[data]
  175. let error
  176. it(`should throw an error when missing ${data}`, async function () {
  177. try {
  178. await this.SAMLIdentityManager.linkAccounts('123', testData, {})
  179. } catch (e) {
  180. error = e
  181. } finally {
  182. expect(error).to.exist
  183. console.log(error)
  184. expect(error.message).to.contain(
  185. 'missing data when linking institution SSO'
  186. )
  187. }
  188. })
  189. }
  190. })
  191. describe('when email is already associated with another Overleaf account', function () {
  192. beforeEach(function () {
  193. this.UserGetter.promises.getUserByAnyEmail.resolves(
  194. this.userEmailExists
  195. )
  196. })
  197. it('should throw an EmailExistsError error', async function () {
  198. let error
  199. try {
  200. await this.SAMLIdentityManager.linkAccounts(
  201. '6005c75b12cbcaf771f4a105',
  202. {
  203. externalUserId: 'not-linked-id',
  204. institutionEmail: 'exists@overleaf.com',
  205. universityId: 'provider-id',
  206. universityName: 'provider-name',
  207. hasEntitlement: true,
  208. userIdAttribute: 'someAttribute',
  209. },
  210. {
  211. intiatorId: '6005c75b12cbcaf771f4a105',
  212. ip: '0:0:0:0',
  213. }
  214. )
  215. } catch (e) {
  216. error = e
  217. } finally {
  218. expect(error).to.be.instanceof(Errors.EmailExistsError)
  219. expect(this.User.findOneAndUpdate).to.not.have.been.called
  220. }
  221. })
  222. })
  223. describe('when email is not affiliated', function () {
  224. beforeEach(function () {
  225. this.UserGetter.promises.getUserByAnyEmail.resolves(this.user)
  226. this.UserGetter.promises.getUserFullEmails.resolves([
  227. {
  228. email: 'not-affiliated@overleaf.com',
  229. },
  230. ])
  231. })
  232. it('should throw SAMLEmailNotAffiliatedError', async function () {
  233. let error
  234. try {
  235. await this.SAMLIdentityManager.linkAccounts(
  236. '6005c75b12cbcaf771f4a105',
  237. {
  238. externalUserId: 'not-linked-id',
  239. institutionEmail: 'not-affiliated@overleaf.com',
  240. universityId: 'provider-id',
  241. universityName: 'provider-name',
  242. hasEntitlement: true,
  243. userIdAttribute: 'someAttribute',
  244. },
  245. {
  246. intiatorId: 'user-id-1',
  247. ip: '0:0:0:0',
  248. }
  249. )
  250. } catch (e) {
  251. error = e
  252. } finally {
  253. expect(error).to.be.instanceof(Errors.SAMLEmailNotAffiliatedError)
  254. expect(this.User.findOneAndUpdate).to.not.have.been.called
  255. }
  256. })
  257. })
  258. describe('when email is affiliated with another institution', function () {
  259. beforeEach(function () {
  260. this.UserGetter.promises.getUserByAnyEmail.resolves(this.user)
  261. this.UserGetter.promises.getUserFullEmails.resolves([
  262. {
  263. email: 'affiliated@overleaf.com',
  264. affiliation: { institution: { id: '987' } },
  265. },
  266. ])
  267. })
  268. it('should throw SAMLEmailAffiliatedWithAnotherInstitutionError', async function () {
  269. let error
  270. try {
  271. await this.SAMLIdentityManager.linkAccounts(
  272. '6005c75b12cbcaf771f4a105',
  273. {
  274. externalUserId: 'not-linked-id',
  275. institutionEmail: 'affiliated@overleaf.com',
  276. universityId: 'provider-id',
  277. universityName: 'provider-name',
  278. hasEntitlement: true,
  279. userIdAttribute: 'someAttribute',
  280. },
  281. {
  282. intiatorId: 'user-id-1',
  283. ip: '0:0:0:0',
  284. }
  285. )
  286. } catch (e) {
  287. error = e
  288. } finally {
  289. expect(error).to.be.instanceof(
  290. Errors.SAMLEmailAffiliatedWithAnotherInstitutionError
  291. )
  292. expect(this.User.findOneAndUpdate).to.not.have.been.called
  293. }
  294. })
  295. })
  296. describe('when institution identifier is already associated with another Overleaf account', function () {
  297. beforeEach(function () {
  298. this.UserGetter.promises.getUserByAnyEmail.resolves(
  299. this.userAlreadyLinked
  300. )
  301. })
  302. it('should throw an SAMLIdentityExistsError error', async function () {
  303. let error
  304. try {
  305. await this.SAMLIdentityManager.linkAccounts(
  306. '6005c75b12cbcaf771f4a105',
  307. {
  308. externalUserId: 'already-linked-id',
  309. institutionEmail: 'linked@overleaf.com',
  310. universityId: 'provider-id',
  311. universityName: 'provider-name',
  312. hasEntitlement: true,
  313. userIdAttribute: 'someAttribute',
  314. },
  315. {
  316. intiatorId: '6005c75b12cbcaf771f4a105',
  317. ip: '0:0:0:0',
  318. }
  319. )
  320. } catch (e) {
  321. error = e
  322. } finally {
  323. expect(error).to.be.instanceof(Errors.SAMLIdentityExistsError)
  324. expect(this.User.findOneAndUpdate).to.not.have.been.called
  325. }
  326. })
  327. })
  328. describe('when institution provider is already associated with the user', function () {
  329. beforeEach(function () {
  330. // first call is to get userWithProvider; resolves with any user
  331. this.UserGetter.promises.getUser.onFirstCall().resolves(this.user)
  332. })
  333. it('should throw an SAMLAlreadyLinkedError error', async function () {
  334. let error
  335. try {
  336. await this.SAMLIdentityManager.linkAccounts(
  337. '6005c75b12cbcaf771f4a105',
  338. {
  339. externalUserId: 'already-linked-id',
  340. institutionEmail: 'linked@overleaf.com',
  341. universityId: 123456,
  342. universityName: 'provider-name',
  343. hasEntitlement: true,
  344. userIdAttribute: 'someAttribute',
  345. },
  346. {
  347. intiatorId: '6005c75b12cbcaf771f4a105',
  348. ip: '0:0:0:0',
  349. }
  350. )
  351. } catch (e) {
  352. error = e
  353. } finally {
  354. expect(
  355. this.UserGetter.promises.getUser
  356. ).to.have.been.calledWithMatch({
  357. _id: ObjectId('6005c75b12cbcaf771f4a105'),
  358. 'samlIdentifiers.providerId': '123456',
  359. })
  360. expect(error).to.be.instanceof(Errors.SAMLAlreadyLinkedError)
  361. expect(this.User.findOneAndUpdate).to.not.have.been.called
  362. }
  363. })
  364. })
  365. it('should pass back errors via UserAuditLogHandler', async function () {
  366. let error
  367. const anError = new Error('oops')
  368. this.UserAuditLogHandler.promises.addEntry.rejects(anError)
  369. try {
  370. await this.SAMLIdentityManager.linkAccounts(
  371. this.user._id,
  372. {
  373. externalUserId: 'externalUserId',
  374. institutionEmail: this.user.email,
  375. universityId: '1',
  376. universityName: 'Overleaf University',
  377. hasEntitlement: false,
  378. userIdAttribute: 'someAttribute',
  379. },
  380. {
  381. intiatorId: '6005c75b12cbcaf771f4a105',
  382. ipAddress: '0:0:0:0',
  383. }
  384. )
  385. } catch (e) {
  386. error = e
  387. } finally {
  388. expect(error).to.exist
  389. expect(error).to.equal(anError)
  390. expect(this.EmailHandler.sendEmail).to.not.have.been.called
  391. expect(this.User.updateOne).to.not.have.been.called
  392. }
  393. })
  394. })
  395. describe('success', function () {
  396. beforeEach(function () {
  397. // first call is to get userWithProvider; should be falsy
  398. this.UserGetter.promises.getUser.onFirstCall().resolves()
  399. this.UserGetter.promises.getUser.onSecondCall().resolves(this.user)
  400. })
  401. it('should update the user audit log', async function () {
  402. const auditLog = {
  403. initiatorId: '6005c75b12cbcaf771f4a105',
  404. ipAddress: '0:0:0:0',
  405. }
  406. await this.SAMLIdentityManager.linkAccounts(
  407. this.user._id,
  408. {
  409. externalUserId: 'externalUserId',
  410. institutionEmail: this.user.email,
  411. universityId: '1',
  412. universityName: 'Overleaf University',
  413. hasEntitlement: false,
  414. userIdAttribute: 'uniqueId',
  415. },
  416. auditLog
  417. )
  418. expect(
  419. this.UserAuditLogHandler.promises.addEntry
  420. ).to.have.been.calledWith(
  421. this.user._id,
  422. 'link-institution-sso',
  423. auditLog.initiatorId,
  424. auditLog.ipAddress,
  425. {
  426. institutionEmail: this.user.email,
  427. providerId: '1',
  428. providerName: 'Overleaf University',
  429. userIdAttribute: 'uniqueId',
  430. }
  431. )
  432. })
  433. it('should send an email notification', async function () {
  434. await this.SAMLIdentityManager.linkAccounts(
  435. this.user._id,
  436. {
  437. externalUserId: 'externalUserId',
  438. institutionEmail: this.user.email,
  439. universityId: '1',
  440. universityName: 'Overleaf University',
  441. hasEntitlement: false,
  442. userIdAttribute: 'someAttribute',
  443. },
  444. {
  445. intiatorId: '6005c75b12cbcaf771f4a105',
  446. ipAddress: '0:0:0:0',
  447. }
  448. )
  449. expect(this.User.findOneAndUpdate).to.have.been.called
  450. expect(this.EmailHandler.sendEmail).to.have.been.calledOnce
  451. const emailArgs = this.EmailHandler.sendEmail.lastCall.args
  452. expect(emailArgs[0]).to.equal('securityAlert')
  453. expect(emailArgs[1].to).to.equal(this.user.email)
  454. expect(emailArgs[1].actionDescribed).to.contain('was linked')
  455. expect(emailArgs[1].message[0]).to.contain('Linked')
  456. expect(emailArgs[1].message[0]).to.contain(this.user.email)
  457. })
  458. })
  459. })
  460. describe('unlinkAccounts', function () {
  461. it('should update the audit log', async function () {
  462. await this.SAMLIdentityManager.unlinkAccounts(
  463. this.user._id,
  464. linkedEmail,
  465. this.user.email,
  466. '1',
  467. 'Overleaf University',
  468. this.auditLog
  469. )
  470. expect(
  471. this.UserAuditLogHandler.promises.addEntry
  472. ).to.have.been.calledOnce.and.calledWithMatch(
  473. this.user._id,
  474. 'unlink-institution-sso',
  475. this.auditLog.initiatorId,
  476. this.auditLog.ipAddress,
  477. {
  478. institutionEmail: linkedEmail,
  479. providerId: '1',
  480. providerName: 'Overleaf University',
  481. }
  482. )
  483. })
  484. it('should remove the identifier', async function () {
  485. await this.SAMLIdentityManager.unlinkAccounts(
  486. this.user._id,
  487. linkedEmail,
  488. this.user.email,
  489. '1',
  490. 'Overleaf University',
  491. this.auditLog
  492. )
  493. const query = {
  494. _id: this.user._id,
  495. }
  496. const update = {
  497. $pull: {
  498. samlIdentifiers: {
  499. providerId: '1',
  500. },
  501. },
  502. }
  503. expect(this.User.updateOne).to.have.been.calledOnce.and.calledWithMatch(
  504. query,
  505. update
  506. )
  507. })
  508. it('should send an email notification', async function () {
  509. await this.SAMLIdentityManager.unlinkAccounts(
  510. this.user._id,
  511. linkedEmail,
  512. this.user.email,
  513. '1',
  514. 'Overleaf University',
  515. this.auditLog
  516. )
  517. expect(this.User.updateOne).to.have.been.called
  518. expect(this.EmailHandler.sendEmail).to.have.been.calledOnce
  519. const emailArgs = this.EmailHandler.sendEmail.lastCall.args
  520. expect(emailArgs[0]).to.equal('securityAlert')
  521. expect(emailArgs[1].to).to.equal(this.user.email)
  522. expect(emailArgs[1].actionDescribed).to.contain('was unlinked')
  523. expect(emailArgs[1].message[0]).to.contain('No longer linked')
  524. expect(emailArgs[1].message[0]).to.contain(linkedEmail)
  525. })
  526. describe('errors', function () {
  527. it('should pass back errors via UserAuditLogHandler', async function () {
  528. let error
  529. const anError = new Error('oops')
  530. this.UserAuditLogHandler.promises.addEntry.rejects(anError)
  531. try {
  532. await this.SAMLIdentityManager.unlinkAccounts(
  533. this.user._id,
  534. linkedEmail,
  535. this.user.email,
  536. '1',
  537. 'Overleaf University',
  538. this.auditLog
  539. )
  540. } catch (e) {
  541. error = e
  542. } finally {
  543. expect(error).to.exist
  544. expect(error).to.equal(anError)
  545. expect(this.EmailHandler.sendEmail).to.not.have.been.called
  546. expect(this.User.updateOne).to.not.have.been.called
  547. }
  548. })
  549. })
  550. })
  551. describe('entitlementAttributeMatches', function () {
  552. it('should return true when entitlement matches on string', function () {
  553. this.SAMLIdentityManager.entitlementAttributeMatches(
  554. 'foo bar',
  555. 'bar'
  556. ).should.equal(true)
  557. })
  558. it('should return false when entitlement does not match on string', function () {
  559. this.SAMLIdentityManager.entitlementAttributeMatches(
  560. 'foo bar',
  561. 'bam'
  562. ).should.equal(false)
  563. })
  564. it('should return false on an invalid matcher', function () {
  565. this.SAMLIdentityManager.entitlementAttributeMatches(
  566. 'foo bar',
  567. '('
  568. ).should.equal(false)
  569. })
  570. it('should log error on an invalid matcher', function () {
  571. this.SAMLIdentityManager.entitlementAttributeMatches('foo bar', '(')
  572. this.logger.error.firstCall.args[0].err.message.should.equal(
  573. 'Invalid regular expression: /(/: Unterminated group'
  574. )
  575. })
  576. it('should return true when entitlement matches on array', function () {
  577. this.SAMLIdentityManager.entitlementAttributeMatches(
  578. ['foo', 'bar'],
  579. 'bar'
  580. ).should.equal(true)
  581. })
  582. it('should return false when entitlement does not match array', function () {
  583. this.SAMLIdentityManager.entitlementAttributeMatches(
  584. ['foo', 'bar'],
  585. 'bam'
  586. ).should.equal(false)
  587. })
  588. })
  589. describe('migrateIdentifier', function () {
  590. const userId = '5efb8b6e9b647b0027e4c0b0'
  591. const externalUserId = '987zyx'
  592. const providerId = 123
  593. const hasEntitlement = false
  594. const institutionEmail = 'someone@email.com'
  595. const providerName = 'Example University'
  596. const auditLog = {
  597. initiatorId: userId,
  598. ipAddress: '0.0.0.0',
  599. migration: {
  600. from: 'uniqueId',
  601. to: 'newUniqueId',
  602. },
  603. }
  604. const userIdAttribute = 'newUniqueId'
  605. it('should remove the old identifier and add the new identifier', async function () {
  606. this.UserGetter.promises.getUser.resolves()
  607. this.UserGetter.promises.getUserByAnyEmail
  608. .withArgs(institutionEmail)
  609. .resolves({ _id: userId, emails: [{ email: institutionEmail }] })
  610. this.UserGetter.promises.getUserFullEmails.withArgs(userId).resolves([
  611. {
  612. email: institutionEmail,
  613. affiliation: { institution: { id: providerId } },
  614. },
  615. ])
  616. await this.SAMLIdentityManager.migrateIdentifier(
  617. userId,
  618. externalUserId,
  619. providerId,
  620. hasEntitlement,
  621. institutionEmail,
  622. providerName,
  623. auditLog,
  624. userIdAttribute
  625. )
  626. expect(this.User.updateOne).to.have.been.calledOnce
  627. const query = {
  628. _id: userId,
  629. 'samlIdentifiers.providerId': providerId.toString(),
  630. }
  631. const update = {
  632. $set: {
  633. 'samlIdentifiers.$.externalUserId': externalUserId,
  634. 'samlIdentifiers.$.userIdAttribute': userIdAttribute,
  635. },
  636. }
  637. expect(this.User.updateOne.lastCall.args).to.deep.equal([query, update])
  638. })
  639. })
  640. describe('unlinkNotMigrated', function () {
  641. const userId = '5efb8b6e9b647b0027e4c0b0'
  642. const providerId = '123'
  643. const institutionEmail = 'someone@email.com'
  644. const providerName = 'Example University'
  645. const auditLog = {
  646. ipAddress: 'N/A',
  647. }
  648. it('should remove the identifier om samlIdentifiers and samlProviderId on the email', async function () {
  649. this.User.findOneAndUpdate = sinon.stub().returns({
  650. exec: sinon.stub().resolves({
  651. _id: userId,
  652. emails: [{ email: institutionEmail, samlProviderId: providerId }],
  653. }),
  654. })
  655. await this.SAMLIdentityManager.unlinkNotMigrated(
  656. userId,
  657. providerId,
  658. providerName,
  659. auditLog
  660. )
  661. expect(this.User.findOneAndUpdate).to.have.been.calledOnce
  662. const query = {
  663. _id: userId,
  664. 'emails.samlProviderId': providerId,
  665. }
  666. const update = {
  667. $pull: {
  668. samlIdentifiers: {
  669. providerId,
  670. },
  671. },
  672. $unset: {
  673. 'emails.$.samlProviderId': 1,
  674. },
  675. }
  676. expect(this.User.findOneAndUpdate.lastCall.args).to.deep.equal([
  677. query,
  678. update,
  679. ])
  680. expect(this.UserAuditLogHandler.promises.addEntry).to.have.been.calledOnce
  681. expect(
  682. this.UserAuditLogHandler.promises.addEntry.lastCall.args
  683. ).to.deep.equal([
  684. userId,
  685. 'unlink-institution-sso-not-migrated',
  686. undefined,
  687. 'N/A',
  688. { providerId, providerName },
  689. ])
  690. expect(this.InstitutionsAPI.promises.removeEntitlement).to.have.been
  691. .calledOnce
  692. expect(
  693. this.InstitutionsAPI.promises.removeEntitlement.lastCall.args
  694. ).to.deep.equal([userId, institutionEmail])
  695. })
  696. })
  697. })