Merge pull request #36223 from overleaf/jpa-zod-web-core-2

[web] core-2: adopt strict zod schema and extend test coverage

GitOrigin-RevId: f26d349fefb6bf9e042cb39117395120c3f7c877
This commit is contained in:
Jakob Ackermann
2026-09-17 11:55:37 +00:00
committed by Copybot
parent e2564f356c
commit 55c23b6812
5 changed files with 233 additions and 32 deletions
@@ -0,0 +1,40 @@
import { expect } from 'chai'
import UserHelper from './helpers/User.mjs'
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
const User = UserHelper.promises
describe('References', function () {
let owner, projectId
beforeEach(async function () {
owner = new User()
await owner.login()
projectId = await owner.createProject('references-test', {
template: 'blank',
})
})
describe('POST /project/:Project_id/references/indexAll', function () {
it('should return the projectId and empty keys', async function () {
const { response, body } = await owner.doRequest('post', {
url: `/project/${projectId}/references/indexAll`,
json: { shouldBroadcast: false },
})
expect(response.statusCode).to.equal(200)
expect(body).to.deep.equal({ projectId, keys: [] })
})
it('should reject a malformed project id with 404', async function () {
const { response, body } = await owner.doRequest('post', {
url: `/project/not-an-object-id/references/indexAll`,
json: { shouldBroadcast: false },
})
expectValidationErrorRaw(
{ statusCode: response.statusCode, body },
404,
'Project_id'
)
})
})
})
@@ -1,5 +1,6 @@
import { beforeEach, describe, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, it, vi } from 'vitest'
import sinon from 'sinon'
import { setReqValidationModeForTests } from '@overleaf/validation-tools'
import MockRequest from '../helpers/MockRequest.mjs'
import MockResponse from '../helpers/MockResponse.mjs'
const modulePath =
@@ -7,7 +8,8 @@ const modulePath =
describe('ReferencesController', function () {
beforeEach(async function (ctx) {
ctx.projectId = '2222'
// Project_id is validated as a Mongo ObjectId
ctx.projectId = '507f191e810c19729de860ea'
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
@@ -142,5 +144,27 @@ describe('ReferencesController', function () {
})
})
})
describe('request validation', function () {
beforeEach(function () {
setReqValidationModeForTests('enforce')
})
afterEach(function () {
setReqValidationModeForTests(null)
})
it('should reject a malformed project id', function (ctx) {
ctx.req.params.Project_id = 'not-an-object-id'
;(() =>
ctx.controller.indexAll(ctx.req, ctx.res, ctx.next)).should.throw()
})
it('should reject an unrecognized body field', function (ctx) {
ctx.req.body.unexpectedField = 'nope'
;(() =>
ctx.controller.indexAll(ctx.req, ctx.res, ctx.next)).should.throw()
})
})
})
})
@@ -1,10 +1,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import mongodb from 'mongodb-legacy'
import sinon from 'sinon'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
import MockResponse from '../helpers/MockResponse.mjs'
import MockRequest from '../helpers/MockRequest.mjs'
import { asZodError } from '@overleaf/validation-tools/testUtils.js'
import {
getRawReqInput,
setReqValidationModeForTests,
} from '@overleaf/validation-tools'
const ObjectId = mongodb.ObjectId
@@ -132,7 +136,7 @@ describe('TpdsController', function () {
await new Promise(resolve => {
const res = new MockResponse(vi)
const req = new MockRequest(vi)
req.params.user_id = ctx.user_id
req.params = { ...getRawReqInput(req).params, user_id: ctx.user_id }
req.body = { projectName: 'foo' }
res.callback = err => {
if (err) resolve(err)
@@ -165,6 +169,10 @@ describe('TpdsController', function () {
}
})
afterEach(function () {
setReqValidationModeForTests(null)
})
it('should throw without any input', async function (ctx) {
const next = sinon.stub()
await ctx.TpdsController.resolveProject(ctx.req, {}, next)
@@ -278,6 +286,41 @@ describe('TpdsController', function () {
ctx.TpdsController.resolveProject(ctx.req, res)
})
})
it('should still resolve a body with an unrecognized field in log mode', async function (ctx) {
setReqValidationModeForTests('log')
await new Promise(resolve => {
ctx.req.body = { projectName: 'projectName', extra: 'nope' }
const res = {
json: payload => {
expect(payload).to.deep.equal({
status: 'success',
projectId: ctx.resolvedProject._id.toString(),
historyId: 42,
otMigrationStage: 1,
})
resolve()
},
}
ctx.TpdsController.resolveProject(ctx.req, res)
})
})
describe('when enforced', function () {
afterEach(function () {
setReqValidationModeForTests(null)
})
it('should reject a body with an unrecognized field', async function (ctx) {
setReqValidationModeForTests('enforce')
ctx.req.body = { projectName: 'projectName', extra: 'nope' }
const next = sinon.stub()
await ctx.TpdsController.resolveProject(ctx.req, {}, next)
expect(next).to.have.been.calledWithMatch({
name: 'InvalidRequestError',
})
})
})
})
describe('getting an update', function () {
@@ -286,7 +329,7 @@ describe('TpdsController', function () {
ctx.path = '/here.txt'
ctx.req = {
params: {
0: `${ctx.projectName}${ctx.path}`,
path: `${ctx.projectName}${ctx.path}`,
user_id: ctx.user_id,
project_id: '',
},
@@ -341,9 +384,11 @@ describe('TpdsController', function () {
it('should process the update with the update receiver by id', async function (ctx) {
await new Promise(resolve => {
const path = '/here.txt'
// project_id is validated as a Mongo ObjectId
const projectId = new ObjectId().toString()
const req = {
pause() {},
params: { 0: path, user_id: ctx.user_id, project_id: '123' },
params: { path, user_id: ctx.user_id, project_id: projectId },
session: {
destroy() {},
},
@@ -355,7 +400,7 @@ describe('TpdsController', function () {
json: () => {
ctx.TpdsUpdateHandler.promises.newUpdate.should.have.been.calledWith(
ctx.user_id,
'123',
projectId,
'', // projectName
'/here.txt',
req,
@@ -400,7 +445,7 @@ describe('TpdsController', function () {
await new Promise(resolve => {
const path = '/projectName/here.txt'
const req = {
params: { 0: path, user_id: ctx.user_id, project_id: '' },
params: { path, user_id: ctx.user_id, project_id: '' },
session: {
destroy() {},
},
@@ -429,8 +474,10 @@ describe('TpdsController', function () {
it('should process the delete with the update receiver by id', async function (ctx) {
await new Promise(resolve => {
const path = '/here.txt'
// project_id is validated as a Mongo ObjectId
const projectId = new ObjectId().toString()
const req = {
params: { 0: path, user_id: ctx.user_id, project_id: '123' },
params: { path, user_id: ctx.user_id, project_id: projectId },
session: {
destroy() {},
},
@@ -442,7 +489,7 @@ describe('TpdsController', function () {
sendStatus: () => {
ctx.TpdsUpdateHandler.promises.deleteUpdate.should.have.been.calledWith(
ctx.user_id,
'123',
projectId,
'', // projectName
'/here.txt',
ctx.source
@@ -525,7 +572,7 @@ describe('TpdsController', function () {
describe('parseParams', function () {
it('should take the project name off the start and replace with slash', function (ctx) {
const path = 'noSlashHere'
const req = { params: { 0: path, user_id: ctx.user_id } }
const req = { params: { path, user_id: ctx.user_id } }
const result = ctx.TpdsController.parseParams(req)
result.userId.should.equal(ctx.user_id)
result.filePath.should.equal('/')
@@ -534,7 +581,7 @@ describe('TpdsController', function () {
it('should take the project name off the start and it with no slashes in', function (ctx) {
const path = '/project/file.tex'
const req = { params: { 0: path, user_id: ctx.user_id } }
const req = { params: { path, user_id: ctx.user_id } }
const result = ctx.TpdsController.parseParams(req)
result.userId.should.equal(ctx.user_id)
result.filePath.should.equal('/file.tex')
@@ -543,7 +590,7 @@ describe('TpdsController', function () {
it('should take the project name of and return a slash for the file path', function (ctx) {
const path = '/project_name'
const req = { params: { 0: path, user_id: ctx.user_id } }
const req = { params: { path, user_id: ctx.user_id } }
const result = ctx.TpdsController.parseParams(req)
result.projectName.should.equal('project_name')
result.filePath.should.equal('/')
@@ -554,8 +601,9 @@ describe('TpdsController', function () {
beforeEach(async function (ctx) {
ctx.req = {
params: {
0: (ctx.path = 'chapters/main.tex'),
project_id: (ctx.project_id = 'project-id-123'),
path: (ctx.path = 'chapters/main.tex'),
// project_id is validated as a Mongo ObjectId
project_id: (ctx.project_id = new ObjectId().toString()),
},
session: {
destroy: sinon.stub(),
@@ -595,8 +643,9 @@ describe('TpdsController', function () {
beforeEach(async function (ctx) {
ctx.req = {
params: {
0: (ctx.path = 'chapters/main.tex'),
project_id: (ctx.project_id = 'project-id-123'),
path: (ctx.path = 'chapters/main.tex'),
// project_id is validated as a Mongo ObjectId
project_id: (ctx.project_id = new ObjectId().toString()),
},
session: {
destroy: sinon.stub(),