Skip to content

Commit 4f47480

Browse files
XGHeavenJacksonTian
authored andcommitted
feat: add reply api (#130)
1 parent 7b31ad2 commit 4f47480

File tree

3 files changed

+381
-1
lines changed

3 files changed

+381
-1
lines changed

app/controller/api/reply.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
'use strict';
2+
3+
const Controller = require('egg').Controller;
4+
5+
const MongoObjectIdSchema = {
6+
type: 'string',
7+
format: /^[0-9a-f]{24}$/i,
8+
};
9+
10+
class ReplyController extends Controller {
11+
async create() {
12+
const { ctx } = this;
13+
ctx.validate({
14+
topic_id: MongoObjectIdSchema,
15+
}, ctx.params);
16+
17+
const topicId = ctx.params.topic_id;
18+
const content = (ctx.request.body.content || '').trim();
19+
const replyId = ctx.request.body.reply_id || null;
20+
21+
if (content === '') {
22+
ctx.status = 400;
23+
ctx.body = {
24+
success: false,
25+
error_msg: '回复内容不能为空',
26+
};
27+
return;
28+
}
29+
30+
const { topic, author } = await ctx.service.topic.getTopicById(topicId);
31+
32+
if (!topic) {
33+
ctx.status = 404;
34+
ctx.body = {
35+
success: false,
36+
error_msg: '话题不存在',
37+
};
38+
return;
39+
}
40+
41+
if (topic.lock) {
42+
ctx.status = 403;
43+
ctx.body = {
44+
success: false,
45+
error_msg: '该话题已被锁定',
46+
};
47+
return;
48+
}
49+
50+
const reply = await ctx.service.reply.newAndSave(content, topicId, ctx.request.user.id, replyId);
51+
await ctx.service.topic.updateLastReply(topicId, reply._id);
52+
// 发送 at 消息,并防止重复 at 作者
53+
const newContent = content.replace('@' + author.loginname + ' ', '');
54+
await ctx.service.at.sendMessageToMentionUsers(newContent, topicId, ctx.request.user.id, reply._id);
55+
56+
const user = await ctx.service.user.getUserById(ctx.request.user.id);
57+
user.score += 5;
58+
user.reply_count += 1;
59+
await user.save();
60+
61+
if (topic.author_id.toString() !== ctx.request.user.id.toString()) {
62+
await ctx.service.message.sendReplyMessage(topic.author_id, ctx.request.user.id, topic._id, reply._id);
63+
}
64+
65+
ctx.body = {
66+
success: true,
67+
reply_id: reply._id,
68+
};
69+
}
70+
71+
async updateUps() {
72+
const { ctx } = this;
73+
ctx.validate({
74+
reply_id: MongoObjectIdSchema,
75+
}, ctx.params);
76+
77+
const replyId = ctx.params.reply_id;
78+
const userId = ctx.request.user.id;
79+
80+
const reply = await ctx.service.reply.getReplyById(replyId);
81+
82+
if (!reply) {
83+
ctx.status = 404;
84+
ctx.body = { success: false, error_msg: '评论不存在' };
85+
return;
86+
}
87+
88+
if (reply.author_id.equals(userId)) {
89+
ctx.status = 403;
90+
ctx.body = { success: false, error_msg: '不能帮自己点赞' };
91+
return;
92+
}
93+
94+
let action = '';
95+
reply.ups = (reply.ups || []);
96+
const ups = reply.ups;
97+
const upIndex = ups.indexOf(userId);
98+
if (upIndex === -1) {
99+
ups.push(userId);
100+
action = 'up';
101+
} else {
102+
ups.splice(upIndex, 1);
103+
action = 'down';
104+
}
105+
106+
await reply.save();
107+
108+
ctx.body = {
109+
action,
110+
success: true,
111+
};
112+
}
113+
}
114+
115+
module.exports = ReplyController;

app/router/api.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module.exports = app => {
77
const apiV1Router = app.router.namespace('/api/v1');
88
const { controller, middleware } = app;
99

10-
const { user, message, topic } = controller.api;
10+
const { user, message, topic, reply } = controller.api;
1111

1212
const tokenRequired = middleware.tokenRequired();
1313
const pagination = middleware.pagination();
@@ -29,4 +29,8 @@ module.exports = app => {
2929
apiV1Router.get('/topic/:id', topic.show);
3030
apiV1Router.post('/topics', tokenRequired, topic.create);
3131
apiV1Router.post('/topics/update', tokenRequired, topic.update);
32+
33+
// 评论
34+
apiV1Router.post('/topic/:topic_id/replies', tokenRequired, reply.create);
35+
apiV1Router.post('/reply/:reply_id/ups', tokenRequired, reply.updateUps);
3236
};
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
'use strict';
2+
3+
const { app, assert } = require('egg-mock/bootstrap');
4+
5+
async function createUser(name) {
6+
const ctx = app.mockContext();
7+
const loginname = `${name}_${Date.now()}`;
8+
return await ctx.service.user.newAndSave(name, loginname, 'pass', `${loginname}@test.com`, 'avatar_url', 'active');
9+
}
10+
11+
describe('test/app/controller/api/reply.test.js', () => {
12+
let user1;
13+
let user2;
14+
let author;
15+
let topic;
16+
17+
before(async () => {
18+
author = await createUser('author_name');
19+
user1 = await createUser('user1');
20+
user2 = await createUser('user2');
21+
});
22+
23+
beforeEach(async () => {
24+
const ctx = app.mockContext();
25+
26+
const title = 'reply test topic';
27+
const content = 'reply test topic';
28+
const tab = 'share';
29+
30+
topic = await ctx.service.topic.newAndSave(title, content, tab, author.id);
31+
32+
assert(topic.title === title);
33+
assert(topic.content === content);
34+
assert(topic.tab === tab);
35+
assert.equal(topic.author_id, author.id);
36+
});
37+
38+
describe('/api/v1/topic/:topic_id/replies', () => {
39+
40+
async function checkMessage(type, senderId, receiverId, replyId, revert = false) {
41+
const ctx = app.mockContext();
42+
43+
const messages = await ctx.service.message.getUnreadMessagesByUserId(receiverId);
44+
let valid = false;
45+
for (const message of messages) {
46+
if (
47+
message.type === type &&
48+
message.master_id.toString() === receiverId &&
49+
message.topic_id.toString() === topic.id &&
50+
message.reply_id.toString() === replyId &&
51+
message.author_id.toString() === senderId
52+
) {
53+
valid = true;
54+
}
55+
}
56+
57+
assert(valid === !revert, `Cannot found "${type}" type message`);
58+
}
59+
60+
it('post should ok', async () => {
61+
const resp = await app.httpRequest()
62+
.post(`/api/v1/topic/${topic.id}/replies`)
63+
.send({
64+
content: 'test normal reply',
65+
accesstoken: user1.accessToken,
66+
})
67+
.expect(200);
68+
69+
await checkMessage('reply', user1.id, author.id, resp.body.reply_id);
70+
});
71+
72+
it('post should 404 when topic is not found', async () => {
73+
const { body } = await app.httpRequest()
74+
.post('/api/v1/topic/012345678901234567890123/replies')
75+
.send({
76+
content: 'no content',
77+
accesstoken: user1.accessToken,
78+
})
79+
.expect(404);
80+
81+
assert.deepEqual(body, {
82+
success: false,
83+
error_msg: '话题不存在',
84+
});
85+
});
86+
87+
it('post should 403 when topic is locked', async () => {
88+
topic.lock = true;
89+
await topic.save();
90+
const { body } = await app.httpRequest()
91+
.post(`/api/v1/topic/${topic.id}/replies`)
92+
.send({
93+
accesstoken: user1.accessToken,
94+
content: 'no content',
95+
})
96+
.expect(403);
97+
98+
assert.deepEqual(body, {
99+
success: false,
100+
error_msg: '该话题已被锁定',
101+
});
102+
});
103+
104+
it('post should ok with reply_id', async () => {
105+
const resp1 = await app.httpRequest()
106+
.post(`/api/v1/topic/${topic.id}/replies`)
107+
.send({
108+
accesstoken: user1.accessToken,
109+
content: 'test reply',
110+
})
111+
.expect(200);
112+
113+
const replyId = resp1.body.reply_id;
114+
await app.httpRequest()
115+
.post(`/api/v1/topic/${topic.id}/replies`)
116+
.send({
117+
accesstoken: user2.accessToken,
118+
content: 'test reply with reply_id',
119+
reply_id: replyId,
120+
})
121+
.expect(200);
122+
});
123+
124+
it('post should ok with at someone', async () => {
125+
await app.httpRequest()
126+
.post(`/api/v1/topic/${topic.id}/replies`)
127+
.send({
128+
accesstoken: user1.accessToken,
129+
content: 'test reply',
130+
})
131+
.expect(200);
132+
133+
const resp = await app.httpRequest()
134+
.post(`/api/v1/topic/${topic.id}/replies`)
135+
.send({
136+
accesstoken: user2.accessToken,
137+
content: `@${user1.loginname} reply`,
138+
})
139+
.expect(200);
140+
141+
const replyId = resp.body.reply_id;
142+
143+
await checkMessage('at', user2.id, user1.id, replyId);
144+
});
145+
146+
it('post should ok when at author only send reply message', async () => {
147+
// 如果一个人评论的时候 at 了作者,那么作者只会收到评论的通知,而不会再收到 at 通知
148+
const resp = await app.httpRequest()
149+
.post(`/api/v1/topic/${topic.id}/replies`)
150+
.send({
151+
accesstoken: user1.accessToken,
152+
content: `@${author.loginname} reply`,
153+
})
154+
.expect(200);
155+
156+
await checkMessage('reply', user1.id, author.id, resp.body.reply_id);
157+
await checkMessage('at', user1.id, author.id, resp.body.reply_id, true);
158+
});
159+
160+
it('post should ok when author reply self topic do not send message', async () => {
161+
// 如果作者回复了自己的主题,那么不发送消息
162+
const resp = await app.httpRequest()
163+
.post(`/api/v1/topic/${topic.id}/replies`)
164+
.send({
165+
accesstoken: author.accessToken,
166+
content: 'reply myself',
167+
})
168+
.expect(200);
169+
170+
await checkMessage('reply', author.id, author.id, resp.body.reply_id, true);
171+
});
172+
});
173+
174+
describe('/api/v1/reply/:reply_id/ups', async () => {
175+
async function postReply() {
176+
const { body } = await app.httpRequest()
177+
.post(`/api/v1/topic/${topic.id}/replies`)
178+
.send({
179+
accesstoken: user1.accessToken,
180+
content: 'reply',
181+
})
182+
.expect(200);
183+
184+
return body.reply_id;
185+
}
186+
187+
async function checkReplyUps(replyId, userId, status) {
188+
const ctx = app.mockContext();
189+
const reply = await ctx.service.reply.getReplyById(replyId);
190+
const ups = reply.ups;
191+
assert((ups.indexOf(userId) !== -1) === status);
192+
}
193+
194+
it('post should ok when up reply', async () => {
195+
const replyId = await postReply();
196+
197+
await checkReplyUps(replyId, user2.id, false);
198+
199+
await app.httpRequest()
200+
.post(`/api/v1/reply/${replyId}/ups`)
201+
.send({
202+
accesstoken: user2.accessToken,
203+
})
204+
.expect(200);
205+
206+
await checkReplyUps(replyId, user2.id, true);
207+
});
208+
209+
it('post should ok when de-up reply', async () => {
210+
const replyId = await postReply();
211+
212+
await checkReplyUps(replyId, user2.id, false);
213+
214+
await app.httpRequest()
215+
.post(`/api/v1/reply/${replyId}/ups`)
216+
.send({
217+
accesstoken: user2.accessToken,
218+
})
219+
.expect(200);
220+
221+
await checkReplyUps(replyId, user2.id, true);
222+
223+
await app.httpRequest()
224+
.post(`/api/v1/reply/${replyId}/ups`)
225+
.send({
226+
accesstoken: user2.accessToken,
227+
})
228+
.expect(200);
229+
230+
await checkReplyUps(replyId, user2.id, false);
231+
});
232+
233+
it('post should 401 when no accesstoken', async () => {
234+
const replyId = await postReply();
235+
236+
await app.httpRequest()
237+
.post(`/api/v1/reply/${replyId}/ups`)
238+
.send({})
239+
.expect(401);
240+
});
241+
242+
it('post should 404 when reply is not found', async () => {
243+
await app.httpRequest()
244+
.post('/api/v1/reply/012345678901234567890123/ups')
245+
.send({
246+
accesstoken: user2.accessToken,
247+
})
248+
.expect(404);
249+
});
250+
251+
it('post should 403 when you ups your reply', async () => {
252+
const replyId = await postReply();
253+
await app.httpRequest()
254+
.post(`/api/v1/reply/${replyId}/ups`)
255+
.send({
256+
accesstoken: user1.accessToken,
257+
})
258+
.expect(403);
259+
});
260+
});
261+
});

0 commit comments

Comments
 (0)