diff --git a/services/project-history/app/js/HttpController.js b/services/project-history/app/js/HttpController.js index d04dd566ed..72424ac9fc 100644 --- a/services/project-history/app/js/HttpController.js +++ b/services/project-history/app/js/HttpController.js @@ -63,7 +63,7 @@ export function initializeProject(req, res, next) { const flushProjectSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), query: z.object({ debug: z.stringbool().default(false), @@ -110,7 +110,7 @@ export function flushProject(req, res, next) { const dumpProjectSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), query: z.object({ count: z.coerce.number().int().optional(), @@ -165,7 +165,7 @@ export function flushOld(req, res, next) { const getDiffSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), query: z.object({ pathname: z.string(), @@ -190,7 +190,7 @@ export function getDiff(req, res, next) { const getFileTreeDiffSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), query: z.object({ from: z.coerce.number().int(), @@ -213,7 +213,7 @@ export function getFileTreeDiff(req, res, next) { const getUpdatesSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), query: z.object({ before: z.coerce.number().int().optional(), @@ -246,7 +246,7 @@ export function getUpdates(req, res, next) { const latestVersionSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), }) @@ -282,7 +282,7 @@ export function latestVersion(req, res, next) { const getFileSnapshotSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), version: z.coerce.number().int(), pathname: z.string(), }), @@ -309,7 +309,7 @@ export function getFileSnapshot(req, res, next) { const getRangesSnapshotSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), version: z.coerce.number().int(), pathname: z.string(), }), @@ -333,7 +333,7 @@ export function getRangesSnapshot(req, res, next) { const getFileMetadataSnapshotSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), version: z.coerce.number().int(), pathname: z.string(), }), @@ -357,7 +357,7 @@ export function getFileMetadataSnapshot(req, res, next) { const getLatestSnapshotSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), }) @@ -382,7 +382,7 @@ export function getLatestSnapshot(req, res, next) { const getChangesInChunkSinceSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), query: z.object({ since: z.coerce.number().int().min(0), @@ -415,7 +415,7 @@ export function getChangesInChunkSince(req, res, next) { const getProjectSnapshotSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), version: z.coerce.number().int(), }), }) @@ -437,7 +437,7 @@ export function getProjectSnapshot(req, res, next) { const getPathsAtVersionSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), version: z.coerce.number().int(), }), }) @@ -477,7 +477,7 @@ export function checkLock(req, res) { const resyncProjectSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), query: z.object({ force: z.stringbool().default(false), @@ -536,7 +536,7 @@ export function resyncProject(req, res, next) { const forceDebugProjectSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), query: z.object({ clear: z.stringbool().default(false), @@ -582,7 +582,7 @@ export function getQueueCounts(req, res, next) { const getLabelsSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), }) @@ -611,7 +611,7 @@ export function getLabels(req, res, next) { const createLabelSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), user_id: zz.objectId().optional(), }), body: z.object({ @@ -683,7 +683,7 @@ export function createLabel(req, res, next) { */ const deleteLabelForUserSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), user_id: zz.objectId(), label_id: zz.objectId(), }), @@ -703,7 +703,7 @@ export function deleteLabelForUser(req, res, next) { const deleteLabelSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), label_id: zz.objectId(), }), }) @@ -785,7 +785,7 @@ export function transferLabels(req, res, next) { const deleteProjectSchema = z.object({ params: z.object({ - project_id: zz.objectId(), + project_id: zz.objectId().or(z.coerce.number()), }), }) diff --git a/services/project-history/test/acceptance/js/NumericProjectIdTests.js b/services/project-history/test/acceptance/js/NumericProjectIdTests.js new file mode 100644 index 0000000000..d4d8180e9c --- /dev/null +++ b/services/project-history/test/acceptance/js/NumericProjectIdTests.js @@ -0,0 +1,139 @@ +import { expect } from 'chai' +import nock from 'nock' +import request from 'request' +import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js' +import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js' + +const MockHistoryStore = () => nock('http://127.0.0.1:3100') +const MockWeb = () => nock('http://127.0.0.1:3000') + +const fixture = path => new URL(`../fixtures/${path}`, import.meta.url) + +/** + * These tests verify that endpoints accept numeric project IDs (in addition to ObjectId strings). + * This is needed for v1 history projects which use numeric project IDs. + */ +describe('NumericProjectId', function () { + beforeEach(async function () { + await ProjectHistoryApp.ensureRunning() + + // Use a numeric project ID (simulating v1 history projects) + this.numericProjectId = 123456 + + MockHistoryStore().post('/api/projects').reply(200, { + projectId: this.numericProjectId, + }) + + const olProject = await ProjectHistoryClient.initializeProject( + this.numericProjectId + ) + this.historyId = olProject.id + + MockWeb() + .get(`/project/${this.numericProjectId}/details`) + .reply(200, { + name: 'Test Project', + overleaf: { history: { id: this.historyId } }, + }) + .persist() + + MockHistoryStore() + .get(`/api/projects/${this.historyId}/latest/history`) + .replyWithFile(200, fixture('chunks/7-8.json')) + .persist() + + MockHistoryStore() + .get(`/api/projects/${this.historyId}/versions/7/history`) + .replyWithFile(200, fixture('chunks/7-8.json')) + .persist() + + MockHistoryStore() + .get(`/api/projects/${this.historyId}/versions/8/history`) + .replyWithFile(200, fixture('chunks/7-8.json')) + .persist() + }) + + afterEach(function () { + nock.cleanAll() + }) + + function makeRequest(options) { + return new Promise((resolve, reject) => { + request(options, (error, response, body) => { + if (error) return reject(error) + resolve({ response, body }) + }) + }) + } + + it('should accept numeric project_id for flush', async function () { + const { response } = await makeRequest({ + method: 'POST', + url: `http://127.0.0.1:3054/project/${this.numericProjectId}/flush`, + }) + expect(response.statusCode).to.equal(204) + }) + + it('should accept numeric project_id for dump', async function () { + const { response } = await makeRequest({ + method: 'GET', + url: `http://127.0.0.1:3054/project/${this.numericProjectId}/dump`, + }) + expect(response.statusCode).to.equal(200) + }) + + it('should accept numeric project_id for filetree diff', async function () { + const { response } = await makeRequest({ + method: 'GET', + url: `http://127.0.0.1:3054/project/${this.numericProjectId}/filetree/diff`, + qs: { from: 7, to: 8 }, + }) + expect(response.statusCode).to.equal(200) + }) + + it('should accept numeric project_id for updates', async function () { + const { response } = await makeRequest({ + method: 'GET', + url: `http://127.0.0.1:3054/project/${this.numericProjectId}/updates`, + qs: { min_count: 1 }, + }) + expect(response.statusCode).to.equal(200) + }) + + it('should accept numeric project_id for version', async function () { + const { response } = await makeRequest({ + method: 'GET', + url: `http://127.0.0.1:3054/project/${this.numericProjectId}/version`, + }) + expect(response.statusCode).to.equal(200) + }) + + it('should accept numeric project_id for snapshot', async function () { + const { response } = await makeRequest({ + method: 'GET', + url: `http://127.0.0.1:3054/project/${this.numericProjectId}/snapshot`, + }) + expect(response.statusCode).to.equal(200) + }) + + it('should accept numeric project_id for getLabels', async function () { + const { response } = await makeRequest({ + method: 'GET', + url: `http://127.0.0.1:3054/project/${this.numericProjectId}/labels`, + }) + expect(response.statusCode).to.equal(200) + }) + + it('should accept numeric project_id for createLabel', async function () { + const { response } = await makeRequest({ + method: 'POST', + url: `http://127.0.0.1:3054/project/${this.numericProjectId}/labels`, + json: { + comment: 'test label', + version: 7, + user_id: '507f1f77bcf86cd799439011', + }, + }) + expect(response.statusCode).to.equal(200) + }) +})