ProjectEditorHandlerTests.js 13 KB

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