| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696 |
- import { vi, expect } from 'vitest'
- import mongodb from 'mongodb-legacy'
- import sinon from 'sinon'
- const modulePath =
- '../../../../app/src/Features/Project/ProjectRootDocManager.mjs'
- const { ObjectId } = mongodb
- describe('ProjectRootDocManager', function () {
- beforeEach(async function (ctx) {
- ctx.project_id = 'project-123'
- ctx.docPaths = {}
- ctx.docId1 = new ObjectId()
- ctx.docId2 = new ObjectId()
- ctx.docId3 = new ObjectId()
- ctx.docId4 = new ObjectId()
- ctx.docPaths[ctx.docId1] = '/chapter1.tex'
- ctx.docPaths[ctx.docId2] = '/main.tex'
- ctx.docPaths[ctx.docId3] = '/nested/chapter1a.tex'
- ctx.docPaths[ctx.docId4] = '/nested/chapter1b.tex'
- ctx.sl_req_id = 'sl-req-id-123'
- ctx.globbyFiles = ['a.tex', 'b.tex', 'main.tex']
- ctx.globby = sinon.stub().resolves(ctx.globbyFiles)
- ctx.fs = {
- promises: {
- readFile: sinon.stub().rejects(new Error('file not found')),
- stat: sinon.stub().resolves({ size: 100 }),
- },
- }
- ctx.ProjectEntityHandler = {
- promises: {},
- }
- ctx.ProjectEntityUpdateHandler = {
- promises: {},
- }
- ctx.ProjectGetter = {
- promises: {},
- }
- vi.doMock(
- '../../../../app/src/Features/Project/ProjectEntityHandler',
- () => ({
- default: ctx.ProjectEntityHandler,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Project/ProjectEntityUpdateHandler',
- () => ({
- default: ctx.ProjectEntityUpdateHandler,
- })
- )
- vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
- default: ctx.ProjectGetter,
- }))
- vi.doMock('../../../../app/src/infrastructure/GracefulShutdown', () => ({
- BackgroundTaskTracker: class {
- add() {}
- done() {}
- },
- }))
- vi.doMock('globby', () => ({
- default: ctx.globby,
- }))
- vi.doMock('fs', () => ({
- default: ctx.fs,
- }))
- ctx.ProjectRootDocManager = (await import(modulePath)).default
- })
- describe('setRootDocAutomatically', function () {
- beforeEach(function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
- .stub()
- .resolves()
- ctx.ProjectEntityUpdateHandler.isPathValidForRootDoc = sinon
- .stub()
- .returns(true)
- })
- describe('when there is a suitable root doc', function () {
- beforeEach(async function (ctx) {
- ctx.docs = {
- '/chapter1.tex': {
- _id: ctx.docId1,
- lines: [
- 'something else',
- '\\begin{document}',
- 'Hello world',
- '\\end{document}',
- ],
- },
- '/main.tex': {
- _id: ctx.docId2,
- lines: [
- 'different line',
- '\\documentclass{article}',
- '\\input{chapter1}',
- ],
- },
- '/nested/chapter1a.tex': {
- _id: ctx.docId3,
- lines: ['Hello world'],
- },
- '/nested/chapter1b.tex': {
- _id: ctx.docId4,
- lines: ['Hello world'],
- },
- }
- ctx.ProjectEntityHandler.promises.getAllDocs = sinon
- .stub()
- .resolves(ctx.docs)
- await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
- ctx.project_id
- )
- })
- it('should check the docs of the project', function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocs
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should set the root doc to the doc containing a documentclass', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc
- .calledWith(ctx.project_id, ctx.docId2)
- .should.equal(true)
- })
- })
- describe('when the root doc is an Rtex file', function () {
- beforeEach(async function (ctx) {
- ctx.docs = {
- '/chapter1.tex': {
- _id: ctx.docId1,
- lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
- },
- '/main.Rtex': {
- _id: ctx.docId2,
- lines: ['\\documentclass{article}', '\\input{chapter1}'],
- },
- }
- ctx.ProjectEntityHandler.promises.getAllDocs = sinon
- .stub()
- .resolves(ctx.docs)
- await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
- ctx.project_id
- )
- })
- it('should set the root doc to the doc containing a documentclass', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc
- .calledWith(ctx.project_id, ctx.docId2)
- .should.equal(true)
- })
- })
- describe('when there is no suitable root doc', function () {
- beforeEach(async function (ctx) {
- ctx.docs = {
- '/chapter1.tex': {
- _id: ctx.docId1,
- lines: ['\\begin{document}', 'Hello world', '\\end{document}'],
- },
- '/style.bst': {
- _id: ctx.docId2,
- lines: ['%Example: \\documentclass{article}'],
- },
- }
- ctx.ProjectEntityHandler.promises.getAllDocs = sinon
- .stub()
- .resolves(ctx.docs)
- await ctx.ProjectRootDocManager.promises.setRootDocAutomatically(
- ctx.project_id
- )
- })
- it('should not set the root doc to the doc containing a documentclass', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc.called.should.equal(
- false
- )
- })
- })
- })
- describe('findRootDocFileFromDirectory', function () {
- beforeEach(function (ctx) {
- ctx.fs.promises.readFile.withArgs('/foo/a.tex').resolves('Hello World!')
- ctx.fs.promises.readFile
- .withArgs('/foo/b.tex')
- .resolves("I'm a little teapot, get me out of here.")
- ctx.fs.promises.readFile
- .withArgs('/foo/main.tex')
- .resolves("Help, I'm trapped in a unit testing factory")
- ctx.fs.promises.readFile
- .withArgs('/foo/c.tex')
- .resolves('Tomato, tomahto.')
- ctx.fs.promises.readFile
- .withArgs('/foo/a/a.tex')
- .resolves('Potato? Potahto. Potootee!')
- ctx.documentclassContent = '% test\n\\documentclass\n% test'
- })
- describe('when there is a file in a subfolder', function () {
- beforeEach(function (ctx) {
- // have to splice globbyFiles weirdly because of the way the stubbed globby method handles references
- ctx.globbyFiles.splice(
- 0,
- ctx.globbyFiles.length,
- 'c.tex',
- 'a.tex',
- 'a/a.tex',
- 'b.tex'
- )
- })
- it('processes the root folder files first, and then the subfolder, in alphabetical order', async function (ctx) {
- const { path } =
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(path).to.equal('a.tex')
- sinon.assert.callOrder(
- ctx.fs.promises.readFile.withArgs('/foo/a.tex'),
- ctx.fs.promises.readFile.withArgs('/foo/b.tex'),
- ctx.fs.promises.readFile.withArgs('/foo/c.tex'),
- ctx.fs.promises.readFile.withArgs('/foo/a/a.tex')
- )
- })
- it('processes smaller files first', async function (ctx) {
- ctx.fs.promises.stat.withArgs('/foo/c.tex').resolves({ size: 1 })
- const { path } =
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(path).to.equal('c.tex')
- sinon.assert.callOrder(
- ctx.fs.promises.readFile.withArgs('/foo/c.tex'),
- ctx.fs.promises.readFile.withArgs('/foo/a.tex'),
- ctx.fs.promises.readFile.withArgs('/foo/b.tex'),
- ctx.fs.promises.readFile.withArgs('/foo/a/a.tex')
- )
- })
- })
- describe('when main.tex contains a documentclass', function () {
- beforeEach(function (ctx) {
- ctx.fs.promises.readFile
- .withArgs('/foo/main.tex')
- .resolves(ctx.documentclassContent)
- })
- it('returns main.tex', async function (ctx) {
- const { path, content } =
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(path).to.equal('main.tex')
- expect(content).to.equal(ctx.documentclassContent)
- })
- it('processes main.text first and stops processing when it finds the content', async function (ctx) {
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex')
- expect(ctx.fs.promises.readFile).not.to.be.calledWith('/foo/a.tex')
- })
- })
- describe('when main.tex does not contain a line starting with \\documentclass', function () {
- beforeEach(function (ctx) {
- ctx.fs.promises.readFile.withArgs('/foo/a.tex').resolves('foo')
- ctx.fs.promises.readFile.withArgs('/foo/main.tex').resolves('foo')
- ctx.fs.promises.readFile.withArgs('/foo/z.tex').resolves('foo')
- ctx.fs.promises.readFile
- .withArgs('/foo/nested/chapter1a.tex')
- .resolves('foo')
- })
- it('returns the first .tex file from the root folder', async function (ctx) {
- ctx.globbyFiles.splice(
- 0,
- ctx.globbyFiles.length,
- 'a.tex',
- 'z.tex',
- 'nested/chapter1a.tex'
- )
- const { path, content } =
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(path).to.equal('a.tex')
- expect(content).to.equal('foo')
- })
- it('returns main.tex file from the root folder', async function (ctx) {
- ctx.globbyFiles.splice(
- 0,
- ctx.globbyFiles.length,
- 'a.tex',
- 'z.tex',
- 'main.tex',
- 'nested/chapter1a.tex'
- )
- const { path, content } =
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(path).to.equal('main.tex')
- expect(content).to.equal('foo')
- })
- })
- describe('when a.tex contains a documentclass', function () {
- beforeEach(function (ctx) {
- ctx.fs.promises.readFile
- .withArgs('/foo/a.tex')
- .resolves(ctx.documentclassContent)
- })
- it('returns a.tex', async function (ctx) {
- const { path, content } =
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(path).to.equal('a.tex')
- expect(content).to.equal(ctx.documentclassContent)
- })
- it('processes main.text first and stops processing when it finds the content', async function (ctx) {
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex')
- expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/a.tex')
- expect(ctx.fs.promises.readFile).not.to.be.calledWith('/foo/b.tex')
- })
- })
- describe('when there is no documentclass', function () {
- it('returns with no error', async function (ctx) {
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- })
- it('processes all the files', async function (ctx) {
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/main.tex')
- expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/a.tex')
- expect(ctx.fs.promises.readFile).to.be.calledWith('/foo/b.tex')
- })
- })
- describe('when there is an error reading a file', function () {
- beforeEach(function (ctx) {
- ctx.fs.promises.readFile
- .withArgs('/foo/a.tex')
- .rejects(new Error('something went wrong'))
- })
- it('returns an error', async function (ctx) {
- let error
- try {
- await ctx.ProjectRootDocManager.promises.findRootDocFileFromDirectory(
- '/foo'
- )
- } catch (err) {
- error = err
- }
- expect(error).to.exist
- })
- })
- })
- describe('setRootDocFromName', function () {
- describe('when there is a suitable root doc', function () {
- beforeEach(async function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
- .stub()
- .resolves(ctx.docPaths)
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
- .stub()
- .resolves()
- await ctx.ProjectRootDocManager.promises.setRootDocFromName(
- ctx.project_id,
- '/main.tex'
- )
- })
- it('should check the docs of the project', function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should set the root doc to main.tex', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc
- .calledWith(ctx.project_id, ctx.docId2.toString())
- .should.equal(true)
- })
- })
- describe('when there is a suitable root doc but the leading slash is missing', function () {
- beforeEach(async function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
- .stub()
- .resolves(ctx.docPaths)
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
- .stub()
- .resolves()
- await ctx.ProjectRootDocManager.promises.setRootDocFromName(
- ctx.project_id,
- 'main.tex'
- )
- })
- it('should check the docs of the project', function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should set the root doc to main.tex', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc
- .calledWith(ctx.project_id, ctx.docId2.toString())
- .should.equal(true)
- })
- })
- describe('when there is a suitable root doc with a basename match', function () {
- beforeEach(async function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
- .stub()
- .resolves(ctx.docPaths)
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
- .stub()
- .resolves()
- await ctx.ProjectRootDocManager.promises.setRootDocFromName(
- ctx.project_id,
- 'chapter1a.tex'
- )
- })
- it('should check the docs of the project', function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should set the root doc using the basename', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc
- .calledWith(ctx.project_id, ctx.docId3.toString())
- .should.equal(true)
- })
- })
- describe('when there is a suitable root doc but the filename is in quotes', function () {
- beforeEach(async function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
- .stub()
- .resolves(ctx.docPaths)
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
- .stub()
- .resolves()
- await ctx.ProjectRootDocManager.promises.setRootDocFromName(
- ctx.project_id,
- "'main.tex'"
- )
- })
- it('should check the docs of the project', function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should set the root doc to main.tex', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc
- .calledWith(ctx.project_id, ctx.docId2.toString())
- .should.equal(true)
- })
- })
- describe('when there is no suitable root doc', function () {
- beforeEach(async function (ctx) {
- ctx.ProjectEntityHandler.promises.getAllDocPathsFromProjectById = sinon
- .stub()
- .resolves(ctx.docPaths)
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
- .stub()
- .resolves()
- await ctx.ProjectRootDocManager.promises.setRootDocFromName(
- ctx.project_id,
- 'other.tex'
- )
- })
- it('should not set the root doc', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc.called.should.equal(
- false
- )
- })
- })
- })
- describe('ensureRootDocumentIsSet', function () {
- beforeEach(function (ctx) {
- ctx.project = {}
- ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project)
- ctx.ProjectRootDocManager.promises.setRootDocAutomatically = sinon
- .stub()
- .resolves()
- })
- describe('when the root doc is set', function () {
- beforeEach(async function (ctx) {
- ctx.project.rootDoc_id = ctx.docId2
- await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet(
- ctx.project_id
- )
- })
- it('should find the project fetching only the rootDoc_id field', function (ctx) {
- ctx.ProjectGetter.promises.getProject
- .calledWith(ctx.project_id, { rootDoc_id: 1 })
- .should.equal(true)
- })
- it('should not try to update the project rootDoc_id', function (ctx) {
- ctx.ProjectRootDocManager.promises.setRootDocAutomatically.called.should.equal(
- false
- )
- })
- })
- describe('when the root doc is not set', function () {
- beforeEach(async function (ctx) {
- await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet(
- ctx.project_id
- )
- })
- it('should find the project with only the rootDoc_id field', function (ctx) {
- ctx.ProjectGetter.promises.getProject
- .calledWith(ctx.project_id, { rootDoc_id: 1 })
- .should.equal(true)
- })
- it('should update the project rootDoc_id', function (ctx) {
- ctx.ProjectRootDocManager.promises.setRootDocAutomatically
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- })
- describe('when the project does not exist', function () {
- beforeEach(function (ctx) {
- ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(null)
- })
- it('should reject', async function (ctx) {
- await expect(
- ctx.ProjectRootDocManager.promises.ensureRootDocumentIsSet(
- ctx.project_id
- )
- ).to.be.rejectedWith('project not found')
- })
- })
- })
- describe('ensureRootDocumentIsValid', function () {
- beforeEach(function (ctx) {
- ctx.project = {}
- ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project)
- ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon
- .stub()
- .resolves(ctx.project)
- ctx.ProjectEntityUpdateHandler.promises.setRootDoc = sinon
- .stub()
- .resolves()
- ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc = sinon
- .stub()
- .resolves()
- ctx.ProjectRootDocManager.promises.setRootDocAutomatically = sinon
- .stub()
- .resolves(null)
- })
- describe('when the root doc is set', function () {
- describe('when the root doc is valid', function () {
- beforeEach(async function (ctx) {
- ctx.project.rootDoc_id = ctx.docId2
- ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId = sinon
- .stub()
- .resolves(ctx.docPaths[ctx.docId2])
- await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid(
- ctx.project_id
- )
- })
- it('should find the project without doc lines', function (ctx) {
- ctx.ProjectGetter.promises.getProjectWithoutDocLines
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should not try to update the project rootDoc_id', function (ctx) {
- ctx.ProjectRootDocManager.promises.setRootDocAutomatically.called.should.equal(
- false
- )
- })
- })
- describe('when the root doc is not valid', function () {
- beforeEach(async function (ctx) {
- ctx.project.rootDoc_id = new ObjectId()
- ctx.ProjectEntityHandler.promises.getDocPathFromProjectByDocId = sinon
- .stub()
- .resolves(null)
- await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid(
- ctx.project_id
- )
- })
- it('should find the project without doc lines', function (ctx) {
- ctx.ProjectGetter.promises.getProjectWithoutDocLines
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should unset the root doc', function (ctx) {
- ctx.ProjectEntityUpdateHandler.promises.unsetRootDoc
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should try to find a new rootDoc', function (ctx) {
- ctx.ProjectRootDocManager.promises.setRootDocAutomatically.called.should.equal(
- true
- )
- })
- })
- })
- describe('when the root doc is not set', function () {
- beforeEach(async function (ctx) {
- await ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid(
- ctx.project_id
- )
- })
- it('should find the project without doc lines', async function (ctx) {
- ctx.ProjectGetter.promises.getProjectWithoutDocLines
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- it('should update the project rootDoc_id', function (ctx) {
- ctx.ProjectRootDocManager.promises.setRootDocAutomatically
- .calledWith(ctx.project_id)
- .should.equal(true)
- })
- })
- describe('when the project does not exist', function () {
- beforeEach(async function (ctx) {
- ctx.ProjectGetter.promises.getProjectWithoutDocLines = sinon
- .stub()
- .resolves(null)
- })
- it('should reject', async function (ctx) {
- await expect(
- ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid(
- ctx.project_id
- )
- ).to.be.rejectedWith('project not found')
- })
- })
- })
- })
|