feat: flag notes API + tests

This commit is contained in:
Julian Lam
2021-07-16 11:07:34 -04:00
parent b5da3f136b
commit e219cf0226
10 changed files with 204 additions and 47 deletions

View File

@@ -132,6 +132,10 @@ paths:
$ref: 'write/flags.yaml'
/flags/{flagId}:
$ref: 'write/flags/flagId.yaml'
/flags/{flagId}/notes:
$ref: 'write/flags/flagId/notes.yaml'
/flags/{flagId}/notes/{datetime}:
$ref: 'write/flags/flagId/notes/datetime.yaml'
/admin/settings/{setting}:
$ref: 'write/admin/settings/setting.yaml'
/admin/analytics/{set}:

View File

@@ -0,0 +1,42 @@
post:
tags:
- flags
summary: append a flag note
description: This operation append a shared note for a given flag. It is only available to privileged users (that is, moderators, global moderators, and administrators).
parameters:
- in: path
name: flagId
schema:
type: number
required: true
description: a valid flag id
example: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
note:
type: string
example: 'test note'
datetime:
type: number
example: 1626446956652
required:
- note
responses:
'200':
description: flag note successfully added or updated
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
allOf:
- $ref: ../../../components/schemas/FlagObject.yaml#/FlagNotesObject
- $ref: ../../../components/schemas/FlagObject.yaml#/FlagHistoryObject

View File

@@ -0,0 +1,34 @@
delete:
tags:
- flags
summary: delete a flag note
description: This operation deletes a shared note for a given flag. It is only available to privileged users (that is, moderators, global moderators, and administrators).
parameters:
- in: path
name: flagId
schema:
type: number
required: true
description: a valid flag id
example: 1
- in: path
name: datetime
schema:
type: number
required: true
description: A valid UNIX timestamp
example: 1626446956652
responses:
'200':
description: Flag note deleted
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../../components/schemas/Status.yaml#/Status
response:
allOf:
- $ref: ../../../../components/schemas/FlagObject.yaml#/FlagNotesObject
- $ref: ../../../../components/schemas/FlagObject.yaml#/FlagHistoryObject

View File

@@ -37,3 +37,44 @@ flagsApi.update = async (caller, data) => {
await flags.update(flagId, caller.uid, data);
return await flags.getHistory(flagId);
};
flagsApi.appendNote = async (caller, data) => {
const allowed = await user.isPrivileged(caller.uid);
if (!allowed) {
throw new Error('[[error:no-privileges]]');
}
if (data.datetime && data.flagId) {
try {
const note = await flags.getNote(data.flagId, data.datetime);
if (note.uid !== caller.uid) {
throw new Error('[[error:no-privileges]]');
}
} catch (e) {
// Okay if not does not exist in database
if (!e.message === '[[error:invalid-data]]') {
throw e;
}
}
}
await flags.appendNote(data.flagId, caller.uid, data.note, data.datetime);
const [notes, history] = await Promise.all([
flags.getNotes(data.flagId),
flags.getHistory(data.flagId),
]);
return { notes: notes, history: history };
};
flagsApi.deleteNote = async (caller, data) => {
const note = await flags.getNote(data.flagId, data.datetime);
if (note.uid !== caller.uid) {
throw new Error('[[error:no-privileges]]');
}
await flags.deleteNote(data.flagId, data.datetime);
const [notes, history] = await Promise.all([
flags.getNotes(data.flagId),
flags.getHistory(data.flagId),
]);
return { notes: notes, history: history };
};

View File

@@ -29,3 +29,21 @@ Flags.update = async (req, res) => {
helpers.formatApiResponse(200, res, { history });
};
Flags.appendNote = async (req, res) => {
console.log('appending note', req.params, req.body);
const payload = await api.flags.appendNote(req, {
flagId: req.params.flagId,
...req.body,
});
helpers.formatApiResponse(200, res, payload);
};
Flags.deleteNote = async (req, res) => {
const payload = await api.flags.deleteNote(req, {
...req.params,
});
helpers.formatApiResponse(200, res, payload);
};

View File

