ProjectGetterTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const modulePath = '../../../../app/src/Features/Project/ProjectGetter.js'
  4. const SandboxedModule = require('sandboxed-module')
  5. const { ObjectId } = require('mongodb')
  6. describe('ProjectGetter', function () {
  7. beforeEach(function () {
  8. this.callback = sinon.stub()
  9. this.project = { _id: new ObjectId() }
  10. this.projectIdStr = this.project._id.toString()
  11. this.deletedProject = { deleterData: { wombat: 'potato' } }
  12. this.userId = new ObjectId()
  13. this.DeletedProject = {
  14. find: sinon.stub().yields(null, [this.deletedProject]),
  15. }
  16. this.Project = {
  17. find: sinon.stub(),
  18. findOne: sinon.stub().yields(null, this.project),
  19. }
  20. this.CollaboratorsGetter = {
  21. getProjectsUserIsMemberOf: sinon.stub().yields(null, {
  22. readAndWrite: [],
  23. readOnly: [],
  24. tokenReadAndWrite: [],
  25. tokenReadOnly: [],
  26. }),
  27. }
  28. this.LockManager = {
  29. runWithLock: sinon
  30. .stub()
  31. .callsFake((namespace, id, runner, callback) => runner(callback)),
  32. }
  33. this.db = {
  34. projects: {
  35. findOne: sinon.stub().yields(null, this.project),
  36. },
  37. users: {},
  38. }
  39. this.ProjectEntityMongoUpdateHandler = {
  40. lockKey: sinon.stub().returnsArg(0),
  41. }
  42. this.ProjectGetter = SandboxedModule.require(modulePath, {
  43. requires: {
  44. '../../infrastructure/mongodb': { db: this.db, ObjectId },
  45. '@overleaf/metrics': {
  46. timeAsyncMethod: sinon.stub(),
  47. },
  48. '../../models/Project': {
  49. Project: this.Project,
  50. },
  51. '../../models/DeletedProject': {
  52. DeletedProject: this.DeletedProject,
  53. },
  54. '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
  55. '../../infrastructure/LockManager': this.LockManager,
  56. './ProjectEntityMongoUpdateHandler':
  57. this.ProjectEntityMongoUpdateHandler,
  58. },
  59. })
  60. })
  61. describe('getProjectWithoutDocLines', function () {
  62. beforeEach(function () {
  63. this.ProjectGetter.getProject = sinon.stub().yields()
  64. })
  65. describe('passing an id', function () {
  66. beforeEach(function () {
  67. this.ProjectGetter.getProjectWithoutDocLines(
  68. this.project._id,
  69. this.callback
  70. )
  71. })
  72. it('should call find with the project id', function () {
  73. this.ProjectGetter.getProject
  74. .calledWith(this.project._id)
  75. .should.equal(true)
  76. })
  77. it('should exclude the doc lines', function () {
  78. const excludes = {
  79. 'rootFolder.docs.lines': 0,
  80. 'rootFolder.folders.docs.lines': 0,
  81. 'rootFolder.folders.folders.docs.lines': 0,
  82. 'rootFolder.folders.folders.folders.docs.lines': 0,
  83. 'rootFolder.folders.folders.folders.folders.docs.lines': 0,
  84. 'rootFolder.folders.folders.folders.folders.folders.docs.lines': 0,
  85. 'rootFolder.folders.folders.folders.folders.folders.folders.docs.lines': 0,
  86. 'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs.lines': 0,
  87. }
  88. this.ProjectGetter.getProject
  89. .calledWith(this.project._id, excludes)
  90. .should.equal(true)
  91. })
  92. it('should call the callback', function () {
  93. this.callback.called.should.equal(true)
  94. })
  95. })
  96. })
  97. describe('getProjectWithOnlyFolders', function () {
  98. beforeEach(function () {
  99. this.ProjectGetter.getProject = sinon.stub().yields()
  100. })
  101. describe('passing an id', function () {
  102. beforeEach(function () {
  103. this.ProjectGetter.getProjectWithOnlyFolders(
  104. this.project._id,
  105. this.callback
  106. )
  107. })
  108. it('should call find with the project id', function () {
  109. this.ProjectGetter.getProject
  110. .calledWith(this.project._id)
  111. .should.equal(true)
  112. })
  113. it('should exclude the docs and files linesaaaa', function () {
  114. const excludes = {
  115. 'rootFolder.docs': 0,
  116. 'rootFolder.fileRefs': 0,
  117. 'rootFolder.folders.docs': 0,
  118. 'rootFolder.folders.fileRefs': 0,
  119. 'rootFolder.folders.folders.docs': 0,
  120. 'rootFolder.folders.folders.fileRefs': 0,
  121. 'rootFolder.folders.folders.folders.docs': 0,
  122. 'rootFolder.folders.folders.folders.fileRefs': 0,
  123. 'rootFolder.folders.folders.folders.folders.docs': 0,
  124. 'rootFolder.folders.folders.folders.folders.fileRefs': 0,
  125. 'rootFolder.folders.folders.folders.folders.folders.docs': 0,
  126. 'rootFolder.folders.folders.folders.folders.folders.fileRefs': 0,
  127. 'rootFolder.folders.folders.folders.folders.folders.folders.docs': 0,
  128. 'rootFolder.folders.folders.folders.folders.folders.folders.fileRefs': 0,
  129. 'rootFolder.folders.folders.folders.folders.folders.folders.folders.docs': 0,
  130. 'rootFolder.folders.folders.folders.folders.folders.folders.folders.fileRefs': 0,
  131. }
  132. this.ProjectGetter.getProject
  133. .calledWith(this.project._id, excludes)
  134. .should.equal(true)
  135. })
  136. it('should call the callback with the project', function () {
  137. this.callback.called.should.equal(true)
  138. })
  139. })
  140. })
  141. describe('getProject', function () {
  142. describe('without projection', function () {
  143. describe('with project id', function () {
  144. beforeEach(function () {
  145. this.ProjectGetter.getProject(this.projectIdStr, this.callback)
  146. })
  147. it('should call findOne with the project id', function () {
  148. expect(this.db.projects.findOne.callCount).to.equal(1)
  149. expect(
  150. this.db.projects.findOne.lastCall.args[0]._id.toString()
  151. ).to.equal(this.projectIdStr)
  152. })
  153. })
  154. describe('without project id', function () {
  155. beforeEach(function () {
  156. this.ProjectGetter.getProject(null, this.callback)
  157. })
  158. it('should callback with error', function () {
  159. expect(this.db.projects.findOne.callCount).to.equal(0)
  160. expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
  161. })
  162. })
  163. })
  164. describe('with projection', function () {
  165. beforeEach(function () {
  166. this.projection = { _id: 1 }
  167. })
  168. describe('with project id', function () {
  169. beforeEach(function () {
  170. this.ProjectGetter.getProject(
  171. this.projectIdStr,
  172. this.projection,
  173. this.callback
  174. )
  175. })
  176. it('should call findOne with the project id', function () {
  177. expect(this.db.projects.findOne.callCount).to.equal(1)
  178. expect(
  179. this.db.projects.findOne.lastCall.args[0]._id.toString()
  180. ).to.equal(this.projectIdStr)
  181. expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({
  182. projection: this.projection,
  183. })
  184. })
  185. })
  186. describe('without project id', function () {
  187. beforeEach(function () {
  188. this.ProjectGetter.getProject(null, this.callback)
  189. })
  190. it('should callback with error', function () {
  191. expect(this.db.projects.findOne.callCount).to.equal(0)
  192. expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
  193. })
  194. })
  195. })
  196. })
  197. describe('getProjectWithoutLock', function () {
  198. describe('without projection', function () {
  199. describe('with project id', function () {
  200. beforeEach(function () {
  201. this.ProjectGetter.getProjectWithoutLock(
  202. this.projectIdStr,
  203. this.callback
  204. )
  205. })
  206. it('should call findOne with the project id', function () {
  207. expect(this.db.projects.findOne.callCount).to.equal(1)
  208. expect(
  209. this.db.projects.findOne.lastCall.args[0]._id.toString()
  210. ).to.equal(this.projectIdStr)
  211. })
  212. })
  213. describe('without project id', function () {
  214. beforeEach(function () {
  215. this.ProjectGetter.getProjectWithoutLock(null, this.callback)
  216. })
  217. it('should callback with error', function () {
  218. expect(this.db.projects.findOne.callCount).to.equal(0)
  219. expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
  220. })
  221. })
  222. })
  223. describe('with projection', function () {
  224. beforeEach(function () {
  225. this.projection = { _id: 1 }
  226. })
  227. describe('with project id', function () {
  228. beforeEach(function () {
  229. this.ProjectGetter.getProjectWithoutLock(
  230. this.project._id,
  231. this.projection,
  232. this.callback
  233. )
  234. })
  235. it('should call findOne with the project id', function () {
  236. expect(this.db.projects.findOne.callCount).to.equal(1)
  237. expect(
  238. this.db.projects.findOne.lastCall.args[0]._id.toString()
  239. ).to.equal(this.projectIdStr)
  240. expect(this.db.projects.findOne.lastCall.args[1]).to.deep.equal({
  241. projection: this.projection,
  242. })
  243. })
  244. })
  245. describe('without project id', function () {
  246. beforeEach(function () {
  247. this.ProjectGetter.getProjectWithoutLock(null, this.callback)
  248. })
  249. it('should callback with error', function () {
  250. expect(this.db.projects.findOne.callCount).to.equal(0)
  251. expect(this.callback.lastCall.args[0]).to.be.instanceOf(Error)
  252. })
  253. })
  254. })
  255. })
  256. describe('findAllUsersProjects', function () {
  257. beforeEach(function () {
  258. this.fields = { mock: 'fields' }
  259. this.projectOwned = { _id: 'mock-owned-projects' }
  260. this.projectRW = { _id: 'mock-rw-projects' }
  261. this.projectRO = { _id: 'mock-ro-projects' }
  262. this.projectTokenRW = { _id: 'mock-token-rw-projects' }
  263. this.projectTokenRO = { _id: 'mock-token-ro-projects' }
  264. this.Project.find
  265. .withArgs({ owner_ref: this.userId }, this.fields)
  266. .yields(null, [this.projectOwned])
  267. })
  268. it('should call the callback with all the projects', function () {
  269. this.CollaboratorsGetter.getProjectsUserIsMemberOf.yields(null, {
  270. readAndWrite: [this.projectRW],
  271. readOnly: [this.projectRO],
  272. tokenReadAndWrite: [this.projectTokenRW],
  273. tokenReadOnly: [this.projectTokenRO],
  274. })
  275. this.ProjectGetter.findAllUsersProjects(
  276. this.userId,
  277. this.fields,
  278. this.callback
  279. )
  280. this.callback
  281. .calledWith(null, {
  282. owned: [this.projectOwned],
  283. readAndWrite: [this.projectRW],
  284. readOnly: [this.projectRO],
  285. tokenReadAndWrite: [this.projectTokenRW],
  286. tokenReadOnly: [this.projectTokenRO],
  287. })
  288. .should.equal(true)
  289. })
  290. it('should remove duplicate projects', function () {
  291. this.CollaboratorsGetter.getProjectsUserIsMemberOf.yields(null, {
  292. readAndWrite: [this.projectRW, this.projectOwned],
  293. readOnly: [this.projectRO, this.projectRW],
  294. tokenReadAndWrite: [this.projectTokenRW, this.projectRO],
  295. tokenReadOnly: [
  296. this.projectTokenRW,
  297. this.projectTokenRO,
  298. this.projectRO,
  299. ],
  300. })
  301. this.ProjectGetter.findAllUsersProjects(
  302. this.userId,
  303. this.fields,
  304. this.callback
  305. )
  306. this.callback
  307. .calledWith(null, {
  308. owned: [this.projectOwned],
  309. readAndWrite: [this.projectRW],
  310. readOnly: [this.projectRO],
  311. tokenReadAndWrite: [this.projectTokenRW],
  312. tokenReadOnly: [this.projectTokenRO],
  313. })
  314. .should.equal(true)
  315. })
  316. })
  317. describe('getProjectIdByReadAndWriteToken', function () {
  318. describe('when project find returns project', function () {
  319. this.beforeEach(function () {
  320. this.ProjectGetter.getProjectIdByReadAndWriteToken(
  321. 'token',
  322. this.callback
  323. )
  324. })
  325. it('should find project with token', function () {
  326. this.Project.findOne
  327. .calledWithMatch({ 'tokens.readAndWrite': 'token' })
  328. .should.equal(true)
  329. })
  330. it('should callback with project id', function () {
  331. this.callback.calledWith(null, this.project._id).should.equal(true)
  332. })
  333. })
  334. describe('when project not found', function () {
  335. this.beforeEach(function () {
  336. this.Project.findOne.yields(null, null)
  337. this.ProjectGetter.getProjectIdByReadAndWriteToken(
  338. 'token',
  339. this.callback
  340. )
  341. })
  342. it('should callback empty', function () {
  343. expect(this.callback.firstCall.args.length).to.equal(0)
  344. })
  345. })
  346. describe('when project find returns error', function () {
  347. this.beforeEach(function () {
  348. this.Project.findOne.yields('error')
  349. this.ProjectGetter.getProjectIdByReadAndWriteToken(
  350. 'token',
  351. this.callback
  352. )
  353. })
  354. it('should callback with error', function () {
  355. this.callback.calledWith('error').should.equal(true)
  356. })
  357. })
  358. })
  359. describe('findUsersProjectsByName', function () {
  360. it('should perform a case-insensitive search', function (done) {
  361. this.project1 = { _id: 1, name: 'find me!' }
  362. this.project2 = { _id: 2, name: 'not me!' }
  363. this.project3 = { _id: 3, name: 'FIND ME!' }
  364. this.project4 = { _id: 4, name: 'Find Me!' }
  365. this.Project.find
  366. .withArgs({ owner_ref: this.userId })
  367. .yields(null, [
  368. this.project1,
  369. this.project2,
  370. this.project3,
  371. this.project4,
  372. ])
  373. this.ProjectGetter.findUsersProjectsByName(
  374. this.userId,
  375. this.project1.name,
  376. (err, projects) => {
  377. if (err != null) {
  378. return done(err)
  379. }
  380. const projectNames = projects.map(project => project.name)
  381. expect(projectNames).to.have.members([
  382. this.project1.name,
  383. this.project3.name,
  384. this.project4.name,
  385. ])
  386. done()
  387. }
  388. )
  389. })
  390. it('should search collaborations as well', function (done) {
  391. this.project1 = { _id: 1, name: 'find me!' }
  392. this.project2 = { _id: 2, name: 'FIND ME!' }
  393. this.project3 = { _id: 3, name: 'Find Me!' }
  394. this.project4 = { _id: 4, name: 'find ME!' }
  395. this.project5 = { _id: 5, name: 'FIND me!' }
  396. this.Project.find
  397. .withArgs({ owner_ref: this.userId })
  398. .yields(null, [this.project1])
  399. this.CollaboratorsGetter.getProjectsUserIsMemberOf.yields(null, {
  400. readAndWrite: [this.project2],
  401. readOnly: [this.project3],
  402. tokenReadAndWrite: [this.project4],
  403. tokenReadOnly: [this.project5],
  404. })
  405. this.ProjectGetter.findUsersProjectsByName(
  406. this.userId,
  407. this.project1.name,
  408. (err, projects) => {
  409. if (err != null) {
  410. return done(err)
  411. }
  412. expect(projects.map(project => project.name)).to.have.members([
  413. this.project1.name,
  414. this.project2.name,
  415. ])
  416. done()
  417. }
  418. )
  419. })
  420. })
  421. describe('getUsersDeletedProjects', function () {
  422. it('should look up the deleted projects by deletedProjectOwnerId', function (done) {
  423. this.ProjectGetter.getUsersDeletedProjects('giraffe', err => {
  424. if (err) {
  425. return done(err)
  426. }
  427. sinon.assert.calledWith(this.DeletedProject.find, {
  428. 'deleterData.deletedProjectOwnerId': 'giraffe',
  429. })
  430. done()
  431. })
  432. })
  433. it('should pass the found projects to the callback', function (done) {
  434. this.ProjectGetter.getUsersDeletedProjects('giraffe', (err, docs) => {
  435. if (err) {
  436. return done(err)
  437. }
  438. expect(docs).to.deep.equal([this.deletedProject])
  439. done()
  440. })
  441. })
  442. })
  443. })