mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-09-02 01:57:38 +02:00
feat: flag notes API + tests
This commit is contained in:
@@ -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}:
|
||||
|
||||
42
public/openapi/write/flags/flagId/notes.yaml
Normal file
42
public/openapi/write/flags/flagId/notes.yaml
Normal 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
|
||||
34
public/openapi/write/flags/flagId/notes/datetime.yaml
Normal file
34
public/openapi/write/flags/flagId/notes/datetime.yaml
Normal 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
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user