@@ -702,7 +702,14 @@ Flags.appendHistory = async function (flagId, uid, changeset) {
Flags.appendNote = async function (flagId, uid, note, datetime) {
if (datetime) {
await Flags.deleteNote(flagId, datetime);
try {
await Flags.deleteNote(flagId, datetime);
} catch (e) {
// Do not throw if note doesn't exist
if (!e.message === '[[error:invalid-data]]') {
throw e;
}
}
}
datetime = datetime || Date.now();

View File

@@ -16,5 +16,8 @@ module.exports = function () {
setupApiRoute(router, 'get', '/:flagId', [...middlewares, middleware.assert.flag], controllers.write.flags.get);
setupApiRoute(router, 'put', '/:flagId', [...middlewares, middleware.assert.flag], controllers.write.flags.update);
setupApiRoute(router, 'post', '/:flagId/notes', [...middlewares, middleware.assert.flag], controllers.write.flags.appendNote);
setupApiRoute(router, 'delete', '/:flagId/notes/:datetime', [...middlewares, middleware.assert.flag], controllers.write.flags.deleteNote);
return router;
};

View File

@@ -34,25 +34,12 @@ SocketFlags.update = async function (socket, data) {
};
SocketFlags.appendNote = async function (socket, data) {
sockets.warnDeprecated(socket, 'POST /api/v3/flags/:flagId/notes');
if (!data || !(data.flagId && data.note)) {
throw new Error('[[error:invalid-data]]');
}
const allowed = await user.isPrivileged(socket.uid);
if (!allowed) {
throw new Error('[[error:no-privileges]]');
}
if (data.datetime && data.flagId) {
const note = await flags.getNote(data.flagId, data.datetime);
if (note.uid !== socket.uid) {
throw new Error('[[error:no-privileges]]');
}
}
await flags.appendNote(data.flagId, socket.uid, data.note, data.datetime);
const [notes, history] = await Promise.all([
flags.getNotes(data.flagId),
flags.getHistory(data.flagId),
]);
return { notes: notes, history: history };
return await api.flags.appendNote(socket, data);
};
SocketFlags.deleteNote = async function (socket, data) {
@@ -60,18 +47,7 @@ SocketFlags.deleteNote = async function (socket, data) {
throw new Error('[[error:invalid-data]]');
}
const note = await flags.getNote(data.flagId, data.datetime);
if (note.uid !== socket.uid) {
throw new Error('[[error:no-privileges]]');
}
await flags.deleteNote(data.flagId, data.datetime);
const [notes, history] = await Promise.all([
flags.getNotes(data.flagId),
flags.getHistory(data.flagId),
]);
return { notes: notes, history: history };
return await api.flags.deleteNote(socket, data);
};
require('../promisify')(SocketFlags);

View File

@@ -167,7 +167,8 @@ describe('API', async () => {
mocks.delete['/posts/{pid}/diffs/{timestamp}'][1].example = (await posts.diffs.list(unprivTopic.postData.pid))[0];
// Create a sample flag
await flags.create('post', 1, unprivUid, 'sample reasons', Date.now());
const { flagId } = await flags.create('post', 1, unprivUid, 'sample reasons', Date.now());
await flags.appendNote(flagId, 1, 'test note', 1626446956652);
// Create a new chat room
await messaging.newRoom(1, [2]);

View File

@@ -623,6 +623,12 @@ describe('Flags', () => {
done();
});
});
it('should insert a note in the past if a datetime is passed in', async () => {
await Flags.appendNote(1, 1, 'this is the first note', 1626446956652);
const note = (await db.getSortedSetRange('flag:1:notes', 0, 0)).pop();
assert.strictEqual('[1,"this is the first note"]', note);
});
});
describe('.getNotes()', () => {
@@ -697,7 +703,7 @@ describe('Flags', () => {
});
});
describe('(websockets)', () => {
describe('(v3 API)', () => {
const SocketFlags = require('../src/socket.io/flags');
let pid;
let tid;
@@ -838,23 +844,48 @@ describe('Flags', () => {
});
describe('.appendNote()', () => {
it('should append a note to the flag', (done) => {
SocketFlags.appendNote({ uid: 2 }, {
flagId: 2,
note: 'lorem ipsum dolor sit amet',
}, (err, data) => {
assert.ifError(err);
assert(data.hasOwnProperty('notes'));
assert(Array.isArray(data.notes));
assert.strictEqual('lorem ipsum dolor sit amet', data.notes[0].content);
assert.strictEqual(2, data.notes[0].uid);
assert(data.hasOwnProperty('history'));
assert(Array.isArray(data.history));
assert.strictEqual(1, Object.keys(data.history[0].fields).length);
assert(data.history[0].fields.hasOwnProperty('notes'));
done();
it('should append a note to the flag', async () => {
const { response } = await request({
method: 'post',
uri: `${nconf.get('url')}/api/v3/flags/2/notes`,
jar,
headers: {
'x-csrf-token': csrfToken,
},
body: {
note: 'lorem ipsum dolor sit amet',
datetime: 1626446956652,
},
json: true,
});
assert(response.hasOwnProperty('notes'));
assert(Array.isArray(response.notes));
assert.strictEqual('lorem ipsum dolor sit amet', response.notes[0].content);
assert.strictEqual(2, response.notes[0].uid);
assert(response.hasOwnProperty('history'));
assert(Array.isArray(response.history));
assert.strictEqual(1, Object.keys(response.history[response.history.length - 1].fields).length);
assert(response.history[response.history.length - 1].fields.hasOwnProperty('notes'));
});
});
describe('.deleteNote()', () => {
it('should delete a note from a flag', async () => {
const { response } = await request({
method: 'delete',
uri: `${nconf.get('url')}/api/v3/flags/2/notes/1626446956652`,
jar,
headers: {
'x-csrf-token': csrfToken,
},
json: true,
});
assert(Array.isArray(response.history));
assert(Array.isArray(response.notes));
assert.strictEqual(response.notes.length, 0);
});
});
});