Fix history label creation for anonymous users (#20200)

* Remove decaffeination artifacts in LabelsTests

* Remove decaffeination artifacts in LabelsManagerTests

* Fix label creation for anonymous users

* Update label creation route in MockProjectHistoryApi tests

* Support both endpoints for backwards compatibility

GitOrigin-RevId: 50ce1ba49388e50f147fb620e0425fea83301c9d
This commit is contained in:
Miguel Serrano 2024-10-01 14:36:38 +02:00 committed by Copybot
parent 3ff142d478
commit cd6631c105
9 changed files with 170 additions and 131 deletions

View file

@ -455,13 +455,19 @@ export function getLabels(req, res, next) {
} }
export function createLabel(req, res, next) { export function createLabel(req, res, next) {
const { project_id: projectId, user_id: userId } = req.params const { project_id: projectId, user_id: userIdParam } = req.params
const { const {
version, version,
comment, comment,
user_id: userIdBody,
created_at: createdAt, created_at: createdAt,
validate_exists: validateExists, validate_exists: validateExists,
} = req.body } = req.body
// Temporarily looking up both params and body while rolling out changes
// in the router path - https://github.com/overleaf/internal/pull/20200
const userId = userIdParam || userIdBody
HistoryApiManager.shouldUseProjectHistory( HistoryApiManager.shouldUseProjectHistory(
projectId, projectId,
(error, shouldUseProjectHistory) => { (error, shouldUseProjectHistory) => {

View file

@ -53,9 +53,11 @@ export function createLabel(
project_id: new ObjectId(projectId), project_id: new ObjectId(projectId),
comment, comment,
version, version,
user_id: new ObjectId(userId),
created_at: createdAt, created_at: createdAt,
} }
if (userId) {
label.user_id = userId
}
db.projectHistoryLabels.insertOne(label, function (error, confirmation) { db.projectHistoryLabels.insertOne(label, function (error, confirmation) {
if (error) { if (error) {
return callback(OError.tag(error)) return callback(OError.tag(error))
@ -125,7 +127,13 @@ function _toObjectId(...args1) {
const args = args1.slice(0, adjustedLength - 1) const args = args1.slice(0, adjustedLength - 1)
const callback = args1[adjustedLength - 1] const callback = args1[adjustedLength - 1]
try { try {
const ids = args.map(id => new ObjectId(id)) const ids = args.map(id => {
if (id) {
return new ObjectId(id)
} else {
return undefined
}
})
callback(null, ...ids) callback(null, ...ids)
} catch (error) { } catch (error) {
callback(error) callback(error)

View file

@ -125,6 +125,23 @@ export function initialize(app) {
app.get('/project/:project_id/labels', HttpController.getLabels) app.get('/project/:project_id/labels', HttpController.getLabels)
app.post(
'/project/:project_id/labels',
validate({
body: {
version: Joi.number().integer().required(),
comment: Joi.string().required(),
created_at: Joi.string(),
validate_exists: Joi.boolean().default(true),
user_id: Joi.string(),
},
}),
HttpController.createLabel
)
// Temporarily maintaining both paths for createLabel to support backwards
// compatibility while rolling out - https://github.com/overleaf/internal/pull/20200
app.post( app.post(
'/project/:project_id/user/:user_id/labels', '/project/:project_id/user/:user_id/labels',
validate({ validate({

View file

@ -1,18 +1,4 @@
/* eslint-disable
no-undef,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import sinon from 'sinon'
import { expect } from 'chai' import { expect } from 'chai'
import Settings from '@overleaf/settings'
import mongodb from 'mongodb-legacy' import mongodb from 'mongodb-legacy'
import nock from 'nock' import nock from 'nock'
import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js' import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
@ -20,14 +6,13 @@ import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
const { ObjectId } = mongodb const { ObjectId } = mongodb
const MockHistoryStore = () => nock('http://127.0.0.1:3100') const MockHistoryStore = () => nock('http://127.0.0.1:3100')
const MockFileStore = () => nock('http://127.0.0.1:3009')
const MockWeb = () => nock('http://127.0.0.1:3000') const MockWeb = () => nock('http://127.0.0.1:3000')
const fixture = path => new URL(`../fixtures/${path}`, import.meta.url) const fixture = path => new URL(`../fixtures/${path}`, import.meta.url)
describe('Labels', function () { describe('Labels', function () {
beforeEach(function (done) { beforeEach(function (done) {
return ProjectHistoryApp.ensureRunning(error => { ProjectHistoryApp.ensureRunning(error => {
if (error != null) { if (error != null) {
throw error throw error
} }
@ -37,7 +22,7 @@ describe('Labels', function () {
projectId: this.historyId, projectId: this.historyId,
}) })
return ProjectHistoryClient.initializeProject( ProjectHistoryClient.initializeProject(
this.historyId, this.historyId,
(error, olProject) => { (error, olProject) => {
if (error != null) { if (error != null) {
@ -68,18 +53,18 @@ describe('Labels', function () {
this.comment2 = 'another saved version comment' this.comment2 = 'another saved version comment'
this.user_id = new ObjectId().toString() this.user_id = new ObjectId().toString()
this.created_at = new Date(1) this.created_at = new Date(1)
return done() done()
} }
) )
}) })
}) })
afterEach(function () { afterEach(function () {
return nock.cleanAll() nock.cleanAll()
}) })
it('can create and get labels', function (done) { it('can create and get labels', function (done) {
return ProjectHistoryClient.createLabel( ProjectHistoryClient.createLabel(
this.project_id, this.project_id,
this.user_id, this.user_id,
7, 7,
@ -89,22 +74,42 @@ describe('Labels', function () {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.getLabels( ProjectHistoryClient.getLabels(this.project_id, (error, labels) => {
this.project_id,
(error, labels) => {
if (error != null) { if (error != null) {
throw error throw error
} }
expect(labels).to.deep.equal([label]) expect(labels).to.deep.equal([label])
return done() done()
})
} }
) )
})
it('can create and get labels with no user id', function (done) {
const userId = undefined
ProjectHistoryClient.createLabel(
this.project_id,
userId,
7,
this.comment,
this.created_at,
(error, label) => {
if (error != null) {
throw error
}
ProjectHistoryClient.getLabels(this.project_id, (error, labels) => {
if (error != null) {
throw error
}
expect(labels).to.deep.equal([label])
done()
})
} }
) )
}) })
it('can delete labels', function (done) { it('can delete labels', function (done) {
return ProjectHistoryClient.createLabel( ProjectHistoryClient.createLabel(
this.project_id, this.project_id,
this.user_id, this.user_id,
7, 7,
@ -114,31 +119,24 @@ describe('Labels', function () {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.deleteLabel( ProjectHistoryClient.deleteLabel(this.project_id, label.id, error => {
this.project_id,
label.id,
error => {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.getLabels( ProjectHistoryClient.getLabels(this.project_id, (error, labels) => {
this.project_id,
(error, labels) => {
if (error != null) { if (error != null) {
throw error throw error
} }
expect(labels).to.deep.equal([]) expect(labels).to.deep.equal([])
return done() done()
} })
) })
}
)
} }
) )
}) })
it('can delete labels for the current user', function (done) { it('can delete labels for the current user', function (done) {
return ProjectHistoryClient.createLabel( ProjectHistoryClient.createLabel(
this.project_id, this.project_id,
this.user_id, this.user_id,
7, 7,
@ -148,7 +146,7 @@ describe('Labels', function () {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.deleteLabelForUser( ProjectHistoryClient.deleteLabelForUser(
this.project_id, this.project_id,
this.user_id, this.user_id,
label.id, label.id,
@ -156,16 +154,13 @@ describe('Labels', function () {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.getLabels( ProjectHistoryClient.getLabels(this.project_id, (error, labels) => {
this.project_id,
(error, labels) => {
if (error != null) { if (error != null) {
throw error throw error
} }
expect(labels).to.deep.equal([]) expect(labels).to.deep.equal([])
return done() done()
} })
)
} }
) )
} }
@ -175,7 +170,7 @@ describe('Labels', function () {
it('can transfer ownership of labels', function (done) { it('can transfer ownership of labels', function (done) {
const fromUser = new ObjectId().toString() const fromUser = new ObjectId().toString()
const toUser = new ObjectId().toString() const toUser = new ObjectId().toString()
return ProjectHistoryClient.createLabel( ProjectHistoryClient.createLabel(
this.project_id, this.project_id,
fromUser, fromUser,
7, 7,
@ -185,7 +180,7 @@ describe('Labels', function () {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.createLabel( ProjectHistoryClient.createLabel(
this.project_id, this.project_id,
fromUser, fromUser,
7, 7,
@ -195,14 +190,14 @@ describe('Labels', function () {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.transferLabelOwnership( ProjectHistoryClient.transferLabelOwnership(
fromUser, fromUser,
toUser, toUser,
error => { error => {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.getLabels( ProjectHistoryClient.getLabels(
this.project_id, this.project_id,
(error, labels) => { (error, labels) => {
if (error != null) { if (error != null) {
@ -224,7 +219,7 @@ describe('Labels', function () {
user_id: toUser, user_id: toUser,
}, },
]) ])
return done() done()
} }
) )
} }
@ -235,8 +230,8 @@ describe('Labels', function () {
) )
}) })
return it('should return labels with summarized updates', function (done) { it('should return labels with summarized updates', function (done) {
return ProjectHistoryClient.createLabel( ProjectHistoryClient.createLabel(
this.project_id, this.project_id,
this.user_id, this.user_id,
8, 8,
@ -246,7 +241,7 @@ describe('Labels', function () {
if (error != null) { if (error != null) {
throw error throw error
} }
return ProjectHistoryClient.getSummarizedUpdates( ProjectHistoryClient.getSummarizedUpdates(
this.project_id, this.project_id,
{ min_count: 1 }, { min_count: 1 },
(error, updates) => { (error, updates) => {
@ -278,7 +273,7 @@ describe('Labels', function () {
}, },
], ],
}) })
return done() done()
} }
) )
} }

View file

@ -259,8 +259,8 @@ export function createLabel(
) { ) {
request.post( request.post(
{ {
url: `http://127.0.0.1:3054/project/${projectId}/user/${userId}/labels`, url: `http://127.0.0.1:3054/project/${projectId}/labels`,
json: { comment, version, created_at: createdAt }, json: { comment, version, created_at: createdAt, user_id: userId },
}, },
(error, res, body) => { (error, res, body) => {
if (error) { if (error) {

View file

@ -412,13 +412,13 @@ describe('HttpController', function () {
this.req = { this.req = {
params: { params: {
project_id: this.projectId, project_id: this.projectId,
user_id: this.userId,
}, },
body: { body: {
version: (this.version = 'label-1'), version: (this.version = 'label-1'),
comment: (this.comment = 'a comment'), comment: (this.comment = 'a comment'),
created_at: (this.created_at = Date.now().toString()), created_at: (this.created_at = Date.now().toString()),
validate_exists: true, validate_exists: true,
user_id: this.userId,
}, },
} }
this.label = { _id: new ObjectId() } this.label = { _id: new ObjectId() }

View file

@ -1,14 +1,3 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import sinon from 'sinon' import sinon from 'sinon'
import { expect } from 'chai' import { expect } from 'chai'
import mongodb from 'mongodb-legacy' import mongodb from 'mongodb-legacy'
@ -53,11 +42,11 @@ describe('LabelsManager', function () {
this.historyId = 123 this.historyId = 123
this.user_id = new ObjectId().toString() this.user_id = new ObjectId().toString()
this.label_id = new ObjectId().toString() this.label_id = new ObjectId().toString()
return (this.callback = sinon.stub()) this.callback = sinon.stub()
}) })
afterEach(function () { afterEach(function () {
return tk.reset() tk.reset()
}) })
describe('getLabels', function () { describe('getLabels', function () {
@ -77,17 +66,17 @@ describe('LabelsManager', function () {
describe('with valid project id', function () { describe('with valid project id', function () {
beforeEach(function () { beforeEach(function () {
return this.LabelsManager.getLabels(this.project_id, this.callback) this.LabelsManager.getLabels(this.project_id, this.callback)
}) })
it('gets the labels state from mongo', function () { it('gets the labels state from mongo', function () {
return expect( expect(this.db.projectHistoryLabels.find).to.have.been.calledWith({
this.db.projectHistoryLabels.find project_id: new ObjectId(this.project_id),
).to.have.been.calledWith({ project_id: new ObjectId(this.project_id) }) })
}) })
return it('returns formatted labels', function () { it('returns formatted labels', function () {
return expect(this.callback).to.have.been.calledWith(null, [ expect(this.callback).to.have.been.calledWith(null, [
sinon.match({ sinon.match({
id: this.label._id, id: this.label._id,
comment: this.label.comment, comment: this.label.comment,
@ -99,11 +88,11 @@ describe('LabelsManager', function () {
}) })
}) })
return describe('with invalid project id', function () { describe('with invalid project id', function () {
return it('returns an error', function (done) { it('returns an error', function (done) {
return this.LabelsManager.getLabels('invalid id', error => { this.LabelsManager.getLabels('invalid id', error => {
expect(error).to.exist expect(error).to.exist
return done() done()
}) })
}) })
}) })
@ -122,7 +111,7 @@ describe('LabelsManager', function () {
this.db.projectHistoryLabels.insertOne.yields(null, { this.db.projectHistoryLabels.insertOne.yields(null, {
insertedId: new ObjectId(this.label_id), insertedId: new ObjectId(this.label_id),
}) })
return this.LabelsManager.createLabel( this.LabelsManager.createLabel(
this.project_id, this.project_id,
this.user_id, this.user_id,
this.version, this.version,
@ -134,27 +123,25 @@ describe('LabelsManager', function () {
}) })
it('flushes unprocessed updates', function () { it('flushes unprocessed updates', function () {
return expect( expect(
this.UpdatesProcessor.processUpdatesForProject this.UpdatesProcessor.processUpdatesForProject
).to.have.been.calledWith(this.project_id) ).to.have.been.calledWith(this.project_id)
}) })
it('finds the V1 project id', function () { it('finds the V1 project id', function () {
return expect(this.WebApiManager.getHistoryId).to.have.been.calledWith( expect(this.WebApiManager.getHistoryId).to.have.been.calledWith(
this.project_id this.project_id
) )
}) })
it('checks there is a chunk for the project + version', function () { it('checks there is a chunk for the project + version', function () {
return expect( expect(
this.HistoryStoreManager.getChunkAtVersion this.HistoryStoreManager.getChunkAtVersion
).to.have.been.calledWith(this.project_id, this.historyId, this.version) ).to.have.been.calledWith(this.project_id, this.historyId, this.version)
}) })
it('create the label in mongo', function () { it('create the label in mongo', function () {
return expect( expect(this.db.projectHistoryLabels.insertOne).to.have.been.calledWith(
this.db.projectHistoryLabels.insertOne
).to.have.been.calledWith(
sinon.match({ sinon.match({
project_id: new ObjectId(this.project_id), project_id: new ObjectId(this.project_id),
comment: this.comment, comment: this.comment,
@ -166,8 +153,8 @@ describe('LabelsManager', function () {
) )
}) })
return it('returns the label', function () { it('returns the label', function () {
return expect(this.callback).to.have.been.calledWith(null, { expect(this.callback).to.have.been.calledWith(null, {
id: new ObjectId(this.label_id), id: new ObjectId(this.label_id),
comment: this.comment, comment: this.comment,
version: this.version, version: this.version,
@ -182,7 +169,7 @@ describe('LabelsManager', function () {
this.db.projectHistoryLabels.insertOne.yields(null, { this.db.projectHistoryLabels.insertOne.yields(null, {
insertedId: new ObjectId(this.label_id), insertedId: new ObjectId(this.label_id),
}) })
return this.LabelsManager.createLabel( this.LabelsManager.createLabel(
this.project_id, this.project_id,
this.user_id, this.user_id,
this.version, this.version,
@ -193,10 +180,8 @@ describe('LabelsManager', function () {
) )
}) })
return it('create the label with the current date', function () { it('create the label with the current date', function () {
return expect( expect(this.db.projectHistoryLabels.insertOne).to.have.been.calledWith(
this.db.projectHistoryLabels.insertOne
).to.have.been.calledWith(
sinon.match({ sinon.match({
project_id: new ObjectId(this.project_id), project_id: new ObjectId(this.project_id),
comment: this.comment, comment: this.comment,
@ -208,13 +193,13 @@ describe('LabelsManager', function () {
}) })
}) })
return describe('with shouldValidateExists = false', function () { describe('with shouldValidateExists = false', function () {
beforeEach(function () { beforeEach(function () {
this.createdAt = new Date(1) this.createdAt = new Date(1)
this.db.projectHistoryLabels.insertOne.yields(null, { this.db.projectHistoryLabels.insertOne.yields(null, {
insertedId: new ObjectId(this.label_id), insertedId: new ObjectId(this.label_id),
}) })
return this.LabelsManager.createLabel( this.LabelsManager.createLabel(
this.project_id, this.project_id,
this.user_id, this.user_id,
this.version, this.version,
@ -225,9 +210,39 @@ describe('LabelsManager', function () {
) )
}) })
return it('checks there is a chunk for the project + version', function () { it('checks there is a chunk for the project + version', function () {
return expect(this.HistoryStoreManager.getChunkAtVersion).to.not.have expect(this.HistoryStoreManager.getChunkAtVersion).to.not.have.been
.been.called .called
})
})
describe('with no userId', function () {
beforeEach(function () {
this.db.projectHistoryLabels.insertOne.yields(null, {
insertedId: new ObjectId(this.label_id),
})
const userId = undefined
this.LabelsManager.createLabel(
this.project_id,
userId,
this.version,
this.comment,
this.createdAt,
false,
this.callback
)
})
it('creates the label without user_id', function () {
expect(this.db.projectHistoryLabels.insertOne).to.have.been.calledWith(
sinon.match({
project_id: new ObjectId(this.project_id),
comment: this.comment,
version: this.version,
user_id: undefined,
created_at: this.now,
})
)
}) })
}) })
}) })
@ -235,7 +250,7 @@ describe('LabelsManager', function () {
describe('deleteLabelForUser', function () { describe('deleteLabelForUser', function () {
beforeEach(function () { beforeEach(function () {
this.db.projectHistoryLabels.deleteOne.yields() this.db.projectHistoryLabels.deleteOne.yields()
return this.LabelsManager.deleteLabelForUser( this.LabelsManager.deleteLabelForUser(
this.project_id, this.project_id,
this.user_id, this.user_id,
this.label_id, this.label_id,
@ -243,10 +258,8 @@ describe('LabelsManager', function () {
) )
}) })
return it('removes the label from the database', function () { it('removes the label from the database', function () {
return expect( expect(this.db.projectHistoryLabels.deleteOne).to.have.been.calledWith(
this.db.projectHistoryLabels.deleteOne
).to.have.been.calledWith(
{ {
_id: new ObjectId(this.label_id), _id: new ObjectId(this.label_id),
project_id: new ObjectId(this.project_id), project_id: new ObjectId(this.project_id),
@ -257,20 +270,18 @@ describe('LabelsManager', function () {
}) })
}) })
return describe('deleteLabel', function () { describe('deleteLabel', function () {
beforeEach(function () { beforeEach(function () {
this.db.projectHistoryLabels.deleteOne.yields() this.db.projectHistoryLabels.deleteOne.yields()
return this.LabelsManager.deleteLabel( this.LabelsManager.deleteLabel(
this.project_id, this.project_id,
this.label_id, this.label_id,
this.callback this.callback
) )
}) })
return it('removes the label from the database', function () { it('removes the label from the database', function () {
return expect( expect(this.db.projectHistoryLabels.deleteOne).to.have.been.calledWith(
this.db.projectHistoryLabels.deleteOne
).to.have.been.calledWith(
{ {
_id: new ObjectId(this.label_id), _id: new ObjectId(this.label_id),
project_id: new ObjectId(this.project_id), project_id: new ObjectId(this.project_id),

View file

@ -196,8 +196,8 @@ module.exports = HistoryController = {
HistoryController._makeRequest( HistoryController._makeRequest(
{ {
method: 'POST', method: 'POST',
url: `${settings.apis.project_history.url}/project/${projectId}/user/${userId}/labels`, url: `${settings.apis.project_history.url}/project/${projectId}/labels`,
json: { comment, version }, json: { comment, version, user_id: userId },
}, },
function (err, label) { function (err, label) {
if (err) { if (err) {
@ -215,7 +215,9 @@ module.exports = HistoryController = {
_enrichLabel(label, callback) { _enrichLabel(label, callback) {
if (!label.user_id) { if (!label.user_id) {
return callback(null, label) const newLabel = Object.assign({}, label)
newLabel.user_display_name = HistoryController._displayNameForUser(null)
return callback(null, newLabel)
} }
UserGetter.getUser( UserGetter.getUser(
label.user_id, label.user_id,
@ -237,7 +239,8 @@ module.exports = HistoryController = {
} }
const uniqueUsers = new Set(labels.map(label => label.user_id)) const uniqueUsers = new Set(labels.map(label => label.user_id))
// For backwards compatibility expect missing user_id fields // For backwards compatibility, and for anonymously created labels in SP
// expect missing user_id fields
uniqueUsers.delete(undefined) uniqueUsers.delete(undefined)
if (!uniqueUsers.size) { if (!uniqueUsers.size) {
@ -255,7 +258,6 @@ module.exports = HistoryController = {
labels.forEach(label => { labels.forEach(label => {
const user = users.get(label.user_id) const user = users.get(label.user_id)
if (!user) return
label.user_display_name = HistoryController._displayNameForUser(user) label.user_display_name = HistoryController._displayNameForUser(user)
}) })
callback(null, labels) callback(null, labels)

View file

@ -103,7 +103,7 @@ class MockProjectHistoryApi extends AbstractMockApi {
} }
}) })
this.app.post('/project/:projectId/user/:user_id/labels', (req, res) => { this.app.post('/project/:projectId/labels', (req, res) => {
const { projectId } = req.params const { projectId } = req.params
const { comment, version } = req.body const { comment, version } = req.body
const labelId = new ObjectId().toString() const labelId = new ObjectId().toString()