diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts index 003ec0831..0fded8975 100644 --- a/src/node/db/Pad.ts +++ b/src/node/db/Pad.ts @@ -294,7 +294,8 @@ class Pad { (!ins && start > 0 && orig[start - 1] === '\n'); if (!willEndWithNewline) ins += '\n'; if (ndel === 0 && ins.length === 0) return; - const changeset = makeSplice(orig, start, ndel, ins); + const attribs = authorId ? [['author', authorId] as [string, string]] : undefined; + const changeset = makeSplice(orig, start, ndel, ins, attribs, this.pool); await this.appendRevision(changeset, authorId); } @@ -394,7 +395,8 @@ class Pad { if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`); text = exports.cleanText(context.content); } - const firstChangeset = makeSplice('\n', 0, 0, text); + const firstAttribs = authorId ? [['author', authorId] as [string, string]] : undefined; + const firstChangeset = makeSplice('\n', 0, 0, text, firstAttribs, this.pool); await this.appendRevision(firstChangeset, authorId); } await hooks.aCallAll('padLoad', {pad: this}); diff --git a/src/node/types/PadType.ts b/src/node/types/PadType.ts index a715426c8..61ca306bb 100644 --- a/src/node/types/PadType.ts +++ b/src/node/types/PadType.ts @@ -15,7 +15,7 @@ export type PadType = { remove: ()=>Promise, text: ()=>string, setText: (text: string, authorId?: string)=>Promise, - appendText: (text: string)=>Promise, + appendText: (text: string, authorId?: string)=>Promise, getHeadRevisionNumber: ()=>number, getRevisionDate: (rev: number)=>Promise, getRevisionChangeset: (rev: number)=>Promise, diff --git a/src/tests/backend/specs/api/appendTextAuthor.ts b/src/tests/backend/specs/api/appendTextAuthor.ts new file mode 100644 index 000000000..e1f4281cb --- /dev/null +++ b/src/tests/backend/specs/api/appendTextAuthor.ts @@ -0,0 +1,96 @@ +'use strict'; + +const assert = require('assert').strict; +const common = require('../../common'); + +let agent: any; +let apiVersion = 1; +const testPadId = `appendTextAuthor_${makeid()}`; + +const endPoint = (point: string) => `/api/${apiVersion}/${point}`; + +function makeid() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 10; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +describe(__filename, function () { + let authorId: string; + + before(async function () { + agent = await common.init(); + const res = await agent.get('/api/') + .expect(200) + .expect('Content-Type', /json/); + apiVersion = res.body.currentVersion; + assert(apiVersion); + + // Create an author + const authorRes = await agent.get(`${endPoint('createAuthor')}?name=TestAuthor`) + .set('Authorization', await common.generateJWTToken()) + .expect(200); + assert.equal(authorRes.body.code, 0); + authorId = authorRes.body.data.authorID; + assert(authorId); + + // Create a pad + await agent.get(`${endPoint('createPad')}?padID=${testPadId}`) + .set('Authorization', await common.generateJWTToken()) + .expect(200); + }); + + it('appendText with authorId attributes the text to that author', async function () { + // Append text with an authorId + const res = await agent.post(endPoint('appendText')) + .set('Authorization', await common.generateJWTToken()) + .send({padID: testPadId, text: 'authored text', authorId}) + .expect(200) + .expect('Content-Type', /json/); + assert.equal(res.body.code, 0); + + // Verify the author appears in the pad's author list + const authorsRes = await agent.get( + `${endPoint('listAuthorsOfPad')}?padID=${testPadId}`) + .set('Authorization', await common.generateJWTToken()) + .expect(200); + assert.equal(authorsRes.body.code, 0); + assert(authorsRes.body.data.authorIDs.includes(authorId), + `Expected authorId ${authorId} in pad authors: ${authorsRes.body.data.authorIDs}`); + }); + + it('appendText without authorId does not attribute to any author', async function () { + const newPadId = `appendTextNoAuthor_${makeid()}`; + const createRes = await agent.get(`${endPoint('createPad')}?padID=${newPadId}`) + .set('Authorization', await common.generateJWTToken()) + .expect(200); + assert.equal(createRes.body.code, 0); + + const appendRes = await agent.post(endPoint('appendText')) + .set('Authorization', await common.generateJWTToken()) + .send({padID: newPadId, text: 'anonymous text'}) + .expect(200); + assert.equal(appendRes.body.code, 0); + + const authorsRes = await agent.get( + `${endPoint('listAuthorsOfPad')}?padID=${newPadId}`) + .set('Authorization', await common.generateJWTToken()) + .expect(200); + assert.equal(authorsRes.body.code, 0); + // No authors should be listed for anonymous text + assert.equal(authorsRes.body.data.authorIDs.length, 0); + + await agent.get(`${endPoint('deletePad')}?padID=${newPadId}`) + .set('Authorization', await common.generateJWTToken()) + .expect(200); + }); + + after(async function () { + await agent.get(`${endPoint('deletePad')}?padID=${testPadId}`) + .set('Authorization', await common.generateJWTToken()) + .expect(200); + }); +});