ApplyingUpdatesToProjectStructureTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. /*
  2. * decaffeinate suggestions:
  3. * DS102: Remove unnecessary code created because of implicit returns
  4. * DS207: Consider shorter variations of null checks
  5. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  6. */
  7. const sinon = require("sinon");
  8. const chai = require("chai");
  9. chai.should();
  10. const Settings = require('settings-sharelatex');
  11. const rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history);
  12. const ProjectHistoryKeys = Settings.redis.project_history.key_schema;
  13. const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
  14. const MockWebApi = require("./helpers/MockWebApi");
  15. const DocUpdaterClient = require("./helpers/DocUpdaterClient");
  16. const DocUpdaterApp = require("./helpers/DocUpdaterApp");
  17. describe("Applying updates to a project's structure", function() {
  18. before(function() {
  19. this.user_id = 'user-id-123';
  20. return this.version = 1234;
  21. });
  22. describe("renaming a file", function() {
  23. before(function(done) {
  24. this.project_id = DocUpdaterClient.randomId();
  25. this.fileUpdate = {
  26. id: DocUpdaterClient.randomId(),
  27. pathname: '/file-path',
  28. newPathname: '/new-file-path'
  29. };
  30. this.fileUpdates = [ this.fileUpdate ];
  31. return DocUpdaterApp.ensureRunning(error => {
  32. if (error != null) { throw error; }
  33. return DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, [], this.fileUpdates, this.version, function(error) {
  34. if (error != null) { throw error; }
  35. return setTimeout(done, 200);
  36. });
  37. });
  38. });
  39. return it("should push the applied file renames to the project history api", function(done) {
  40. rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
  41. if (error != null) { throw error; }
  42. const update = JSON.parse(updates[0]);
  43. update.file.should.equal(this.fileUpdate.id);
  44. update.pathname.should.equal('/file-path');
  45. update.new_pathname.should.equal('/new-file-path');
  46. update.meta.user_id.should.equal(this.user_id);
  47. update.meta.ts.should.be.a('string');
  48. update.version.should.equal(`${this.version}.0`);
  49. return done();
  50. });
  51. return null;
  52. });
  53. });
  54. describe("renaming a document", function() {
  55. before(function() {
  56. this.docUpdate = {
  57. id: DocUpdaterClient.randomId(),
  58. pathname: '/doc-path',
  59. newPathname: '/new-doc-path'
  60. };
  61. return this.docUpdates = [ this.docUpdate ];});
  62. describe("when the document is not loaded", function() {
  63. before(function(done) {
  64. this.project_id = DocUpdaterClient.randomId();
  65. DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
  66. if (error != null) { throw error; }
  67. return setTimeout(done, 200);
  68. });
  69. return null;
  70. });
  71. return it("should push the applied doc renames to the project history api", function(done) {
  72. rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
  73. if (error != null) { throw error; }
  74. const update = JSON.parse(updates[0]);
  75. update.doc.should.equal(this.docUpdate.id);
  76. update.pathname.should.equal('/doc-path');
  77. update.new_pathname.should.equal('/new-doc-path');
  78. update.meta.user_id.should.equal(this.user_id);
  79. update.meta.ts.should.be.a('string');
  80. update.version.should.equal(`${this.version}.0`);
  81. return done();
  82. });
  83. return null;
  84. });
  85. });
  86. return describe("when the document is loaded", function() {
  87. before(function(done) {
  88. this.project_id = DocUpdaterClient.randomId();
  89. MockWebApi.insertDoc(this.project_id, this.docUpdate.id, {});
  90. DocUpdaterClient.preloadDoc(this.project_id, this.docUpdate.id, error => {
  91. if (error != null) { throw error; }
  92. sinon.spy(MockWebApi, "getDocument");
  93. return DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
  94. if (error != null) { throw error; }
  95. return setTimeout(done, 200);
  96. });
  97. });
  98. return null;
  99. });
  100. after(() => MockWebApi.getDocument.restore());
  101. it("should update the doc", function(done) {
  102. DocUpdaterClient.getDoc(this.project_id, this.docUpdate.id, (error, res, doc) => {
  103. doc.pathname.should.equal(this.docUpdate.newPathname);
  104. return done();
  105. });
  106. return null;
  107. });
  108. return it("should push the applied doc renames to the project history api", function(done) {
  109. rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
  110. if (error != null) { throw error; }
  111. const update = JSON.parse(updates[0]);
  112. update.doc.should.equal(this.docUpdate.id);
  113. update.pathname.should.equal('/doc-path');
  114. update.new_pathname.should.equal('/new-doc-path');
  115. update.meta.user_id.should.equal(this.user_id);
  116. update.meta.ts.should.be.a('string');
  117. update.version.should.equal(`${this.version}.0`);
  118. return done();
  119. });
  120. return null;
  121. });
  122. });
  123. });
  124. describe("renaming multiple documents and files", function() {
  125. before(function() {
  126. this.docUpdate0 = {
  127. id: DocUpdaterClient.randomId(),
  128. pathname: '/doc-path0',
  129. newPathname: '/new-doc-path0'
  130. };
  131. this.docUpdate1 = {
  132. id: DocUpdaterClient.randomId(),
  133. pathname: '/doc-path1',
  134. newPathname: '/new-doc-path1'
  135. };
  136. this.docUpdates = [ this.docUpdate0, this.docUpdate1 ];
  137. this.fileUpdate0 = {
  138. id: DocUpdaterClient.randomId(),
  139. pathname: '/file-path0',
  140. newPathname: '/new-file-path0'
  141. };
  142. this.fileUpdate1 = {
  143. id: DocUpdaterClient.randomId(),
  144. pathname: '/file-path1',
  145. newPathname: '/new-file-path1'
  146. };
  147. return this.fileUpdates = [ this.fileUpdate0, this.fileUpdate1 ];});
  148. return describe("when the documents are not loaded", function() {
  149. before(function(done) {
  150. this.project_id = DocUpdaterClient.randomId();
  151. DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, this.fileUpdates, this.version, function(error) {
  152. if (error != null) { throw error; }
  153. return setTimeout(done, 200);
  154. });
  155. return null;
  156. });
  157. return it("should push the applied doc renames to the project history api", function(done) {
  158. rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
  159. if (error != null) { throw error; }
  160. let update = JSON.parse(updates[0]);
  161. update.doc.should.equal(this.docUpdate0.id);
  162. update.pathname.should.equal('/doc-path0');
  163. update.new_pathname.should.equal('/new-doc-path0');
  164. update.meta.user_id.should.equal(this.user_id);
  165. update.meta.ts.should.be.a('string');
  166. update.version.should.equal(`${this.version}.0`);
  167. update = JSON.parse(updates[1]);
  168. update.doc.should.equal(this.docUpdate1.id);
  169. update.pathname.should.equal('/doc-path1');
  170. update.new_pathname.should.equal('/new-doc-path1');
  171. update.meta.user_id.should.equal(this.user_id);
  172. update.meta.ts.should.be.a('string');
  173. update.version.should.equal(`${this.version}.1`);
  174. update = JSON.parse(updates[2]);
  175. update.file.should.equal(this.fileUpdate0.id);
  176. update.pathname.should.equal('/file-path0');
  177. update.new_pathname.should.equal('/new-file-path0');
  178. update.meta.user_id.should.equal(this.user_id);
  179. update.meta.ts.should.be.a('string');
  180. update.version.should.equal(`${this.version}.2`);
  181. update = JSON.parse(updates[3]);
  182. update.file.should.equal(this.fileUpdate1.id);
  183. update.pathname.should.equal('/file-path1');
  184. update.new_pathname.should.equal('/new-file-path1');
  185. update.meta.user_id.should.equal(this.user_id);
  186. update.meta.ts.should.be.a('string');
  187. update.version.should.equal(`${this.version}.3`);
  188. return done();
  189. });
  190. return null;
  191. });
  192. });
  193. });
  194. describe("adding a file", function() {
  195. before(function(done) {
  196. this.project_id = DocUpdaterClient.randomId();
  197. this.fileUpdate = {
  198. id: DocUpdaterClient.randomId(),
  199. pathname: '/file-path',
  200. url: 'filestore.example.com'
  201. };
  202. this.fileUpdates = [ this.fileUpdate ];
  203. DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, [], this.fileUpdates, this.version, function(error) {
  204. if (error != null) { throw error; }
  205. return setTimeout(done, 200);
  206. });
  207. return null;
  208. });
  209. return it("should push the file addition to the project history api", function(done) {
  210. rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
  211. if (error != null) { throw error; }
  212. const update = JSON.parse(updates[0]);
  213. update.file.should.equal(this.fileUpdate.id);
  214. update.pathname.should.equal('/file-path');
  215. update.url.should.equal('filestore.example.com');
  216. update.meta.user_id.should.equal(this.user_id);
  217. update.meta.ts.should.be.a('string');
  218. update.version.should.equal(`${this.version}.0`);
  219. return done();
  220. });
  221. return null;
  222. });
  223. });
  224. describe("adding a doc", function() {
  225. before(function(done) {
  226. this.project_id = DocUpdaterClient.randomId();
  227. this.docUpdate = {
  228. id: DocUpdaterClient.randomId(),
  229. pathname: '/file-path',
  230. docLines: 'a\nb'
  231. };
  232. this.docUpdates = [ this.docUpdate ];
  233. DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
  234. if (error != null) { throw error; }
  235. return setTimeout(done, 200);
  236. });
  237. return null;
  238. });
  239. return it("should push the doc addition to the project history api", function(done) {
  240. rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
  241. if (error != null) { throw error; }
  242. const update = JSON.parse(updates[0]);
  243. update.doc.should.equal(this.docUpdate.id);
  244. update.pathname.should.equal('/file-path');
  245. update.docLines.should.equal('a\nb');
  246. update.meta.user_id.should.equal(this.user_id);
  247. update.meta.ts.should.be.a('string');
  248. update.version.should.equal(`${this.version}.0`);
  249. return done();
  250. });
  251. return null;
  252. });
  253. });
  254. describe("with enough updates to flush to the history service", function() {
  255. before(function(done) {
  256. this.project_id = DocUpdaterClient.randomId();
  257. this.user_id = DocUpdaterClient.randomId();
  258. this.version0 = 12345;
  259. this.version1 = this.version0 + 1;
  260. const updates = [];
  261. for (let v = 0; v <= 599; v++) { // Should flush after 500 ops
  262. updates.push({
  263. id: DocUpdaterClient.randomId(),
  264. pathname: '/file-' + v,
  265. docLines: 'a\nb'
  266. });
  267. }
  268. sinon.spy(MockProjectHistoryApi, "flushProject");
  269. // Send updates in chunks to causes multiple flushes
  270. const projectId = this.project_id;
  271. const userId = this.project_id;
  272. DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(0, 250), [], this.version0, function(error) {
  273. if (error != null) { throw error; }
  274. return DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(250), [], this.version1, function(error) {
  275. if (error != null) { throw error; }
  276. return setTimeout(done, 2000);
  277. });
  278. });
  279. return null;
  280. });
  281. after(() => MockProjectHistoryApi.flushProject.restore());
  282. return it("should flush project history", function() {
  283. return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
  284. });
  285. });
  286. return describe("with too few updates to flush to the history service", function() {
  287. before(function(done) {
  288. this.project_id = DocUpdaterClient.randomId();
  289. this.user_id = DocUpdaterClient.randomId();
  290. this.version0 = 12345;
  291. this.version1 = this.version0 + 1;
  292. const updates = [];
  293. for (let v = 0; v <= 42; v++) { // Should flush after 500 ops
  294. updates.push({
  295. id: DocUpdaterClient.randomId(),
  296. pathname: '/file-' + v,
  297. docLines: 'a\nb'
  298. });
  299. }
  300. sinon.spy(MockProjectHistoryApi, "flushProject");
  301. // Send updates in chunks
  302. const projectId = this.project_id;
  303. const userId = this.project_id;
  304. DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(0, 10), [], this.version0, function(error) {
  305. if (error != null) { throw error; }
  306. return DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(10), [], this.version1, function(error) {
  307. if (error != null) { throw error; }
  308. return setTimeout(done, 2000);
  309. });
  310. });
  311. return null;
  312. });
  313. after(() => MockProjectHistoryApi.flushProject.restore());
  314. return it("should not flush project history", function() {
  315. return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(false);
  316. });
  317. });
  318. });