Add tests for CommentRoutes

This commit is contained in:
Jerko Steiner 2019-03-12 11:46:41 +05:00
parent cec34260ce
commit a4be36d159
5 changed files with 240 additions and 44 deletions

View File

@ -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)
})
})
})

View File

@ -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()
}
}

View File

@ -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)
}

View 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)

View 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
}
}