TpdsUpdateSender.test.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import { beforeEach, describe, expect, it, vi } from 'vitest'
  2. import mongodb from 'mongodb-legacy'
  3. import path from 'node:path'
  4. import sinon from 'sinon'
  5. const { ObjectId } = mongodb
  6. const modulePath = path.join(
  7. import.meta.dirname,
  8. '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.mjs'
  9. )
  10. const projectId = 'project_id_here'
  11. const userId = new ObjectId()
  12. const readOnlyRef = new ObjectId()
  13. const collaberatorRef = new ObjectId()
  14. const projectName = 'project_name_here'
  15. const thirdPartyDataStoreApiUrl = 'http://third-party-json-store.herokuapp.com'
  16. const siteUrl = 'http://127.0.0.1:3000'
  17. const filestoreUrl = 'filestore.overleaf.com'
  18. const projectHistoryUrl = 'http://project-history:3054'
  19. describe('TpdsUpdateSender', function () {
  20. beforeEach(async function (ctx) {
  21. ctx.fakeUser = {
  22. _id: '12390i',
  23. }
  24. ctx.memberIds = [userId, collaberatorRef, readOnlyRef]
  25. ctx.enqueueUrl = new URL(
  26. 'http://tpdsworker/enqueue/web_to_tpds_http_requests'
  27. )
  28. ctx.CollaboratorsGetter = {
  29. promises: {
  30. getInvitedMemberIds: sinon.stub().resolves(ctx.memberIds),
  31. },
  32. }
  33. ctx.docstoreUrl = 'docstore.overleaf.env'
  34. ctx.response = {
  35. ok: true,
  36. json: sinon.stub(),
  37. }
  38. ctx.FetchUtils = {
  39. fetchNothing: sinon.stub().resolves(),
  40. }
  41. ctx.settings = {
  42. siteUrl,
  43. apis: {
  44. thirdPartyDataStore: { url: thirdPartyDataStoreApiUrl },
  45. filestore: {
  46. url: filestoreUrl,
  47. },
  48. docstore: {
  49. pubUrl: ctx.docstoreUrl,
  50. },
  51. project_history: {
  52. url: projectHistoryUrl,
  53. },
  54. },
  55. }
  56. const getUsers = sinon.stub()
  57. getUsers
  58. .withArgs({
  59. _id: {
  60. $in: ctx.memberIds,
  61. },
  62. 'dropbox.access_token.uid': { $ne: null },
  63. })
  64. .resolves(
  65. ctx.memberIds.map(userId => {
  66. return { _id: userId }
  67. })
  68. )
  69. ctx.UserGetter = {
  70. promises: { getUsers },
  71. }
  72. vi.doMock('mongodb-legacy', () => ({
  73. default: { ObjectId },
  74. }))
  75. vi.doMock('@overleaf/settings', () => ({
  76. default: ctx.settings,
  77. }))
  78. vi.doMock('@overleaf/fetch-utils', () => ctx.FetchUtils)
  79. vi.doMock(
  80. '../../../../app/src/Features/Collaborators/CollaboratorsGetter',
  81. () => ({
  82. default: ctx.CollaboratorsGetter,
  83. })
  84. )
  85. vi.doMock('../../../../app/src/Features/User/UserGetter.mjs', () => ({
  86. default: ctx.UserGetter,
  87. }))
  88. vi.doMock('@overleaf/metrics', () => ({
  89. default: {
  90. inc() {},
  91. },
  92. }))
  93. ctx.TpdsUpdateSender = (await import(modulePath)).default
  94. })
  95. describe('enqueue', function () {
  96. it('should not call request if there is no tpdsworker url', async function (ctx) {
  97. await ctx.TpdsUpdateSender.promises.enqueue(null, null, null)
  98. ctx.FetchUtils.fetchNothing.should.not.have.been.called
  99. })
  100. it('should post the message to the tpdsworker', async function (ctx) {
  101. ctx.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
  102. const group0 = 'myproject'
  103. const method0 = 'somemethod0'
  104. const job0 = 'do something'
  105. await ctx.TpdsUpdateSender.promises.enqueue(group0, method0, job0)
  106. ctx.FetchUtils.fetchNothing.should.have.been.calledWithMatch(
  107. ctx.enqueueUrl,
  108. {
  109. method: 'POST',
  110. json: { group: group0, job: job0, method: method0 },
  111. }
  112. )
  113. })
  114. })
  115. describe('sending updates', function () {
  116. beforeEach(function (ctx) {
  117. ctx.settings.apis.tpdsworker = { url: 'http://tpdsworker' }
  118. })
  119. it('queues a post the file with user and file id and hash', async function (ctx) {
  120. const fileId = '4545345'
  121. const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
  122. const historyId = 91525
  123. const path = '/some/path/here.jpg'
  124. await ctx.TpdsUpdateSender.promises.addFile({
  125. projectId,
  126. historyId,
  127. fileId,
  128. hash,
  129. path,
  130. projectName,
  131. })
  132. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  133. ctx.enqueueUrl,
  134. {
  135. json: {
  136. group: userId,
  137. method: 'pipeStreamFrom',
  138. job: {
  139. method: 'post',
  140. streamOrigin: `${projectHistoryUrl}/project/${historyId}/blob/${hash}`,
  141. uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent(
  142. projectName
  143. )}${encodeURIComponent(path)}`,
  144. headers: {},
  145. },
  146. },
  147. }
  148. )
  149. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  150. ctx.enqueueUrl,
  151. {
  152. json: {
  153. group: collaberatorRef,
  154. },
  155. }
  156. )
  157. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  158. ctx.enqueueUrl,
  159. {
  160. json: {
  161. group: readOnlyRef,
  162. job: {},
  163. },
  164. }
  165. )
  166. })
  167. it('post doc with stream origin of docstore', async function (ctx) {
  168. const docId = '4545345'
  169. const path = '/some/path/here.tex'
  170. const lines = ['line1', 'line2', 'line3']
  171. await ctx.TpdsUpdateSender.promises.addDoc({
  172. projectId,
  173. docId,
  174. path,
  175. docLines: lines,
  176. projectName,
  177. })
  178. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  179. ctx.enqueueUrl,
  180. {
  181. json: {
  182. group: userId,
  183. method: 'pipeStreamFrom',
  184. job: {
  185. method: 'post',
  186. uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent(
  187. projectName
  188. )}${encodeURIComponent(path)}`,
  189. streamOrigin: `${ctx.docstoreUrl}/project/${projectId}/doc/${docId}/raw`,
  190. headers: {},
  191. },
  192. },
  193. }
  194. )
  195. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  196. ctx.enqueueUrl,
  197. {
  198. json: {
  199. group: collaberatorRef,
  200. job: {
  201. headers: {},
  202. },
  203. },
  204. }
  205. )
  206. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  207. ctx.enqueueUrl,
  208. {
  209. json: {
  210. group: readOnlyRef,
  211. job: {
  212. headers: {},
  213. },
  214. },
  215. }
  216. )
  217. })
  218. it('deleting entity', async function (ctx) {
  219. const path = '/path/here/t.tex'
  220. const subtreeEntityIds = ['id1', 'id2']
  221. await ctx.TpdsUpdateSender.promises.deleteEntity({
  222. projectId,
  223. path,
  224. projectName,
  225. subtreeEntityIds,
  226. })
  227. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  228. ctx.enqueueUrl,
  229. {
  230. json: {
  231. group: userId,
  232. method: 'standardHttpRequest',
  233. job: {
  234. method: 'delete',
  235. uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity/${encodeURIComponent(
  236. projectName
  237. )}${encodeURIComponent(path)}`,
  238. headers: {},
  239. json: { subtreeEntityIds },
  240. },
  241. },
  242. }
  243. )
  244. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  245. ctx.enqueueUrl,
  246. {
  247. json: {
  248. group: collaberatorRef,
  249. job: {
  250. headers: {},
  251. },
  252. },
  253. }
  254. )
  255. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  256. ctx.enqueueUrl,
  257. {
  258. json: {
  259. group: readOnlyRef,
  260. job: {
  261. headers: {},
  262. },
  263. },
  264. }
  265. )
  266. })
  267. it('moving entity', async function (ctx) {
  268. const startPath = 'staring/here/file.tex'
  269. const endPath = 'ending/here/file.tex'
  270. await ctx.TpdsUpdateSender.promises.moveEntity({
  271. projectId,
  272. startPath,
  273. endPath,
  274. projectName,
  275. })
  276. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  277. ctx.enqueueUrl,
  278. {
  279. json: {
  280. group: userId,
  281. method: 'standardHttpRequest',
  282. job: {
  283. method: 'put',
  284. uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity`,
  285. json: {
  286. startPath: `/${projectName}/${startPath}`,
  287. endPath: `/${projectName}/${endPath}`,
  288. },
  289. headers: {},
  290. },
  291. },
  292. }
  293. )
  294. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  295. ctx.enqueueUrl,
  296. {
  297. json: {
  298. group: collaberatorRef,
  299. job: {
  300. headers: {},
  301. },
  302. },
  303. }
  304. )
  305. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  306. ctx.enqueueUrl,
  307. {
  308. json: {
  309. group: readOnlyRef,
  310. job: {
  311. headers: {},
  312. },
  313. },
  314. }
  315. )
  316. })
  317. it('should be able to rename a project using the move entity func', async function (ctx) {
  318. const oldProjectName = '/oldProjectName/'
  319. const newProjectName = '/newProjectName/'
  320. await ctx.TpdsUpdateSender.promises.moveEntity({
  321. projectId,
  322. projectName: oldProjectName,
  323. newProjectName,
  324. })
  325. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  326. ctx.enqueueUrl,
  327. {
  328. json: {
  329. group: userId,
  330. method: 'standardHttpRequest',
  331. job: {
  332. method: 'put',
  333. uri: `${thirdPartyDataStoreApiUrl}/user/${userId}/entity`,
  334. json: {
  335. startPath: oldProjectName,
  336. endPath: newProjectName,
  337. },
  338. headers: {},
  339. },
  340. },
  341. }
  342. )
  343. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  344. ctx.enqueueUrl,
  345. {
  346. json: {
  347. group: collaberatorRef,
  348. job: {
  349. headers: {},
  350. },
  351. },
  352. }
  353. )
  354. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  355. ctx.enqueueUrl,
  356. {
  357. json: {
  358. group: readOnlyRef,
  359. job: {
  360. headers: {},
  361. },
  362. },
  363. }
  364. )
  365. })
  366. it('pollDropboxForUser', async function (ctx) {
  367. await ctx.TpdsUpdateSender.promises.pollDropboxForUser(userId)
  368. expect(ctx.FetchUtils.fetchNothing).to.have.been.calledWithMatch(
  369. ctx.enqueueUrl,
  370. {
  371. json: {
  372. group: userId,
  373. method: 'standardHttpRequest',
  374. job: {
  375. method: 'post',
  376. uri: `${thirdPartyDataStoreApiUrl}/user/poll`,
  377. json: {
  378. user_ids: [userId],
  379. },
  380. },
  381. },
  382. }
  383. )
  384. })
  385. })
  386. describe('user not linked to dropbox', function () {
  387. beforeEach(function (ctx) {
  388. ctx.UserGetter.promises.getUsers
  389. .withArgs({
  390. _id: {
  391. $in: ctx.memberIds,
  392. },
  393. 'dropbox.access_token.uid': { $ne: null },
  394. })
  395. .resolves([])
  396. })
  397. it('does not make request to tpds', async function (ctx) {
  398. const fileId = '4545345'
  399. const hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
  400. const historyId = 91525
  401. const path = '/some/path/here.jpg'
  402. await ctx.TpdsUpdateSender.promises.addFile({
  403. projectId,
  404. historyId,
  405. hash,
  406. fileId,
  407. path,
  408. projectName,
  409. })
  410. ctx.FetchUtils.fetchNothing.should.not.have.been.called
  411. })
  412. })
  413. })