diff --git a/services/web/app/src/infrastructure/Validation.js b/services/web/app/src/infrastructure/Validation.js index 72629ae64a..d572d9573f 100644 --- a/services/web/app/src/infrastructure/Validation.js +++ b/services/web/app/src/infrastructure/Validation.js @@ -9,6 +9,11 @@ const { zz, ParamsError, } = require('@overleaf/validation-tools') +const { isZodErrorLike, fromError } = require('zod-validation-error') + +/** + * @typedef {import('express').ErrorRequestHandler} ErrorRequestHandler + */ const objectIdValidator = { type: 'objectId', @@ -31,10 +36,21 @@ const objectIdValidator = { } const Joi = CelebrateJoi.extend(objectIdValidator) -const errorMiddleware = errors() +const errorMiddleware = [ + errors(), + /** @type {ErrorRequestHandler} */ + (err, req, res, next) => { + if (!isZodErrorLike(err)) { + return next(err) + } + + res.status(400).json({ ...fromError(err), statusCode: 400 }) + }, +] /** * Validation middleware + * @deprecated Please use Zod schemas and `validateReq` instead */ function validate(schema) { return celebrate(schema, { allowUnknown: true }) diff --git a/services/web/test/acceptance/src/ProjectCRUDTests.mjs b/services/web/test/acceptance/src/ProjectCRUDTests.mjs index a443f8852f..71bad436d0 100644 --- a/services/web/test/acceptance/src/ProjectCRUDTests.mjs +++ b/services/web/test/acceptance/src/ProjectCRUDTests.mjs @@ -190,7 +190,7 @@ describe('Project CRUD', function () { const project = await Project.findById(this.projectId).exec() expect(project.publicAccesLevel).to.equal('tokenBased') }) - it('returns a 400 when publicAccessLevel is set an unsupported access level', async function () { + it('returns a 400 when publicAccessLevel is an unsupported access level', async function () { await this.user.makePrivate(this.projectId) const { response, body } = await this.user.doRequest('POST', { url: `/project/${this.projectId}/settings/admin`, @@ -199,7 +199,7 @@ describe('Project CRUD', function () { }, }) expect(response.statusCode).to.equal(400) - expect(body).to.include('Unexpected access level') + expect(body.details[0].message).to.equal('unexpected access level') const project = await Project.findById(this.projectId).exec() expect(project.publicAccesLevel).to.equal('private') }) diff --git a/services/web/test/acceptance/src/ProjectInviteTests.mjs b/services/web/test/acceptance/src/ProjectInviteTests.mjs index 35294f5bfc..1eb9090436 100644 --- a/services/web/test/acceptance/src/ProjectInviteTests.mjs +++ b/services/web/test/acceptance/src/ProjectInviteTests.mjs @@ -375,8 +375,10 @@ describe('ProjectInviteTests', function () { return done(err) } expect(response.statusCode).to.equal(400) - expect(response.body.validation.body.message).to.equal( - '"email" must be a string' + expect(body.details).to.have.lengthOf(1) + expect(response.body.details[0].path).to.eql(['body', 'email']) + expect(response.body.details[0].message).to.equal( + 'Invalid input: expected string, received object' ) done() } @@ -402,8 +404,13 @@ describe('ProjectInviteTests', function () { return done(err) } expect(response.statusCode).to.equal(400) - expect(response.body.validation.body.message).to.equal( - '"privileges" must be one of [readOnly, readAndWrite, review]' + expect(body.details).to.have.lengthOf(1) + expect(response.body.details[0].path).to.eql([ + 'body', + 'privileges', + ]) + expect(response.body.details[0].message).to.equal( + 'Invalid option: expected one of "readOnly"|"readAndWrite"|"review"' ) done() }