ProjectGetter.test.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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('getProject', function () {
  82. describe('without projection', function () {
  83. describe('with project id', function () {
  84. beforeEach(async function (ctx) {
  85. await ctx.ProjectGetter.promises.getProject(ctx.projectIdStr)
  86. })
  87. it('should call findOne with the project id', function (ctx) {
  88. expect(ctx.db.projects.findOne.callCount).to.equal(1)
  89. expect(
  90. ctx.db.projects.findOne.lastCall.args[0]._id.toString()
  91. ).to.equal(ctx.projectIdStr)
  92. })
  93. })
  94. describe('without project id', function () {
  95. it('should be rejected', function (ctx) {
  96. expect(
  97. ctx.ProjectGetter.promises.getProject(null)
  98. ).to.be.rejectedWith('no project id provided')
  99. expect(ctx.db.projects.findOne.callCount).to.equal(0)
  100. })
  101. })
  102. })
  103. describe('with projection', function () {
  104. beforeEach(function (ctx) {
  105. ctx.projection = { _id: 1 }
  106. })
  107. describe('with project id', function () {
  108. beforeEach(async function (ctx) {
  109. await ctx.ProjectGetter.promises.getProject(
  110. ctx.projectIdStr,
  111. ctx.projection
  112. )
  113. })
  114. it('should call findOne with the project id', function (ctx) {
  115. expect(ctx.db.projects.findOne.callCount).to.equal(1)
  116. expect(
  117. ctx.db.projects.findOne.lastCall.args[0]._id.toString()
  118. ).to.equal(ctx.projectIdStr)
  119. expect(ctx.db.projects.findOne.lastCall.args[1]).to.deep.equal({
  120. projection: ctx.projection,
  121. })
  122. })
  123. })
  124. describe('without project id', function () {
  125. it('should be rejected', function (ctx) {
  126. expect(
  127. ctx.ProjectGetter.promises.getProject(null)
  128. ).to.be.rejectedWith('no project id provided')
  129. expect(ctx.db.projects.findOne.callCount).to.equal(0)
  130. })
  131. })
  132. })
  133. })
  134. describe('getProjectWithoutLock', function () {
  135. describe('without projection', function () {
  136. describe('with project id', function () {
  137. beforeEach(async function (ctx) {
  138. await ctx.ProjectGetter.promises.getProjectWithoutLock(
  139. ctx.projectIdStr
  140. )
  141. })
  142. it('should call findOne with the project id', function (ctx) {
  143. expect(ctx.db.projects.findOne.callCount).to.equal(1)
  144. expect(
  145. ctx.db.projects.findOne.lastCall.args[0]._id.toString()
  146. ).to.equal(ctx.projectIdStr)
  147. })
  148. })
  149. describe('without project id', function () {
  150. it('should be rejected', function (ctx) {
  151. expect(
  152. ctx.ProjectGetter.promises.getProjectWithoutLock(null)
  153. ).to.be.rejectedWith('no project id provided')
  154. expect(ctx.db.projects.findOne.callCount).to.equal(0)
  155. })
  156. })
  157. })
  158. describe('with projection', function () {
  159. beforeEach(function (ctx) {
  160. ctx.projection = { _id: 1 }
  161. })
  162. describe('with project id', function () {
  163. beforeEach(async function (ctx) {
  164. await ctx.ProjectGetter.promises.getProjectWithoutLock(
  165. ctx.project._id,
  166. ctx.projection
  167. )
  168. })
  169. it('should call findOne with the project id', function (ctx) {
  170. expect(ctx.db.projects.findOne.callCount).to.equal(1)
  171. expect(
  172. ctx.db.projects.findOne.lastCall.args[0]._id.toString()
  173. ).to.equal(ctx.projectIdStr)
  174. expect(ctx.db.projects.findOne.lastCall.args[1]).to.deep.equal({
  175. projection: ctx.projection,
  176. })
  177. })
  178. })
  179. describe('without project id', function () {
  180. it('should be rejected', function (ctx) {
  181. expect(
  182. ctx.ProjectGetter.promises.getProjectWithoutLock(null)
  183. ).to.be.rejectedWith('no project id provided')
  184. expect(ctx.db.projects.findOne.callCount).to.equal(0)
  185. })
  186. })
  187. })
  188. })
  189. describe('findAllUsersProjects', function () {
  190. beforeEach(function (ctx) {
  191. ctx.fields = { mock: 'fields' }
  192. ctx.projectOwned = { _id: 'mock-owned-projects' }
  193. ctx.projectRW = { _id: 'mock-rw-projects' }
  194. ctx.projectReview = { _id: 'mock-review-projects' }
  195. ctx.projectRO = { _id: 'mock-ro-projects' }
  196. ctx.projectTokenRW = { _id: 'mock-token-rw-projects' }
  197. ctx.projectTokenRO = { _id: 'mock-token-ro-projects' }
  198. ctx.Project.find
  199. .withArgs({ owner_ref: ctx.userId }, ctx.fields)
  200. .returns({ exec: sinon.stub().resolves([ctx.projectOwned]) })
  201. })
  202. it('should return a promise with all the projects', async function (ctx) {
  203. ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
  204. readAndWrite: [ctx.projectRW],
  205. readOnly: [ctx.projectRO],
  206. tokenReadAndWrite: [ctx.projectTokenRW],
  207. tokenReadOnly: [ctx.projectTokenRO],
  208. review: [ctx.projectReview],
  209. })
  210. const projects = await ctx.ProjectGetter.promises.findAllUsersProjects(
  211. ctx.userId,
  212. ctx.fields
  213. )
  214. expect(projects).to.deep.equal({
  215. owned: [ctx.projectOwned],
  216. readAndWrite: [ctx.projectRW],
  217. readOnly: [ctx.projectRO],
  218. tokenReadAndWrite: [ctx.projectTokenRW],
  219. tokenReadOnly: [ctx.projectTokenRO],
  220. review: [ctx.projectReview],
  221. })
  222. })
  223. it('should remove duplicate projects', async function (ctx) {
  224. ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
  225. readAndWrite: [ctx.projectRW, ctx.projectOwned],
  226. readOnly: [ctx.projectRO, ctx.projectRW],
  227. tokenReadAndWrite: [ctx.projectTokenRW, ctx.projectRO],
  228. tokenReadOnly: [ctx.projectTokenRW, ctx.projectTokenRO, ctx.projectRO],
  229. review: [ctx.projectReview],
  230. })
  231. const projects = await ctx.ProjectGetter.promises.findAllUsersProjects(
  232. ctx.userId,
  233. ctx.fields
  234. )
  235. expect(projects).to.deep.equal({
  236. owned: [ctx.projectOwned],
  237. readAndWrite: [ctx.projectRW],
  238. readOnly: [ctx.projectRO],
  239. tokenReadAndWrite: [ctx.projectTokenRW],
  240. tokenReadOnly: [ctx.projectTokenRO],
  241. review: [ctx.projectReview],
  242. })
  243. })
  244. })
  245. describe('getProjectIdByReadAndWriteToken', function () {
  246. describe('when project find returns project', function () {
  247. beforeEach(async function (ctx) {
  248. ctx.projectIdFound =
  249. await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
  250. 'token'
  251. )
  252. })
  253. it('should find project with token', function (ctx) {
  254. ctx.Project.findOne
  255. .calledWithMatch({ 'tokens.readAndWrite': 'token' })
  256. .should.equal(true)
  257. })
  258. it('should return the project id', function (ctx) {
  259. expect(ctx.projectIdFound).to.equal(ctx.project._id)
  260. })
  261. })
  262. describe('when project not found', function () {
  263. it('should return undefined', async function (ctx) {
  264. ctx.Project.findOne.returns({ exec: sinon.stub().resolves(null) })
  265. const projectId =
  266. await ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken(
  267. 'token'
  268. )
  269. expect(projectId).to.equal(undefined)
  270. })
  271. })
  272. describe('when project find returns error', function () {
  273. beforeEach(async function (ctx) {
  274. ctx.Project.findOne.returns({ exec: sinon.stub().rejects() })
  275. })
  276. it('should be rejected', function (ctx) {
  277. expect(
  278. ctx.ProjectGetter.promises.getProjectIdByReadAndWriteToken('token')
  279. ).to.be.rejected
  280. })
  281. })
  282. })
  283. describe('findUsersProjectsByName', function () {
  284. it('should perform a case-insensitive search', async function (ctx) {
  285. ctx.project1 = { _id: 1, name: 'find me!' }
  286. ctx.project2 = { _id: 2, name: 'not me!' }
  287. ctx.project3 = { _id: 3, name: 'FIND ME!' }
  288. ctx.project4 = { _id: 4, name: 'Find Me!' }
  289. ctx.Project.find.withArgs({ owner_ref: ctx.userId }).returns({
  290. exec: sinon
  291. .stub()
  292. .resolves([ctx.project1, ctx.project2, ctx.project3, ctx.project4]),
  293. })
  294. const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName(
  295. ctx.userId,
  296. ctx.project1.name
  297. )
  298. const projectNames = projects.map(project => project.name)
  299. expect(projectNames).to.have.members([
  300. ctx.project1.name,
  301. ctx.project3.name,
  302. ctx.project4.name,
  303. ])
  304. })
  305. it('should search collaborations as well', async function (ctx) {
  306. ctx.project1 = { _id: 1, name: 'find me!' }
  307. ctx.project2 = { _id: 2, name: 'FIND ME!' }
  308. ctx.project3 = { _id: 3, name: 'Find Me!' }
  309. ctx.project4 = { _id: 4, name: 'find ME!' }
  310. ctx.project5 = { _id: 5, name: 'FIND me!' }
  311. ctx.Project.find
  312. .withArgs({ owner_ref: ctx.userId })
  313. .returns({ exec: sinon.stub().resolves([ctx.project1]) })
  314. ctx.CollaboratorsGetter.promises.getProjectsUserIsMemberOf.resolves({
  315. readAndWrite: [ctx.project2],
  316. readOnly: [ctx.project3],
  317. tokenReadAndWrite: [ctx.project4],
  318. tokenReadOnly: [ctx.project5],
  319. })
  320. const projects = await ctx.ProjectGetter.promises.findUsersProjectsByName(
  321. ctx.userId,
  322. ctx.project1.name
  323. )
  324. expect(projects.map(project => project.name)).to.have.members([
  325. ctx.project1.name,
  326. ctx.project2.name,
  327. ])
  328. })
  329. })
  330. describe('getUsersDeletedProjects', function () {
  331. it('should look up the deleted projects by deletedProjectOwnerId', async function (ctx) {
  332. await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
  333. sinon.assert.calledWith(ctx.DeletedProject.find, {
  334. 'deleterData.deletedProjectOwnerId': 'giraffe',
  335. })
  336. })
  337. it('should pass the found projects to the callback', async function (ctx) {
  338. const docs =
  339. await ctx.ProjectGetter.promises.getUsersDeletedProjects('giraffe')
  340. expect(docs).to.deep.equal([ctx.deletedProject])
  341. })
  342. })
  343. describe('findAllDebugProjects', function () {
  344. it('should find all projects with overleaf.isDebugCopyOf of type objectId', async function (ctx) {
  345. await ctx.ProjectGetter.promises.findAllDebugProjects('fields')
  346. sinon.assert.calledWith(ctx.Project.find, {
  347. 'overleaf.isDebugCopyOf': { $type: 'objectId' },
  348. })
  349. sinon.assert.calledWith(ctx.Project.find().populate, 'owner_ref', [
  350. 'email',
  351. 'name',
  352. ])
  353. sinon.assert.calledOnce(ctx.Project.find().exec)
  354. })
  355. })
  356. describe('existUsersDebugProjectsOlderThan', function () {
  357. it('should check for existence of debug projects older than given days', async function (ctx) {
  358. const days = 10
  359. const cutoffDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
  360. const exists =
  361. await ctx.ProjectGetter.promises.existUsersDebugProjectsOlderThan(
  362. ctx.userId,
  363. days
  364. )
  365. sinon.assert.calledWith(ctx.Project.exists, {
  366. owner_ref: ctx.userId,
  367. 'overleaf.isDebugCopyOf': { $type: 'objectId' },
  368. lastUpdated: { $lt: cutoffDate },
  369. })
  370. expect(exists).to.equal(true)
  371. })
  372. })
  373. })