mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #6317 from overleaf/jpa-send-explicit-content-type
[web] send explicit content type in responses GitOrigin-RevId: d5aeaba57a7d2fc053fbf5adc2299fb46e435341
This commit is contained in:
parent
c97e95aeba
commit
d720d6affa
43 changed files with 390 additions and 224 deletions
|
@ -423,6 +423,10 @@ lint: lint_locales
|
||||||
lint_locales:
|
lint_locales:
|
||||||
bin/lint_locales
|
bin/lint_locales
|
||||||
|
|
||||||
|
lint: lint_flag_res_send_usage
|
||||||
|
lint_flag_res_send_usage:
|
||||||
|
bin/lint_flag_res_send_usage
|
||||||
|
|
||||||
lint_in_docker:
|
lint_in_docker:
|
||||||
$(RUN_LINT_FORMAT) make lint -j --output-sync
|
$(RUN_LINT_FORMAT) make lint -j --output-sync
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ module.exports = CaptchaMiddleware = {
|
||||||
{ statusCode: response.statusCode, body },
|
{ statusCode: response.statusCode, body },
|
||||||
'failed recaptcha siteverify request'
|
'failed recaptcha siteverify request'
|
||||||
)
|
)
|
||||||
return res.status(400).send({
|
return res.status(400).json({
|
||||||
errorReason: 'cannot_verify_user_not_robot',
|
errorReason: 'cannot_verify_user_not_robot',
|
||||||
message: {
|
message: {
|
||||||
text: 'Sorry, we could not verify that you are not a robot. Please check that Google reCAPTCHA is not being blocked by an ad blocker or firewall.',
|
text: 'Sorry, we could not verify that you are not a robot. Please check that Google reCAPTCHA is not being blocked by an ad blocker or firewall.',
|
||||||
|
|
|
@ -131,7 +131,7 @@ module.exports = CollaboratorsInviteController = {
|
||||||
{ projectId, email, sendingUserId },
|
{ projectId, email, sendingUserId },
|
||||||
'invalid email address'
|
'invalid email address'
|
||||||
)
|
)
|
||||||
return res.status(400).send({ errorReason: 'invalid_email' })
|
return res.status(400).json({ errorReason: 'invalid_email' })
|
||||||
}
|
}
|
||||||
return CollaboratorsInviteController._checkRateLimit(
|
return CollaboratorsInviteController._checkRateLimit(
|
||||||
sendingUserId,
|
sendingUserId,
|
||||||
|
|
|
@ -117,7 +117,7 @@ module.exports = CompileController = {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
return res.status(200).send()
|
return res.sendStatus(200)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -159,15 +159,12 @@ module.exports = CompileController = {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
res.contentType('application/json')
|
return res.json({
|
||||||
return res.status(200).send(
|
status,
|
||||||
JSON.stringify({
|
outputFiles,
|
||||||
status,
|
clsiServerId,
|
||||||
outputFiles,
|
validationProblems,
|
||||||
clsiServerId,
|
})
|
||||||
validationProblems,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -564,8 +561,7 @@ module.exports = CompileController = {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
res.contentType('application/json')
|
return res.json(body)
|
||||||
return res.send(body)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -70,7 +70,7 @@ module.exports = ContactsController = {
|
||||||
contacts = contacts.concat(
|
contacts = contacts.concat(
|
||||||
...Array.from(additional_contacts || [])
|
...Array.from(additional_contacts || [])
|
||||||
)
|
)
|
||||||
return res.send({
|
return res.json({
|
||||||
contacts,
|
contacts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ function contactsAuthenticationMiddleware() {
|
||||||
if (SessionManager.isUserLoggedIn(req.session)) {
|
if (SessionManager.isUserLoggedIn(req.session)) {
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
res.send({ contacts: [] })
|
res.json({ contacts: [] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
|
||||||
const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
|
const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
|
||||||
const logger = require('@overleaf/logger')
|
const logger = require('@overleaf/logger')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
|
const { plainTextResponse } = require('../../infrastructure/Response')
|
||||||
|
|
||||||
function getDocument(req, res, next) {
|
function getDocument(req, res, next) {
|
||||||
const { Project_id: projectId, doc_id: docId } = req.params
|
const { Project_id: projectId, doc_id: docId } = req.params
|
||||||
|
@ -47,8 +48,7 @@ function getDocument(req, res, next) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
if (plain) {
|
if (plain) {
|
||||||
res.type('text/plain')
|
plainTextResponse(res, lines.join('\n'))
|
||||||
res.send(lines.join('\n'))
|
|
||||||
} else {
|
} else {
|
||||||
const projectHistoryId = _.get(project, 'overleaf.history.id')
|
const projectHistoryId = _.get(project, 'overleaf.history.id')
|
||||||
const projectHistoryDisplay = _.get(
|
const projectHistoryDisplay = _.get(
|
||||||
|
|
|
@ -17,6 +17,7 @@ const Metrics = require('@overleaf/metrics')
|
||||||
const ProjectGetter = require('../Project/ProjectGetter')
|
const ProjectGetter = require('../Project/ProjectGetter')
|
||||||
const ProjectZipStreamManager = require('./ProjectZipStreamManager')
|
const ProjectZipStreamManager = require('./ProjectZipStreamManager')
|
||||||
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||||
|
const { prepareZipAttachment } = require('../../infrastructure/Response')
|
||||||
|
|
||||||
module.exports = ProjectDownloadsController = {
|
module.exports = ProjectDownloadsController = {
|
||||||
downloadProject(req, res, next) {
|
downloadProject(req, res, next) {
|
||||||
|
@ -41,10 +42,7 @@ module.exports = ProjectDownloadsController = {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
res.setContentDisposition('attachment', {
|
prepareZipAttachment(res, `${project.name}.zip`)
|
||||||
filename: `${project.name}.zip`,
|
|
||||||
})
|
|
||||||
res.contentType('application/zip')
|
|
||||||
return stream.pipe(res)
|
return stream.pipe(res)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -69,10 +67,10 @@ module.exports = ProjectDownloadsController = {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
res.setContentDisposition('attachment', {
|
prepareZipAttachment(
|
||||||
filename: `Overleaf Projects (${project_ids.length} items).zip`,
|
res,
|
||||||
})
|
`Overleaf Projects (${project_ids.length} items).zip`
|
||||||
res.contentType('application/zip')
|
)
|
||||||
return stream.pipe(res)
|
return stream.pipe(res)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ const logger = require('@overleaf/logger')
|
||||||
const SessionManager = require('../Authentication/SessionManager')
|
const SessionManager = require('../Authentication/SessionManager')
|
||||||
const SamlLogHandler = require('../SamlLog/SamlLogHandler')
|
const SamlLogHandler = require('../SamlLog/SamlLogHandler')
|
||||||
const HttpErrorHandler = require('./HttpErrorHandler')
|
const HttpErrorHandler = require('./HttpErrorHandler')
|
||||||
|
const { plainTextResponse } = require('../../infrastructure/Response')
|
||||||
|
|
||||||
module.exports = ErrorController = {
|
module.exports = ErrorController = {
|
||||||
notFound(req, res) {
|
notFound(req, res) {
|
||||||
|
@ -62,11 +63,11 @@ module.exports = ErrorController = {
|
||||||
} else if (error instanceof Errors.InvalidError) {
|
} else if (error instanceof Errors.InvalidError) {
|
||||||
logger.warn({ err: error, url: req.url }, 'invalid error')
|
logger.warn({ err: error, url: req.url }, 'invalid error')
|
||||||
res.status(400)
|
res.status(400)
|
||||||
res.send(error.message)
|
plainTextResponse(res, error.message)
|
||||||
} else if (error instanceof Errors.InvalidNameError) {
|
} else if (error instanceof Errors.InvalidNameError) {
|
||||||
logger.warn({ err: error, url: req.url }, 'invalid name error')
|
logger.warn({ err: error, url: req.url }, 'invalid name error')
|
||||||
res.status(400)
|
res.status(400)
|
||||||
res.send(error.message)
|
plainTextResponse(res, error.message)
|
||||||
} else if (error instanceof Errors.SAMLSessionDataMissing) {
|
} else if (error instanceof Errors.SAMLSessionDataMissing) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ err: error, url: req.url },
|
{ err: error, url: req.url },
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const logger = require('@overleaf/logger')
|
const logger = require('@overleaf/logger')
|
||||||
const Settings = require('@overleaf/settings')
|
const Settings = require('@overleaf/settings')
|
||||||
|
const { plainTextResponse } = require('../../infrastructure/Response')
|
||||||
|
|
||||||
function renderJSONError(res, message, info = {}) {
|
function renderJSONError(res, message, info = {}) {
|
||||||
if (info.message) {
|
if (info.message) {
|
||||||
|
@ -23,7 +24,7 @@ function handleGeneric500Error(req, res, statusCode, message) {
|
||||||
case 'json':
|
case 'json':
|
||||||
return renderJSONError(res, message)
|
return renderJSONError(res, message)
|
||||||
default:
|
default:
|
||||||
return res.send('internal server error')
|
return plainTextResponse(res, 'internal server error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ function handleGeneric400Error(req, res, statusCode, message, info = {}) {
|
||||||
case 'json':
|
case 'json':
|
||||||
return renderJSONError(res, message, info)
|
return renderJSONError(res, message, info)
|
||||||
default:
|
default:
|
||||||
return res.send('client error')
|
return plainTextResponse(res, 'client error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +91,7 @@ module.exports = HttpErrorHandler = {
|
||||||
case 'json':
|
case 'json':
|
||||||
return renderJSONError(res, message, info)
|
return renderJSONError(res, message, info)
|
||||||
default:
|
default:
|
||||||
return res.send('conflict')
|
return plainTextResponse(res, 'conflict')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -102,7 +103,7 @@ module.exports = HttpErrorHandler = {
|
||||||
case 'json':
|
case 'json':
|
||||||
return renderJSONError(res, message, info)
|
return renderJSONError(res, message, info)
|
||||||
default:
|
default:
|
||||||
return res.send('restricted')
|
return plainTextResponse(res, 'restricted')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -114,7 +115,7 @@ module.exports = HttpErrorHandler = {
|
||||||
case 'json':
|
case 'json':
|
||||||
return renderJSONError(res, message, info)
|
return renderJSONError(res, message, info)
|
||||||
default:
|
default:
|
||||||
return res.send('not found')
|
return plainTextResponse(res, 'not found')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -129,7 +130,7 @@ module.exports = HttpErrorHandler = {
|
||||||
case 'json':
|
case 'json':
|
||||||
return renderJSONError(res, message, info)
|
return renderJSONError(res, message, info)
|
||||||
default:
|
default:
|
||||||
return res.send('unprocessable entity')
|
return plainTextResponse(res, 'unprocessable entity')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -155,7 +156,7 @@ module.exports = HttpErrorHandler = {
|
||||||
case 'json':
|
case 'json':
|
||||||
return renderJSONError(res, message, {})
|
return renderJSONError(res, message, {})
|
||||||
default:
|
default:
|
||||||
return res.send(message)
|
return plainTextResponse(res, message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ const logger = require('@overleaf/logger')
|
||||||
const FileStoreHandler = require('./FileStoreHandler')
|
const FileStoreHandler = require('./FileStoreHandler')
|
||||||
const ProjectLocator = require('../Project/ProjectLocator')
|
const ProjectLocator = require('../Project/ProjectLocator')
|
||||||
const Errors = require('../Errors/Errors')
|
const Errors = require('../Errors/Errors')
|
||||||
|
const { preparePlainTextResponse } = require('../../infrastructure/Response')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getFile(req, res) {
|
getFile(req, res) {
|
||||||
|
@ -34,7 +35,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
// mobile safari will try to render html files, prevent this
|
// mobile safari will try to render html files, prevent this
|
||||||
if (isMobileSafari(userAgent) && isHtml(file)) {
|
if (isMobileSafari(userAgent) && isHtml(file)) {
|
||||||
res.setHeader('Content-Type', 'text/plain')
|
preparePlainTextResponse(res)
|
||||||
}
|
}
|
||||||
res.setContentDisposition('attachment', { filename: file.name })
|
res.setContentDisposition('attachment', { filename: file.name })
|
||||||
stream.pipe(res)
|
stream.pipe(res)
|
||||||
|
@ -57,7 +58,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res.set('Content-Length', fileSize)
|
res.setHeader('Content-Length', fileSize)
|
||||||
res.status(200).end()
|
res.status(200).end()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,6 @@ module.exports = {
|
||||||
check(req, res, next) {
|
check(req, res, next) {
|
||||||
if (!settings.siteIsOpen || !settings.editorIsOpen) {
|
if (!settings.siteIsOpen || !settings.editorIsOpen) {
|
||||||
// always return successful health checks when site is closed
|
// always return successful health checks when site is closed
|
||||||
res.contentType('application/json')
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
} else {
|
} else {
|
||||||
// detach from express for cleaner stack traces
|
// detach from express for cleaner stack traces
|
||||||
|
@ -92,9 +91,6 @@ module.exports = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function prettyJSON(blob) {
|
|
||||||
return JSON.stringify(blob, null, 2) + '\n'
|
|
||||||
}
|
|
||||||
async function runSmokeTestsDetached(req, res) {
|
async function runSmokeTestsDetached(req, res) {
|
||||||
function isAborted() {
|
function isAborted() {
|
||||||
return req.aborted
|
return req.aborted
|
||||||
|
@ -120,6 +116,5 @@ async function runSmokeTestsDetached(req, res) {
|
||||||
response = { stats, error: err.message }
|
response = { stats, error: err.message }
|
||||||
}
|
}
|
||||||
if (isAborted()) return
|
if (isAborted()) return
|
||||||
res.contentType('application/json')
|
res.status(status).json(response)
|
||||||
res.status(status).send(prettyJSON(response))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
|
||||||
const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
|
const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
|
||||||
const RestoreManager = require('./RestoreManager')
|
const RestoreManager = require('./RestoreManager')
|
||||||
const { pipeline } = require('stream')
|
const { pipeline } = require('stream')
|
||||||
|
const { prepareZipAttachment } = require('../../infrastructure/Response')
|
||||||
|
|
||||||
module.exports = HistoryController = {
|
module.exports = HistoryController = {
|
||||||
selectHistoryApi(req, res, next) {
|
selectHistoryApi(req, res, next) {
|
||||||
|
@ -414,10 +415,7 @@ module.exports = HistoryController = {
|
||||||
delete response.headers['content-disposition']
|
delete response.headers['content-disposition']
|
||||||
delete response.headers['content-type']
|
delete response.headers['content-type']
|
||||||
res.status(response.statusCode)
|
res.status(response.statusCode)
|
||||||
res.setContentDisposition('attachment', {
|
prepareZipAttachment(res, `${name}.zip`)
|
||||||
filename: `${name}.zip`,
|
|
||||||
})
|
|
||||||
res.contentType('application/zip')
|
|
||||||
pipeline(response, res, err => {
|
pipeline(response, res, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|
|
@ -26,7 +26,7 @@ module.exports = {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
} else {
|
} else {
|
||||||
return res.send(projectsDeactivated)
|
return res.json(projectsDeactivated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,6 +37,7 @@ const {
|
||||||
FileCannotRefreshError,
|
FileCannotRefreshError,
|
||||||
} = require('./LinkedFilesErrors')
|
} = require('./LinkedFilesErrors')
|
||||||
const Modules = require('../../infrastructure/Modules')
|
const Modules = require('../../infrastructure/Modules')
|
||||||
|
const { plainTextResponse } = require('../../infrastructure/Response')
|
||||||
|
|
||||||
module.exports = LinkedFilesController = {
|
module.exports = LinkedFilesController = {
|
||||||
Agents: _.extend(
|
Agents: _.extend(
|
||||||
|
@ -138,55 +139,70 @@ module.exports = LinkedFilesController = {
|
||||||
|
|
||||||
handleError(error, req, res, next) {
|
handleError(error, req, res, next) {
|
||||||
if (error instanceof AccessDeniedError) {
|
if (error instanceof AccessDeniedError) {
|
||||||
return res.status(403).send('You do not have access to this project')
|
res.status(403)
|
||||||
|
plainTextResponse(res, 'You do not have access to this project')
|
||||||
} else if (error instanceof BadDataError) {
|
} else if (error instanceof BadDataError) {
|
||||||
return res.status(400).send('The submitted data is not valid')
|
res.status(400)
|
||||||
|
plainTextResponse(res, 'The submitted data is not valid')
|
||||||
} else if (error instanceof BadEntityTypeError) {
|
} else if (error instanceof BadEntityTypeError) {
|
||||||
return res.status(400).send('The file is the wrong type')
|
res.status(400)
|
||||||
|
plainTextResponse(res, 'The file is the wrong type')
|
||||||
} else if (error instanceof SourceFileNotFoundError) {
|
} else if (error instanceof SourceFileNotFoundError) {
|
||||||
return res.status(404).send('Source file not found')
|
res.status(404)
|
||||||
|
plainTextResponse(res, 'Source file not found')
|
||||||
} else if (error instanceof ProjectNotFoundError) {
|
} else if (error instanceof ProjectNotFoundError) {
|
||||||
return res.status(404).send('Project not found')
|
res.status(404)
|
||||||
|
plainTextResponse(res, 'Project not found')
|
||||||
} else if (error instanceof V1ProjectNotFoundError) {
|
} else if (error instanceof V1ProjectNotFoundError) {
|
||||||
return res
|
res.status(409)
|
||||||
.status(409)
|
plainTextResponse(
|
||||||
.send(
|
res,
|
||||||
'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file'
|
'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file'
|
||||||
)
|
)
|
||||||
} else if (error instanceof CompileFailedError) {
|
} else if (error instanceof CompileFailedError) {
|
||||||
return res
|
res.status(422)
|
||||||
.status(422)
|
plainTextResponse(
|
||||||
.send(res.locals.translate('generic_linked_file_compile_error'))
|
res,
|
||||||
|
res.locals.translate('generic_linked_file_compile_error')
|
||||||
|
)
|
||||||
} else if (error instanceof OutputFileFetchFailedError) {
|
} else if (error instanceof OutputFileFetchFailedError) {
|
||||||
return res.status(404).send('Could not get output file')
|
res.status(404)
|
||||||
|
plainTextResponse(res, 'Could not get output file')
|
||||||
} else if (error instanceof UrlFetchFailedError) {
|
} else if (error instanceof UrlFetchFailedError) {
|
||||||
return res
|
res.status(422)
|
||||||
.status(422)
|
plainTextResponse(
|
||||||
.send(
|
res,
|
||||||
`Your URL could not be reached (${error.statusCode} status code). Please check it and try again.`
|
`Your URL could not be reached (${error.statusCode} status code). Please check it and try again.`
|
||||||
)
|
)
|
||||||
} else if (error instanceof InvalidUrlError) {
|
} else if (error instanceof InvalidUrlError) {
|
||||||
return res
|
res.status(422)
|
||||||
.status(422)
|
plainTextResponse(
|
||||||
.send('Your URL is not valid. Please check it and try again.')
|
res,
|
||||||
|
'Your URL is not valid. Please check it and try again.'
|
||||||
|
)
|
||||||
} else if (error instanceof NotOriginalImporterError) {
|
} else if (error instanceof NotOriginalImporterError) {
|
||||||
return res
|
res.status(400)
|
||||||
.status(400)
|
plainTextResponse(
|
||||||
.send('You are not the user who originally imported this file')
|
res,
|
||||||
|
'You are not the user who originally imported this file'
|
||||||
|
)
|
||||||
} else if (error instanceof FeatureNotAvailableError) {
|
} else if (error instanceof FeatureNotAvailableError) {
|
||||||
return res.status(400).send('This feature is not enabled on your account')
|
res.status(400)
|
||||||
|
plainTextResponse(res, 'This feature is not enabled on your account')
|
||||||
} else if (error instanceof RemoteServiceError) {
|
} else if (error instanceof RemoteServiceError) {
|
||||||
return res.status(502).send('The remote service produced an error')
|
res.status(502)
|
||||||
|
plainTextResponse(res, 'The remote service produced an error')
|
||||||
} else if (error instanceof FileCannotRefreshError) {
|
} else if (error instanceof FileCannotRefreshError) {
|
||||||
return res.status(400).send('This file cannot be refreshed')
|
res.status(400)
|
||||||
|
plainTextResponse(res, 'This file cannot be refreshed')
|
||||||
} else if (error.message === 'project_has_too_many_files') {
|
} else if (error.message === 'project_has_too_many_files') {
|
||||||
return res.status(400).send('too many files')
|
res.status(400)
|
||||||
|
plainTextResponse(res, 'too many files')
|
||||||
} else if (/\bECONNREFUSED\b/.test(error.message)) {
|
} else if (/\bECONNREFUSED\b/.test(error.message)) {
|
||||||
return res
|
res.status(500)
|
||||||
.status(500)
|
plainTextResponse(res, 'Importing references is not currently available')
|
||||||
.send('Importing references is not currently available')
|
|
||||||
} else {
|
} else {
|
||||||
return next(error)
|
next(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ module.exports = {
|
||||||
return notification
|
return notification
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
res.send(unreadNotifications)
|
res.json(unreadNotifications)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -249,7 +249,7 @@ const ProjectController = {
|
||||||
const { projectName } = req.body
|
const { projectName } = req.body
|
||||||
logger.log({ projectId, projectName }, 'cloning project')
|
logger.log({ projectId, projectName }, 'cloning project')
|
||||||
if (!SessionManager.isUserLoggedIn(req.session)) {
|
if (!SessionManager.isUserLoggedIn(req.session)) {
|
||||||
return res.send({ redir: '/register' })
|
return res.json({ redir: '/register' })
|
||||||
}
|
}
|
||||||
const currentUser = SessionManager.getSessionUser(req.session)
|
const currentUser = SessionManager.getSessionUser(req.session)
|
||||||
const { first_name: firstName, last_name: lastName, email } = currentUser
|
const { first_name: firstName, last_name: lastName, email } = currentUser
|
||||||
|
@ -265,7 +265,7 @@ const ProjectController = {
|
||||||
})
|
})
|
||||||
return next(err)
|
return next(err)
|
||||||
}
|
}
|
||||||
res.send({
|
res.json({
|
||||||
name: project.name,
|
name: project.name,
|
||||||
project_id: project._id,
|
project_id: project._id,
|
||||||
owner_ref: project.owner_ref,
|
owner_ref: project.owner_ref,
|
||||||
|
@ -306,7 +306,7 @@ const ProjectController = {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
return next(err)
|
return next(err)
|
||||||
}
|
}
|
||||||
res.send({
|
res.json({
|
||||||
project_id: project._id,
|
project_id: project._id,
|
||||||
owner_ref: project.owner_ref,
|
owner_ref: project.owner_ref,
|
||||||
owner: {
|
owner: {
|
||||||
|
|
|
@ -27,14 +27,14 @@ module.exports = {
|
||||||
if (url === '/check') {
|
if (url === '/check') {
|
||||||
if (!language) {
|
if (!language) {
|
||||||
logger.error('"language" field should be included for spell checking')
|
logger.error('"language" field should be included for spell checking')
|
||||||
return res.status(422).send(JSON.stringify({ misspellings: [] }))
|
return res.status(422).json({ misspellings: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!languageCodeIsSupported(language)) {
|
if (!languageCodeIsSupported(language)) {
|
||||||
// this log statement can be changed to 'error' once projects with
|
// this log statement can be changed to 'error' once projects with
|
||||||
// unsupported languages are removed from the DB
|
// unsupported languages are removed from the DB
|
||||||
logger.info({ language }, 'language not supported')
|
logger.info({ language }, 'language not supported')
|
||||||
return res.status(422).send(JSON.stringify({ misspellings: [] }))
|
return res.status(422).json({ misspellings: [] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ module.exports = ProjectUploadController = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return res.send({ success: true, project_id: project._id })
|
return res.json({ success: true, project_id: project._id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -77,7 +77,7 @@ module.exports = ProjectUploadController = {
|
||||||
{ projectId: project_id, fileName: name },
|
{ projectId: project_id, fileName: name },
|
||||||
'bad name when trying to upload file'
|
'bad name when trying to upload file'
|
||||||
)
|
)
|
||||||
return res.status(422).send({
|
return res.status(422).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'invalid_filename',
|
error: 'invalid_filename',
|
||||||
})
|
})
|
||||||
|
@ -106,20 +106,20 @@ module.exports = ProjectUploadController = {
|
||||||
'error uploading file'
|
'error uploading file'
|
||||||
)
|
)
|
||||||
if (error.name === 'InvalidNameError') {
|
if (error.name === 'InvalidNameError') {
|
||||||
return res.status(422).send({
|
return res.status(422).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'invalid_filename',
|
error: 'invalid_filename',
|
||||||
})
|
})
|
||||||
} else if (error.message === 'project_has_too_many_files') {
|
} else if (error.message === 'project_has_too_many_files') {
|
||||||
return res.status(422).send({
|
return res.status(422).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'project_has_too_many_files',
|
error: 'project_has_too_many_files',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return res.status(422).send({ success: false })
|
return res.status(422).json({ success: false })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return res.send({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
entity_id: entity != null ? entity._id : undefined,
|
entity_id: entity != null ? entity._id : undefined,
|
||||||
entity_type: entity != null ? entity.type : undefined,
|
entity_type: entity != null ? entity.type : undefined,
|
||||||
|
|
|
@ -14,6 +14,7 @@ const SessionManager = require('../Authentication/SessionManager')
|
||||||
const UserMembershipHandler = require('./UserMembershipHandler')
|
const UserMembershipHandler = require('./UserMembershipHandler')
|
||||||
const Errors = require('../Errors/Errors')
|
const Errors = require('../Errors/Errors')
|
||||||
const EmailHelper = require('../Helpers/EmailHelper')
|
const EmailHelper = require('../Helpers/EmailHelper')
|
||||||
|
const { csvAttachment } = require('../../infrastructure/Response')
|
||||||
const CSVParser = require('json2csv').Parser
|
const CSVParser = require('json2csv').Parser
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -146,9 +147,7 @@ module.exports = {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
const csvParser = new CSVParser({ fields })
|
const csvParser = new CSVParser({ fields })
|
||||||
res.header('Content-Disposition', 'attachment; filename=Group.csv')
|
csvAttachment(res, csvParser.parse(users), 'Group.csv')
|
||||||
res.contentType('text/csv')
|
|
||||||
return res.send(csvParser.parse(users))
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
48
services/web/app/src/infrastructure/Response.js
Normal file
48
services/web/app/src/infrastructure/Response.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
function csvAttachment(res, body, filename) {
|
||||||
|
if (!filename || !filename.endsWith('.csv')) {
|
||||||
|
throw new Error('filename must end with .csv')
|
||||||
|
}
|
||||||
|
// res.attachment sets both content-type and content-disposition headers.
|
||||||
|
res.attachment(filename)
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||||
|
res.send(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparePlainTextResponse(res) {
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||||
|
res.contentType('text/plain; charset=utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
function plainTextResponse(res, body) {
|
||||||
|
preparePlainTextResponse(res)
|
||||||
|
res.send(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
function xmlResponse(res, body) {
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||||
|
res.contentType('application/xml; charset=utf-8')
|
||||||
|
res.send(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareZipAttachment(res, filename) {
|
||||||
|
if (!filename || !filename.endsWith('.zip')) {
|
||||||
|
throw new Error('filename must end with .zip')
|
||||||
|
}
|
||||||
|
// res.attachment sets both content-type and content-disposition headers.
|
||||||
|
res.attachment(filename)
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||||
|
}
|
||||||
|
|
||||||
|
function zipAttachment(res, body, filename) {
|
||||||
|
prepareZipAttachment(res, filename)
|
||||||
|
res.send(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
csvAttachment,
|
||||||
|
plainTextResponse,
|
||||||
|
preparePlainTextResponse,
|
||||||
|
prepareZipAttachment,
|
||||||
|
xmlResponse,
|
||||||
|
zipAttachment,
|
||||||
|
}
|
|
@ -61,6 +61,7 @@ const {
|
||||||
const logger = require('@overleaf/logger')
|
const logger = require('@overleaf/logger')
|
||||||
const _ = require('underscore')
|
const _ = require('underscore')
|
||||||
const { expressify } = require('./util/promises')
|
const { expressify } = require('./util/promises')
|
||||||
|
const { plainTextResponse } = require('./infrastructure/Response')
|
||||||
|
|
||||||
module.exports = { initialize }
|
module.exports = { initialize }
|
||||||
|
|
||||||
|
@ -1018,23 +1019,27 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||||
AdminController.unregisterServiceWorker
|
AdminController.unregisterServiceWorker
|
||||||
)
|
)
|
||||||
|
|
||||||
privateApiRouter.get('/perfTest', (req, res) => res.send('hello'))
|
privateApiRouter.get('/perfTest', (req, res) => {
|
||||||
|
plainTextResponse(res, 'hello')
|
||||||
|
})
|
||||||
|
|
||||||
publicApiRouter.get('/status', (req, res) => {
|
publicApiRouter.get('/status', (req, res) => {
|
||||||
if (!Settings.siteIsOpen) {
|
if (!Settings.siteIsOpen) {
|
||||||
res.send('web site is closed (web)')
|
plainTextResponse(res, 'web site is closed (web)')
|
||||||
} else if (!Settings.editorIsOpen) {
|
} else if (!Settings.editorIsOpen) {
|
||||||
res.send('web editor is closed (web)')
|
plainTextResponse(res, 'web editor is closed (web)')
|
||||||
} else {
|
} else {
|
||||||
res.send('web sharelatex is alive (web)')
|
plainTextResponse(res, 'web sharelatex is alive (web)')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
privateApiRouter.get('/status', (req, res) =>
|
privateApiRouter.get('/status', (req, res) => {
|
||||||
res.send('web sharelatex is alive (api)')
|
plainTextResponse(res, 'web sharelatex is alive (api)')
|
||||||
)
|
})
|
||||||
|
|
||||||
// used by kubernetes health-check and acceptance tests
|
// used by kubernetes health-check and acceptance tests
|
||||||
webRouter.get('/dev/csrf', (req, res) => res.send(res.locals.csrfToken))
|
webRouter.get('/dev/csrf', (req, res) => {
|
||||||
|
plainTextResponse(res, res.locals.csrfToken)
|
||||||
|
})
|
||||||
|
|
||||||
publicApiRouter.get(
|
publicApiRouter.get(
|
||||||
'/health_check',
|
'/health_check',
|
||||||
|
@ -1085,7 +1090,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||||
const projectId = req.params.Project_id
|
const projectId = req.params.Project_id
|
||||||
const sendRes = _.once(function (statusCode, message) {
|
const sendRes = _.once(function (statusCode, message) {
|
||||||
res.status(statusCode)
|
res.status(statusCode)
|
||||||
res.send(message)
|
plainTextResponse(res, message)
|
||||||
ClsiCookieManager.clearServerId(projectId)
|
ClsiCookieManager.clearServerId(projectId)
|
||||||
}) // force every compile to a new server
|
}) // force every compile to a new server
|
||||||
// set a timeout
|
// set a timeout
|
||||||
|
|
47
services/web/bin/lint_flag_res_send_usage
Executable file
47
services/web/bin/lint_flag_res_send_usage
Executable file
|
@ -0,0 +1,47 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
POTENTIAL_SEND_USAGE=$(\
|
||||||
|
grep \
|
||||||
|
--files-with-matches \
|
||||||
|
--recursive \
|
||||||
|
app.js \
|
||||||
|
app/ \
|
||||||
|
modules/*/app \
|
||||||
|
test/acceptance/ \
|
||||||
|
modules/*/test/acceptance/ \
|
||||||
|
--regex "\.send\b" \
|
||||||
|
--regex "\bsend(" \
|
||||||
|
)
|
||||||
|
HELPER_MODULE="app/src/infrastructure/Response.js"
|
||||||
|
if [[ "$POTENTIAL_SEND_USAGE" == "$HELPER_MODULE" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for file in ${POTENTIAL_SEND_USAGE}; do
|
||||||
|
if [[ "$file" == "$HELPER_MODULE" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<MSG >&2
|
||||||
|
|
||||||
|
ERROR: $file contains a potential use of 'res.send'.
|
||||||
|
|
||||||
|
---
|
||||||
|
$(grep -n -C 3 "$file" --regex "\.send\b" --regex "\bsend(")
|
||||||
|
---
|
||||||
|
|
||||||
|
Using 'res.send' is prone to introducing XSS vulnerabilities.
|
||||||
|
|
||||||
|
Consider using 'res.json' or one of the helpers in $HELPER_MODULE.
|
||||||
|
|
||||||
|
If this is a false-positive, consider using a more specific name than 'send'
|
||||||
|
for your newly introduced function.
|
||||||
|
|
||||||
|
Links:
|
||||||
|
- https://github.com/overleaf/internal/issues/6268
|
||||||
|
|
||||||
|
MSG
|
||||||
|
exit 1
|
||||||
|
done
|
|
@ -224,6 +224,7 @@
|
||||||
"chai-exclude": "^2.0.3",
|
"chai-exclude": "^2.0.3",
|
||||||
"chaid": "^1.0.2",
|
"chaid": "^1.0.2",
|
||||||
"cheerio": "^1.0.0-rc.3",
|
"cheerio": "^1.0.0-rc.3",
|
||||||
|
"content-disposition": "^0.5.0",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"css-loader": "^3.5.2",
|
"css-loader": "^3.5.2",
|
||||||
"es6-promise": "^4.2.8",
|
"es6-promise": "^4.2.8",
|
||||||
|
|
|
@ -7,12 +7,15 @@ const Settings = require('@overleaf/settings')
|
||||||
const User = require('./helpers/User').promises
|
const User = require('./helpers/User').promises
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
|
const {
|
||||||
|
plainTextResponse,
|
||||||
|
} = require('../../../app/src/infrastructure/Response')
|
||||||
const LinkedUrlProxy = express()
|
const LinkedUrlProxy = express()
|
||||||
LinkedUrlProxy.get('/', (req, res, next) => {
|
LinkedUrlProxy.get('/', (req, res, next) => {
|
||||||
if (req.query.url === 'http://example.com/foo') {
|
if (req.query.url === 'http://example.com/foo') {
|
||||||
return res.send('foo foo foo')
|
return plainTextResponse(res, 'foo foo foo')
|
||||||
} else if (req.query.url === 'http://example.com/bar') {
|
} else if (req.query.url === 'http://example.com/bar') {
|
||||||
return res.send('bar bar bar')
|
return plainTextResponse(res, 'bar bar bar')
|
||||||
} else {
|
} else {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ class MockChatApi extends AbstractMockApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
getGlobalMessages(req, res) {
|
getGlobalMessages(req, res) {
|
||||||
res.send(this.projects[req.params.project_id] || [])
|
res.json(this.projects[req.params.project_id] || [])
|
||||||
}
|
}
|
||||||
|
|
||||||
sendGlobalMessage(req, res) {
|
sendGlobalMessage(req, res) {
|
||||||
|
@ -19,7 +19,7 @@ class MockChatApi extends AbstractMockApi {
|
||||||
}
|
}
|
||||||
this.projects[projectId] = this.projects[projectId] || []
|
this.projects[projectId] = this.projects[projectId] || []
|
||||||
this.projects[projectId].push(message)
|
this.projects[projectId].push(message)
|
||||||
res.sendStatus(201).send(Object.assign({ room_id: projectId }, message))
|
res.json(Object.assign({ room_id: projectId }, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
applyRoutes() {
|
applyRoutes() {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
const AbstractMockApi = require('./AbstractMockApi')
|
const AbstractMockApi = require('./AbstractMockApi')
|
||||||
|
const {
|
||||||
|
plainTextResponse,
|
||||||
|
} = require('../../../../app/src/infrastructure/Response')
|
||||||
|
|
||||||
class MockClsiApi extends AbstractMockApi {
|
class MockClsiApi extends AbstractMockApi {
|
||||||
static compile(req, res) {
|
static compile(req, res) {
|
||||||
res.status(200).send({
|
res.json({
|
||||||
compile: {
|
compile: {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
error: null,
|
error: null,
|
||||||
|
@ -36,9 +39,9 @@ class MockClsiApi extends AbstractMockApi {
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
const filename = req.params[0]
|
const filename = req.params[0]
|
||||||
if (filename === 'project.pdf') {
|
if (filename === 'project.pdf') {
|
||||||
res.status(200).send('mock-pdf')
|
plainTextResponse(res, 'mock-pdf')
|
||||||
} else if (filename === 'project.log') {
|
} else if (filename === 'project.log') {
|
||||||
res.status(200).send('mock-log')
|
plainTextResponse(res, 'mock-log')
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(404)
|
res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
@ -48,12 +51,12 @@ class MockClsiApi extends AbstractMockApi {
|
||||||
this.app.get(
|
this.app.get(
|
||||||
'/project/:project_id/user/:user_id/build/:build_id/output/:output_path',
|
'/project/:project_id/user/:user_id/build/:build_id/output/:output_path',
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
res.status(200).send('hello')
|
plainTextResponse(res, 'hello')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
this.app.get('/project/:project_id/status', (req, res) => {
|
this.app.get('/project/:project_id/status', (req, res) => {
|
||||||
res.status(200).send()
|
res.sendStatus(200)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const AbstractMockApi = require('./AbstractMockApi')
|
const AbstractMockApi = require('./AbstractMockApi')
|
||||||
|
const {
|
||||||
|
plainTextResponse,
|
||||||
|
} = require('../../../../app/src/infrastructure/Response')
|
||||||
|
|
||||||
class MockFilestoreApi extends AbstractMockApi {
|
class MockFilestoreApi extends AbstractMockApi {
|
||||||
reset() {
|
reset() {
|
||||||
|
@ -24,7 +27,7 @@ class MockFilestoreApi extends AbstractMockApi {
|
||||||
this.app.get('/project/:projectId/file/:fileId', (req, res) => {
|
this.app.get('/project/:projectId/file/:fileId', (req, res) => {
|
||||||
const { projectId, fileId } = req.params
|
const { projectId, fileId } = req.params
|
||||||
const { content } = this.files[projectId][fileId]
|
const { content } = this.files[projectId][fileId]
|
||||||
res.send(content)
|
plainTextResponse(res, content)
|
||||||
})
|
})
|
||||||
|
|
||||||
// handle file copying
|
// handle file copying
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
const AbstractMockApi = require('./AbstractMockApi')
|
const AbstractMockApi = require('./AbstractMockApi')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const { ObjectId } = require('mongodb')
|
const { ObjectId } = require('mongodb')
|
||||||
|
const {
|
||||||
|
plainTextResponse,
|
||||||
|
} = require('../../../../app/src/infrastructure/Response')
|
||||||
|
|
||||||
class MockProjectHistoryApi extends AbstractMockApi {
|
class MockProjectHistoryApi extends AbstractMockApi {
|
||||||
reset() {
|
reset() {
|
||||||
|
@ -64,7 +67,7 @@ class MockProjectHistoryApi extends AbstractMockApi {
|
||||||
const { projectId, version, pathname } = req.params
|
const { projectId, version, pathname } = req.params
|
||||||
const key = `${projectId}:${version}:${pathname}`
|
const key = `${projectId}:${version}:${pathname}`
|
||||||
if (this.oldFiles[key] != null) {
|
if (this.oldFiles[key] != null) {
|
||||||
res.send(this.oldFiles[key])
|
plainTextResponse(res, this.oldFiles[key])
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(404)
|
res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const AbstractMockApi = require('./AbstractMockApi')
|
const AbstractMockApi = require('./AbstractMockApi')
|
||||||
const SubscriptionController = require('../../../../app/src/Features/Subscription/SubscriptionController')
|
const SubscriptionController = require('../../../../app/src/Features/Subscription/SubscriptionController')
|
||||||
|
const { xmlResponse } = require('../../../../app/src/infrastructure/Response')
|
||||||
|
|
||||||
class MockRecurlyApi extends AbstractMockApi {
|
class MockRecurlyApi extends AbstractMockApi {
|
||||||
reset() {
|
reset() {
|
||||||
|
@ -28,9 +29,11 @@ class MockRecurlyApi extends AbstractMockApi {
|
||||||
this.app.get('/subscriptions/:id', (req, res) => {
|
this.app.get('/subscriptions/:id', (req, res) => {
|
||||||
const subscription = this.getMockSubscriptionById(req.params.id)
|
const subscription = this.getMockSubscriptionById(req.params.id)
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
res.status(404).end()
|
res.sendStatus(404)
|
||||||
} else {
|
} else {
|
||||||
res.send(`\
|
xmlResponse(
|
||||||
|
res,
|
||||||
|
`\
|
||||||
<subscription>
|
<subscription>
|
||||||
<plan><plan_code>${subscription.planCode}</plan_code></plan>
|
<plan><plan_code>${subscription.planCode}</plan_code></plan>
|
||||||
<currency>${subscription.currency}</currency>
|
<currency>${subscription.currency}</currency>
|
||||||
|
@ -42,22 +45,26 @@ class MockRecurlyApi extends AbstractMockApi {
|
||||||
<account href="accounts/${subscription.account.id}" />
|
<account href="accounts/${subscription.account.id}" />
|
||||||
<trial_ends_at type="datetime">${subscription.trial_ends_at}</trial_ends_at>
|
<trial_ends_at type="datetime">${subscription.trial_ends_at}</trial_ends_at>
|
||||||
</subscription>\
|
</subscription>\
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.get('/accounts/:id', (req, res) => {
|
this.app.get('/accounts/:id', (req, res) => {
|
||||||
const subscription = this.getMockSubscriptionByAccountId(req.params.id)
|
const subscription = this.getMockSubscriptionByAccountId(req.params.id)
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
res.status(404).end()
|
res.sendStatus(404)
|
||||||
} else {
|
} else {
|
||||||
res.send(`\
|
xmlResponse(
|
||||||
|
res,
|
||||||
|
`\
|
||||||
<account>
|
<account>
|
||||||
<account_code>${req.params.id}</account_code>
|
<account_code>${req.params.id}</account_code>
|
||||||
<hosted_login_token>${subscription.account.hosted_login_token}</hosted_login_token>
|
<hosted_login_token>${subscription.account.hosted_login_token}</hosted_login_token>
|
||||||
<email>${subscription.account.email}</email>
|
<email>${subscription.account.email}</email>
|
||||||
</account>\
|
</account>\
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -67,15 +74,18 @@ class MockRecurlyApi extends AbstractMockApi {
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
const subscription = this.getMockSubscriptionByAccountId(req.params.id)
|
const subscription = this.getMockSubscriptionByAccountId(req.params.id)
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
res.status(404).end()
|
res.sendStatus(404)
|
||||||
} else {
|
} else {
|
||||||
Object.assign(subscription.account, req.body.account)
|
Object.assign(subscription.account, req.body.account)
|
||||||
res.send(`\
|
xmlResponse(
|
||||||
|
res,
|
||||||
|
`\
|
||||||
<account>
|
<account>
|
||||||
<account_code>${req.params.id}</account_code>
|
<account_code>${req.params.id}</account_code>
|
||||||
<email>${subscription.account.email}</email>
|
<email>${subscription.account.email}</email>
|
||||||
</account>\
|
</account>\
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -83,15 +93,18 @@ class MockRecurlyApi extends AbstractMockApi {
|
||||||
this.app.get('/coupons/:code', (req, res) => {
|
this.app.get('/coupons/:code', (req, res) => {
|
||||||
const coupon = this.coupons[req.params.code]
|
const coupon = this.coupons[req.params.code]
|
||||||
if (!coupon) {
|
if (!coupon) {
|
||||||
res.status(404).end()
|
res.sendStatus(404)
|
||||||
} else {
|
} else {
|
||||||
res.send(`\
|
xmlResponse(
|
||||||
|
res,
|
||||||
|
`\
|
||||||
<coupon>
|
<coupon>
|
||||||
<coupon_code>${req.params.code}</coupon_code>
|
<coupon_code>${req.params.code}</coupon_code>
|
||||||
<name>${coupon.name || ''}</name>
|
<name>${coupon.name || ''}</name>
|
||||||
<description>${coupon.description || ''}</description>
|
<description>${coupon.description || ''}</description>
|
||||||
</coupon>\
|
</coupon>\
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -107,11 +120,14 @@ class MockRecurlyApi extends AbstractMockApi {
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(`\
|
xmlResponse(
|
||||||
|
res,
|
||||||
|
`\
|
||||||
<redemptions type="array">
|
<redemptions type="array">
|
||||||
${redemptionsListXml}
|
${redemptionsListXml}
|
||||||
</redemptions>\
|
</redemptions>\
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
const AbstractMockApi = require('./AbstractMockApi')
|
const AbstractMockApi = require('./AbstractMockApi')
|
||||||
const { EventEmitter } = require('events')
|
const { EventEmitter } = require('events')
|
||||||
|
const {
|
||||||
|
zipAttachment,
|
||||||
|
prepareZipAttachment,
|
||||||
|
} = require('../../../../app/src/infrastructure/Response')
|
||||||
|
|
||||||
class MockV1HistoryApi extends AbstractMockApi {
|
class MockV1HistoryApi extends AbstractMockApi {
|
||||||
reset() {
|
reset() {
|
||||||
|
@ -13,10 +17,10 @@ class MockV1HistoryApi extends AbstractMockApi {
|
||||||
this.app.get(
|
this.app.get(
|
||||||
'/api/projects/:project_id/version/:version/zip',
|
'/api/projects/:project_id/version/:version/zip',
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
res.header('content-disposition', 'attachment; name=project.zip')
|
zipAttachment(
|
||||||
res.header('content-type', 'application/octet-stream')
|
res,
|
||||||
res.send(
|
`Mock zip for ${req.params.project_id} at version ${req.params.version}`,
|
||||||
`Mock zip for ${req.params.project_id} at version ${req.params.version}`
|
'project.zip'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -27,13 +31,14 @@ class MockV1HistoryApi extends AbstractMockApi {
|
||||||
if (!(this.fakeZipCall++ > 0)) {
|
if (!(this.fakeZipCall++ > 0)) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
res.header('content-disposition', 'attachment; name=project.zip')
|
|
||||||
res.header('content-type', 'application/octet-stream')
|
|
||||||
if (req.params.version === '42') {
|
if (req.params.version === '42') {
|
||||||
return res.send(
|
return zipAttachment(
|
||||||
`Mock zip for ${req.params.project_id} at version ${req.params.version}`
|
res,
|
||||||
|
`Mock zip for ${req.params.project_id} at version ${req.params.version}`,
|
||||||
|
'project.zip'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
prepareZipAttachment(res, 'project.zip')
|
||||||
const writeChunk = () => {
|
const writeChunk = () => {
|
||||||
res.write('chunk' + this.sentChunks++)
|
res.write('chunk' + this.sentChunks++)
|
||||||
}
|
}
|
||||||
|
|
|
@ -481,7 +481,7 @@ describe('AuthenticationController', function () {
|
||||||
|
|
||||||
describe('requireOauth', function () {
|
describe('requireOauth', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.res.send = sinon.stub()
|
this.res.json = sinon.stub()
|
||||||
this.res.status = sinon.stub().returns(this.res)
|
this.res.status = sinon.stub().returns(this.res)
|
||||||
this.res.sendStatus = sinon.stub()
|
this.res.sendStatus = sinon.stub()
|
||||||
this.middleware = this.AuthenticationController.requireOauth()
|
this.middleware = this.AuthenticationController.requireOauth()
|
||||||
|
|
|
@ -784,7 +784,7 @@ describe('CompileController', function () {
|
||||||
.yields(null, { content: 'body' })
|
.yields(null, { content: 'body' })
|
||||||
this.req.params = { Project_id: this.project_id }
|
this.req.params = { Project_id: this.project_id }
|
||||||
this.req.query = { clsiserverid: 'node-42' }
|
this.req.query = { clsiserverid: 'node-42' }
|
||||||
this.res.send = sinon.stub()
|
this.res.json = sinon.stub()
|
||||||
this.res.contentType = sinon.stub()
|
this.res.contentType = sinon.stub()
|
||||||
return this.CompileController.wordCount(this.req, this.res, this.next)
|
return this.CompileController.wordCount(this.req, this.res, this.next)
|
||||||
})
|
})
|
||||||
|
@ -796,7 +796,7 @@ describe('CompileController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a 200 and body', function () {
|
it('should return a 200 and body', function () {
|
||||||
return this.res.send.calledWith({ content: 'body' }).should.equal(true)
|
return this.res.json.calledWith({ content: 'body' }).should.equal(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,7 @@ const sinon = require('sinon')
|
||||||
const { assert, expect } = require('chai')
|
const { assert, expect } = require('chai')
|
||||||
const modulePath = '../../../../app/src/Features/Contacts/ContactController.js'
|
const modulePath = '../../../../app/src/Features/Contacts/ContactController.js'
|
||||||
const SandboxedModule = require('sandboxed-module')
|
const SandboxedModule = require('sandboxed-module')
|
||||||
|
const MockResponse = require('../helpers/MockResponse')
|
||||||
|
|
||||||
describe('ContactController', function () {
|
describe('ContactController', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
@ -30,9 +31,7 @@ describe('ContactController', function () {
|
||||||
|
|
||||||
this.next = sinon.stub()
|
this.next = sinon.stub()
|
||||||
this.req = {}
|
this.req = {}
|
||||||
this.res = {}
|
this.res = new MockResponse()
|
||||||
this.res.status = sinon.stub().returns(this.req)
|
|
||||||
return (this.res.send = sinon.stub())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getContacts', function () {
|
describe('getContacts', function () {
|
||||||
|
@ -105,7 +104,7 @@ describe('ContactController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a formatted list of contacts in contact list order, without holding accounts', function () {
|
it('should return a formatted list of contacts in contact list order, without holding accounts', function () {
|
||||||
return this.res.send.args[0][0].contacts.should.deep.equal([
|
return this.res.json.args[0][0].contacts.should.deep.equal([
|
||||||
{
|
{
|
||||||
id: 'contact-1',
|
id: 'contact-1',
|
||||||
email: 'joe@example.com',
|
email: 'joe@example.com',
|
||||||
|
|
|
@ -46,8 +46,6 @@ describe('ProjectDownloadsController', function () {
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(1, null, this.stream)
|
.callsArgWith(1, null, this.stream)
|
||||||
this.req.params = { Project_id: this.project_id }
|
this.req.params = { Project_id: this.project_id }
|
||||||
this.res.contentType = sinon.stub()
|
|
||||||
this.res.header = sinon.stub()
|
|
||||||
this.project_name = 'project name with accênts'
|
this.project_name = 'project name with accênts'
|
||||||
this.ProjectGetter.getProject = sinon
|
this.ProjectGetter.getProject = sinon
|
||||||
.stub()
|
.stub()
|
||||||
|
@ -92,9 +90,11 @@ describe('ProjectDownloadsController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should name the downloaded file after the project', function () {
|
it('should name the downloaded file after the project', function () {
|
||||||
return this.res.setContentDisposition
|
this.res.headers.should.deep.equal({
|
||||||
.calledWith('attachment', { filename: `${this.project_name}.zip` })
|
'Content-Disposition': `attachment; filename="${this.project_name}.zip"`,
|
||||||
.should.equal(true)
|
'Content-Type': 'application/zip',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should record the action via Metrics', function () {
|
it('should record the action via Metrics', function () {
|
||||||
|
@ -110,8 +110,6 @@ describe('ProjectDownloadsController', function () {
|
||||||
.callsArgWith(1, null, this.stream)
|
.callsArgWith(1, null, this.stream)
|
||||||
this.project_ids = ['project-1', 'project-2']
|
this.project_ids = ['project-1', 'project-2']
|
||||||
this.req.query = { project_ids: this.project_ids.join(',') }
|
this.req.query = { project_ids: this.project_ids.join(',') }
|
||||||
this.res.contentType = sinon.stub()
|
|
||||||
this.res.header = sinon.stub()
|
|
||||||
this.DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon
|
this.DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(1)
|
.callsArgWith(1)
|
||||||
|
@ -146,11 +144,12 @@ describe('ProjectDownloadsController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should name the downloaded file after the project', function () {
|
it('should name the downloaded file after the project', function () {
|
||||||
return this.res.setContentDisposition
|
this.res.headers.should.deep.equal({
|
||||||
.calledWith('attachment', {
|
'Content-Disposition':
|
||||||
filename: 'Overleaf Projects (2 items).zip',
|
'attachment; filename="Overleaf Projects (2 items).zip"',
|
||||||
})
|
'Content-Type': 'application/zip',
|
||||||
.should.equal(true)
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should record the action via Metrics', function () {
|
it('should record the action via Metrics', function () {
|
||||||
|
|
|
@ -2,6 +2,7 @@ const { expect } = require('chai')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const SandboxedModule = require('sandboxed-module')
|
const SandboxedModule = require('sandboxed-module')
|
||||||
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
const Errors = require('../../../../app/src/Features/Errors/Errors')
|
||||||
|
const MockResponse = require('../helpers/MockResponse')
|
||||||
|
|
||||||
const MODULE_PATH =
|
const MODULE_PATH =
|
||||||
'../../../../app/src/Features/FileStore/FileStoreController.js'
|
'../../../../app/src/Features/FileStore/FileStoreController.js'
|
||||||
|
@ -33,12 +34,7 @@ describe('FileStoreController', function () {
|
||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
this.res = {
|
this.res = new MockResponse()
|
||||||
set: sinon.stub().returnsThis(),
|
|
||||||
setHeader: sinon.stub(),
|
|
||||||
setContentDisposition: sinon.stub(),
|
|
||||||
status: sinon.stub().returnsThis(),
|
|
||||||
}
|
|
||||||
this.file = { name: 'myfile.png' }
|
this.file = { name: 'myfile.png' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -108,9 +104,7 @@ describe('FileStoreController', function () {
|
||||||
describe('from a non-ios browser', function () {
|
describe('from a non-ios browser', function () {
|
||||||
it('should not set Content-Type', function (done) {
|
it('should not set Content-Type', function (done) {
|
||||||
this.stream.pipe = des => {
|
this.stream.pipe = des => {
|
||||||
this.res.setHeader
|
this.res.headers.should.deep.equal({})
|
||||||
.calledWith('Content-Type', 'text/plain')
|
|
||||||
.should.equal(false)
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
this.controller.getFile(this.req, this.res)
|
this.controller.getFile(this.req, this.res)
|
||||||
|
@ -128,9 +122,10 @@ describe('FileStoreController', function () {
|
||||||
|
|
||||||
it("should set Content-Type to 'text/plain'", function (done) {
|
it("should set Content-Type to 'text/plain'", function (done) {
|
||||||
this.stream.pipe = des => {
|
this.stream.pipe = des => {
|
||||||
this.res.setHeader
|
this.res.headers.should.deep.equal({
|
||||||
.calledWith('Content-Type', 'text/plain')
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
.should.equal(true)
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
})
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
this.controller.getFile(this.req, this.res)
|
this.controller.getFile(this.req, this.res)
|
||||||
|
@ -148,9 +143,10 @@ describe('FileStoreController', function () {
|
||||||
|
|
||||||
it("should set Content-Type to 'text/plain'", function (done) {
|
it("should set Content-Type to 'text/plain'", function (done) {
|
||||||
this.stream.pipe = des => {
|
this.stream.pipe = des => {
|
||||||
this.res.setHeader
|
this.res.headers.should.deep.equal({
|
||||||
.calledWith('Content-Type', 'text/plain')
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
.should.equal(true)
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
})
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
this.controller.getFile(this.req, this.res)
|
this.controller.getFile(this.req, this.res)
|
||||||
|
@ -183,9 +179,7 @@ describe('FileStoreController', function () {
|
||||||
|
|
||||||
it('Should not set the Content-type', function (done) {
|
it('Should not set the Content-type', function (done) {
|
||||||
this.stream.pipe = des => {
|
this.stream.pipe = des => {
|
||||||
this.res.setHeader
|
this.res.headers.should.deep.equal({})
|
||||||
.calledWith('Content-Type', 'text/plain')
|
|
||||||
.should.equal(false)
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
this.controller.getFile(this.req, this.res)
|
this.controller.getFile(this.req, this.res)
|
||||||
|
@ -208,7 +202,7 @@ describe('FileStoreController', function () {
|
||||||
|
|
||||||
this.res.end = () => {
|
this.res.end = () => {
|
||||||
expect(this.res.status.lastCall.args).to.deep.equal([200])
|
expect(this.res.status.lastCall.args).to.deep.equal([200])
|
||||||
expect(this.res.set.lastCall.args).to.deep.equal([
|
expect(this.res.header.lastCall.args).to.deep.equal([
|
||||||
'Content-Length',
|
'Content-Length',
|
||||||
expectedFileSize,
|
expectedFileSize,
|
||||||
])
|
])
|
||||||
|
|
|
@ -45,7 +45,7 @@ describe('NotificationsController', function () {
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(1, null, allNotifications)
|
.callsArgWith(1, null, allNotifications)
|
||||||
this.controller.getAllUnreadNotifications(this.req, {
|
this.controller.getAllUnreadNotifications(this.req, {
|
||||||
send: body => {
|
json: body => {
|
||||||
body.should.deep.equal(allNotifications)
|
body.should.deep.equal(allNotifications)
|
||||||
this.handler.getUserNotifications.calledWith(userId).should.equal(true)
|
this.handler.getUserNotifications.calledWith(userId).should.equal(true)
|
||||||
done()
|
done()
|
||||||
|
|
|
@ -343,7 +343,7 @@ describe('ProjectController', function () {
|
||||||
|
|
||||||
describe('cloneProject', function () {
|
describe('cloneProject', function () {
|
||||||
it('should call the project duplicator', function (done) {
|
it('should call the project duplicator', function (done) {
|
||||||
this.res.send = json => {
|
this.res.json = json => {
|
||||||
this.ProjectDuplicator.duplicate
|
this.ProjectDuplicator.duplicate
|
||||||
.calledWith(this.user, this.project_id, this.projectName)
|
.calledWith(this.user, this.project_id, this.projectName)
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
|
@ -357,7 +357,7 @@ describe('ProjectController', function () {
|
||||||
describe('newProject', function () {
|
describe('newProject', function () {
|
||||||
it('should call the projectCreationHandler with createExampleProject', function (done) {
|
it('should call the projectCreationHandler with createExampleProject', function (done) {
|
||||||
this.req.body.template = 'example'
|
this.req.body.template = 'example'
|
||||||
this.res.send = json => {
|
this.res.json = json => {
|
||||||
this.ProjectCreationHandler.createExampleProject
|
this.ProjectCreationHandler.createExampleProject
|
||||||
.calledWith(this.user._id, this.projectName)
|
.calledWith(this.user._id, this.projectName)
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
|
@ -371,7 +371,7 @@ describe('ProjectController', function () {
|
||||||
|
|
||||||
it('should call the projectCreationHandler with createBasicProject', function (done) {
|
it('should call the projectCreationHandler with createBasicProject', function (done) {
|
||||||
this.req.body.template = 'basic'
|
this.req.body.template = 'basic'
|
||||||
this.res.send = json => {
|
this.res.json = json => {
|
||||||
this.ProjectCreationHandler.createExampleProject.called.should.equal(
|
this.ProjectCreationHandler.createExampleProject.called.should.equal(
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
|
@ -43,7 +43,6 @@ describe('ReferencesController', function () {
|
||||||
}
|
}
|
||||||
this.res = new MockResponse()
|
this.res = new MockResponse()
|
||||||
this.res.json = sinon.stub()
|
this.res.json = sinon.stub()
|
||||||
this.res.send = sinon.stub()
|
|
||||||
this.res.sendStatus = sinon.stub()
|
this.res.sendStatus = sinon.stub()
|
||||||
return (this.fakeResponseData = {
|
return (this.fakeResponseData = {
|
||||||
projectId: this.projectId,
|
projectId: this.projectId,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const SandboxedModule = require('sandboxed-module')
|
const SandboxedModule = require('sandboxed-module')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
|
const MockResponse = require('../helpers/MockResponse')
|
||||||
const modulePath = require('path').join(
|
const modulePath = require('path').join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../../../../app/src/Features/Spelling/SpellingController.js'
|
'../../../../app/src/Features/Spelling/SpellingController.js'
|
||||||
|
@ -52,11 +53,7 @@ describe('SpellingController', function () {
|
||||||
headers: { Host: SPELLING_HOST },
|
headers: { Host: SPELLING_HOST },
|
||||||
}
|
}
|
||||||
|
|
||||||
this.res = {}
|
this.res = new MockResponse()
|
||||||
this.res.send = sinon.stub()
|
|
||||||
this.res.status = sinon.stub().returns(this.res)
|
|
||||||
this.res.end = sinon.stub()
|
|
||||||
this.res.json = sinon.stub()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('proxyRequestToSpellingApi', function () {
|
describe('proxyRequestToSpellingApi', function () {
|
||||||
|
@ -104,9 +101,7 @@ describe('SpellingController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an empty misspellings array', function () {
|
it('should return an empty misspellings array', function () {
|
||||||
this.res.send
|
this.res.json.calledWith({ misspellings: [] }).should.equal(true)
|
||||||
.calledWith(JSON.stringify({ misspellings: [] }))
|
|
||||||
.should.equal(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a 422 status', function () {
|
it('should return a 422 status', function () {
|
||||||
|
@ -142,9 +137,7 @@ describe('SpellingController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an empty misspellings array', function () {
|
it('should return an empty misspellings array', function () {
|
||||||
this.res.send
|
this.res.json.calledWith({ misspellings: [] }).should.equal(true)
|
||||||
.calledWith(JSON.stringify({ misspellings: [] }))
|
|
||||||
.should.equal(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a 422 status', function () {
|
it('should return a 422 status', function () {
|
||||||
|
|
|
@ -100,10 +100,12 @@ describe('ProjectUploadController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a successful response to the FileUploader client', function () {
|
it('should return a successful response to the FileUploader client', function () {
|
||||||
return expect(this.res.body).to.deep.equal({
|
return expect(this.res.body).to.deep.equal(
|
||||||
success: true,
|
JSON.stringify({
|
||||||
project_id: this.project_id,
|
success: true,
|
||||||
})
|
project_id: this.project_id,
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should record the time taken to do the upload', function () {
|
it('should record the time taken to do the upload', function () {
|
||||||
|
@ -200,11 +202,13 @@ describe('ProjectUploadController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a successful response to the FileUploader client', function () {
|
it('should return a successful response to the FileUploader client', function () {
|
||||||
return expect(this.res.body).to.deep.equal({
|
return expect(this.res.body).to.deep.equal(
|
||||||
success: true,
|
JSON.stringify({
|
||||||
entity_id: this.entity._id,
|
success: true,
|
||||||
entity_type: 'file',
|
entity_id: this.entity._id,
|
||||||
})
|
entity_type: 'file',
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should time the request', function () {
|
it('should time the request', function () {
|
||||||
|
@ -225,9 +229,11 @@ describe('ProjectUploadController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an unsuccessful response to the FileUploader client', function () {
|
it('should return an unsuccessful response to the FileUploader client', function () {
|
||||||
return expect(this.res.body).to.deep.equal({
|
return expect(this.res.body).to.deep.equal(
|
||||||
success: false,
|
JSON.stringify({
|
||||||
})
|
success: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -240,10 +246,12 @@ describe('ProjectUploadController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an unsuccessful response to the FileUploader client', function () {
|
it('should return an unsuccessful response to the FileUploader client', function () {
|
||||||
return expect(this.res.body).to.deep.equal({
|
return expect(this.res.body).to.deep.equal(
|
||||||
success: false,
|
JSON.stringify({
|
||||||
error: 'project_has_too_many_files',
|
success: false,
|
||||||
})
|
error: 'project_has_too_many_files',
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -254,10 +262,12 @@ describe('ProjectUploadController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a a non success response', function () {
|
it('should return a a non success response', function () {
|
||||||
return expect(this.res.body).to.deep.equal({
|
return expect(this.res.body).to.deep.equal(
|
||||||
success: false,
|
JSON.stringify({
|
||||||
error: 'invalid_filename',
|
success: false,
|
||||||
})
|
error: 'invalid_filename',
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -280,9 +280,6 @@ describe('UserMembershipController', function () {
|
||||||
this.req.entity = this.subscription
|
this.req.entity = this.subscription
|
||||||
this.req.entityConfig = EntityConfigs.groupManagers
|
this.req.entityConfig = EntityConfigs.groupManagers
|
||||||
this.res = new MockResponse()
|
this.res = new MockResponse()
|
||||||
this.res.contentType = sinon.stub()
|
|
||||||
this.res.header = sinon.stub()
|
|
||||||
this.res.send = sinon.stub()
|
|
||||||
return this.UserMembershipController.exportCsv(this.req, this.res)
|
return this.UserMembershipController.exportCsv(this.req, this.res)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -295,14 +292,14 @@ describe('UserMembershipController', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should set the correct content type on the request', function () {
|
it('should set the correct content type on the request', function () {
|
||||||
return assertCalledWith(this.res.contentType, 'text/csv')
|
return assertCalledWith(this.res.contentType, 'text/csv; charset=utf-8')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should name the exported csv file', function () {
|
it('should name the exported csv file', function () {
|
||||||
return assertCalledWith(
|
return assertCalledWith(
|
||||||
this.res.header,
|
this.res.header,
|
||||||
'Content-Disposition',
|
'Content-Disposition',
|
||||||
'attachment; filename=Group.csv'
|
'attachment; filename="Group.csv"'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,13 @@
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||||
*/
|
*/
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
|
const Path = require('path')
|
||||||
|
const contentDisposition = require('content-disposition')
|
||||||
|
|
||||||
class MockResponse {
|
class MockResponse {
|
||||||
static initClass() {
|
static initClass() {
|
||||||
|
// Added via ExpressLocals.
|
||||||
this.prototype.setContentDisposition = sinon.stub()
|
this.prototype.setContentDisposition = sinon.stub()
|
||||||
|
|
||||||
this.prototype.header = sinon.stub()
|
|
||||||
|
|
||||||
this.prototype.contentType = sinon.stub()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -28,6 +27,18 @@ class MockResponse {
|
||||||
this.returned = false
|
this.returned = false
|
||||||
this.headers = {}
|
this.headers = {}
|
||||||
this.locals = {}
|
this.locals = {}
|
||||||
|
|
||||||
|
sinon.spy(this, 'contentType')
|
||||||
|
sinon.spy(this, 'header')
|
||||||
|
sinon.spy(this, 'json')
|
||||||
|
sinon.spy(this, 'send')
|
||||||
|
sinon.spy(this, 'sendStatus')
|
||||||
|
sinon.spy(this, 'status')
|
||||||
|
sinon.spy(this, 'render')
|
||||||
|
}
|
||||||
|
|
||||||
|
header(field, val) {
|
||||||
|
this.headers[field] = val
|
||||||
}
|
}
|
||||||
|
|
||||||
render(template, variables) {
|
render(template, variables) {
|
||||||
|
@ -100,7 +111,7 @@ class MockResponse {
|
||||||
}
|
}
|
||||||
this.statusCode = status
|
this.statusCode = status
|
||||||
this.returned = true
|
this.returned = true
|
||||||
this.type = 'application/json'
|
this.contentType('application/json')
|
||||||
if (status >= 200 && status < 300) {
|
if (status >= 200 && status < 300) {
|
||||||
this.success = true
|
this.success = true
|
||||||
} else {
|
} else {
|
||||||
|
@ -120,7 +131,7 @@ class MockResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeader(header, value) {
|
setHeader(header, value) {
|
||||||
return (this.headers[header] = value)
|
this.header(header, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(timout) {
|
setTimeout(timout) {
|
||||||
|
@ -133,8 +144,29 @@ class MockResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachment(filename) {
|
||||||
|
switch (Path.extname(filename)) {
|
||||||
|
case '.csv':
|
||||||
|
this.contentType('text/csv; charset=utf-8')
|
||||||
|
break
|
||||||
|
case '.zip':
|
||||||
|
this.contentType('application/zip')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('unexpected extension')
|
||||||
|
}
|
||||||
|
this.header('Content-Disposition', contentDisposition(filename))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType(type) {
|
||||||
|
this.header('Content-Type', type)
|
||||||
|
this.type = type
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
type(type) {
|
type(type) {
|
||||||
return (this.type = type)
|
return this.contentType(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MockResponse.initClass()
|
MockResponse.initClass()
|
||||||
|
|
Loading…
Reference in a new issue