TpdsUpdateSenderTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. const { ObjectId } = require('mongodb')
  2. const SandboxedModule = require('sandboxed-module')
  3. const path = require('path')
  4. const sinon = require('sinon')
  5. const { expect } = require('chai')
  6. const modulePath = path.join(
  7. __dirname,
  8. '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js'
  9. )
  10. const projectId = 'project_id_here'
  11. const userId = ObjectId()
  12. const readOnlyRef = ObjectId()
  13. const collaberatorRef = ObjectId()
  14. const projectName = 'project_name_here'
  15. const thirdPartyDataStoreApiUrl = 'http://third-party-json-store.herokuapp.com'
  16. const siteUrl = 'http://www.localhost:3000'
  17. const filestoreUrl = 'filestore.sharelatex.com'
  18. const projectArchiverUrl = 'project-archiver.overleaf.com'
  19. describe('TpdsUpdateSender', function () {
  20. beforeEach(function () {
  21. this.fakeUser = {
  22. _id: '12390i',
  23. }
  24. this.memberIds = [userId, collaberatorRef, readOnlyRef]
  25. this.CollaboratorsGetter = {
  26. promises: {
  27. getInvitedMemberIds: sinon.stub().resolves(this.memberIds),
  28. },
  29. }
  30. this.docstoreUrl = 'docstore.sharelatex.env'
  31. this.response = {
  32. ok: true,
  33. json: sinon.stub(),
  34. }
  35. this.fetch = sinon.stub().resolves(this.response)
  36. this.settings = {
  37. siteUrl,
  38. apis: {
  39. thirdPartyDataStore: { url: thirdPartyDataStoreApiUrl },
  40. filestore: {
  41. url: filestoreUrl,
  42. },
  43. docstore: {
  44. pubUrl: this.docstoreUrl,
  45. },
  46. },
  47. }
  48. const getUsers = sinon.stub()
  49. getUsers
  50. .withArgs({
  51. _id: {
  52. $in: this.memberIds,
  53. },
  54. 'dropbox.access_token.uid': { $ne: null },
  55. })
  56. .resolves(
  57. this.memberIds.map(userId => {
  58. return { _id: userId }
  59. })
  60. )
  61. this.UserGetter = {
  62. promises: { getUsers },
  63. }
  64. this.TpdsUpdateSender = SandboxedModule.require(modulePath, {
  65. requires: {
  66. mongodb: { ObjectId },
  67. '@overleaf/settings': this.settings,
  68. 'node-fetch': this.fetch,
  69. '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
  70. '../User/UserGetter.js': this.UserGetter,
  71. '@overleaf/metrics': {
  72. inc() {},
  73. },
  74. },
  75. })
  76. })
  77. describe('enqueue', function () {
  78. it('should not call request if there is no tpdsworker url', async function () {
  79. await this.TpdsUpdateSender.promises.enqueue(null, null, null)
  80. this.fetch.should.not.have.been.called
  81. })
  82. it('should post the message to the tpdsworker', async function () {
  83. this.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
  84. const group0 = 'myproject'
  85. const method0 = 'somemethod0'
  86. const job0 = 'do something'
  87. await this.TpdsUpdateSender.promises.enqueue(group0, method0, job0)
  88. this.fetch.should.have.been.calledWith(
  89. new URL('http://tpdsworker/enqueue/web_to_tpds_http_requests'),
  90. sinon.match({ method: 'POST' })
  91. )
  92. const opts = this.fetch.firstCall.args[1]
  93. const body = JSON.parse(opts.body)
  94. body.group.should.equal(group0)
  95. body.job.should.equal(job0)
  96. body.method.should.equal(method0)
  97. })
  98. })
  99. describe('sending updates', function () {
  100. beforeEach(function () {
  101. this.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
  102. })
  103. it('queues a post the file with user and file id', async function () {
  104. const fileId = '4545345'
  105. const path = '/some/path/here.jpg'
  106. await this.TpdsUpdateSender.promises.addFile({
  107. projectId,
  108. fileId,
  109. path,
  110. projectName,
  111. })
  112. const {
  113. group: group0,
  114. job: job0,
  115. method: method0,
  116. } = JSON.parse(this.fetch.firstCall.args[1].body)
  117. group0.should.equal(userId.toString())
  118. method0.should.equal('pipeStreamFrom')
  119. job0.method.should.equal('post')
  120. job0.streamOrigin.should.equal(
  121. `${filestoreUrl}/project/${projectId}/file/${fileId}`
  122. )
  123. const expectedUrl = `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent(
  124. projectName
  125. )}${encodeURIComponent(path)}`
  126. job0.uri.should.equal(expectedUrl)
  127. job0.headers.sl_all_user_ids.should.equal(JSON.stringify([userId]))
  128. job0.headers.sl_project_owner_user_id.should.equal(userId.toString())
  129. const { group: group1, job: job1 } = JSON.parse(
  130. this.fetch.secondCall.args[1].body
  131. )
  132. group1.should.equal(collaberatorRef.toString())
  133. job1.headers.sl_all_user_ids.should.equal(
  134. JSON.stringify([collaberatorRef])
  135. )
  136. job1.headers.sl_project_owner_user_id.should.equal(userId.toString())
  137. const { group: group2, job: job2 } = JSON.parse(
  138. this.fetch.thirdCall.args[1].body
  139. )
  140. group2.should.equal(readOnlyRef.toString())
  141. job2.headers.sl_all_user_ids.should.equal(JSON.stringify([readOnlyRef]))
  142. job2.headers.sl_project_owner_user_id.should.equal(userId.toString())
  143. })
  144. it('post doc with stream origin of docstore', async function () {
  145. const docId = '4545345'
  146. const path = '/some/path/here.tex'
  147. const lines = ['line1', 'line2', 'line3']
  148. await this.TpdsUpdateSender.promises.addDoc({
  149. projectId,
  150. docId,
  151. path,
  152. docLines: lines,
  153. projectName,
  154. })
  155. const {
  156. group: group0,
  157. job: job0,
  158. method: method0,
  159. } = JSON.parse(this.fetch.firstCall.args[1].body)
  160. group0.should.equal(userId.toString())
  161. method0.should.equal('pipeStreamFrom')
  162. job0.method.should.equal('post')
  163. const expectedUrl = `${thirdPartyDataStoreApiUrl}/user/${userId.toString()}/entity/${encodeURIComponent(
  164. projectName
  165. )}${encodeURIComponent(path)}`
  166. job0.uri.should.equal(expectedUrl)
  167. job0.streamOrigin.should.equal(
  168. `${this.docstoreUrl}/project/${projectId}/doc/${docId}/raw`
  169. )
  170. job0.headers.sl_all_user_ids.should.eql(JSON.stringify([userId]))
  171. const { group: group1, job: job1 } = JSON.parse(
  172. this.fetch.secondCall.args[1].body
  173. )
  174. group1.should.equal(collaberatorRef.toString())
  175. job1.headers.sl_all_user_ids.should.equal(
  176. JSON.stringify([collaberatorRef])
  177. )
  178. const { group: group2, job: job2 } = JSON.parse(
  179. this.fetch.thirdCall.args[1].body
  180. )
  181. group2.should.equal(readOnlyRef.toString())
  182. job2.headers.sl_all_user_ids.should.equal(JSON.stringify([readOnlyRef]))
  183. })
  184. it('deleting entity', async function () {
  185. const path = '/path/here/t.tex'
  186. const subtreeEntityIds = ['id1', 'id2']
  187. await this.TpdsUpdateSender.promises.deleteEntity({
  188. projectId,
  189. path,
  190. projectName,
  191. subtreeEntityIds,
  192. })
  193. const {
  194. group: group0,
  195. job: job0,
  196. method: method0,
  197. } = JSON.parse(this.fetch.firstCall.args[1].body)
  198. group0.should.equal(userId.toString())
  199. method0.should.equal('standardHttpRequest')
  200. job0.method.should.equal('delete')
  201. const expectedUrl = `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent(
  202. projectName
  203. )}${encodeURIComponent(path)}`
  204. job0.headers.sl_all_user_ids.should.eql(JSON.stringify([userId]))
  205. job0.uri.should.equal(expectedUrl)
  206. expect(job0.json).to.deep.equal({ subtreeEntityIds })
  207. const { group: group1, job: job1 } = JSON.parse(
  208. this.fetch.secondCall.args[1].body
  209. )
  210. group1.should.equal(collaberatorRef.toString())
  211. job1.headers.sl_all_user_ids.should.equal(
  212. JSON.stringify([collaberatorRef])
  213. )
  214. const { group: group2, job: job2 } = JSON.parse(
  215. this.fetch.thirdCall.args[1].body
  216. )
  217. group2.should.equal(readOnlyRef.toString())
  218. job2.headers.sl_all_user_ids.should.equal(JSON.stringify([readOnlyRef]))
  219. })
  220. it('moving entity', async function () {
  221. const startPath = 'staring/here/file.tex'
  222. const endPath = 'ending/here/file.tex'
  223. await this.TpdsUpdateSender.promises.moveEntity({
  224. projectId,
  225. startPath,
  226. endPath,
  227. projectName,
  228. })
  229. const {
  230. group: group0,
  231. job: job0,
  232. method: method0,
  233. } = JSON.parse(this.fetch.firstCall.args[1].body)
  234. group0.should.equal(userId.toString())
  235. method0.should.equal('standardHttpRequest')
  236. job0.method.should.equal('put')
  237. job0.uri.should.equal(
  238. `${thirdPartyDataStoreApiUrl}/user/${userId}/entity`
  239. )
  240. job0.json.startPath.should.equal(`/${projectName}/${startPath}`)
  241. job0.json.endPath.should.equal(`/${projectName}/${endPath}`)
  242. job0.headers.sl_all_user_ids.should.eql(JSON.stringify([userId]))
  243. const { group: group1, job: job1 } = JSON.parse(
  244. this.fetch.secondCall.args[1].body
  245. )
  246. group1.should.equal(collaberatorRef.toString())
  247. job1.headers.sl_all_user_ids.should.equal(
  248. JSON.stringify([collaberatorRef])
  249. )
  250. const { group: group2, job: job2 } = JSON.parse(
  251. this.fetch.thirdCall.args[1].body
  252. )
  253. group2.should.equal(readOnlyRef.toString())
  254. job2.headers.sl_all_user_ids.should.equal(JSON.stringify([readOnlyRef]))
  255. })
  256. it('should be able to rename a project using the move entity func', async function () {
  257. const oldProjectName = '/oldProjectName/'
  258. const newProjectName = '/newProjectName/'
  259. await this.TpdsUpdateSender.promises.moveEntity({
  260. projectId,
  261. projectName: oldProjectName,
  262. newProjectName,
  263. })
  264. const {
  265. group: group0,
  266. job: job0,
  267. method: method0,
  268. } = JSON.parse(this.fetch.firstCall.args[1].body)
  269. group0.should.equal(userId.toString())
  270. method0.should.equal('standardHttpRequest')
  271. job0.method.should.equal('put')
  272. job0.uri.should.equal(
  273. `${thirdPartyDataStoreApiUrl}/user/${userId}/entity`
  274. )
  275. job0.json.startPath.should.equal(oldProjectName)
  276. job0.json.endPath.should.equal(newProjectName)
  277. job0.headers.sl_all_user_ids.should.eql(JSON.stringify([userId]))
  278. const { group: group1, job: job1 } = JSON.parse(
  279. this.fetch.secondCall.args[1].body
  280. )
  281. group1.should.equal(collaberatorRef.toString())
  282. job1.headers.sl_all_user_ids.should.equal(
  283. JSON.stringify([collaberatorRef])
  284. )
  285. const { group: group2, job: job2 } = JSON.parse(
  286. this.fetch.thirdCall.args[1].body
  287. )
  288. group2.should.equal(readOnlyRef.toString())
  289. job2.headers.sl_all_user_ids.should.equal(JSON.stringify([readOnlyRef]))
  290. })
  291. it('pollDropboxForUser', async function () {
  292. await this.TpdsUpdateSender.promises.pollDropboxForUser(userId.toString())
  293. const {
  294. group: group0,
  295. job: job0,
  296. method: method0,
  297. } = JSON.parse(this.fetch.firstCall.args[1].body)
  298. group0.should.equal(userId.toString())
  299. method0.should.equal('standardHttpRequest')
  300. job0.method.should.equal('post')
  301. job0.uri.should.equal(`${thirdPartyDataStoreApiUrl}/user/poll`)
  302. job0.json.user_ids[0].should.equal(userId.toString())
  303. })
  304. })
  305. describe('deleteProject', function () {
  306. it('should not call request if there is no project archiver url', async function () {
  307. await this.TpdsUpdateSender.promises.deleteProject({ projectId })
  308. this.fetch.should.not.have.been.called
  309. })
  310. it('should make a delete request to project archiver', async function () {
  311. this.settings.apis.project_archiver = { url: projectArchiverUrl }
  312. await this.TpdsUpdateSender.promises.deleteProject({ projectId })
  313. this.fetch.should.have.been.calledWith(
  314. `${projectArchiverUrl}/project/${projectId}`,
  315. { method: 'DELETE' }
  316. )
  317. })
  318. })
  319. describe('user not linked to dropbox', function () {
  320. beforeEach(function () {
  321. this.UserGetter.promises.getUsers
  322. .withArgs({
  323. _id: {
  324. $in: this.memberIds,
  325. },
  326. 'dropbox.access_token.uid': { $ne: null },
  327. })
  328. .resolves([])
  329. })
  330. })
  331. it('does not make request to tpds', async function () {
  332. const fileId = '4545345'
  333. const path = '/some/path/here.jpg'
  334. await this.TpdsUpdateSender.promises.addFile({
  335. projectId,
  336. fileId,
  337. path,
  338. projectName,
  339. })
  340. this.fetch.should.not.have.been.called
  341. })
  342. })