Skip to content

Commit

Permalink
Merge pull request #274 from cofacts/block-articlereply-feedbacks
Browse files Browse the repository at this point in the history
Block user script processes comments & feedbacks
  • Loading branch information
MrOrz authored Jan 31, 2022
2 parents 05b0367 + 3fb929d commit 8439ff3
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 74 deletions.
135 changes: 77 additions & 58 deletions src/graphql/mutations/CreateOrUpdateArticleReplyFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,79 @@ export function getArticleReplyFeedbackId({
return `${articleId}__${replyId}__${userId}__${appId}`;
}

/**
* Updates the positive and negative feedback count of the article reply with
* specified `articleId` and `replyId`.
*
* @param {string} articleId
* @param {string} replyId
* @param {object[]} feedbacks
* @returns {object} The updated article reply
*/
export async function updateArticleReplyByFeedbacks(
articleId,
replyId,
feedbacks
) {
const [positiveFeedbackCount, negativeFeedbackCount] = feedbacks
.filter(({ status }) => status === 'NORMAL')
.reduce(
(agg, { score }) => {
if (score === 1) {
agg[0] += 1;
} else if (score === -1) {
agg[1] += 1;
}
return agg;
},
[0, 0]
);

const { body: articleReplyUpdateResult } = await client.update({
index: 'articles',
type: 'doc',
id: articleId,
body: {
script: {
source: `
int idx = 0;
int replyCount = ctx._source.articleReplies.size();
for(; idx < replyCount; idx += 1) {
HashMap articleReply = ctx._source.articleReplies.get(idx);
if( articleReply.get('replyId').equals(params.replyId) ) {
break;
}
}
if( idx === replyCount ) {
ctx.op = 'none';
} else {
ctx._source.articleReplies.get(idx).put(
'positiveFeedbackCount', params.positiveFeedbackCount);
ctx._source.articleReplies.get(idx).put(
'negativeFeedbackCount', params.negativeFeedbackCount);
}
`,
params: {
replyId,
positiveFeedbackCount,
negativeFeedbackCount,
},
},
},
_source: true,
});

/* istanbul ignore if */
if (articleReplyUpdateResult.result !== 'updated') {
throw new Error(`Cannot article ${articleId}'s feedback count`);
}

return articleReplyUpdateResult.get._source.articleReplies.find(
articleReply => articleReply.replyId === replyId
);
}

export default {
description: 'Create or update a feedback on an article-reply connection',
type: ArticleReply,
Expand Down Expand Up @@ -72,64 +145,10 @@ export default {
replyId,
});

const [positiveFeedbackCount, negativeFeedbackCount] = feedbacks
.filter(({ status }) => status === 'NORMAL')
.reduce(
(agg, { score }) => {
if (score === 1) {
agg[0] += 1;
} else if (score === -1) {
agg[1] += 1;
}
return agg;
},
[0, 0]
);

const { body: articleReplyUpdateResult } = await client.update({
index: 'articles',
type: 'doc',
id: articleId,
body: {
script: {
source: `
int idx = 0;
int replyCount = ctx._source.articleReplies.size();
for(; idx < replyCount; idx += 1) {
HashMap articleReply = ctx._source.articleReplies.get(idx);
if( articleReply.get('replyId').equals(params.replyId) ) {
break;
}
}
if( idx === replyCount ) {
ctx.op = 'none';
} else {
ctx._source.articleReplies.get(idx).put(
'positiveFeedbackCount', params.positiveFeedbackCount);
ctx._source.articleReplies.get(idx).put(
'negativeFeedbackCount', params.negativeFeedbackCount);
}
`,
params: {
replyId,
positiveFeedbackCount,
negativeFeedbackCount,
},
},
},
_source: true,
});

/* istanbul ignore if */
if (articleReplyUpdateResult.result !== 'updated') {
throw new Error(
`Cannot article ${articleId}'s feedback count for feedback ID = ${id}`
);
}

const updatedArticleReply = articleReplyUpdateResult.get._source.articleReplies.find(
articleReply => articleReply.replyId === replyId
const updatedArticleReply = await updateArticleReplyByFeedbacks(
articleId,
replyId,
feedbacks
);

/* istanbul ignore if */
Expand Down
63 changes: 63 additions & 0 deletions src/scripts/__fixtures__/blockUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,20 @@ export default {
createdAt: '2021-11-11T00:00:00.000Z',
},

// Article with already-blocked contents
'/articles/doc/some-article': {
replyRequestCount: 1,
normalArticleReplyCount: 0,
lastRequestedAt: '2021-01-01T00:00.000Z',
articleReplies: [
{
replyId: 'some-reply',
userId: 'already-blocked',
appId: 'APP_ID',
status: 'BLOCKED',
updatedAt: '2021-11-11T00:00:00.000Z',
},
],
},

'/replyrequests/doc/replyrequest-to-block': {
Expand All @@ -34,8 +45,60 @@ export default {
createdAt: '2021-10-10T00:00:00.000Z',
},

// Article with normal contents to block
'/articles/doc/modified-article': {
replyRequestCount: 2,
normalArticleReplyCount: 2,
lastRequestedAt: '2021-01-01T00:00:01.000Z',

articleReplies: [
{
replyId: 'valid-reply',
userId: 'valid-user',
appId: 'APP_ID',
status: 'NORMAL',
updatedAt: '2021-11-11T00:00:00.000Z',
positiveFeedbackCount: 1,
negativeFeedbackCount: 1,
},
{
replyId: 'some-reply',
userId: 'user-to-block',
appId: 'APP_ID',
status: 'NORMAL',
updatedAt: '2021-11-11T00:00:00.000Z',
positiveFeedbackCount: 0,
negativeFeedbackCount: 1,
},
],
},

'/articlereplyfeedbacks/doc/f-normal': {
userId: 'valid-user',
articleId: 'modified-article',
replyId: 'valid-reply',
score: 1,
status: 'NORMAL',
},
'/articlereplyfeedbacks/doc/f-normal-2': {
userId: 'valid-user',
articleId: 'modified-article',
replyId: 'some-reply',
score: -1,
status: 'NORMAL',
},
'/articlereplyfeedbacks/doc/f-spam': {
userId: 'user-to-block',
articleId: 'modified-article',
replyId: 'valid-reply',
score: -1,
status: 'NORMAL',
},
'/articlereplyfeedbacks/doc/f-already-blocked': {
userId: 'already-blocked',
articleId: 'some-article',
replyId: 'some-reply',
score: 1,
status: 'BLOCKED',
},
};
81 changes: 70 additions & 11 deletions src/scripts/__tests__/blockUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ it('fails if userId is not valid', async () => {
);
});

/**
* Asserts the document in database is the same as in the fixture,
* i.e. the document is not modified
*
* @param {string} fixtureKey
* @param {{index: string; id: string;}} clientGetArgs - Arguments for client.get()
*/
async function expectSameAsFixture(fixtureKey, clientGetArgs) {
const {
body: { _source: docInDb },
} = await client.get({ ...clientGetArgs, type: 'doc' });
expect(docInDb).toMatchObject(fixtures[fixtureKey]);
}

it('correctly sets the block reason and updates status of their works', async () => {
await blockUser({
userId: 'user-to-block',
Expand All @@ -36,24 +50,29 @@ it('correctly sets the block reason and updates status of their works', async ()
}
`);

// Assert valid contents remain as-is
//
// Check reply requests
//
await expectSameAsFixture('/replyrequests/doc/valid-reply-request', {
index: 'replyrequests',
id: 'valid-reply-request',
});
await expectSameAsFixture('/articlereplyfeedbacks/doc/f-normal', {
index: 'articlereplyfeedbacks',
id: 'f-normal',
});

// Assert reply requests that is already blocked are not selected to update
// Assert contents that is already blocked are not selected to update
//
const {
body: { _source: someArticleWithAlreadyBlockedReplyRequest },
} = await client.get({
await expectSameAsFixture('/articles/doc/some-article', {
index: 'articles',
type: 'doc',
id: 'some-article',
});
expect(someArticleWithAlreadyBlockedReplyRequest).toMatchObject(
fixtures['/articles/doc/some-article']
);
await expectSameAsFixture('/articlereplyfeedbacks/doc/f-already-blocked', {
index: 'articlereplyfeedbacks',
id: 'f-already-blocked',
});

// Assert normal reply requests being blocked and article being updated
// Assert normal contents being blocked and article being updated
//
const {
body: { _source: replyRequestToBlock },
Expand All @@ -63,6 +82,7 @@ it('correctly sets the block reason and updates status of their works', async ()
id: 'replyrequest-to-block',
});
expect(replyRequestToBlock.status).toEqual('BLOCKED');

const {
body: { _source: modifiedArticle },
} = await client.get({
Expand All @@ -71,11 +91,50 @@ it('correctly sets the block reason and updates status of their works', async ()
id: 'modified-article',
});
expect(modifiedArticle).toMatchObject({
// Only the article reply by valid-user
normalArticleReplyCount: 1,

// Only replyrequests/doc/valid-reply-request
replyRequestCount: 1,
lastRequestedAt:
fixtures['/replyrequests/doc/valid-reply-request'].createdAt,
});

// Assert article reply being blocked
expect(modifiedArticle.articleReplies[1]).toMatchObject({
userId: 'user-to-block',
status: 'BLOCKED',

// the negative feedback given by valid user is still there
positiveFeedbackCount:
fixtures['/articles/doc/modified-article'].articleReplies[1]
.positiveFeedbackCount,
negativeFeedbackCount:
fixtures['/articles/doc/modified-article'].articleReplies[1]
.negativeFeedbackCount,
});

// Assert article reply feedback is being blocked
//
const {
body: { _source: articleReplyFeedbackToBlock },
} = await client.get({
index: 'articlereplyfeedbacks',
type: 'doc',
id: 'f-spam',
});
expect(articleReplyFeedbackToBlock.status).toEqual('BLOCKED');

// Assert malicious negative feedback is removed from normal article reply
//
expect(modifiedArticle.articleReplies[0]).toMatchObject({
userId: 'valid-user',
status: 'NORMAL',
positiveFeedbackCount:
fixtures['/articles/doc/modified-article'].articleReplies[0]
.positiveFeedbackCount,
negativeFeedbackCount: 0, // Originally 1
});
});

// it('still updates statuses of blocked user even if they are blocked previously')
Loading

0 comments on commit 8439ff3

Please sign in to comment.