ProjectRootDocManager.test.mjs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. import { vi, expect } from 'vitest'
  2. import mongodb from 'mongodb-legacy'
  3. import sinon from 'sinon'
  4. const modulePath =
  5. '../../../../app/src/Features/Project/ProjectRootDocManager.mjs'
  6. const { ObjectId } = mongodb
  7. describe('ProjectRootDocManager', function () {
  8. beforeEach(async function (ctx) {
  9. ctx.project_id = 'project-123'
  10. ctx.docPaths = {}
  11. ctx.docId1 = new ObjectId()
  12. ctx.docId2 = new ObjectId()
  13. ctx.docId3 = new ObjectId()
  14. ctx.docId4 = new ObjectId()
  15. ctx.docPaths[ctx.docId1] = '/chapter1.tex'
  16. ctx.docPaths[ctx.docId2] = '/main.tex'
  17. ctx.docPaths[ctx.docId3] = '/nested/chapter1a.tex'
  18. ctx.docPaths[ctx.docId4] = '/nested/chapter1b.tex'
  19. ctx.sl_req_id = 'sl-req-id-123'
  20. ctx.callback = sinon.stub()
  21. ctx.globbyFiles = ['a.tex', 'b.tex', 'main.tex']
  22. ctx.globby = sinon.stub().resolves(ctx.globbyFiles)
  23. ctx.fs = {
  24. readFile: sinon.stub().callsArgWith(2, new Error('file not found')),
  25. stat: sinon.stub().callsArgWith(1, null, { size: 100 }),
  26. }
  27. vi.doMock(
  28. '../../../../app/src/Features/Project/ProjectEntityHandler',
  29. () => ({
  30. default: (ctx.ProjectEntityHandler = {}),
  31. })
  32. )
  33. vi.doMock(
  34. '../../../../app/src/Features/Project/ProjectEntityUpdateHandler',
  35. () => ({
  36. default: (ctx.ProjectEntityUpdateHandler = {}),
  37. })
  38. )
  39. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  40. default: (ctx.ProjectGetter = {}),
  41. }))
  42. vi.doMock('../../../../app/src/infrastructure/GracefulShutdown', () => ({
  43. BackgroundTaskTracker: class {
  44. add() {}
  45. done() {}
  46. },
  47. }))
  48. vi.doMock('globby', () => ({
  49. default: ctx.globby,
  50. }))
  51. vi.doMock('fs', () => ({
  52. default: ctx.fs,
  53. }))
  54. ctx.ProjectRootDocManager = (await import(modulePath)).default
  55. })
  56. describe('setRootDocAutomatically', function () {
  57. beforeEach(function (ctx) {
  58. ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
  59. ctx.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon
  60. .stub()
  61. .returns(true)
  62. })
  63. describe('when there is a suitable root doc', function () {
  64. beforeEach(async function (ctx) {
  65. ctx.docs = {
  66. '/chapter1.tex': {
  67. _id: ctx.docId1,
  68. lines: [
  69. 'something else',
  70. '\\begin{document}',
  71. 'Hello world',
  72. '\\end{document}',
  73. ],
  74. },
  75. '/main.tex': {
  76. _id: ctx.docId2,
  77. lines: [
  78. 'different line',
  79. '\\documentclass{article}',
  80. '\\input{chapter1}',
  81. ],
  82. },
  83. '/nested/chapter1a.tex': {
  84. _id: ctx.docId3,
  85. lines: ['Hello world'],
  86. },
  87. '/nested/chapter1b.tex': {
  88. _id: ctx.docId4,
  89. lines: ['Hello world'],
  90. },
  91. }
  92. ctx.ProjectEntityHandler.getAllDocs = sinon
  93. .stub()
  94. .callsArgWith(1, null, ctx.docs)
  95. await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
  96. ctx.project_id
  97. )
  98. })
  99. it('should check the docs of the project', function (ctx) {
  100. ctx.ProjectEntityHandler.getAllDocs
  101. .calledWith(ctx.project_id)
  102. .should.equal(true)
  103. })
  104. it('should set the root doc to the doc containing a documentclass', function (ctx) {
  105. ctx.ProjectEntityUpdateHandler.setRootDoc
  106. .calledWith(ctx.project_id, ctx.docId2)
  107. .should.equal(true)
  108. })
  109. })
  110. describe('when the root doc is an Rtex file', function () {
  111. beforeEach(async function (ctx) {
  112. ctx.docs = {
  113. '/chapter1.tex': {
  114. _id: ctx.docId1,
  115. lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
  116. },
  117. '/main.Rtex': {
  118. _id: ctx.docId2,
  119. lines: ['\\documentclass{article}', '\\input{chapter1}'],
  120. },
  121. }
  122. ctx.ProjectEntityHandler.getAllDocs = sinon
  123. .stub()
  124. .callsArgWith(1, null, ctx.docs)
  125. await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
  126. ctx.project_id
  127. )
  128. })
  129. it('should set the root doc to the doc containing a documentclass', function (ctx) {
  130. ctx.ProjectEntityUpdateHandler.setRootDoc
  131. .calledWith(ctx.project_id, ctx.docId2)
  132. .should.equal(true)
  133. })
  134. })
  135. describe('when there is no suitable root doc', function () {
  136. beforeEach(async function (ctx) {
  137. ctx.docs = {
  138. '/chapter1.tex': {
  139. _id: ctx.docId1,
  140. lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
  141. },
  142. '/style.bst': {
  143. _id: ctx.docId2,
  144. lines: ['%Example: \\documentclass{article}'],
  145. },
  146. }
  147. ctx.ProjectEntityHandler.getAllDocs = sinon
  148. .stub()
  149. .callsArgWith(1, null, ctx.docs)
  150. await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
  151. ctx.project_id
  152. )
  153. })
  154. it('should not set the root doc to the doc containing a documentclass', function (ctx) {
  155. ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
  156. })
  157. })
  158. })
  159. describe('findRootDocFileFromDirectory', function () {
  160. beforeEach(function (ctx) {
  161. ctx.fs.readFile
  162. .withArgs('/foo/a.tex')
  163. .callsArgWith(2, null, 'Hello World!')
  164. ctx.fs.readFile
  165. .withArgs('/foo/b.tex')
  166. .callsArgWith(2, null, "I'm a little teapot, get me out of here.")
  167. ctx.fs.readFile
  168. .withArgs('/foo/main.tex')
  169. .callsArgWith(2, null, "Help, I'm trapped in a unit testing factory")
  170. ctx.fs.readFile
  171. .withArgs('/foo/c.tex')
  172. .callsArgWith(2, null, 'Tomato, tomahto.')
  173. ctx.fs.readFile
  174. .withArgs('/foo/a/a.tex')
  175. .callsArgWith(2, null, 'Potato? Potahto. Potootee!')
  176. ctx.documentclassContent = '% test\n\\documentclass\n% test'
  177. })
  178. describe('when there is a file in a subfolder', function () {
  179. beforeEach(function (ctx) {
  180. // have to splice globbyFiles weirdly because of the way the stubbed globby method handles references
  181. ctx.globbyFiles.splice(
  182. 0,
  183. ctx.globbyFiles.length,
  184. 'c.tex',
  185. 'a.tex',
  186. 'a/a.tex',
  187. 'b.tex'
  188. )
  189. })
  190. it('processes the root folder files first, and then the subfolder, in alphabetical order', async function (ctx) {
  191. const { path } =
  192. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  193. '/foo'
  194. )
  195. expect(path).to.equal('a.tex')
  196. sinon.assert.callOrder(
  197. ctx.fs.readFile.withArgs('/foo/a.tex'),
  198. ctx.fs.readFile.withArgs('/foo/b.tex'),
  199. ctx.fs.readFile.withArgs('/foo/c.tex'),
  200. ctx.fs.readFile.withArgs('/foo/a/a.tex')
  201. )
  202. })
  203. it('processes smaller files first', async function (ctx) {
  204. ctx.fs.stat.withArgs('/foo/c.tex').callsArgWith(1, null, { size: 1 })
  205. const { path } =
  206. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  207. '/foo'
  208. )
  209. expect(path).to.equal('c.tex')
  210. sinon.assert.callOrder(
  211. ctx.fs.readFile.withArgs('/foo/c.tex'),
  212. ctx.fs.readFile.withArgs('/foo/a.tex'),
  213. ctx.fs.readFile.withArgs('/foo/b.tex'),
  214. ctx.fs.readFile.withArgs('/foo/a/a.tex')
  215. )
  216. })
  217. })
  218. describe('when main.tex contains a documentclass', function () {
  219. beforeEach(function (ctx) {
  220. ctx.fs.readFile
  221. .withArgs('/foo/main.tex')
  222. .callsArgWith(2, null, ctx.documentclassContent)
  223. })
  224. it('returns main.tex', async function (ctx) {
  225. const { path, content } =
  226. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  227. '/foo'
  228. )
  229. expect(path).to.equal('main.tex')
  230. expect(content).to.equal(ctx.documentclassContent)
  231. })
  232. it('processes main.text first and stops processing when it finds the content', async function (ctx) {
  233. await ctx.ProjectRootDocManager.findRootDocFileFromDirectory('/foo')
  234. expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
  235. expect(ctx.fs.readFile).not.to.be.calledWith('/foo/a.tex')
  236. })
  237. })
  238. describe('when main.tex does not contain a line starting with \\documentclass', function () {
  239. beforeEach(function (ctx) {
  240. ctx.fs.readFile.withArgs('/foo/a.tex').callsArgWith(2, null, 'foo')
  241. ctx.fs.readFile.withArgs('/foo/main.tex').callsArgWith(2, null, 'foo')
  242. ctx.fs.readFile.withArgs('/foo/z.tex').callsArgWith(2, null, 'foo')
  243. ctx.fs.readFile
  244. .withArgs('/foo/nested/chapter1a.tex')
  245. .callsArgWith(2, null, 'foo')
  246. })
  247. it('returns the first .tex file from the root folder', async function (ctx) {
  248. ctx.globbyFiles.splice(
  249. 0,
  250. ctx.globbyFiles.length,
  251. 'a.tex',
  252. 'z.tex',
  253. 'nested/chapter1a.tex'
  254. )
  255. const { path, content } =
  256. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  257. '/foo'
  258. )
  259. expect(path).to.equal('a.tex')
  260. expect(content).to.equal('foo')
  261. })
  262. it('returns main.tex file from the root folder', async function (ctx) {
  263. ctx.globbyFiles.splice(
  264. 0,
  265. ctx.globbyFiles.length,
  266. 'a.tex',
  267. 'z.tex',
  268. 'main.tex',
  269. 'nested/chapter1a.tex'
  270. )
  271. const { path, content } =
  272. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  273. '/foo'
  274. )
  275. expect(path).to.equal('main.tex')
  276. expect(content).to.equal('foo')
  277. })
  278. })
  279. describe('when a.tex contains a documentclass', function () {
  280. beforeEach(function (ctx) {
  281. ctx.fs.readFile
  282. .withArgs('/foo/a.tex')
  283. .callsArgWith(2, null, ctx.documentclassContent)
  284. })
  285. it('returns a.tex', async function (ctx) {
  286. const { path, content } =
  287. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  288. '/foo'
  289. )
  290. expect(path).to.equal('a.tex')
  291. expect(content).to.equal(ctx.documentclassContent)
  292. })
  293. it('processes main.text first and stops processing when it finds the content', async function (ctx) {
  294. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  295. '/foo'
  296. )
  297. expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
  298. expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex')
  299. expect(ctx.fs.readFile).not.to.be.calledWith('/foo/b.tex')
  300. })
  301. })
  302. describe('when there is no documentclass', function () {
  303. it('returns with no error', async function (ctx) {
  304. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  305. '/foo'
  306. )
  307. })
  308. it('processes all the files', async function (ctx) {
  309. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  310. '/foo'
  311. )
  312. expect(ctx.fs.readFile).to.be.calledWith('/foo/main.tex')
  313. expect(ctx.fs.readFile).to.be.calledWith('/foo/a.tex')
  314. expect(ctx.fs.readFile).to.be.calledWith('/foo/b.tex')
  315. })
  316. })
  317. describe('when there is an error reading a file', function () {
  318. beforeEach(function (ctx) {
  319. ctx.fs.readFile
  320. .withArgs('/foo/a.tex')
  321. .callsArgWith(2, new Error('something went wrong'))
  322. })
  323. it('returns an error', async function (ctx) {
  324. let error
  325. try {
  326. await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  327. '/foo'
  328. )
  329. } catch (err) {
  330. error = err
  331. }
  332. expect(error).to.exist
  333. })
  334. })
  335. })
  336. describe('setRootDocFromName', function () {
  337. describe('when there is a suitable root doc', function () {
  338. beforeEach(async function (ctx) {
  339. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
  340. .stub()
  341. .callsArgWith(1, null, ctx.docPaths)
  342. ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
  343. await ctx.ProjectRootDocManager.promises.setRootDocFromName(
  344. ctx.project_id,
  345. '/main.tex'
  346. )
  347. })
  348. it('should check the docs of the project', function (ctx) {
  349. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
  350. .calledWith(ctx.project_id)
  351. .should.equal(true)
  352. })
  353. it('should set the root doc to main.tex', function (ctx) {
  354. ctx.ProjectEntityUpdateHandler.setRootDoc
  355. .calledWith(ctx.project_id, ctx.docId2.toString())
  356. .should.equal(true)
  357. })
  358. })
  359. describe('when there is a suitable root doc but the leading slash is missing', function () {
  360. beforeEach(async function (ctx) {
  361. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
  362. .stub()
  363. .callsArgWith(1, null, ctx.docPaths)
  364. ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
  365. await ctx.ProjectRootDocManager.promises.setRootDocFromName(
  366. ctx.project_id,
  367. 'main.tex'
  368. )
  369. })
  370. it('should check the docs of the project', function (ctx) {
  371. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
  372. .calledWith(ctx.project_id)
  373. .should.equal(true)
  374. })
  375. it('should set the root doc to main.tex', function (ctx) {
  376. ctx.ProjectEntityUpdateHandler.setRootDoc
  377. .calledWith(ctx.project_id, ctx.docId2.toString())
  378. .should.equal(true)
  379. })
  380. })
  381. describe('when there is a suitable root doc with a basename match', function () {
  382. beforeEach(async function (ctx) {
  383. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
  384. .stub()
  385. .callsArgWith(1, null, ctx.docPaths)
  386. ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
  387. await ctx.ProjectRootDocManager.promises.setRootDocFromName(
  388. ctx.project_id,
  389. 'chapter1a.tex'
  390. )
  391. })
  392. it('should check the docs of the project', function (ctx) {
  393. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
  394. .calledWith(ctx.project_id)
  395. .should.equal(true)
  396. })
  397. it('should set the root doc using the basename', function (ctx) {
  398. ctx.ProjectEntityUpdateHandler.setRootDoc
  399. .calledWith(ctx.project_id, ctx.docId3.toString())
  400. .should.equal(true)
  401. })
  402. })
  403. describe('when there is a suitable root doc but the filename is in quotes', function () {
  404. beforeEach(async function (ctx) {
  405. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
  406. .stub()
  407. .callsArgWith(1, null, ctx.docPaths)
  408. ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
  409. await ctx.ProjectRootDocManager.promises.setRootDocFromName(
  410. ctx.project_id,
  411. "'main.tex'"
  412. )
  413. })
  414. it('should check the docs of the project', function (ctx) {
  415. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById
  416. .calledWith(ctx.project_id)
  417. .should.equal(true)
  418. })
  419. it('should set the root doc to main.tex', function (ctx) {
  420. ctx.ProjectEntityUpdateHandler.setRootDoc
  421. .calledWith(ctx.project_id, ctx.docId2.toString())
  422. .should.equal(true)
  423. })
  424. })
  425. describe('when there is no suitable root doc', function () {
  426. beforeEach(async function (ctx) {
  427. ctx.ProjectEntityHandler.getAllDocPathsFromProjectById = sinon
  428. .stub()
  429. .callsArgWith(1, null, ctx.docPaths)
  430. ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2)
  431. await ctx.ProjectRootDocManager.promises.setRootDocFromName(
  432. ctx.project_id,
  433. 'other.tex'
  434. )
  435. })
  436. it('should not set the root doc', function (ctx) {
  437. ctx.ProjectEntityUpdateHandler.setRootDoc.called.should.equal(false)
  438. })
  439. })
  440. })
  441. describe('ensureRootDocumentIsSet', function () {
  442. beforeEach(function (ctx) {
  443. ctx.project = {}
  444. ctx.ProjectGetter.getProject = sinon
  445. .stub()
  446. .callsArgWith(2, null, ctx.project)
  447. ctx.ProjectRootDocManager.setRootDocAutomatically = sinon
  448. .stub()
  449. .callsArgWith(1, null)
  450. })
  451. describe('when the root doc is set', function () {
  452. beforeEach(function (ctx) {
  453. ctx.project.rootDoc_id = ctx.docId2
  454. ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
  455. ctx.project_id,
  456. ctx.callback
  457. )
  458. })
  459. it('should find the project fetching only the rootDoc_id field', function (ctx) {
  460. ctx.ProjectGetter.getProject
  461. .calledWith(ctx.project_id, { rootDoc_id: 1 })
  462. .should.equal(true)
  463. })
  464. it('should not try to update the project rootDoc_id', function (ctx) {
  465. ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
  466. false
  467. )
  468. })
  469. it('should call the callback', function (ctx) {
  470. ctx.callback.called.should.equal(true)
  471. })
  472. })
  473. describe('when the root doc is not set', function () {
  474. beforeEach(function (ctx) {
  475. ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
  476. ctx.project_id,
  477. ctx.callback
  478. )
  479. })
  480. it('should find the project with only the rootDoc_id field', function (ctx) {
  481. ctx.ProjectGetter.getProject
  482. .calledWith(ctx.project_id, { rootDoc_id: 1 })
  483. .should.equal(true)
  484. })
  485. it('should update the project rootDoc_id', function (ctx) {
  486. ctx.ProjectRootDocManager.setRootDocAutomatically
  487. .calledWith(ctx.project_id)
  488. .should.equal(true)
  489. })
  490. it('should call the callback', function (ctx) {
  491. ctx.callback.called.should.equal(true)
  492. })
  493. })
  494. describe('when the project does not exist', function () {
  495. beforeEach(function (ctx) {
  496. ctx.ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, null)
  497. ctx.ProjectRootDocManager.ensureRootDocumentIsSet(
  498. ctx.project_id,
  499. ctx.callback
  500. )
  501. })
  502. it('should call the callback with an error', function (ctx) {
  503. ctx.callback
  504. .calledWith(
  505. sinon.match
  506. .instanceOf(Error)
  507. .and(sinon.match.has('message', 'project not found'))
  508. )
  509. .should.equal(true)
  510. })
  511. })
  512. })
  513. describe('ensureRootDocumentIsValid', function () {
  514. beforeEach(function (ctx) {
  515. ctx.project = {}
  516. ctx.ProjectGetter.getProject = sinon
  517. .stub()
  518. .callsArgWith(2, null, ctx.project)
  519. ctx.ProjectGetter.getProjectWithoutDocLines = sinon
  520. .stub()
  521. .callsArgWith(1, null, ctx.project)
  522. ctx.ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields()
  523. ctx.ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields()
  524. ctx.ProjectRootDocManager.setRootDocAutomatically = sinon
  525. .stub()
  526. .callsArgWith(1, null)
  527. })
  528. describe('when the root doc is set', function () {
  529. describe('when the root doc is valid', function () {
  530. beforeEach(function (ctx) {
  531. ctx.project.rootDoc_id = ctx.docId2
  532. ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
  533. .stub()
  534. .callsArgWith(2, null, ctx.docPaths[ctx.docId2])
  535. ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
  536. ctx.project_id,
  537. ctx.callback
  538. )
  539. })
  540. it('should find the project without doc lines', function (ctx) {
  541. ctx.ProjectGetter.getProjectWithoutDocLines
  542. .calledWith(ctx.project_id)
  543. .should.equal(true)
  544. })
  545. it('should not try to update the project rootDoc_id', function (ctx) {
  546. ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
  547. false
  548. )
  549. })
  550. it('should call the callback', function (ctx) {
  551. ctx.callback.called.should.equal(true)
  552. })
  553. })
  554. describe('when the root doc is not valid', function () {
  555. beforeEach(function (ctx) {
  556. ctx.project.rootDoc_id = new ObjectId()
  557. ctx.ProjectEntityHandler.getDocPathFromProjectByDocId = sinon
  558. .stub()
  559. .callsArgWith(2, null, null)
  560. ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
  561. ctx.project_id,
  562. ctx.callback
  563. )
  564. })
  565. it('should find the project without doc lines', function (ctx) {
  566. ctx.ProjectGetter.getProjectWithoutDocLines
  567. .calledWith(ctx.project_id)
  568. .should.equal(true)
  569. })
  570. it('should unset the root doc', function (ctx) {
  571. ctx.ProjectEntityUpdateHandler.unsetRootDoc
  572. .calledWith(ctx.project_id)
  573. .should.equal(true)
  574. })
  575. it('should try to find a new rootDoc', function (ctx) {
  576. ctx.ProjectRootDocManager.setRootDocAutomatically.called.should.equal(
  577. true
  578. )
  579. })
  580. it('should call the callback', function (ctx) {
  581. ctx.callback.called.should.equal(true)
  582. })
  583. })
  584. })
  585. describe('when the root doc is not set', function () {
  586. beforeEach(function (ctx) {
  587. ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
  588. ctx.project_id,
  589. ctx.callback
  590. )
  591. })
  592. it('should find the project without doc lines', function (ctx) {
  593. ctx.ProjectGetter.getProjectWithoutDocLines
  594. .calledWith(ctx.project_id)
  595. .should.equal(true)
  596. })
  597. it('should update the project rootDoc_id', function (ctx) {
  598. ctx.ProjectRootDocManager.setRootDocAutomatically
  599. .calledWith(ctx.project_id)
  600. .should.equal(true)
  601. })
  602. it('should call the callback', function (ctx) {
  603. ctx.callback.called.should.equal(true)
  604. })
  605. })
  606. describe('when the project does not exist', function () {
  607. beforeEach(function (ctx) {
  608. ctx.ProjectGetter.getProjectWithoutDocLines = sinon
  609. .stub()
  610. .callsArgWith(1, null, null)
  611. ctx.ProjectRootDocManager.ensureRootDocumentIsValid(
  612. ctx.project_id,
  613. ctx.callback
  614. )
  615. })
  616. it('should call the callback with an error', function (ctx) {
  617. ctx.callback
  618. .calledWith(
  619. sinon.match
  620. .instanceOf(Error)
  621. .and(sinon.match.has('message', 'project not found'))
  622. )
  623. .should.equal(true)
  624. })
  625. })
  626. })
  627. })