Merge pull request #2985 from overleaf/msm-oerror-remove-unprocessable-entity-error

Replace UnprocessableEntityError with calls to unprocessableEntity() handler

GitOrigin-RevId: 4bba389c8cdf87a40137d49db571fa81aaac4239
This commit is contained in:
Miguel Serrano
2020-07-17 02:06:19 +00:00
committed by Copybot
parent 397bd034c7
commit 6562f3003d
8 changed files with 167 additions and 51 deletions
@@ -12,6 +12,7 @@ const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
const AuthenticationController = require('../Authentication/AuthenticationController') const AuthenticationController = require('../Authentication/AuthenticationController')
const Errors = require('../Errors/Errors') const Errors = require('../Errors/Errors')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
const { expressify } = require('../../util/promises') const { expressify } = require('../../util/promises')
@@ -272,11 +273,11 @@ async function convertDocToFile(req, res, next) {
info: { public: { message: 'Document not found' } } info: { public: { message: 'Document not found' } }
}) })
} else if (err instanceof Errors.DocHasRangesError) { } else if (err instanceof Errors.DocHasRangesError) {
throw new HttpErrors.UnprocessableEntityError({ return HttpErrorHandler.unprocessableEntity(
info: { req,
public: { message: 'Document has comments or tracked changes' } res,
} 'Document has comments or tracked changes'
}) )
} else { } else {
throw err throw err
} }
@@ -1,5 +1,16 @@
const logger = require('logger-sharelatex') const logger = require('logger-sharelatex')
function renderJSONError(res, message, info) {
const fullInfo = { message, ...info }
if (info.message) {
logger.warn(
info,
`http error info shouldn't contain a 'message' field, will be overridden`
)
}
res.json(fullInfo)
}
module.exports = { module.exports = {
forbidden(req, res, message = 'restricted', info = {}) { forbidden(req, res, message = 'restricted', info = {}) {
res.status(403) res.status(403)
@@ -7,16 +18,24 @@ module.exports = {
case 'html': case 'html':
return res.render('user/restricted', { title: 'restricted' }) return res.render('user/restricted', { title: 'restricted' })
case 'json': case 'json':
const fullInfo = { message, ...info } return renderJSONError(res, message, info)
if (info.message) {
logger.warn(
info,
`http error info shouldn't contain a 'message' field, will be overridden`
)
}
return res.json(fullInfo)
default: default:
return res.send('restricted') return res.send('restricted')
} }
},
unprocessableEntity(req, res, message = 'unprocessable entity', info = {}) {
res.status(422)
switch (req.accepts(['html', 'json'])) {
case 'html':
return res.render('general/400', {
title: 'Client Error',
message: message
})
case 'json':
return renderJSONError(res, message, info)
default:
return res.send('unprocessable entity')
}
} }
} }
@@ -30,7 +30,9 @@ const planFeatures = require('./planFeatures')
const GroupPlansData = require('./GroupPlansData') const GroupPlansData = require('./GroupPlansData')
const V1SubscriptionManager = require('./V1SubscriptionManager') const V1SubscriptionManager = require('./V1SubscriptionManager')
const Errors = require('../Errors/Errors') const Errors = require('../Errors/Errors')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const SubscriptionErrors = require('./Errors') const SubscriptionErrors = require('./Errors')
const OError = require('@overleaf/o-error')
const HttpErrors = require('@overleaf/o-error/http') const HttpErrors = require('@overleaf/o-error/http')
module.exports = SubscriptionController = { module.exports = SubscriptionController = {
@@ -82,11 +84,7 @@ module.exports = SubscriptionController = {
const user = AuthenticationController.getSessionUser(req) const user = AuthenticationController.getSessionUser(req)
const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
if (!plan) { if (!plan) {
return next( return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
new HttpErrors.UnprocessableEntityError({
info: { public: { message: 'Plan not found' } }
})
)
} }
return LimitationsManager.userHasV1OrV2Subscription(user, function( return LimitationsManager.userHasV1OrV2Subscription(user, function(
err, err,
@@ -224,13 +222,16 @@ module.exports = SubscriptionController = {
return res.sendStatus(201) return res.sendStatus(201)
} }
if (err instanceof SubscriptionErrors.RecurlyTransactionError) { if (
return next( err instanceof SubscriptionErrors.RecurlyTransactionError ||
new HttpErrors.UnprocessableEntityError({}).withCause(err) err instanceof Errors.InvalidError
) ) {
} else if (err instanceof Errors.InvalidError) { logger.warn(err)
return next( return HttpErrorHandler.unprocessableEntity(
new HttpErrors.UnprocessableEntityError({}).withCause(err) req,
res,
err.message,
OError.getFullInfo(err).public
) )
} }
@@ -13,6 +13,7 @@ const UserSessionsManager = require('./UserSessionsManager')
const UserUpdater = require('./UserUpdater') const UserUpdater = require('./UserUpdater')
const SudoModeHandler = require('../SudoMode/SudoModeHandler') const SudoModeHandler = require('../SudoMode/SudoModeHandler')
const Errors = require('../Errors/Errors') const Errors = require('../Errors/Errors')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
const HttpErrors = require('@overleaf/o-error/http') const HttpErrors = require('@overleaf/o-error/http')
const EmailHandler = require('../Email/EmailHandler') const EmailHandler = require('../Email/EmailHandler')
@@ -108,10 +109,12 @@ const UserController = {
errorData.info.public = { errorData.info.public = {
error: 'SubscriptionAdminDeletionError' error: 'SubscriptionAdminDeletionError'
} }
return next( logger.warn(new OError(errorData).withCause(err))
new HttpErrors.UnprocessableEntityError(errorData).withCause( return HttpErrorHandler.unprocessableEntity(
err req,
) res,
errorData.message,
errorData.info.public
) )
} else { } else {
return next(new OError(errorData).withCause(err)) return next(new OError(errorData).withCause(err))
@@ -122,6 +122,9 @@ describe('EditorHttpController', function() {
convertDocToFile: sinon.stub().resolves(this.file) convertDocToFile: sinon.stub().resolves(this.file)
} }
} }
this.HttpErrorHandler = {
unprocessableEntity: sinon.stub()
}
this.EditorHttpController = SandboxedModule.require(MODULE_PATH, { this.EditorHttpController = SandboxedModule.require(MODULE_PATH, {
globals: { globals: {
console: console console: console
@@ -144,6 +147,7 @@ describe('EditorHttpController', function() {
'../../infrastructure/FileWriter': this.FileWriter, '../../infrastructure/FileWriter': this.FileWriter,
'../Project/ProjectEntityUpdateHandler': this '../Project/ProjectEntityUpdateHandler': this
.ProjectEntityUpdateHandler, .ProjectEntityUpdateHandler,
'../Errors/HttpErrorHandler': this.HttpErrorHandler,
'../Errors/Errors': Errors, '../Errors/Errors': Errors,
'@overleaf/o-error/http': HttpErrors '@overleaf/o-error/http': HttpErrors
} }
@@ -540,14 +544,19 @@ describe('EditorHttpController', function() {
}) })
describe('when the doc has ranges', function() { describe('when the doc has ranges', function() {
it('should return a 422', function(done) { it('should return a 422 - Unprocessable Entity', function(done) {
this.ProjectEntityUpdateHandler.promises.convertDocToFile.rejects( this.ProjectEntityUpdateHandler.promises.convertDocToFile.rejects(
new Errors.DocHasRangesError({}) new Errors.DocHasRangesError({})
) )
this.EditorHttpController.convertDocToFile(this.req, this.res, err => { this.HttpErrorHandler.unprocessableEntity = sinon.spy(
expect(err).to.be.instanceof(HttpErrors.UnprocessableEntityError) (req, res, message) => {
done() expect(req).to.exist
}) expect(res).to.exist
expect(message).to.equal('Document has comments or tracked changes')
done()
}
)
this.EditorHttpController.convertDocToFile(this.req, this.res)
}) })
}) })
}) })
@@ -45,4 +45,42 @@ describe('HttpErrorHandler', function() {
}) })
}) })
}) })
describe('unprocessableEntity', function() {
it('returns 422', function() {
this.HttpErrorHandler.unprocessableEntity(this.req, this.res)
expect(this.res.statusCode).to.equal(422)
})
it('should print a message when no content-type is included', function() {
this.HttpErrorHandler.unprocessableEntity(this.req, this.res)
expect(this.res.body).to.equal('unprocessable entity')
})
it("should render a template including the error message when content-type is 'html'", function() {
this.req.accepts = () => 'html'
this.HttpErrorHandler.unprocessableEntity(this.req, this.res, 'an error')
expect(this.res.renderedTemplate).to.equal('general/400')
expect(this.res.renderedVariables).to.deep.equal({
title: 'Client Error',
message: 'an error'
})
})
it("should return a json object when content-type is 'json'", function() {
this.req.accepts = () => 'json'
this.HttpErrorHandler.unprocessableEntity(
this.req,
this.res,
'an error',
{
foo: 'bar'
}
)
expect(JSON.parse(this.res.body)).to.deep.equal({
message: 'an error',
foo: 'bar'
})
})
})
}) })
@@ -121,6 +121,9 @@ describe('SubscriptionController', function() {
'./FeaturesUpdater': (this.FeaturesUpdater = {}), './FeaturesUpdater': (this.FeaturesUpdater = {}),
'./GroupPlansData': (this.GroupPlansData = {}), './GroupPlansData': (this.GroupPlansData = {}),
'./V1SubscriptionManager': (this.V1SubscriptionManager = {}), './V1SubscriptionManager': (this.V1SubscriptionManager = {}),
'../Errors/HttpErrorHandler': (this.HttpErrorHandler = {
unprocessableEntity: sinon.stub()
}),
'../Errors/Errors': Errors, '../Errors/Errors': Errors,
'./Errors': SubscriptionErrors, './Errors': SubscriptionErrors,
'@overleaf/o-error/http': HttpErrors '@overleaf/o-error/http': HttpErrors
@@ -240,18 +243,21 @@ describe('SubscriptionController', function() {
}) })
describe('with an invalid plan code', function() { describe('with an invalid plan code', function() {
it('should return 422 error', function(done) { it('should return 422 error - Unprocessable Entity', function(done) {
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith( this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
1, 1,
null, null,
false false
) )
this.PlansLocator.findLocalPlanInSettings.returns(null) this.PlansLocator.findLocalPlanInSettings.returns(null)
this.next = error => { this.HttpErrorHandler.unprocessableEntity = sinon.spy(
expect(error).to.exist (req, res, message) => {
expect(error.statusCode).to.equal(422) expect(req).to.exist
done() expect(res).to.exist
} expect(message).to.deep.equal('Plan not found')
done()
}
)
return this.SubscriptionController.paymentPage( return this.SubscriptionController.paymentPage(
this.req, this.req,
this.res, this.res,
@@ -470,6 +476,39 @@ describe('SubscriptionController', function() {
}) })
return done() return done()
}) })
it('should handle recurly errors', function(done) {
this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
this.SubscriptionHandler.createSubscription.yields(
new SubscriptionErrors.RecurlyTransactionError({})
)
this.HttpErrorHandler.unprocessableEntity = sinon.spy(
(req, res, info) => {
expect(req).to.exist
expect(res).to.exist
expect(info).to.deep.equal('Unknown transaction error')
done()
}
)
return this.SubscriptionController.createSubscription(this.req, this.res)
})
it('should handle invalid error', function(done) {
this.LimitationsManager.userHasV1OrV2Subscription.yields(null, false)
this.SubscriptionHandler.createSubscription.yields(
new Errors.InvalidError({})
)
this.HttpErrorHandler.unprocessableEntity = sinon.spy((req, res) => {
expect(req).to.exist
expect(res).to.exist
done()
})
return this.SubscriptionController.createSubscription(this.req, this.res)
})
}) })
describe('updateSubscription via post', function() { describe('updateSubscription via post', function() {
@@ -70,6 +70,9 @@ describe('UserController', function() {
revokeAllUserSessions: sinon.stub().callsArgWith(2, null) revokeAllUserSessions: sinon.stub().callsArgWith(2, null)
} }
this.SudoModeHandler = { clearSudoMode: sinon.stub() } this.SudoModeHandler = { clearSudoMode: sinon.stub() }
this.HttpErrorHandler = {
unprocessableEntity: sinon.stub()
}
this.UserController = SandboxedModule.require(modulePath, { this.UserController = SandboxedModule.require(modulePath, {
globals: { globals: {
console: console console: console
@@ -95,6 +98,7 @@ describe('UserController', function() {
'./UserHandler': this.UserHandler, './UserHandler': this.UserHandler,
'./UserSessionsManager': this.UserSessionsManager, './UserSessionsManager': this.UserSessionsManager,
'../SudoMode/SudoModeHandler': this.SudoModeHandler, '../SudoMode/SudoModeHandler': this.SudoModeHandler,
'../Errors/HttpErrorHandler': this.HttpErrorHandler,
'settings-sharelatex': this.settings, 'settings-sharelatex': this.settings,
'logger-sharelatex': { 'logger-sharelatex': {
log() {}, log() {},
@@ -233,17 +237,19 @@ describe('UserController', function() {
.yields(new Errors.SubscriptionAdminDeletionError()) .yields(new Errors.SubscriptionAdminDeletionError())
}) })
it('should return a json error', function(done) { it('should return a HTTP Unprocessable Entity error', function(done) {
this.UserController.tryDeleteUser(this.req, null, error => { this.HttpErrorHandler.unprocessableEntity = sinon.spy(
expect(error).to.be.instanceof(HttpErrors.UnprocessableEntityError) (req, res, message, info) => {
expect( expect(req).to.exist
OError.hasCauseInstanceOf( expect(res).to.exist
error, expect(message).to.equal('error while deleting user account')
Errors.SubscriptionAdminDeletionError expect(info).to.deep.equal({
) error: 'SubscriptionAdminDeletionError'
).to.be.true })
done() done()
}) }
)
this.UserController.tryDeleteUser(this.req, this.res)
}) })
}) })