FileSystemImportManagerTests.js 9.8 KB

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