SAMLIdentityManagerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  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. expect(error.message).to.contain(
  184. 'missing data when linking institution SSO'
  185. )
  186. }
  187. })
  188. }
  189. })
  190. describe('when email is already associated with another Overleaf account', function () {
  191. beforeEach(function () {
  192. this.UserGetter.promises.getUserByAnyEmail.resolves(
  193. this.userEmailExists
  194. )
  195. })
  196. it('should throw an EmailExistsError error', async function () {
  197. let error
  198. try {
  199. await this.SAMLIdentityManager.linkAccounts(
  200. '6005c75b12cbcaf771f4a105',
  201. {
  202. externalUserId: 'not-linked-id',
  203. institutionEmail: 'exists@overleaf.com',
  204. universityId: 'provider-id',
  205. universityName: 'provider-name',
  206. hasEntitlement: true,
  207. userIdAttribute: 'someAttribute',
  208. },
  209. {
  210. intiatorId: '6005c75b12cbcaf771f4a105',
  211. ip: '0:0:0:0',
  212. }
  213. )
  214. } catch (e) {
  215. error = e
  216. } finally {
  217. expect(error).to.be.instanceof(Errors.EmailExistsError)
  218. expect(this.User.findOneAndUpdate).to.not.have.been.called
  219. }
  220. })
  221. })
  222. describe('when email is not affiliated', function () {
  223. beforeEach(function () {
  224. this.UserGetter.promises.getUserByAnyEmail.resolves(this.user)
  225. this.UserGetter.promises.getUserFullEmails.resolves([
  226. {
  227. email: 'not-affiliated@overleaf.com',
  228. },
  229. ])
  230. })
  231. it('should throw SAMLEmailNotAffiliatedError', async function () {
  232. let error
  233. try {
  234. await this.SAMLIdentityManager.linkAccounts(
  235. '6005c75b12cbcaf771f4a105',
  236. {
  237. externalUserId: 'not-linked-id',
  238. institutionEmail: 'not-affiliated@overleaf.com',
  239. universityId: 'provider-id',
  240. universityName: 'provider-name',
  241. hasEntitlement: true,
  242. userIdAttribute: 'someAttribute',
  243. },
  244. {
  245. intiatorId: 'user-id-1',
  246. ip: '0:0:0:0',
  247. }
  248. )
  249. } catch (e) {
  250. error = e
  251. } finally {
  252. expect(error).to.be.instanceof(Errors.SAMLEmailNotAffiliatedError)
  253. expect(this.User.findOneAndUpdate).to.not.have.been.called
  254. }
  255. })
  256. })
  257. describe('when email is affiliated with another institution', function () {
  258. beforeEach(function () {
  259. this.UserGetter.promises.getUserByAnyEmail.resolves(this.user)
  260. this.UserGetter.promises.getUserFullEmails.resolves([
  261. {
  262. email: 'affiliated@overleaf.com',
  263. affiliation: { institution: { id: '987' } },
  264. },
  265. ])
  266. })
  267. it('should throw SAMLEmailAffiliatedWithAnotherInstitutionError', async function () {
  268. let error
  269. try {
  270. await this.SAMLIdentityManager.linkAccounts(
  271. '6005c75b12cbcaf771f4a105',
  272. {
  273. externalUserId: 'not-linked-id',
  274. institutionEmail: 'affiliated@overleaf.com',
  275. universityId: 'provider-id',
  276. universityName: 'provider-name',
  277. hasEntitlement: true,
  278. userIdAttribute: 'someAttribute',
  279. },
  280. {
  281. intiatorId: 'user-id-1',
  282. ip: '0:0:0:0',
  283. }
  284. )
  285. } catch (e) {
  286. error = e
  287. } finally {
  288. expect(error).to.be.instanceof(
  289. Errors.SAMLEmailAffiliatedWithAnotherInstitutionError
  290. )
  291. expect(this.User.findOneAndUpdate).to.not.have.been.called
  292. }
  293. })
  294. })
  295. describe('when institution identifier is already associated with another Overleaf account', function () {
  296. beforeEach(function () {
  297. this.UserGetter.promises.getUserByAnyEmail.resolves(
  298. this.userAlreadyLinked
  299. )
  300. })
  301. it('should throw an SAMLIdentityExistsError error', async function () {
  302. let error
  303. try {
  304. await this.SAMLIdentityManager.linkAccounts(
  305. '6005c75b12cbcaf771f4a105',
  306. {
  307. externalUserId: 'already-linked-id',
  308. institutionEmail: 'linked@overleaf.com',
  309. universityId: 'provider-id',
  310. universityName: 'provider-name',
  311. hasEntitlement: true,
  312. userIdAttribute: 'someAttribute',
  313. },
  314. {
  315. intiatorId: '6005c75b12cbcaf771f4a105',
  316. ip: '0:0:0:0',
  317. }
  318. )
  319. } catch (e) {
  320. error = e
  321. } finally {
  322. expect(error).to.be.instanceof(Errors.SAMLIdentityExistsError)
  323. expect(this.User.findOneAndUpdate).to.not.have.been.called
  324. }
  325. })
  326. })
  327. describe('when institution provider is already associated with the user', function () {
  328. beforeEach(function () {
  329. // first call is to get userWithProvider; resolves with any user
  330. this.UserGetter.promises.getUser.onFirstCall().resolves(this.user)
  331. })
  332. it('should throw an SAMLAlreadyLinkedError error', async function () {
  333. let error
  334. try {
  335. await this.SAMLIdentityManager.linkAccounts(
  336. '6005c75b12cbcaf771f4a105',
  337. {
  338. externalUserId: 'already-linked-id',
  339. institutionEmail: 'linked@overleaf.com',
  340. universityId: 123456,
  341. universityName: 'provider-name',
  342. hasEntitlement: true,
  343. userIdAttribute: 'someAttribute',
  344. },
  345. {
  346. intiatorId: '6005c75b12cbcaf771f4a105',
  347. ip: '0:0:0:0',
  348. }
  349. )
  350. } catch (e) {
  351. error = e
  352. } finally {
  353. expect(
  354. this.UserGetter.promises.getUser
  355. ).to.have.been.calledWithMatch({
  356. _id: new ObjectId('6005c75b12cbcaf771f4a105'),
  357. 'samlIdentifiers.providerId': '123456',
  358. })
  359. expect(error).to.be.instanceof(Errors.SAMLAlreadyLinkedError)
  360. expect(this.User.findOneAndUpdate).to.not.have.been.called
  361. }
  362. })
  363. })
  364. it('should pass back errors via UserAuditLogHandler', async function () {
  365. let error
  366. const anError = new Error('oops')
  367. this.UserAuditLogHandler.promises.addEntry.rejects(anError)
  368. try {
  369. await this.SAMLIdentityManager.linkAccounts(
  370. this.user._id,
  371. {
  372. externalUserId: 'externalUserId',
  373. institutionEmail: this.user.email,
  374. universityId: '1',
  375. universityName: 'Overleaf University',
  376. hasEntitlement: false,
  377. userIdAttribute: 'someAttribute',
  378. },
  379. {
  380. intiatorId: '6005c75b12cbcaf771f4a105',
  381. ipAddress: '0:0:0:0',
  382. }
  383. )
  384. } catch (e) {
  385. error = e
  386. } finally {
  387. expect(error).to.exist
  388. expect(error).to.equal(anError)
  389. expect(this.EmailHandler.sendEmail).to.not.have.been.called
  390. expect(this.User.updateOne).to.not.have.been.called
  391. }
  392. })
  393. })
  394. describe('success', function () {
  395. beforeEach(function () {
  396. // first call is to get userWithProvider; should be falsy
  397. this.UserGetter.promises.getUser.onFirstCall().resolves()
  398. this.UserGetter.promises.getUser.onSecondCall().resolves(this.user)
  399. })
  400. it('should update the user audit log', async function () {
  401. const auditLog = {
  402. initiatorId: '6005c75b12cbcaf771f4a105',
  403. ipAddress: '0:0:0:0',
  404. }
  405. await this.SAMLIdentityManager.linkAccounts(
  406. this.user._id,
  407. {
  408. externalUserId: 'externalUserId',
  409. institutionEmail: this.user.email,
  410. universityId: '1',
  411. universityName: 'Overleaf University',
  412. hasEntitlement: false,
  413. userIdAttribute: 'uniqueId',
  414. },
  415. auditLog
  416. )
  417. expect(
  418. this.UserAuditLogHandler.promises.addEntry
  419. ).to.have.been.calledWith(
  420. this.user._id,
  421. 'link-institution-sso',
  422. auditLog.initiatorId,
  423. auditLog.ipAddress,
  424. {
  425. institutionEmail: this.user.email,
  426. providerId: '1',
  427. providerName: 'Overleaf University',
  428. userIdAttribute: 'uniqueId',
  429. }
  430. )
  431. })
  432. it('should send an email notification', async function () {
  433. await this.SAMLIdentityManager.linkAccounts(
  434. this.user._id,
  435. {
  436. externalUserId: 'externalUserId',
  437. institutionEmail: this.user.email,
  438. universityId: '1',
  439. universityName: 'Overleaf University',
  440. hasEntitlement: false,
  441. userIdAttribute: 'someAttribute',
  442. },
  443. {
  444. intiatorId: '6005c75b12cbcaf771f4a105',
  445. ipAddress: '0:0:0:0',
  446. }
  447. )
  448. expect(this.User.findOneAndUpdate).to.have.been.called
  449. expect(this.EmailHandler.sendEmail).to.have.been.calledOnce
  450. const emailArgs = this.EmailHandler.sendEmail.lastCall.args
  451. expect(emailArgs[0]).to.equal('securityAlert')
  452. expect(emailArgs[1].to).to.equal(this.user.email)
  453. expect(emailArgs[1].actionDescribed).to.contain('was linked')
  454. expect(emailArgs[1].message[0]).to.contain('Linked')
  455. expect(emailArgs[1].message[0]).to.contain(this.user.email)
  456. })
  457. })
  458. })
  459. describe('unlinkAccounts', function () {
  460. it('should update the audit log', async function () {
  461. await this.SAMLIdentityManager.unlinkAccounts(
  462. this.user._id,
  463. linkedEmail,
  464. this.user.email,
  465. '1',
  466. 'Overleaf University',
  467. this.auditLog
  468. )
  469. expect(
  470. this.UserAuditLogHandler.promises.addEntry
  471. ).to.have.been.calledOnce.and.calledWithMatch(
  472. this.user._id,
  473. 'unlink-institution-sso',
  474. this.auditLog.initiatorId,
  475. this.auditLog.ipAddress,
  476. {
  477. institutionEmail: linkedEmail,
  478. providerId: '1',
  479. providerName: 'Overleaf University',
  480. }
  481. )
  482. })
  483. it('should remove the identifier', async function () {
  484. await this.SAMLIdentityManager.unlinkAccounts(
  485. this.user._id,
  486. linkedEmail,
  487. this.user.email,
  488. '1',
  489. 'Overleaf University',
  490. this.auditLog
  491. )
  492. const query = {
  493. _id: this.user._id,
  494. }
  495. const update = {
  496. $pull: {
  497. samlIdentifiers: {
  498. providerId: '1',
  499. },
  500. },
  501. }
  502. expect(this.User.updateOne).to.have.been.calledOnce.and.calledWithMatch(
  503. query,
  504. update
  505. )
  506. })
  507. it('should send an email notification', async function () {
  508. await this.SAMLIdentityManager.unlinkAccounts(
  509. this.user._id,
  510. linkedEmail,
  511. this.user.email,
  512. '1',
  513. 'Overleaf University',
  514. this.auditLog
  515. )
  516. expect(this.User.updateOne).to.have.been.called
  517. expect(this.EmailHandler.sendEmail).to.have.been.calledOnce
  518. const emailArgs = this.EmailHandler.sendEmail.lastCall.args
  519. expect(emailArgs[0]).to.equal('securityAlert')
  520. expect(emailArgs[1].to).to.equal(this.user.email)
  521. expect(emailArgs[1].actionDescribed).to.contain('was unlinked')
  522. expect(emailArgs[1].message[0]).to.contain('No longer linked')
  523. expect(emailArgs[1].message[0]).to.contain(linkedEmail)
  524. })
  525. describe('errors', function () {
  526. it('should pass back errors via UserAuditLogHandler', async function () {
  527. let error
  528. const anError = new Error('oops')
  529. this.UserAuditLogHandler.promises.addEntry.rejects(anError)
  530. try {
  531. await this.SAMLIdentityManager.unlinkAccounts(
  532. this.user._id,
  533. linkedEmail,
  534. this.user.email,
  535. '1',
  536. 'Overleaf University',
  537. this.auditLog
  538. )
  539. } catch (e) {
  540. error = e
  541. } finally {
  542. expect(error).to.exist
  543. expect(error).to.equal(anError)
  544. expect(this.EmailHandler.sendEmail).to.not.have.been.called
  545. expect(this.User.updateOne).to.not.have.been.called
  546. }
  547. })
  548. })
  549. })
  550. describe('entitlementAttributeMatches', function () {
  551. it('should return true when entitlement matches on string', function () {
  552. this.SAMLIdentityManager.entitlementAttributeMatches(
  553. 'foo bar',
  554. 'bar'
  555. ).should.equal(true)
  556. })
  557. it('should return false when entitlement does not match on string', function () {
  558. this.SAMLIdentityManager.entitlementAttributeMatches(
  559. 'foo bar',
  560. 'bam'
  561. ).should.equal(false)
  562. })
  563. it('should return false on an invalid matcher', function () {
  564. this.SAMLIdentityManager.entitlementAttributeMatches(
  565. 'foo bar',
  566. '('
  567. ).should.equal(false)
  568. })
  569. it('should log error on an invalid matcher', function () {
  570. this.SAMLIdentityManager.entitlementAttributeMatches('foo bar', '(')
  571. this.logger.error.firstCall.args[0].err.message.should.equal(
  572. 'Invalid regular expression: /(/: Unterminated group'
  573. )
  574. })
  575. it('should return true when entitlement matches on array', function () {
  576. this.SAMLIdentityManager.entitlementAttributeMatches(
  577. ['foo', 'bar'],
  578. 'bar'
  579. ).should.equal(true)
  580. })
  581. it('should return false when entitlement does not match array', function () {
  582. this.SAMLIdentityManager.entitlementAttributeMatches(
  583. ['foo', 'bar'],
  584. 'bam'
  585. ).should.equal(false)
  586. })
  587. })
  588. describe('migrateIdentifier', function () {
  589. const userId = '5efb8b6e9b647b0027e4c0b0'
  590. const externalUserId = '987zyx'
  591. const providerId = 123
  592. const hasEntitlement = false
  593. const institutionEmail = 'someone@email.com'
  594. const providerName = 'Example University'
  595. const auditLog = {
  596. initiatorId: userId,
  597. ipAddress: '0.0.0.0',
  598. migration: {
  599. from: 'uniqueId',
  600. to: 'newUniqueId',
  601. },
  602. }
  603. const userIdAttribute = 'newUniqueId'
  604. it('should remove the old identifier and add the new identifier', async function () {
  605. this.UserGetter.promises.getUser.resolves()
  606. this.UserGetter.promises.getUserByAnyEmail
  607. .withArgs(institutionEmail)
  608. .resolves({ _id: userId, emails: [{ email: institutionEmail }] })
  609. this.UserGetter.promises.getUserFullEmails.withArgs(userId).resolves([
  610. {
  611. email: institutionEmail,
  612. affiliation: { institution: { id: providerId } },
  613. },
  614. ])
  615. await this.SAMLIdentityManager.migrateIdentifier(
  616. userId,
  617. externalUserId,
  618. providerId,
  619. hasEntitlement,
  620. institutionEmail,
  621. providerName,
  622. auditLog,
  623. userIdAttribute
  624. )
  625. expect(this.User.updateOne).to.have.been.calledOnce
  626. const query = {
  627. _id: userId,
  628. 'samlIdentifiers.providerId': providerId.toString(),
  629. }
  630. const update = {
  631. $set: {
  632. 'samlIdentifiers.$.externalUserId': externalUserId,
  633. 'samlIdentifiers.$.userIdAttribute': userIdAttribute,
  634. },
  635. }
  636. expect(this.User.updateOne.lastCall.args).to.deep.equal([query, update])
  637. })
  638. })
  639. describe('unlinkNotMigrated', function () {
  640. const userId = '5efb8b6e9b647b0027e4c0b0'
  641. const providerId = '123'
  642. const institutionEmail = 'someone@email.com'
  643. const providerName = 'Example University'
  644. const auditLog = {
  645. ipAddress: 'N/A',
  646. }
  647. it('should remove the identifier om samlIdentifiers and samlProviderId on the email', async function () {
  648. this.User.findOneAndUpdate = sinon.stub().returns({
  649. exec: sinon.stub().resolves({
  650. _id: userId,
  651. emails: [{ email: institutionEmail, samlProviderId: providerId }],
  652. }),
  653. })
  654. await this.SAMLIdentityManager.unlinkNotMigrated(
  655. userId,
  656. providerId,
  657. providerName,
  658. auditLog
  659. )
  660. expect(this.User.findOneAndUpdate).to.have.been.calledOnce
  661. const query = {
  662. _id: userId,
  663. 'emails.samlProviderId': providerId,
  664. }
  665. const update = {
  666. $pull: {
  667. samlIdentifiers: {
  668. providerId,
  669. },
  670. },
  671. $unset: {
  672. 'emails.$.samlProviderId': 1,
  673. },
  674. }
  675. expect(this.User.findOneAndUpdate.lastCall.args).to.deep.equal([
  676. query,
  677. update,
  678. ])
  679. expect(this.UserAuditLogHandler.promises.addEntry).to.have.been.calledOnce
  680. expect(
  681. this.UserAuditLogHandler.promises.addEntry.lastCall.args
  682. ).to.deep.equal([
  683. userId,
  684. 'unlink-institution-sso-not-migrated',
  685. undefined,
  686. 'N/A',
  687. { providerId, providerName },
  688. ])
  689. expect(this.InstitutionsAPI.promises.removeEntitlement).to.have.been
  690. .calledOnce
  691. expect(
  692. this.InstitutionsAPI.promises.removeEntitlement.lastCall.args
  693. ).to.deep.equal([userId, institutionEmail])
  694. })
  695. })
  696. })