ProjectEditorHandler.test.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. import _ from 'lodash'
  2. import { expect } from 'vitest'
  3. const modulePath = '../../../../app/src/Features/Project/ProjectEditorHandler'
  4. describe('ProjectEditorHandler', function () {
  5. beforeEach(async function (ctx) {
  6. ctx.project = {
  7. _id: 'project-id',
  8. owner_ref: 'owner-id',
  9. name: 'Project Name',
  10. rootDoc_id: 'file-id',
  11. publicAccesLevel: 'private',
  12. deletedByExternalDataSource: false,
  13. rootFolder: [
  14. {
  15. _id: 'root-folder-id',
  16. name: '',
  17. docs: [],
  18. fileRefs: [],
  19. folders: [
  20. {
  21. _id: 'sub-folder-id',
  22. name: 'folder',
  23. docs: [
  24. {
  25. _id: 'doc-id',
  26. name: 'main.tex',
  27. lines: (ctx.lines = ['line 1', 'line 2', 'line 3']),
  28. },
  29. ],
  30. fileRefs: [
  31. {
  32. _id: 'file-id',
  33. name: 'image.png',
  34. created: (ctx.created = new Date()),
  35. size: 1234,
  36. },
  37. ],
  38. folders: [],
  39. },
  40. ],
  41. },
  42. ],
  43. }
  44. ctx.ownerMember = {
  45. user: (ctx.owner = {
  46. _id: 'owner-id',
  47. first_name: 'Owner',
  48. last_name: 'Overleaf',
  49. email: 'owner@overleaf.com',
  50. features: {
  51. compileTimeout: 240,
  52. },
  53. }),
  54. privilegeLevel: 'owner',
  55. }
  56. ctx.members = [
  57. {
  58. user: {
  59. _id: 'read-only-id',
  60. first_name: 'Read',
  61. last_name: 'Only',
  62. email: 'read-only@overleaf.com',
  63. },
  64. privilegeLevel: 'readOnly',
  65. },
  66. {
  67. user: {
  68. _id: 'read-write-id',
  69. first_name: 'Read',
  70. last_name: 'Write',
  71. email: 'read-write@overleaf.com',
  72. },
  73. privilegeLevel: 'readAndWrite',
  74. },
  75. ]
  76. ctx.invites = [
  77. {
  78. _id: 'invite_one',
  79. email: 'user-one@example.com',
  80. privileges: 'readOnly',
  81. projectId: ctx.project._id,
  82. token: 'my-secret-token1',
  83. },
  84. {
  85. _id: 'invite_two',
  86. email: 'user-two@example.com',
  87. privileges: 'readOnly',
  88. projectId: ctx.project._id,
  89. token: 'my-secret-token2',
  90. },
  91. ]
  92. ctx.handler = (await import(modulePath)).default
  93. })
  94. describe('buildProjectModelView', function () {
  95. describe('with owner, members and invites included', function () {
  96. beforeEach(function (ctx) {
  97. ctx.result = ctx.handler.buildProjectModelView(
  98. ctx.project,
  99. ctx.ownerMember,
  100. ctx.members,
  101. ctx.invites,
  102. false
  103. )
  104. })
  105. it('should include the id', function (ctx) {
  106. expect(ctx.result._id).to.exist
  107. ctx.result._id.should.equal('project-id')
  108. })
  109. it('should include the name', function (ctx) {
  110. expect(ctx.result.name).to.exist
  111. ctx.result.name.should.equal('Project Name')
  112. })
  113. it('should include the root doc id', function (ctx) {
  114. expect(ctx.result.rootDoc_id).to.exist
  115. ctx.result.rootDoc_id.should.equal('file-id')
  116. })
  117. it('should include the public access level', function (ctx) {
  118. expect(ctx.result.publicAccesLevel).to.exist
  119. ctx.result.publicAccesLevel.should.equal('private')
  120. })
  121. it('should include the owner', function (ctx) {
  122. expect(ctx.result.owner).to.exist
  123. ctx.result.owner._id.should.equal('owner-id')
  124. ctx.result.owner.email.should.equal('owner@overleaf.com')
  125. ctx.result.owner.first_name.should.equal('Owner')
  126. ctx.result.owner.last_name.should.equal('Overleaf')
  127. ctx.result.owner.privileges.should.equal('owner')
  128. })
  129. it('should gather readOnly_refs and collaberators_refs into a list of members', function (ctx) {
  130. const findMember = id => {
  131. for (const member of ctx.result.members) {
  132. if (member._id === id) {
  133. return member
  134. }
  135. }
  136. return null
  137. }
  138. ctx.result.members.length.should.equal(2)
  139. expect(findMember('read-only-id')).to.exist
  140. findMember('read-only-id').privileges.should.equal('readOnly')
  141. findMember('read-only-id').first_name.should.equal('Read')
  142. findMember('read-only-id').last_name.should.equal('Only')
  143. findMember('read-only-id').email.should.equal('read-only@overleaf.com')
  144. expect(findMember('read-write-id')).to.exist
  145. findMember('read-write-id').privileges.should.equal('readAndWrite')
  146. findMember('read-write-id').first_name.should.equal('Read')
  147. findMember('read-write-id').last_name.should.equal('Write')
  148. findMember('read-write-id').email.should.equal(
  149. 'read-write@overleaf.com'
  150. )
  151. })
  152. it('should include folders in the project', function (ctx) {
  153. ctx.result.rootFolder[0]._id.should.equal('root-folder-id')
  154. ctx.result.rootFolder[0].name.should.equal('')
  155. ctx.result.rootFolder[0].folders[0]._id.should.equal('sub-folder-id')
  156. ctx.result.rootFolder[0].folders[0].name.should.equal('folder')
  157. })
  158. it('should not duplicate folder contents', function (ctx) {
  159. ctx.result.rootFolder[0].docs.length.should.equal(0)
  160. ctx.result.rootFolder[0].fileRefs.length.should.equal(0)
  161. })
  162. it('should include files in the project', function (ctx) {
  163. ctx.result.rootFolder[0].folders[0].fileRefs[0]._id.should.equal(
  164. 'file-id'
  165. )
  166. ctx.result.rootFolder[0].folders[0].fileRefs[0].name.should.equal(
  167. 'image.png'
  168. )
  169. ctx.result.rootFolder[0].folders[0].fileRefs[0].created.should.equal(
  170. ctx.created
  171. )
  172. expect(ctx.result.rootFolder[0].folders[0].fileRefs[0].size).not.to
  173. .exist
  174. })
  175. it('should include docs in the project but not the lines', function (ctx) {
  176. ctx.result.rootFolder[0].folders[0].docs[0]._id.should.equal('doc-id')
  177. ctx.result.rootFolder[0].folders[0].docs[0].name.should.equal(
  178. 'main.tex'
  179. )
  180. expect(ctx.result.rootFolder[0].folders[0].docs[0].lines).not.to.exist
  181. })
  182. it('should include invites', function (ctx) {
  183. expect(ctx.result.invites).to.exist
  184. ctx.result.invites.should.deep.equal(
  185. ctx.invites.map(invite =>
  186. _.pick(invite, ['_id', 'email', 'privileges'])
  187. )
  188. )
  189. })
  190. it('invites should not include the token', function (ctx) {
  191. for (const invite of ctx.result.invites) {
  192. expect(invite.token).not.to.exist
  193. }
  194. })
  195. it('should have the correct features', function (ctx) {
  196. expect(ctx.result.features.compileTimeout).to.equal(240)
  197. })
  198. })
  199. describe('with a restricted user', function () {
  200. beforeEach(function (ctx) {
  201. ctx.result = ctx.handler.buildProjectModelView(
  202. ctx.project,
  203. ctx.ownerMember,
  204. [],
  205. [],
  206. true
  207. )
  208. })
  209. it('should include the id', function (ctx) {
  210. expect(ctx.result._id).to.exist
  211. ctx.result._id.should.equal('project-id')
  212. })
  213. it('should include the name', function (ctx) {
  214. expect(ctx.result.name).to.exist
  215. ctx.result.name.should.equal('Project Name')
  216. })
  217. it('should include the root doc id', function (ctx) {
  218. expect(ctx.result.rootDoc_id).to.exist
  219. ctx.result.rootDoc_id.should.equal('file-id')
  220. })
  221. it('should include the public access level', function (ctx) {
  222. expect(ctx.result.publicAccesLevel).to.exist
  223. ctx.result.publicAccesLevel.should.equal('private')
  224. })
  225. it('should hide the owner', function (ctx) {
  226. expect(ctx.result.owner).to.deep.equal({ _id: 'owner-id' })
  227. })
  228. it('should hide members', function (ctx) {
  229. ctx.result.members.length.should.equal(0)
  230. })
  231. it('should include folders in the project', function (ctx) {
  232. ctx.result.rootFolder[0]._id.should.equal('root-folder-id')
  233. ctx.result.rootFolder[0].name.should.equal('')
  234. ctx.result.rootFolder[0].folders[0]._id.should.equal('sub-folder-id')
  235. ctx.result.rootFolder[0].folders[0].name.should.equal('folder')
  236. })
  237. it('should not duplicate folder contents', function (ctx) {
  238. ctx.result.rootFolder[0].docs.length.should.equal(0)
  239. ctx.result.rootFolder[0].fileRefs.length.should.equal(0)
  240. })
  241. it('should include files in the project', function (ctx) {
  242. ctx.result.rootFolder[0].folders[0].fileRefs[0]._id.should.equal(
  243. 'file-id'
  244. )
  245. ctx.result.rootFolder[0].folders[0].fileRefs[0].name.should.equal(
  246. 'image.png'
  247. )
  248. ctx.result.rootFolder[0].folders[0].fileRefs[0].created.should.equal(
  249. ctx.created
  250. )
  251. expect(ctx.result.rootFolder[0].folders[0].fileRefs[0].size).not.to
  252. .exist
  253. })
  254. it('should include docs in the project but not the lines', function (ctx) {
  255. ctx.result.rootFolder[0].folders[0].docs[0]._id.should.equal('doc-id')
  256. ctx.result.rootFolder[0].folders[0].docs[0].name.should.equal(
  257. 'main.tex'
  258. )
  259. expect(ctx.result.rootFolder[0].folders[0].docs[0].lines).not.to.exist
  260. })
  261. it('should hide invites', function (ctx) {
  262. expect(ctx.result.invites).to.have.length(0)
  263. })
  264. it('should have the correct features', function (ctx) {
  265. expect(ctx.result.features.compileTimeout).to.equal(240)
  266. })
  267. })
  268. describe('deletedByExternalDataSource', function () {
  269. it('should set the deletedByExternalDataSource flag to false when it is not there', function (ctx) {
  270. delete ctx.project.deletedByExternalDataSource
  271. const result = ctx.handler.buildProjectModelView(
  272. ctx.project,
  273. ctx.ownerMember,
  274. ctx.members,
  275. [],
  276. false
  277. )
  278. result.deletedByExternalDataSource.should.equal(false)
  279. })
  280. it('should set the deletedByExternalDataSource flag to false when it is false', function (ctx) {
  281. const result = ctx.handler.buildProjectModelView(
  282. ctx.project,
  283. ctx.ownerMember,
  284. ctx.members,
  285. [],
  286. false
  287. )
  288. result.deletedByExternalDataSource.should.equal(false)
  289. })
  290. it('should set the deletedByExternalDataSource flag to true when it is true', function (ctx) {
  291. ctx.project.deletedByExternalDataSource = true
  292. const result = ctx.handler.buildProjectModelView(
  293. ctx.project,
  294. ctx.ownerMember,
  295. ctx.members,
  296. [],
  297. false
  298. )
  299. result.deletedByExternalDataSource.should.equal(true)
  300. })
  301. })
  302. describe('features', function () {
  303. beforeEach(function (ctx) {
  304. ctx.owner.features = {
  305. versioning: true,
  306. collaborators: 3,
  307. compileGroup: 'priority',
  308. compileTimeout: 96,
  309. }
  310. ctx.result = ctx.handler.buildProjectModelView(
  311. ctx.project,
  312. ctx.ownerMember,
  313. ctx.members,
  314. [],
  315. false
  316. )
  317. })
  318. it('should copy the owner features to the project', function (ctx) {
  319. ctx.result.features.versioning.should.equal(
  320. ctx.owner.features.versioning
  321. )
  322. ctx.result.features.collaborators.should.equal(
  323. ctx.owner.features.collaborators
  324. )
  325. ctx.result.features.compileGroup.should.equal(
  326. ctx.owner.features.compileGroup
  327. )
  328. ctx.result.features.compileTimeout.should.equal(
  329. ctx.owner.features.compileTimeout
  330. )
  331. })
  332. })
  333. describe('trackChangesState', function () {
  334. describe('when the owner does not have the trackChanges feature', function () {
  335. beforeEach(function (ctx) {
  336. ctx.owner.features = {
  337. trackChanges: false,
  338. }
  339. ctx.result = ctx.handler.buildProjectModelView(
  340. ctx.project,
  341. ctx.ownerMember,
  342. ctx.members,
  343. [],
  344. false
  345. )
  346. })
  347. it('should not emit trackChangesState', function (ctx) {
  348. expect(ctx.result.trackChangesState).to.not.exist
  349. })
  350. })
  351. describe('when the owner has got the trackChanges feature', function () {
  352. beforeEach(function (ctx) {
  353. ctx.owner.features = {
  354. trackChanges: true,
  355. }
  356. })
  357. function genCase([dbEntry, expected]) {
  358. describe(`when track_changes is ${JSON.stringify(
  359. dbEntry
  360. )}`, function () {
  361. beforeEach(function (ctx) {
  362. ctx.project.track_changes = dbEntry
  363. ctx.result = ctx.handler.buildProjectModelView(
  364. ctx.project,
  365. ctx.ownerMember,
  366. ctx.members,
  367. [],
  368. false
  369. )
  370. })
  371. it(`should set trackChangesState=${expected}`, function (ctx) {
  372. expect(ctx.result.trackChangesState).to.deep.equal(expected)
  373. })
  374. })
  375. }
  376. const CASES = [
  377. [null, false],
  378. [false, false],
  379. [true, true],
  380. [{ someId: true }, { someId: true }],
  381. ]
  382. CASES.map(genCase)
  383. })
  384. })
  385. })
  386. })