ProjectGetter.test.mjs 16 KB

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