FileSystemImportManagerTests.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const mockFs = require('mock-fs')
  4. const SandboxedModule = require('sandboxed-module')
  5. const { ObjectId } = require('mongodb-legacy')
  6. const Settings = require('@overleaf/settings')
  7. const MODULE_PATH =
  8. '../../../../app/src/Features/Uploads/FileSystemImportManager.js'
  9. describe('FileSystemImportManager', function () {
  10. beforeEach(function () {
  11. this.projectId = new ObjectId()
  12. this.folderId = new ObjectId()
  13. this.newFolderId = new ObjectId()
  14. this.userId = new ObjectId()
  15. this.EditorController = {
  16. promises: {
  17. addDoc: sinon.stub().resolves(),
  18. addFile: sinon.stub().resolves(),
  19. upsertDoc: sinon.stub().resolves(),
  20. upsertFile: sinon.stub().resolves(),
  21. addFolder: sinon.stub().resolves({ _id: this.newFolderId }),
  22. },
  23. }
  24. this.FileSystemImportManager = SandboxedModule.require(MODULE_PATH, {
  25. requires: {
  26. '@overleaf/settings': {
  27. textExtensions: ['tex', 'txt'],
  28. editableFilenames: [
  29. 'latexmkrc',
  30. '.latexmkrc',
  31. 'makefile',
  32. 'gnumakefile',
  33. ],
  34. fileIgnorePattern: Settings.fileIgnorePattern, // use the real pattern from the default settings
  35. },
  36. '../Editor/EditorController': this.EditorController,
  37. },
  38. })
  39. })
  40. describe('importDir', function () {
  41. beforeEach(async function () {
  42. mockFs({
  43. 'import-test': {
  44. 'main.tex': 'My thesis',
  45. 'link-to-main.tex': mockFs.symlink({ path: 'import-test/main.tex' }),
  46. '.DS_Store': 'Should be ignored',
  47. images: {
  48. 'cat.jpg': Buffer.from([1, 2, 3, 4]),
  49. },
  50. 'line-endings': {
  51. 'unix.txt': 'one\ntwo\nthree',
  52. 'mac.txt': 'uno\rdos\rtres',
  53. 'windows.txt': 'ein\r\nzwei\r\ndrei',
  54. 'mixed.txt': 'uno\rdue\r\ntre\nquattro',
  55. },
  56. encodings: {
  57. 'utf16le.txt': Buffer.from('\ufeffétonnant!', 'utf16le'),
  58. 'latin1.txt': Buffer.from('tétanisant!', 'latin1'),
  59. },
  60. },
  61. symlink: mockFs.symlink({ path: 'import-test' }),
  62. })
  63. this.entries =
  64. await this.FileSystemImportManager.promises.importDir('import-test')
  65. this.projectPaths = this.entries.map(x => x.projectPath)
  66. })
  67. afterEach(function () {
  68. mockFs.restore()
  69. })
  70. it('should import regular docs', function () {
  71. expect(this.entries).to.deep.include({
  72. type: 'doc',
  73. projectPath: '/main.tex',
  74. lines: ['My thesis'],
  75. })
  76. })
  77. it('should skip symlinks inside the import folder', function () {
  78. expect(this.projectPaths).not.to.include('/link-to-main.tex')
  79. })
  80. it('should skip ignored files', function () {
  81. expect(this.projectPaths).not.to.include('/.DS_Store')
  82. })
  83. it('should import binary files', function () {
  84. expect(this.entries).to.deep.include({
  85. type: 'file',
  86. projectPath: '/images/cat.jpg',
  87. fsPath: 'import-test/images/cat.jpg',
  88. })
  89. })
  90. it('should deal with Mac/Windows/Unix line endings', function () {
  91. expect(this.entries).to.deep.include({
  92. type: 'doc',
  93. projectPath: '/line-endings/unix.txt',
  94. lines: ['one', 'two', 'three'],
  95. })
  96. expect(this.entries).to.deep.include({
  97. type: 'doc',
  98. projectPath: '/line-endings/mac.txt',
  99. lines: ['uno', 'dos', 'tres'],
  100. })
  101. expect(this.entries).to.deep.include({
  102. type: 'doc',
  103. projectPath: '/line-endings/windows.txt',
  104. lines: ['ein', 'zwei', 'drei'],
  105. })
  106. expect(this.entries).to.deep.include({
  107. type: 'doc',
  108. projectPath: '/line-endings/mixed.txt',
  109. lines: ['uno', 'due', 'tre', 'quattro'],
  110. })
  111. })
  112. it('should import documents with latin1 encoding', function () {
  113. expect(this.entries).to.deep.include({
  114. type: 'doc',
  115. projectPath: '/encodings/latin1.txt',
  116. lines: ['tétanisant!'],
  117. })
  118. })
  119. it('should import documents with utf16-le encoding', function () {
  120. expect(this.entries).to.deep.include({
  121. type: 'doc',
  122. projectPath: '/encodings/utf16le.txt',
  123. lines: ['\ufeffétonnant!'],
  124. })
  125. })
  126. it('should error when the root folder is a symlink', async function () {
  127. await expect(this.FileSystemImportManager.promises.importDir('symlink'))
  128. .to.be.rejected
  129. })
  130. })
  131. describe('addEntity', function () {
  132. describe('with directory', function () {
  133. beforeEach(async function () {
  134. mockFs({
  135. path: {
  136. to: {
  137. folder: {
  138. 'doc.tex': 'one\ntwo\nthree',
  139. 'image.jpg': Buffer.from([1, 2, 3, 4]),
  140. },
  141. },
  142. },
  143. })
  144. await this.FileSystemImportManager.promises.addEntity(
  145. this.userId,
  146. this.projectId,
  147. this.folderId,
  148. 'folder',
  149. 'path/to/folder',
  150. false
  151. )
  152. })
  153. afterEach(function () {
  154. mockFs.restore()
  155. })
  156. it('should add a folder to the project', function () {
  157. this.EditorController.promises.addFolder.should.have.been.calledWith(
  158. this.projectId,
  159. this.folderId,
  160. 'folder',
  161. 'upload'
  162. )
  163. })
  164. it("should add the folder's contents", function () {
  165. this.EditorController.promises.addDoc.should.have.been.calledWith(
  166. this.projectId,
  167. this.newFolderId,
  168. 'doc.tex',
  169. ['one', 'two', 'three'],
  170. 'upload',
  171. this.userId
  172. )
  173. this.EditorController.promises.addFile.should.have.been.calledWith(
  174. this.projectId,
  175. this.newFolderId,
  176. 'image.jpg',
  177. 'path/to/folder/image.jpg',
  178. null,
  179. 'upload',
  180. this.userId
  181. )
  182. })
  183. })
  184. describe('with binary file', function () {
  185. beforeEach(function () {
  186. mockFs({ 'uploaded-file': Buffer.from([1, 2, 3, 4]) })
  187. })
  188. afterEach(function () {
  189. mockFs.restore()
  190. })
  191. describe('with replace set to false', function () {
  192. beforeEach(async function () {
  193. await this.FileSystemImportManager.promises.addEntity(
  194. this.userId,
  195. this.projectId,
  196. this.folderId,
  197. 'image.jpg',
  198. 'uploaded-file',
  199. false
  200. )
  201. })
  202. it('should add the file', function () {
  203. this.EditorController.promises.addFile.should.have.been.calledWith(
  204. this.projectId,
  205. this.folderId,
  206. 'image.jpg',
  207. 'uploaded-file',
  208. null,
  209. 'upload',
  210. this.userId
  211. )
  212. })
  213. })
  214. describe('with replace set to true', function () {
  215. beforeEach(async function () {
  216. await this.FileSystemImportManager.promises.addEntity(
  217. this.userId,
  218. this.projectId,
  219. this.folderId,
  220. 'image.jpg',
  221. 'uploaded-file',
  222. true
  223. )
  224. })
  225. it('should add the file', function () {
  226. this.EditorController.promises.upsertFile.should.have.been.calledWith(
  227. this.projectId,
  228. this.folderId,
  229. 'image.jpg',
  230. 'uploaded-file',
  231. null,
  232. 'upload',
  233. this.userId
  234. )
  235. })
  236. })
  237. })
  238. for (const [lineEndingDescription, lineEnding] of [
  239. ['Unix', '\n'],
  240. ['Mac', '\r'],
  241. ['Windows', '\r\n'],
  242. ]) {
  243. describe(`with text file (${lineEndingDescription} line endings)`, function () {
  244. beforeEach(function () {
  245. mockFs({
  246. path: {
  247. to: { 'uploaded-file': `one${lineEnding}two${lineEnding}three` },
  248. },
  249. })
  250. })
  251. afterEach(function () {
  252. mockFs.restore()
  253. })
  254. describe('with replace set to false', function () {
  255. beforeEach(async function () {
  256. await this.FileSystemImportManager.promises.addEntity(
  257. this.userId,
  258. this.projectId,
  259. this.folderId,
  260. 'doc.tex',
  261. 'path/to/uploaded-file',
  262. false
  263. )
  264. })
  265. it('should insert the doc', function () {
  266. this.EditorController.promises.addDoc.should.have.been.calledWith(
  267. this.projectId,
  268. this.folderId,
  269. 'doc.tex',
  270. ['one', 'two', 'three'],
  271. 'upload',
  272. this.userId
  273. )
  274. })
  275. })
  276. describe('with replace set to true', function () {
  277. beforeEach(async function () {
  278. await this.FileSystemImportManager.promises.addEntity(
  279. this.userId,
  280. this.projectId,
  281. this.folderId,
  282. 'doc.tex',
  283. 'path/to/uploaded-file',
  284. true
  285. )
  286. })
  287. it('should upsert the doc', function () {
  288. this.EditorController.promises.upsertDoc.should.have.been.calledWith(
  289. this.projectId,
  290. this.folderId,
  291. 'doc.tex',
  292. ['one', 'two', 'three'],
  293. 'upload',
  294. this.userId
  295. )
  296. })
  297. })
  298. })
  299. }
  300. describe('with symlink', function () {
  301. beforeEach(function () {
  302. mockFs({
  303. path: { to: { symlink: mockFs.symlink({ path: '/etc/passwd' }) } },
  304. })
  305. })
  306. afterEach(function () {
  307. mockFs.restore()
  308. })
  309. it('should stop with an error', async function () {
  310. await expect(
  311. this.FileSystemImportManager.promises.addEntity(
  312. this.userId,
  313. this.projectId,
  314. this.folderId,
  315. 'main.tex',
  316. 'path/to/symlink',
  317. false
  318. )
  319. ).to.be.rejectedWith('path is symlink')
  320. this.EditorController.promises.addFolder.should.not.have.been.called
  321. this.EditorController.promises.addDoc.should.not.have.been.called
  322. this.EditorController.promises.addFile.should.not.have.been.called
  323. })
  324. })
  325. })
  326. })