Add tests for CommentRoutes
This commit is contained in:
parent
cec34260ce
commit
a4be36d159
@ -1,5 +1,5 @@
|
|||||||
import * as CommentTestUtils from './CommentTestUtils'
|
import * as CommentTestUtils from './CommentTestUtils'
|
||||||
import {IComment, IStory} from '@rondo/common'
|
import {IStory} from '@rondo/common'
|
||||||
import {createSite} from '../site/SiteTestUtils'
|
import {createSite} from '../site/SiteTestUtils'
|
||||||
import {getStory} from '../story/StoryTestUtils'
|
import {getStory} from '../story/StoryTestUtils'
|
||||||
import {test} from '../test'
|
import {test} from '../test'
|
||||||
@ -25,6 +25,13 @@ describe('comment', () => {
|
|||||||
story = await getStory(t, storyUrl)
|
story = await getStory(t, storyUrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function createComment() {
|
||||||
|
return CommentTestUtils.createRootComment(t, {
|
||||||
|
storyId: story.id,
|
||||||
|
message: 'test',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function createChildComment() {
|
async function createChildComment() {
|
||||||
const parent = await CommentTestUtils.createRootComment(t, {
|
const parent = await CommentTestUtils.createRootComment(t, {
|
||||||
storyId: story.id,
|
storyId: story.id,
|
||||||
@ -79,14 +86,8 @@ describe('comment', () => {
|
|||||||
|
|
||||||
describe('PUT /comments/:commentId', () => {
|
describe('PUT /comments/:commentId', () => {
|
||||||
|
|
||||||
let comment: IComment
|
|
||||||
beforeEach(async () => {
|
|
||||||
comment = await CommentTestUtils.createRootComment(t, {
|
|
||||||
storyId: story.id,
|
|
||||||
message: 'test',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('updates a comment', async () => {
|
it('updates a comment', async () => {
|
||||||
|
const comment = await createComment()
|
||||||
await t.put('/comments/:commentId', {
|
await t.put('/comments/:commentId', {
|
||||||
params: {
|
params: {
|
||||||
commentId: comment.id,
|
commentId: comment.id,
|
||||||
@ -99,6 +100,7 @@ describe('comment', () => {
|
|||||||
|
|
||||||
const c = await CommentTestUtils.getCommentById(t, comment.id)
|
const c = await CommentTestUtils.getCommentById(t, comment.id)
|
||||||
expect(c.message).toEqual('test2')
|
expect(c.message).toEqual('test2')
|
||||||
|
|
||||||
// TODO save edit history
|
// TODO save edit history
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -108,23 +110,122 @@ describe('comment', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('DELETE /comments/:commentId', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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 {BaseService} from '../services/BaseService'
|
||||||
import {Comment} from '../entities/Comment'
|
import {Comment} from '../entities/Comment'
|
||||||
|
import {ICommentService} from './ICommentService'
|
||||||
import {ICommentTree} from '@rondo/common'
|
import {ICommentTree} from '@rondo/common'
|
||||||
import {IEditCommentParams} from './IEditCommentParams'
|
import {IEditCommentParams} from './IEditCommentParams'
|
||||||
import {ICommentService} from './ICommentService'
|
|
||||||
import {INewCommentParams} from './INewCommentParams'
|
import {INewCommentParams} from './INewCommentParams'
|
||||||
import {INewRootCommentParams} from './INewRootCommentParams'
|
import {INewRootCommentParams} from './INewRootCommentParams'
|
||||||
import {Spam} from '../entities/Spam'
|
import {Spam} from '../entities/Spam'
|
||||||
@ -124,53 +125,62 @@ export class CommentService extends BaseService implements ICommentService {
|
|||||||
id: commentId,
|
id: commentId,
|
||||||
userId,
|
userId,
|
||||||
}, {
|
}, {
|
||||||
message: '(this message has been removed by the user)',
|
message: '(this message has been removed)',
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.findOne(commentId)
|
return this.findOne(commentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async upVote(commentId: number, userId: number) {
|
async upVote(commentId: number, userId: number) {
|
||||||
|
try {
|
||||||
await this.getRepository(Vote)
|
await this.getRepository(Vote)
|
||||||
.save({
|
.save({
|
||||||
commentId,
|
commentId,
|
||||||
userId,
|
userId,
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
UniqueTransformer.transform(err, 'Already upvoted!')
|
||||||
|
}
|
||||||
|
|
||||||
await this.getRepository(Comment)
|
await this.getRepository(Comment)
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.update()
|
.update()
|
||||||
.where({ id: commentId })
|
.where({ id: commentId })
|
||||||
.set({
|
.set({
|
||||||
score: () => 'score + 1',
|
votes: () => 'votes + 1',
|
||||||
})
|
})
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteVote(commentId: number, userId: number) {
|
async deleteVote(commentId: number, userId: number) {
|
||||||
const result = await this.getRepository(Vote)
|
await this.getRepository(Vote)
|
||||||
.delete({
|
.delete({
|
||||||
commentId,
|
commentId,
|
||||||
userId,
|
userId,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.affected && result.affected === 1) {
|
// TODO rows.affected returns undefined or SQLite driver. This is an
|
||||||
|
// alternative query that does not depend on it.
|
||||||
await this.getRepository(Comment)
|
await this.getRepository(Comment)
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.update()
|
.update()
|
||||||
.where({ id: commentId })
|
.where({ id: commentId })
|
||||||
.set({
|
.set({
|
||||||
score: () => 'score - 1',
|
votes: () => '(select count(*) from vote where commentId = comment.id)',
|
||||||
})
|
})
|
||||||
}
|
.execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
async markAsSpam(commentId: number, userId: number) {
|
async markAsSpam(commentId: number, userId: number) {
|
||||||
|
try {
|
||||||
await this.getRepository(Spam)
|
await this.getRepository(Spam)
|
||||||
.save({
|
.save({
|
||||||
commentId,
|
commentId,
|
||||||
userId,
|
userId,
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
UniqueTransformer.transform(err, 'Already marked as spam!')
|
||||||
|
}
|
||||||
|
|
||||||
await this.getRepository(Comment)
|
await this.getRepository(Comment)
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -183,21 +193,22 @@ export class CommentService extends BaseService implements ICommentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async unmarkAsSpam(commentId: number, userId: number) {
|
async unmarkAsSpam(commentId: number, userId: number) {
|
||||||
const result = await this.getRepository(Spam)
|
await this.getRepository(Spam)
|
||||||
.delete({
|
.delete({
|
||||||
commentId,
|
commentId,
|
||||||
userId,
|
userId,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.affected && result.affected === 1) {
|
// TODO rows.affected returns undefined or SQLite driver. This is an
|
||||||
|
// alternative query that does not depend on it.
|
||||||
await this.getRepository(Comment)
|
await this.getRepository(Comment)
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.update()
|
.update()
|
||||||
.where({ id: commentId })
|
.where({ id: commentId })
|
||||||
.set({
|
.set({
|
||||||
spams: () => 'spams - 1',
|
spams: () => '(select count(*) from spam where commentId = comment.id)',
|
||||||
})
|
})
|
||||||
}
|
.execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,3 +65,51 @@ export async function getCommentById(
|
|||||||
|
|
||||||
return response.body!
|
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