Merge pull request #30323 from overleaf/ar/promisify-DocstoreManager

[web] promisify DocstoreManager

GitOrigin-RevId: 351b9868a1c29066b6c98d92e5b513e10f4f6764
This commit is contained in:
Andrew Rumble
2026-01-29 09:46:42 +00:00
committed by Copybot
parent 3ddc20e424
commit 610398d099
2 changed files with 551 additions and 534 deletions
@@ -1,91 +1,108 @@
import { promisify } from 'node:util'
import { promisifyMultiResult, callbackify } from '@overleaf/promise-utils'
// @ts-check
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
import OError from '@overleaf/o-error'
import logger from '@overleaf/logger'
import settings from '@overleaf/settings'
import Errors from '../Errors/Errors.js'
import { fetchJson } from '@overleaf/fetch-utils'
import Request from 'request'
const request = Request.defaults({ jar: false })
import {
fetchJson,
fetchNothing,
RequestFailedError,
} from '@overleaf/fetch-utils'
import path from 'node:path'
/**
* @import { ObjectId } from 'mongodb'
*/
const TIMEOUT = 30 * 1000 // request timeout
function deleteDoc(projectId, docId, name, deletedAt, callback) {
const url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
/**
*
* @param {string | ObjectId} projectId
* @param {string | ObjectId} docId
* @param {string} name
* @param {Date} deletedAt
* @return {Promise<void>}
*/
async function deleteDoc(projectId, docId, name, deletedAt) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join(
'project',
projectId.toString(),
'doc',
docId.toString()
)
const docMetaData = { deleted: true, deletedAt, name }
const options = { url, json: docMetaData, timeout: TIMEOUT }
request.patch(options, (error, res) => {
if (error) {
return callback(error)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
callback(null)
} else if (res.statusCode === 404) {
error = new Errors.NotFoundError({
message: 'tried to delete doc not in docstore',
info: {
projectId,
docId,
},
})
callback(error) // maybe suppress the error when delete doc which is not present?
} else {
error = new OError(
`docstore api responded with non-success code: ${res.statusCode}`,
const options = {
json: docMetaData,
signal: AbortSignal.timeout(TIMEOUT),
method: 'PATCH',
}
try {
await fetchNothing(url, options)
} catch (error) {
if (error instanceof RequestFailedError) {
if (error.response.status === 404) {
// maybe suppress the error when delete doc which is not present?
throw new Errors.NotFoundError({
message: 'tried to delete doc not in docstore',
info: {
projectId,
docId,
},
})
}
throw new OError(
`docstore api responded with non-success code: ${error.response.status}`,
{
projectId,
docId,
}
)
callback(error)
}
})
throw error
}
}
/**
* @param {string} projectId
*/
function getAllDocs(projectId, callback) {
const url = `${settings.apis.docstore.url}/project/${projectId}/doc`
request.get(
{
url,
timeout: TIMEOUT,
json: true,
},
(error, res, docs) => {
if (error) {
return callback(error)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
callback(null, docs)
} else {
error = new OError(
`docstore api responded with non-success code: ${res.statusCode}`,
{ projectId }
)
callback(error)
}
}
)
}
function getAllDeletedDocs(projectId, callback) {
const url = `${settings.apis.docstore.url}/project/${projectId}/doc-deleted`
request.get({ url, timeout: TIMEOUT, json: true }, (error, res, docs) => {
if (error) {
callback(OError.tag(error, 'could not get deleted docs from docstore'))
} else if (res.statusCode === 200) {
callback(null, docs)
} else {
callback(
new OError(
`docstore api responded with non-success code: ${res.statusCode}`,
{ projectId }
)
async function getAllDocs(projectId) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join('project', projectId.toString(), 'doc')
try {
return await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
} catch (error) {
if (error instanceof RequestFailedError) {
throw new OError(
`docstore api responded with non-success code: ${error.response.status}`,
{ projectId }
)
}
})
throw error
}
}
/**
*
* @param {string|ObjectId} projectId
* @return {Promise<*>}
*/
async function getAllDeletedDocs(projectId) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join('project', projectId.toString(), 'doc-deleted')
try {
return await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
} catch (error) {
if (error instanceof RequestFailedError) {
throw new OError(
`docstore api responded with non-success code: ${error.response.status}`,
{ projectId }
)
}
throw OError.tag(error, 'could not get deleted docs from docstore')
}
}
/**
@@ -106,231 +123,257 @@ async function getTrackedChangesUserIds(projectId) {
/**
* @param {string} projectId
* @param {Callback} callback
*/
function getAllRanges(projectId, callback) {
const url = `${settings.apis.docstore.url}/project/${projectId}/ranges`
request.get(
{
url,
timeout: TIMEOUT,
json: true,
},
(error, res, docs) => {
if (error) {
return callback(error)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
callback(null, docs)
} else {
error = new OError(
`docstore api responded with non-success code: ${res.statusCode}`,
{ projectId }
)
callback(error)
}
async function getAllRanges(projectId) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join('project', projectId, 'ranges')
try {
return await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
} catch (error) {
if (error instanceof RequestFailedError) {
throw new OError(
`docstore api responded with non-success code: ${error.response.status}`,
{ projectId }
)
}
)
throw error
}
}
function getDoc(projectId, docId, options, callback) {
if (options == null) {
options = {}
}
if (typeof options === 'function') {
callback = options
options = {}
}
const requestParams = { timeout: TIMEOUT, json: true }
/**
*
* @param {string | ObjectId} projectId
* @param {string | ObjectId} docId
* @param {{ peek?: boolean, include_deleted?: boolean }} options
* @return {Promise<{lines: *, rev: *, version: *, ranges: *}>}
*/
async function getDoc(projectId, docId, options = {}) {
const url = new URL(settings.apis.docstore.url)
if (options.peek) {
requestParams.url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}/peek`
url.pathname = path.posix.join(
'project',
projectId.toString(),
'doc',
docId.toString(),
'peek'
)
} else {
requestParams.url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
url.pathname = path.posix.join(
'project',
projectId.toString(),
'doc',
docId.toString()
)
}
if (options.include_deleted) {
requestParams.qs = { include_deleted: 'true' }
url.searchParams.set('include_deleted', 'true')
}
request.get(requestParams, (error, res, doc) => {
if (error) {
return callback(error)
try {
const doc = await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
logger.debug(
{ docId, projectId, version: doc.version, rev: doc.rev },
'got doc from docstore api'
)
return {
lines: doc.lines,
rev: doc.rev,
version: doc.version,
ranges: doc.ranges,
}
if (res.statusCode >= 200 && res.statusCode < 300) {
logger.debug(
{ docId, projectId, version: doc.version, rev: doc.rev },
'got doc from docstore api'
)
callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
} else if (res.statusCode === 404) {
error = new Errors.NotFoundError({
message: 'doc not found in docstore',
info: {
projectId,
docId,
},
})
callback(error)
} else {
error = new OError(
`docstore api responded with non-success code: ${res.statusCode}`,
} catch (error) {
if (error instanceof RequestFailedError) {
if (error.response.status === 404) {
throw new Errors.NotFoundError({
message: 'doc not found in docstore',
info: {
projectId,
docId,
},
})
}
throw new OError(
`docstore api responded with non-success code: ${error.response.status}`,
{
projectId,
docId,
}
)
callback(error)
}
})
throw error
}
}
function isDocDeleted(projectId, docId, callback) {
const url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}/deleted`
request.get({ url, timeout: TIMEOUT, json: true }, (err, res, body) => {
if (err) {
callback(err)
} else if (res.statusCode === 200) {
callback(null, body.deleted)
} else if (res.statusCode === 404) {
callback(
new Errors.NotFoundError({
/**
*
* @param {string} projectId
* @param {string} docId
* @return {Promise<boolean>}
*/
async function isDocDeleted(projectId, docId) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join('project', projectId, 'doc', docId, 'deleted')
try {
const doc = await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
return doc.deleted
} catch (error) {
if (error instanceof RequestFailedError) {
if (error.response.status === 404) {
throw new Errors.NotFoundError({
message: 'doc does not exist in project',
info: { projectId, docId },
})
)
} else {
callback(
new OError(
`docstore api responded with non-success code: ${res.statusCode}`,
{ projectId, docId }
)
}
throw new OError(
`docstore api responded with non-success code: ${error.response.status}`,
{ projectId, docId }
)
}
})
throw error
}
}
function updateDoc(projectId, docId, lines, version, ranges, callback) {
const url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
request.post(
{
url,
timeout: TIMEOUT,
/**
*
* @param {string} projectId
* @param {string} docId
* @param {string[]} lines
* @param {number} version
* @param ranges
* @return {Promise<{modified: *, rev: *}>}
*/
async function updateDoc(projectId, docId, lines, version, ranges) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join('project', projectId, 'doc', docId)
try {
const result = await fetchJson(url, {
method: 'POST',
signal: AbortSignal.timeout(TIMEOUT),
json: {
lines,
version,
ranges,
},
},
(error, res, result) => {
if (error) {
return callback(error)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
logger.debug(
{ projectId, docId },
'update doc in docstore url finished'
)
callback(null, result.modified, result.rev)
} else {
error = new OError(
`docstore api responded with non-success code: ${res.statusCode}`,
{ projectId, docId }
)
callback(error)
}
})
logger.debug({ projectId, docId }, 'update doc in docstore url finished')
return { modified: result.modified, rev: result.rev }
} catch (error) {
if (error instanceof RequestFailedError) {
throw new OError(
`docstore api responded with non-success code: ${error.response.status}`,
{ projectId, docId }
)
}
)
throw error
}
}
/**
* Asks docstore whether any doc in the project has ranges
*
* @param {string} proejctId
* @param {Callback} callback
* @param {string} projectId
*/
function projectHasRanges(projectId, callback) {
const url = `${settings.apis.docstore.url}/project/${projectId}/has-ranges`
request.get({ url, timeout: TIMEOUT, json: true }, (err, res, body) => {
if (err) {
return callback(err)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
callback(null, body.projectHasRanges)
} else {
callback(
new OError(
`docstore api responded with non-success code: ${res.statusCode}`,
{ projectId }
)
async function projectHasRanges(projectId) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join('project', projectId, 'has-ranges')
try {
const body = await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
return body.projectHasRanges
} catch (error) {
if (error instanceof RequestFailedError) {
throw new OError(
`docstore api responded with non-success code: ${error.response.status}`,
{ projectId }
)
}
})
throw error
}
}
function archiveProject(projectId, callback) {
_operateOnProject(projectId, 'archive', callback)
/**
*
* @param {string|ObjectId} projectId
* @return {Promise<void>}
*/
async function archiveProject(projectId) {
await _operateOnProject(projectId, 'archive')
}
/**
*
* @param {string|ObjectId} projectId
* @return {Promise<void>}
*/
async function unarchiveProject(projectId) {
await _operateOnProject(projectId, 'unarchive')
}
/**
*
* @param {string|ObjectId} projectId
* @return {Promise<void>}
*/
async function destroyProject(projectId) {
await _operateOnProject(projectId, 'destroy')
}
function unarchiveProject(projectId, callback) {
_operateOnProject(projectId, 'unarchive', callback)
}
function destroyProject(projectId, callback) {
_operateOnProject(projectId, 'destroy', callback)
}
function _operateOnProject(projectId, method, callback) {
const url = `${settings.apis.docstore.url}/project/${projectId}/${method}`
/**
*
* @param {string|ObjectId} projectId
* @param {string} method
* @return {Promise<void>}
* @private
*/
async function _operateOnProject(projectId, method) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join('project', projectId.toString(), method)
logger.debug({ projectId }, `calling ${method} for project in docstore`)
// use default timeout for archiving/unarchiving/destroying
request.post(url, (err, res, docs) => {
if (err) {
OError.tag(err, `error calling ${method} project in docstore`, {
projectId,
})
return callback(err)
}
if (res.statusCode >= 200 && res.statusCode < 300) {
callback()
} else {
try {
// use default timeout for archiving/unarchiving/destroying
await fetchNothing(url, {
method: 'POST',
})
} catch (err) {
if (err instanceof RequestFailedError) {
const error = new Error(
`docstore api responded with non-success code: ${res.statusCode}`
`docstore api responded with non-success code: ${err.response.status}`
)
logger.warn(
{ err: error, projectId },
`error calling ${method} project in docstore`
)
callback(error)
throw error
}
})
throw OError.tag(err, `error calling ${method} project in docstore`, {
projectId,
})
}
}
export default {
deleteDoc,
getAllDocs,
getAllDeletedDocs,
getAllRanges,
getDoc,
deleteDoc: callbackify(deleteDoc),
getAllDocs: callbackify(getAllDocs),
getAllDeletedDocs: callbackify(getAllDeletedDocs),
getAllRanges: callbackify(getAllRanges),
getDoc: callbackifyMultiResult(getDoc, ['lines', 'rev', 'version', 'ranges']),
getCommentThreadIds: callbackify(getCommentThreadIds),
getTrackedChangesUserIds: callbackify(getTrackedChangesUserIds),
isDocDeleted,
updateDoc,
projectHasRanges,
archiveProject,
unarchiveProject,
destroyProject,
isDocDeleted: callbackify(isDocDeleted),
updateDoc: callbackifyMultiResult(updateDoc, ['modified', 'rev']),
projectHasRanges: callbackify(projectHasRanges),
archiveProject: callbackify(archiveProject),
unarchiveProject: callbackify(unarchiveProject),
destroyProject: callbackify(destroyProject),
promises: {
deleteDoc: promisify(deleteDoc),
getAllDocs: promisify(getAllDocs),
getAllDeletedDocs: promisify(getAllDeletedDocs),
getAllRanges: promisify(getAllRanges),
getDoc: promisifyMultiResult(getDoc, ['lines', 'rev', 'version', 'ranges']),
deleteDoc,
getAllDocs,
getAllDeletedDocs,
getAllRanges,
getDoc,
getCommentThreadIds,
getTrackedChangesUserIds,
isDocDeleted: promisify(isDocDeleted),
updateDoc: promisifyMultiResult(updateDoc, ['modified', 'rev']),
projectHasRanges: promisify(projectHasRanges),
archiveProject: promisify(archiveProject),
unarchiveProject: promisify(unarchiveProject),
destroyProject: promisify(destroyProject),
isDocDeleted,
updateDoc,
projectHasRanges,
archiveProject,
unarchiveProject,
destroyProject,
},
}
@@ -1,7 +1,8 @@
import { beforeAll, beforeEach, describe, it, vi, expect } from 'vitest'
import sinon from 'sinon'
import { assert, beforeAll, beforeEach, describe, it, vi, expect } from 'vitest'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
import tk from 'timekeeper'
import { RequestFailedError } from '@overleaf/fetch-utils'
const modulePath = '../../../../app/src/Features/Docstore/DocstoreManager'
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
@@ -9,31 +10,33 @@ vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
)
describe('DocstoreManager', function () {
beforeEach(async function (ctx) {
ctx.requestDefaults = sinon.stub().returns((ctx.request = sinon.stub()))
let DocstoreManager, FetchUtils, projectId, docId, settings
vi.doMock('request', () => ({
default: {
defaults: ctx.requestDefaults,
beforeEach(async function () {
settings = {
apis: {
docstore: {
url: 'http://docstore.overleaf.com',
},
},
}))
}
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
apis: {
docstore: {
url: 'docstore.overleaf.com',
},
},
}),
default: settings,
}))
ctx.DocstoreManager = (await import(modulePath)).default
FetchUtils = {
fetchNothing: vi.fn().mockResolvedValue(),
fetchJson: vi.fn().mockResolvedValue({}),
RequestFailedError,
}
ctx.requestDefaults.calledWith({ jar: false }).should.equal(true)
vi.doMock('@overleaf/fetch-utils', () => FetchUtils)
ctx.project_id = 'project-id-123'
ctx.doc_id = 'doc-id-123'
DocstoreManager = (await import(modulePath)).default
projectId = 'project-id-123'
docId = 'doc-id-123'
})
describe('deleteDoc', function () {
@@ -46,43 +49,40 @@ describe('DocstoreManager', function () {
tk.reset()
})
beforeEach(async function (ctx) {
ctx.request.patch = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, '')
await ctx.DocstoreManager.promises.deleteDoc(
ctx.project_id,
ctx.doc_id,
beforeEach(async function () {
await DocstoreManager.promises.deleteDoc(
projectId,
docId,
'wombat.tex',
new Date()
)
})
it('should delete the doc in the docstore api', function (ctx) {
ctx.request.patch
.calledWith({
url: `${ctx.settings.apis.docstore.url}/project/${ctx.project_id}/doc/${ctx.doc_id}`,
json: { deleted: true, deletedAt: new Date(), name: 'wombat.tex' },
timeout: 30 * 1000,
})
.should.equal(true)
it('should delete the doc in the docstore api', function () {
const url = new URL(settings.apis.docstore.url)
url.pathname = `/project/${projectId}/doc/${docId}`
expect(FetchUtils.fetchNothing).toHaveBeenCalledWith(url, {
json: { deleted: true, deletedAt: new Date(), name: 'wombat.tex' },
signal: expect.anything(),
method: 'PATCH',
})
})
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.patch = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
beforeEach(function () {
FetchUtils.fetchNothing.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.deleteDoc(
ctx.project_id,
ctx.doc_id,
await DocstoreManager.promises.deleteDoc(
projectId,
docId,
'main.tex',
new Date()
)
@@ -99,18 +99,18 @@ describe('DocstoreManager', function () {
})
describe('with a missing (404) response code', function () {
beforeEach(function (ctx) {
ctx.request.patch = sinon
.stub()
.callsArgWith(1, null, { statusCode: 404 }, '')
beforeEach(function () {
FetchUtils.fetchNothing.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 404 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.deleteDoc(
ctx.project_id,
ctx.doc_id,
await DocstoreManager.promises.deleteDoc(
projectId,
docId,
'main.tex',
new Date()
)
@@ -128,74 +128,72 @@ describe('DocstoreManager', function () {
})
describe('updateDoc', function () {
beforeEach(function (ctx) {
ctx.lines = ['mock', 'doc', 'lines']
ctx.rev = 5
ctx.version = 42
ctx.ranges = { mock: 'ranges' }
ctx.modified = true
let lines, modified, ranges, rev, updateDocResponse, version
beforeEach(function () {
lines = ['mock', 'doc', 'lines']
rev = 5
version = 42
ranges = { mock: 'ranges' }
modified = true
})
describe('with a successful response code', async function () {
beforeEach(async function (ctx) {
ctx.request.post = sinon
.stub()
.callsArgWith(
1,
null,
{ statusCode: 204 },
{ modified: ctx.modified, rev: ctx.rev }
)
ctx.updateDocResponse = await ctx.DocstoreManager.promises.updateDoc(
ctx.project_id,
ctx.doc_id,
ctx.lines,
ctx.version,
ctx.ranges
beforeEach(async function () {
FetchUtils.fetchJson.mockResolvedValue({
modified,
rev,
})
updateDocResponse = await DocstoreManager.promises.updateDoc(
projectId,
docId,
lines,
version,
ranges
)
})
it('should update the doc in the docstore api', function (ctx) {
ctx.request.post
.calledWith({
url: `${ctx.settings.apis.docstore.url}/project/${ctx.project_id}/doc/${ctx.doc_id}`,
timeout: 30 * 1000,
it('should update the doc in the docstore api', function () {
expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
new URL(
`${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
),
{
signal: expect.anything(),
method: 'POST',
json: {
lines: ctx.lines,
version: ctx.version,
ranges: ctx.ranges,
lines,
version,
ranges,
},
})
.should.equal(true)
}
)
})
it('should return the modified status and revision', function (ctx) {
expect(ctx.updateDocResponse).to.haveOwnProperty(
'modified',
ctx.modified
)
expect(ctx.updateDocResponse).to.haveOwnProperty('rev', ctx.rev)
it('should return the modified status and revision', function () {
expect(updateDocResponse).to.haveOwnProperty('modified', modified)
expect(updateDocResponse).to.haveOwnProperty('rev', rev)
})
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
beforeEach(function () {
FetchUtils.fetchJson.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.updateDoc(
ctx.project_id,
ctx.doc_id,
ctx.lines,
ctx.version,
ctx.ranges
await DocstoreManager.promises.updateDoc(
projectId,
docId,
lines,
version,
ranges
)
assert.fail('updateDoc should have thrown an error')
} catch (err) {
error = err
}
@@ -210,56 +208,60 @@ describe('DocstoreManager', function () {
})
describe('getDoc', function () {
beforeEach(function (ctx) {
ctx.doc = {
lines: (ctx.lines = ['mock', 'doc', 'lines']),
rev: (ctx.rev = 5),
version: (ctx.version = 42),
ranges: (ctx.ranges = { mock: 'ranges' }),
let doc, getDocResponse, lines, ranges, rev, version
beforeEach(function () {
lines = ['mock', 'doc', 'lines']
rev = 5
version = 42
ranges = { mock: 'ranges' }
doc = {
lines,
rev,
version,
ranges,
}
})
describe('with a successful response code', function () {
beforeEach(async function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, ctx.doc)
ctx.getDocResponse = await ctx.DocstoreManager.promises.getDoc(
ctx.project_id,
ctx.doc_id
beforeEach(async function () {
FetchUtils.fetchJson.mockResolvedValue(doc)
getDocResponse = await DocstoreManager.promises.getDoc(projectId, docId)
})
it('should get the doc from the docstore api', function () {
expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
new URL(
`${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
),
{
signal: expect.anything(),
}
)
})
it('should get the doc from the docstore api', function (ctx) {
ctx.request.get.should.have.been.calledWith({
url: `${ctx.settings.apis.docstore.url}/project/${ctx.project_id}/doc/${ctx.doc_id}`,
timeout: 30 * 1000,
json: true,
})
})
it('should resolve with the lines, version and rev', function (ctx) {
expect(ctx.getDocResponse).to.eql({
lines: ctx.lines,
rev: ctx.rev,
version: ctx.version,
ranges: ctx.ranges,
it('should resolve with the lines, version and rev', function () {
expect(getDocResponse).to.eql({
lines,
rev,
version,
ranges,
})
})
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
beforeEach(function () {
FetchUtils.fetchJson.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.getDoc(ctx.project_id, ctx.doc_id)
await DocstoreManager.promises.getDoc(projectId, docId)
assert.fail('getDoc should have thrown an error')
} catch (err) {
error = err
}
@@ -273,67 +275,68 @@ describe('DocstoreManager', function () {
})
describe('with include_deleted=true', function () {
beforeEach(async function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, ctx.doc)
ctx.getDocResponse = await ctx.DocstoreManager.promises.getDoc(
ctx.project_id,
ctx.doc_id,
beforeEach(async function () {
FetchUtils.fetchJson.mockResolvedValue(doc)
getDocResponse = await DocstoreManager.promises.getDoc(
projectId,
docId,
{ include_deleted: true }
)
})
it('should get the doc from the docstore api (including deleted)', function (ctx) {
ctx.request.get.should.have.been.calledWith({
url: `${ctx.settings.apis.docstore.url}/project/${ctx.project_id}/doc/${ctx.doc_id}`,
qs: { include_deleted: 'true' },
timeout: 30 * 1000,
json: true,
})
it('should get the doc from the docstore api (including deleted)', function () {
expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
new URL(
`${settings.apis.docstore.url}/project/${projectId}/doc/${docId}?include_deleted=true`
),
{
signal: expect.anything(),
}
)
})
it('should resolve with the lines, version and rev', function (ctx) {
expect(ctx.getDocResponse).to.eql({
lines: ctx.lines,
rev: ctx.rev,
version: ctx.version,
ranges: ctx.ranges,
it('should resolve with the lines, version and rev', function () {
expect(getDocResponse).to.eql({
lines,
rev,
version,
ranges,
})
})
})
describe('with peek=true', function () {
beforeEach(async function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 }, ctx.doc)
await ctx.DocstoreManager.promises.getDoc(ctx.project_id, ctx.doc_id, {
beforeEach(async function () {
await DocstoreManager.promises.getDoc(projectId, docId, {
peek: true,
})
})
it('should call the docstore peek url', function (ctx) {
ctx.request.get.should.have.been.calledWith({
url: `${ctx.settings.apis.docstore.url}/project/${ctx.project_id}/doc/${ctx.doc_id}/peek`,
timeout: 30 * 1000,
json: true,
})
it('should call the docstore peek url', function () {
expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
new URL(
`${settings.apis.docstore.url}/project/${projectId}/doc/${docId}/peek`
),
{
signal: expect.anything(),
}
)
})
})
describe('with a missing (404) response code', function () {
beforeEach(function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 404 }, '')
beforeEach(function () {
FetchUtils.fetchJson.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 404 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.getDoc(ctx.project_id, ctx.doc_id)
await DocstoreManager.promises.getDoc(projectId, docId)
assert.fail('getDoc should have thrown an error')
} catch (err) {
error = err
}
@@ -347,47 +350,40 @@ describe('DocstoreManager', function () {
describe('getAllDocs', function () {
describe('with a successful response code', function () {
let getAllDocsResult
beforeEach(async function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(
1,
null,
{ statusCode: 204 },
(ctx.docs = [{ _id: 'mock-doc-id' }])
)
getAllDocsResult = await ctx.DocstoreManager.promises.getAllDocs(
ctx.project_id
let docs
beforeEach(async function () {
docs = [{ _id: 'mock-doc-id' }]
FetchUtils.fetchJson.mockResolvedValue(docs)
getAllDocsResult = await DocstoreManager.promises.getAllDocs(projectId)
})
it('should get all the project docs in the docstore api', function () {
expect(FetchUtils.fetchJson).toBeCalledWith(
new URL(`${settings.apis.docstore.url}/project/${projectId}/doc`),
{
signal: expect.anything(),
}
)
})
it('should get all the project docs in the docstore api', function (ctx) {
ctx.request.get
.calledWith({
url: `${ctx.settings.apis.docstore.url}/project/${ctx.project_id}/doc`,
timeout: 30 * 1000,
json: true,
})
.should.equal(true)
})
it('should return the docs', function (ctx) {
expect(getAllDocsResult).to.eql(ctx.docs)
it('should return the docs', function () {
expect(getAllDocsResult).to.eql(docs)
})
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
beforeEach(function () {
FetchUtils.fetchJson.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.getAllDocs(ctx.project_id)
await DocstoreManager.promises.getAllDocs(projectId)
assert.fail('getAllDocs should have thrown an error')
} catch (err) {
error = err
}
@@ -404,40 +400,41 @@ describe('DocstoreManager', function () {
describe('getAllDeletedDocs', function () {
describe('with a successful response code', function () {
let getAllDeletedDocsResponse
beforeEach(async function (ctx) {
ctx.docs = [{ _id: 'mock-doc-id', name: 'foo.tex' }]
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 200 }, ctx.docs)
let docs
beforeEach(async function () {
docs = [{ _id: 'mock-doc-id', name: 'foo.tex' }]
FetchUtils.fetchJson.mockResolvedValue(docs)
getAllDeletedDocsResponse =
await ctx.DocstoreManager.promises.getAllDeletedDocs(ctx.project_id)
await DocstoreManager.promises.getAllDeletedDocs(projectId)
})
it('should get all the project docs in the docstore api', function (ctx) {
ctx.request.get.should.have.been.calledWith({
url: `${ctx.settings.apis.docstore.url}/project/${ctx.project_id}/doc-deleted`,
timeout: 30 * 1000,
json: true,
})
it('should get all the project docs in the docstore api', function () {
expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
new URL(
`${settings.apis.docstore.url}/project/${projectId}/doc-deleted`
),
{
signal: expect.anything(),
}
)
})
it('should resolve with the docs', function (ctx) {
expect(getAllDeletedDocsResponse).to.eql(ctx.docs)
it('should resolve with the docs', function () {
expect(getAllDeletedDocsResponse).to.eql(docs)
})
})
describe('with an error', function () {
beforeEach(async function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, new Error('connect failed'))
beforeEach(async function () {
FetchUtils.fetchJson.mockRejectedValue(new Error('connect failed'))
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.getAllDocs(ctx.project_id)
await DocstoreManager.promises.getAllDeletedDocs(projectId)
assert.fail('getAllDeletedDocs should have thrown an error')
} catch (err) {
error = err
}
@@ -448,17 +445,18 @@ describe('DocstoreManager', function () {
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 })
beforeEach(function () {
FetchUtils.fetchJson.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.getAllDocs(ctx.project_id)
await DocstoreManager.promises.getAllDeletedDocs(projectId)
assert.fail('getAllDeletedDocs should have thrown an error')
} catch (err) {
error = err
}
@@ -475,47 +473,41 @@ describe('DocstoreManager', function () {
describe('getAllRanges', function () {
describe('with a successful response code', function () {
let getAllRangesResult
beforeEach(async function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(
1,
null,
{ statusCode: 204 },
(ctx.docs = [{ _id: 'mock-doc-id', ranges: 'mock-ranges' }])
)
getAllRangesResult = await ctx.DocstoreManager.promises.getAllRanges(
ctx.project_id
let docs
beforeEach(async function () {
docs = [{ _id: 'mock-doc-id', ranges: 'mock-ranges' }]
FetchUtils.fetchJson.mockResolvedValue(docs)
getAllRangesResult =
await DocstoreManager.promises.getAllRanges(projectId)
})
it('should get all the project doc ranges in the docstore api', function () {
expect(FetchUtils.fetchJson).toHaveBeenCalledWith(
new URL(`${settings.apis.docstore.url}/project/${projectId}/ranges`),
{
signal: expect.anything(),
}
)
})
it('should get all the project doc ranges in the docstore api', function (ctx) {
ctx.request.get
.calledWith({
url: `${ctx.settings.apis.docstore.url}/project/${ctx.project_id}/ranges`,
timeout: 30 * 1000,
json: true,
})
.should.equal(true)
})
it('should return the docs', async function (ctx) {
expect(getAllRangesResult).to.eql(ctx.docs)
it('should return the docs', async function () {
expect(getAllRangesResult).to.eql(docs)
})
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.get = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 }, '')
beforeEach(function () {
FetchUtils.fetchJson.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.getAllRanges(ctx.project_id)
await DocstoreManager.promises.getAllRanges(projectId)
assert.fail('getAllRanges should have thrown an error')
} catch (err) {
error = err
}
@@ -531,31 +523,25 @@ describe('DocstoreManager', function () {
describe('archiveProject', function () {
describe('with a successful response code', function () {
beforeEach(function (ctx) {
ctx.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 })
})
it('should resolve', async function (ctx) {
await expect(
ctx.DocstoreManager.promises.archiveProject(ctx.project_id)
).to.eventually.be.fulfilled
it('should resolve', async function () {
await expect(DocstoreManager.promises.archiveProject(projectId)).to
.eventually.be.fulfilled
})
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 })
beforeEach(function () {
FetchUtils.fetchNothing.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.archiveProject(ctx.project_id)
await DocstoreManager.promises.archiveProject(projectId)
assert.fail('archiveProject should have thrown an error')
} catch (err) {
error = err
}
@@ -571,31 +557,25 @@ describe('DocstoreManager', function () {
describe('unarchiveProject', function () {
describe('with a successful response code', function () {
beforeEach(function (ctx) {
ctx.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 })
})
it('should resolve', async function (ctx) {
await expect(
ctx.DocstoreManager.promises.unarchiveProject(ctx.project_id)
).to.eventually.be.fulfilled
it('should resolve', async function () {
await expect(DocstoreManager.promises.unarchiveProject(projectId)).to
.eventually.be.fulfilled
})
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 })
beforeEach(function () {
FetchUtils.fetchNothing.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.unarchiveProject(ctx.project_id)
await DocstoreManager.promises.unarchiveProject(projectId)
assert.fail('unarchiveProject should have thrown an error')
} catch (err) {
error = err
}
@@ -611,31 +591,25 @@ describe('DocstoreManager', function () {
describe('destroyProject', function () {
describe('with a successful response code', function () {
beforeEach(function (ctx) {
ctx.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 204 })
})
it('should resolve', async function (ctx) {
await expect(
ctx.DocstoreManager.promises.destroyProject(ctx.project_id)
).to.eventually.be.fulfilled
it('should resolve', async function () {
await expect(DocstoreManager.promises.destroyProject(projectId)).to
.eventually.be.fulfilled
})
})
describe('with a failed response code', function () {
beforeEach(function (ctx) {
ctx.request.post = sinon
.stub()
.callsArgWith(1, null, { statusCode: 500 })
beforeEach(function () {
FetchUtils.fetchNothing.mockImplementation((url, opts) => {
throw new RequestFailedError(url, opts, { status: 500 })
})
})
it('should reject with an error', async function (ctx) {
it('should reject with an error', async function () {
let error
try {
await ctx.DocstoreManager.promises.destroyProject(ctx.project_id)
await DocstoreManager.promises.destroyProject(projectId)
assert.fail('destroyProject should have thrown an error')
} catch (err) {
error = err
}