UserGetterTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. const { ObjectId } = require('mongodb')
  2. const should = require('chai').should()
  3. const SandboxedModule = require('sandboxed-module')
  4. const assert = require('assert')
  5. const path = require('path')
  6. const sinon = require('sinon')
  7. const modulePath = path.join(
  8. __dirname,
  9. '../../../../app/src/Features/User/UserGetter'
  10. )
  11. const { expect } = require('chai')
  12. const Errors = require('../../../../app/src/Features/Errors/Errors')
  13. const {
  14. normalizeQuery,
  15. normalizeMultiQuery
  16. } = require('../../../../app/src/Features/Helpers/Mongo')
  17. describe('UserGetter', function() {
  18. beforeEach(function() {
  19. this.fakeUser = {
  20. _id: '12390i',
  21. email: 'email2@foo.bar',
  22. emails: [
  23. {
  24. email: 'email1@foo.bar',
  25. reversedHostname: 'rab.oof',
  26. confirmedAt: new Date()
  27. },
  28. { email: 'email2@foo.bar', reversedHostname: 'rab.oof' }
  29. ]
  30. }
  31. this.findOne = sinon.stub().callsArgWith(2, null, this.fakeUser)
  32. this.findToArrayStub = sinon.stub().yields(null, [this.fakeUser])
  33. this.find = sinon.stub().returns({ toArray: this.findToArrayStub })
  34. this.Mongo = {
  35. db: {
  36. users: {
  37. findOne: this.findOne,
  38. find: this.find
  39. }
  40. },
  41. ObjectId
  42. }
  43. const settings = { apis: { v1: { url: 'v1.url', user: '', pass: '' } } }
  44. this.getUserAffiliations = sinon.stub().resolves([])
  45. this.UserGetter = SandboxedModule.require(modulePath, {
  46. globals: {
  47. console: console
  48. },
  49. requires: {
  50. '../Helpers/Mongo': { normalizeQuery, normalizeMultiQuery },
  51. 'logger-sharelatex': {
  52. log() {}
  53. },
  54. '../../infrastructure/mongodb': this.Mongo,
  55. '@overleaf/metrics': {
  56. timeAsyncMethod: sinon.stub()
  57. },
  58. 'settings-sharelatex': settings,
  59. '../Institutions/InstitutionsAPI': {
  60. promises: {
  61. getUserAffiliations: this.getUserAffiliations
  62. }
  63. },
  64. '../../infrastructure/Features': {
  65. hasFeature: sinon.stub().returns(true)
  66. }
  67. }
  68. })
  69. })
  70. describe('getUser', function() {
  71. it('should get user', function(done) {
  72. const query = { _id: '000000000000000000000000' }
  73. const projection = { email: 1 }
  74. this.UserGetter.getUser(query, projection, (error, user) => {
  75. expect(error).to.not.exist
  76. this.findOne.called.should.equal(true)
  77. this.findOne.calledWith(query, { projection }).should.equal(true)
  78. user.should.deep.equal(this.fakeUser)
  79. done()
  80. })
  81. })
  82. it('should not allow null query', function(done) {
  83. this.UserGetter.getUser(null, {}, error => {
  84. error.should.exist
  85. error.message.should.equal('no query provided')
  86. done()
  87. })
  88. })
  89. })
  90. describe('getUsers', function() {
  91. it('should get users with array of userIds', function(done) {
  92. const query = [new ObjectId()]
  93. const projection = { email: 1 }
  94. this.UserGetter.getUsers(query, projection, (error, users) => {
  95. expect(error).to.not.exist
  96. this.find.should.have.been.calledWithMatch(
  97. { _id: { $in: query } },
  98. { projection }
  99. )
  100. users.should.deep.equal([this.fakeUser])
  101. done()
  102. })
  103. })
  104. it('should not allow null query', function(done) {
  105. this.UserGetter.getUser(null, {}, error => {
  106. error.should.exist
  107. error.message.should.equal('no query provided')
  108. done()
  109. })
  110. })
  111. })
  112. describe('getUserFullEmails', function() {
  113. it('should get user', function(done) {
  114. this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
  115. const projection = { email: 1, emails: 1, samlIdentifiers: 1 }
  116. this.UserGetter.getUserFullEmails(
  117. this.fakeUser._id,
  118. (error, fullEmails) => {
  119. expect(error).to.not.exist
  120. this.UserGetter.promises.getUser.called.should.equal(true)
  121. this.UserGetter.promises.getUser
  122. .calledWith(this.fakeUser._id, projection)
  123. .should.equal(true)
  124. done()
  125. }
  126. )
  127. })
  128. it('should fetch emails data', function(done) {
  129. this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
  130. this.UserGetter.getUserFullEmails(
  131. this.fakeUser._id,
  132. (error, fullEmails) => {
  133. expect(error).to.not.exist
  134. assert.deepEqual(fullEmails, [
  135. {
  136. email: 'email1@foo.bar',
  137. reversedHostname: 'rab.oof',
  138. confirmedAt: this.fakeUser.emails[0].confirmedAt,
  139. emailHasInstitutionLicence: false,
  140. default: false
  141. },
  142. {
  143. email: 'email2@foo.bar',
  144. reversedHostname: 'rab.oof',
  145. emailHasInstitutionLicence: false,
  146. default: true
  147. }
  148. ])
  149. done()
  150. }
  151. )
  152. })
  153. it('should merge affiliation data', function(done) {
  154. this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
  155. const affiliationsData = [
  156. {
  157. email: 'email1@foo.bar',
  158. role: 'Prof',
  159. department: 'Maths',
  160. inferred: false,
  161. licence: 'pro_plus',
  162. institution: {
  163. name: 'University Name',
  164. isUniversity: true,
  165. confirmed: true
  166. }
  167. }
  168. ]
  169. this.getUserAffiliations.resolves(affiliationsData)
  170. this.UserGetter.getUserFullEmails(
  171. this.fakeUser._id,
  172. (error, fullEmails) => {
  173. expect(error).to.not.exist
  174. assert.deepEqual(fullEmails, [
  175. {
  176. email: 'email1@foo.bar',
  177. reversedHostname: 'rab.oof',
  178. confirmedAt: this.fakeUser.emails[0].confirmedAt,
  179. default: false,
  180. emailHasInstitutionLicence: true,
  181. affiliation: {
  182. institution: affiliationsData[0].institution,
  183. inferred: affiliationsData[0].inferred,
  184. department: affiliationsData[0].department,
  185. role: affiliationsData[0].role,
  186. licence: affiliationsData[0].licence
  187. }
  188. },
  189. {
  190. email: 'email2@foo.bar',
  191. reversedHostname: 'rab.oof',
  192. emailHasInstitutionLicence: false,
  193. default: true
  194. }
  195. ])
  196. done()
  197. }
  198. )
  199. })
  200. it('should merge SAML identifier', function(done) {
  201. const fakeSamlIdentifiers = [
  202. { providerId: 'saml_id', exteranlUserId: 'whatever' }
  203. ]
  204. const fakeUserWithSaml = this.fakeUser
  205. fakeUserWithSaml.emails[0].samlProviderId = 'saml_id'
  206. fakeUserWithSaml.samlIdentifiers = fakeSamlIdentifiers
  207. this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
  208. this.getUserAffiliations.resolves([])
  209. this.UserGetter.getUserFullEmails(
  210. this.fakeUser._id,
  211. (error, fullEmails) => {
  212. expect(error).to.not.exist
  213. assert.deepEqual(fullEmails, [
  214. {
  215. email: 'email1@foo.bar',
  216. reversedHostname: 'rab.oof',
  217. confirmedAt: this.fakeUser.emails[0].confirmedAt,
  218. default: false,
  219. emailHasInstitutionLicence: false,
  220. samlProviderId: 'saml_id',
  221. samlIdentifier: fakeSamlIdentifiers[0]
  222. },
  223. {
  224. email: 'email2@foo.bar',
  225. reversedHostname: 'rab.oof',
  226. emailHasInstitutionLicence: false,
  227. default: true
  228. }
  229. ])
  230. done()
  231. }
  232. )
  233. })
  234. it('should get user when it has no emails field', function(done) {
  235. this.fakeUser = {
  236. _id: '12390i',
  237. email: 'email2@foo.bar'
  238. }
  239. this.UserGetter.promises.getUser = sinon.stub().resolves(this.fakeUser)
  240. const projection = { email: 1, emails: 1, samlIdentifiers: 1 }
  241. this.UserGetter.getUserFullEmails(
  242. this.fakeUser._id,
  243. (error, fullEmails) => {
  244. expect(error).to.not.exist
  245. this.UserGetter.promises.getUser.called.should.equal(true)
  246. this.UserGetter.promises.getUser
  247. .calledWith(this.fakeUser._id, projection)
  248. .should.equal(true)
  249. assert.deepEqual(fullEmails, [])
  250. done()
  251. }
  252. )
  253. })
  254. })
  255. describe('getUserbyMainEmail', function() {
  256. it('query user by main email', function(done) {
  257. const email = 'hello@world.com'
  258. const projection = { emails: 1 }
  259. this.UserGetter.getUserByMainEmail(email, projection, (error, user) => {
  260. expect(error).to.not.exist
  261. this.findOne.called.should.equal(true)
  262. this.findOne.calledWith({ email }, { projection }).should.equal(true)
  263. done()
  264. })
  265. })
  266. it('return user if found', function(done) {
  267. const email = 'hello@world.com'
  268. this.UserGetter.getUserByMainEmail(email, (error, user) => {
  269. expect(error).to.not.exist
  270. user.should.deep.equal(this.fakeUser)
  271. done()
  272. })
  273. })
  274. it('trim email', function(done) {
  275. const email = 'hello@world.com'
  276. this.UserGetter.getUserByMainEmail(` ${email} `, (error, user) => {
  277. expect(error).to.not.exist
  278. this.findOne.called.should.equal(true)
  279. this.findOne.calledWith({ email }).should.equal(true)
  280. done()
  281. })
  282. })
  283. })
  284. describe('getUserByAnyEmail', function() {
  285. it('query user for any email', function(done) {
  286. const email = 'hello@world.com'
  287. const expectedQuery = {
  288. emails: { $exists: true },
  289. 'emails.email': email
  290. }
  291. const projection = { emails: 1 }
  292. this.UserGetter.getUserByAnyEmail(
  293. ` ${email} `,
  294. projection,
  295. (error, user) => {
  296. expect(error).to.not.exist
  297. this.findOne
  298. .calledWith(expectedQuery, { projection })
  299. .should.equal(true)
  300. user.should.deep.equal(this.fakeUser)
  301. done()
  302. }
  303. )
  304. })
  305. it('query contains $exists:true so partial index is used', function(done) {
  306. const expectedQuery = {
  307. emails: { $exists: true },
  308. 'emails.email': ''
  309. }
  310. this.UserGetter.getUserByAnyEmail('', {}, (error, user) => {
  311. expect(error).to.not.exist
  312. this.findOne
  313. .calledWith(expectedQuery, { projection: {} })
  314. .should.equal(true)
  315. done()
  316. })
  317. })
  318. it('checks main email as well', function(done) {
  319. this.findOne.callsArgWith(2, null, null)
  320. const email = 'hello@world.com'
  321. const projection = { emails: 1 }
  322. this.UserGetter.getUserByAnyEmail(
  323. ` ${email} `,
  324. projection,
  325. (error, user) => {
  326. expect(error).to.not.exist
  327. this.findOne.calledTwice.should.equal(true)
  328. this.findOne.calledWith({ email }, { projection }).should.equal(true)
  329. done()
  330. }
  331. )
  332. })
  333. })
  334. describe('getUsersByHostname', function() {
  335. it('should find user by hostname', function(done) {
  336. const hostname = 'bar.foo'
  337. const expectedQuery = {
  338. emails: { $exists: true },
  339. 'emails.reversedHostname': hostname
  340. .split('')
  341. .reverse()
  342. .join('')
  343. }
  344. const projection = { emails: 1 }
  345. this.UserGetter.getUsersByHostname(
  346. hostname,
  347. projection,
  348. (error, users) => {
  349. expect(error).to.not.exist
  350. this.find.calledOnce.should.equal(true)
  351. this.find.calledWith(expectedQuery, { projection }).should.equal(true)
  352. done()
  353. }
  354. )
  355. })
  356. })
  357. describe('getUsersByAnyConfirmedEmail', function() {
  358. it('should find users by confirmed email', function(done) {
  359. const emails = ['confirmed@example.com']
  360. this.UserGetter.getUsersByAnyConfirmedEmail(emails, (error, users) => {
  361. expect(error).to.not.exist
  362. expect(this.find).to.be.calledOnceWith(
  363. {
  364. 'emails.email': { $in: emails }, // use the index on emails.email
  365. emails: {
  366. $exists: true,
  367. $elemMatch: {
  368. email: { $in: emails },
  369. confirmedAt: { $exists: true }
  370. }
  371. }
  372. },
  373. { projection: {} }
  374. )
  375. done()
  376. })
  377. })
  378. })
  379. describe('getUsersByV1Id', function() {
  380. it('should find users by list of v1 ids', function(done) {
  381. const v1Ids = [501]
  382. const expectedQuery = {
  383. 'overleaf.id': { $in: v1Ids }
  384. }
  385. const projection = { emails: 1 }
  386. this.UserGetter.getUsersByV1Ids(v1Ids, projection, (error, users) => {
  387. expect(error).to.not.exist
  388. this.find.calledOnce.should.equal(true)
  389. this.find.calledWith(expectedQuery, { projection }).should.equal(true)
  390. done()
  391. })
  392. })
  393. })
  394. describe('ensureUniqueEmailAddress', function() {
  395. beforeEach(function() {
  396. this.UserGetter.getUserByAnyEmail = sinon.stub()
  397. })
  398. it('should return error if existing user is found', function(done) {
  399. this.UserGetter.getUserByAnyEmail.callsArgWith(1, null, this.fakeUser)
  400. this.UserGetter.ensureUniqueEmailAddress(this.newEmail, err => {
  401. should.exist(err)
  402. expect(err).to.be.an.instanceof(Errors.EmailExistsError)
  403. done()
  404. })
  405. })
  406. it('should return null if no user is found', function(done) {
  407. this.UserGetter.getUserByAnyEmail.callsArgWith(1)
  408. this.UserGetter.ensureUniqueEmailAddress(this.newEmail, err => {
  409. should.not.exist(err)
  410. done()
  411. })
  412. })
  413. })
  414. })