Add tests for CommentRoutes
This commit is contained in:
parent
cec34260ce
commit
a4be36d159
@ -1,5 +1,5 @@
|
||||
import * as CommentTestUtils from './CommentTestUtils'
|
||||
import {IComment, IStory} from '@rondo/common'
|
||||
import {IStory} from '@rondo/common'
|
||||
import {createSite} from '../site/SiteTestUtils'
|
||||
import {getStory} from '../story/StoryTestUtils'
|
||||
import {test} from '../test'
|
||||
@ -25,6 +25,13 @@ describe('comment', () => {
|
||||
story = await getStory(t, storyUrl)
|
||||
})
|
||||
|
||||
async function createComment() {
|
||||
return CommentTestUtils.createRootComment(t, {
|
||||
storyId: story.id,
|
||||
message: 'test',
|
||||
})
|
||||
}
|
||||
|
||||
async function createChildComment() {
|
||||
const parent = await CommentTestUtils.createRootComment(t, {
|
||||
storyId: story.id,
|
||||
@ -79,14 +86,8 @@ describe('comment', () => {
|
||||
|
||||
describe('PUT /comments/:commentId', () => {
|
||||
|
||||
let comment: IComment
|
||||
beforeEach(async () => {
|
||||
comment = await CommentTestUtils.createRootComment(t, {
|
||||
storyId: story.id,
|
||||
message: 'test',
|
||||
})
|
||||
})
|
||||
it('updates a comment', async () => {
|
||||
const comment = await createComment()
|
||||
await t.put('/comments/:commentId', {
|
||||
params: {
|
||||
commentId: comment.id,
|
||||
@ -99,6 +100,7 @@ describe('comment', () => {
|
||||
|
||||
const c = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(c.message).toEqual('test2')
|
||||
|
||||
// TODO save edit history
|
||||
})
|
||||
|
||||
@ -108,23 +110,122 @@ describe('comment', () => {
|
||||
})
|
||||
|
||||
describe('DELETE /comments/:commentId', () => {
|
||||
|
||||
it('soft deletes a comment', async () => {
|
||||
const comment = await createComment()
|
||||
await t.delete('/comments/:commentId', {
|
||||
params: {
|
||||
commentId: comment.id,
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
const comment2 = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(comment2.message)
|
||||
.toEqual('(this message has been removed)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /comments/:commentId/vote', () => {
|
||||
it('adds a new comment vote', async () => {
|
||||
const comment = await createComment()
|
||||
await CommentTestUtils.upVote(t, comment.id)
|
||||
const c = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(c.votes).toEqual(1)
|
||||
})
|
||||
it('can only upvote once', async () => {
|
||||
const comment = await createComment()
|
||||
async function upVote() {
|
||||
return t.post('/comments/:commentId/vote', {
|
||||
params: {
|
||||
commentId: comment.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const responses = (await Promise.all([
|
||||
upVote(),
|
||||
upVote(),
|
||||
])).map(r => r.status)
|
||||
|
||||
expect(responses).toContain(200)
|
||||
expect(responses).toContain(400)
|
||||
|
||||
const c = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(c.votes).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /comments/:commentId/vote', () => {
|
||||
|
||||
it('removes a comment vote', async () => {
|
||||
let comment = await createComment()
|
||||
await CommentTestUtils.upVote(t, comment.id)
|
||||
comment = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(comment.votes).toEqual(1)
|
||||
await CommentTestUtils.downVote(t, comment.id)
|
||||
comment = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(comment.votes).toEqual(0)
|
||||
})
|
||||
it('can only downvote once', async () => {
|
||||
let comment = await createComment()
|
||||
await CommentTestUtils.upVote(t, comment.id)
|
||||
await Promise.all([
|
||||
CommentTestUtils.downVote(t, comment.id),
|
||||
CommentTestUtils.downVote(t, comment.id),
|
||||
])
|
||||
comment = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(comment.votes).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /comments/:commentId/spam', () => {
|
||||
it('adds a new spam report', async () => {
|
||||
const comment = await createComment()
|
||||
await CommentTestUtils.markAsSpam(t, comment.id)
|
||||
const c = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(c.spams).toEqual(1)
|
||||
})
|
||||
it('can only report a spam once', async () => {
|
||||
const comment = await createComment()
|
||||
async function markAsSpam() {
|
||||
return t.post('/comments/:commentId/spam', {
|
||||
params: {
|
||||
commentId: comment.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const responses = (await Promise.all([
|
||||
markAsSpam(),
|
||||
markAsSpam(),
|
||||
])).map(r => r.status)
|
||||
|
||||
expect(responses).toContain(200)
|
||||
expect(responses).toContain(400)
|
||||
|
||||
const c = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(c.spams).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /comments/:commentId/spam', () => {
|
||||
|
||||
it('removes a spam report', async () => {
|
||||
let comment = await createComment()
|
||||
await CommentTestUtils.markAsSpam(t, comment.id)
|
||||
comment = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(comment.spams).toEqual(1)
|
||||
await CommentTestUtils.unmarkAsSpam(t, comment.id)
|
||||
comment = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(comment.spams).toEqual(0)
|
||||
})
|
||||
it('can only remove a spam report once', async () => {
|
||||
let comment = await createComment()
|
||||
await CommentTestUtils.markAsSpam(t, comment.id)
|
||||
await Promise.all([
|
||||
CommentTestUtils.unmarkAsSpam(t, comment.id),
|
||||
CommentTestUtils.unmarkAsSpam(t, comment.id),
|
||||
])
|
||||
comment = await CommentTestUtils.getCommentById(t, comment.id)
|
||||
expect(comment.spams).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import {UniqueTransformer} from '../error/ErrorTransformer'
|
||||
import {BaseService} from '../services/BaseService'
|
||||
import {Comment} from '../entities/Comment'
|
||||
import {ICommentService} from './ICommentService'
|
||||
import {ICommentTree} from '@rondo/common'
|
||||
import {IEditCommentParams} from './IEditCommentParams'
|
||||
import {ICommentService} from './ICommentService'
|
||||
import {INewCommentParams} from './INewCommentParams'
|
||||
import {INewRootCommentParams} from './INewRootCommentParams'
|
||||
import {Spam} from '../entities/Spam'
|
||||
@ -124,53 +125,62 @@ export class CommentService extends BaseService implements ICommentService {
|
||||
id: commentId,
|
||||
userId,
|
||||
}, {
|
||||
message: '(this message has been removed by the user)',
|
||||
message: '(this message has been removed)',
|
||||
})
|
||||
|
||||
return this.findOne(commentId)
|
||||
}
|
||||
|
||||
async upVote(commentId: number, userId: number) {
|
||||
await this.getRepository(Vote)
|
||||
.save({
|
||||
commentId,
|
||||
userId,
|
||||
})
|
||||
try {
|
||||
await this.getRepository(Vote)
|
||||
.save({
|
||||
commentId,
|
||||
userId,
|
||||
})
|
||||
} catch (err) {
|
||||
UniqueTransformer.transform(err, 'Already upvoted!')
|
||||
}
|
||||
|
||||
await this.getRepository(Comment)
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.where({ id: commentId })
|
||||
.set({
|
||||
score: () => 'score + 1',
|
||||
votes: () => 'votes + 1',
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
async deleteVote(commentId: number, userId: number) {
|
||||
const result = await this.getRepository(Vote)
|
||||
await this.getRepository(Vote)
|
||||
.delete({
|
||||
commentId,
|
||||
userId,
|
||||
})
|
||||
|
||||
if (result.affected && result.affected === 1) {
|
||||
await this.getRepository(Comment)
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.where({ id: commentId })
|
||||
.set({
|
||||
score: () => 'score - 1',
|
||||
})
|
||||
}
|
||||
// TODO rows.affected returns undefined or SQLite driver. This is an
|
||||
// alternative query that does not depend on it.
|
||||
await this.getRepository(Comment)
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.where({ id: commentId })
|
||||
.set({
|
||||
votes: () => '(select count(*) from vote where commentId = comment.id)',
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
async markAsSpam(commentId: number, userId: number) {
|
||||
await this.getRepository(Spam)
|
||||
.save({
|
||||
commentId,
|
||||
userId,
|
||||
})
|
||||
try {
|
||||
await this.getRepository(Spam)
|
||||
.save({
|
||||
commentId,
|
||||
userId,
|
||||
})
|
||||
} catch (err) {
|
||||
UniqueTransformer.transform(err, 'Already marked as spam!')
|
||||
}
|
||||
|
||||
await this.getRepository(Comment)
|
||||
.createQueryBuilder()
|
||||
@ -183,21 +193,22 @@ export class CommentService extends BaseService implements ICommentService {
|
||||
}
|
||||
|
||||
async unmarkAsSpam(commentId: number, userId: number) {
|
||||
const result = await this.getRepository(Spam)
|
||||
await this.getRepository(Spam)
|
||||
.delete({
|
||||
commentId,
|
||||
userId,
|
||||
})
|
||||
|
||||
if (result.affected && result.affected === 1) {
|
||||
await this.getRepository(Comment)
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.where({ id: commentId })
|
||||
.set({
|
||||
spams: () => 'spams - 1',
|
||||
})
|
||||
}
|
||||
// TODO rows.affected returns undefined or SQLite driver. This is an
|
||||
// alternative query that does not depend on it.
|
||||
await this.getRepository(Comment)
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.where({ id: commentId })
|
||||
.set({
|
||||
spams: () => '(select count(*) from spam where commentId = comment.id)',
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -65,3 +65,51 @@ export async function getCommentById(
|
||||
|
||||
return response.body!
|
||||
}
|
||||
|
||||
export async function upVote(
|
||||
t: RequestTester<IAPIDef>,
|
||||
commentId: number,
|
||||
) {
|
||||
await t.post('/comments/:commentId/vote', {
|
||||
params: {
|
||||
commentId,
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
export async function downVote(
|
||||
t: RequestTester<IAPIDef>,
|
||||
commentId: number,
|
||||
) {
|
||||
await t.delete('/comments/:commentId/vote', {
|
||||
params: {
|
||||
commentId,
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
export async function markAsSpam(
|
||||
t: RequestTester<IAPIDef>,
|
||||
commentId: number,
|
||||
) {
|
||||
await t.post('/comments/:commentId/spam', {
|
||||
params: {
|
||||
commentId,
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
export async function unmarkAsSpam(
|
||||
t: RequestTester<IAPIDef>,
|
||||
commentId: number,
|
||||
) {
|
||||
await t.delete('/comments/:commentId/spam', {
|
||||
params: {
|
||||
commentId,
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
25
packages/server/src/error/ErrorTransformer.ts
Normal file
25
packages/server/src/error/ErrorTransformer.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {TransformedError} from './TransformedError'
|
||||
|
||||
export class ErrorTransformer {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly match: RegExp | string,
|
||||
) {}
|
||||
|
||||
transform(err: Error, message: string) {
|
||||
if (!this.isMatch(err)) {
|
||||
throw err
|
||||
}
|
||||
throw new TransformedError(this.status, message, err)
|
||||
}
|
||||
|
||||
protected isMatch(err: Error): boolean {
|
||||
const {match} = this
|
||||
if (match instanceof RegExp) {
|
||||
return match.test(err.message)
|
||||
}
|
||||
return match === err.message
|
||||
}
|
||||
}
|
||||
|
||||
export const UniqueTransformer = new ErrorTransformer(400, /unique/i)
|
||||
11
packages/server/src/error/TransformedError.ts
Normal file
11
packages/server/src/error/TransformedError.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export class TransformedError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly message: string,
|
||||
readonly cause: Error,
|
||||
) {
|
||||
super(message)
|
||||
Error.captureStackTrace(this)
|
||||
this.stack! += '\n' + cause.stack
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user