Bladeren bron

Refactor posts route including GET DELETE and POST http verbs

sbkwgh 8 jaren geleden
bovenliggende
commit
c777350d89
5 gewijzigde bestanden met toevoegingen van 211 en 161 verwijderingen
  1. 11 0
      lib/errors.js
  2. 27 0
      models/Notification.js
  3. 26 1
      models/post.js
  4. 76 103
      routes/post.js
  5. 71 57
      test/thread_post.js

+ 11 - 0
lib/errors.js

@@ -61,4 +61,15 @@ for(var errorName in Errors) {
 
 ProcessedErrors.VALIDATION_ERROR = 'VALIDATION_ERROR';
 
+ProcessedErrors.sequelizeValidation = (sequelize, obj) => {
+	return new sequelize.ValidationError(obj.error, [
+		new sequelize.ValidationErrorItem(
+			obj.error,
+			'Validation error',
+			obj.path,
+			obj.value
+		)
+	])
+}
+
 module.exports = ProcessedErrors

+ 27 - 0
models/Notification.js

@@ -1,3 +1,5 @@
+const Errors = require('../lib/errors')
+
 module.exports = (sequelize, DataTypes) => {
 	let Notification = sequelize.define('Notification', {
 		interacted: {
@@ -15,6 +17,19 @@ module.exports = (sequelize, DataTypes) => {
 				Notification.hasOne(models.PostNotification)
 				Notification.belongsTo(models.User)
 			},
+			filterMentions (mentions) {
+				//If mentions is not an array of strings
+				if(!Array.isArray(mentions) || mentions.filter(m => typeof m !== 'string').length) {
+					throw Errors.sequelizeValidation(sequelize, {
+						error: 'mentions must be an array of strings',
+						value: mentions
+					})
+				}
+
+				return mentions.filter((mention, pos, self) => {
+					return self.indexOf(mention) === pos
+				})
+			},
 			//Props fields: userFrom, usernameTo, post, type
 			async createPostNotification (props) {
 				let { PostNotification, User, Post } = sequelize.models
@@ -40,6 +55,18 @@ module.exports = (sequelize, DataTypes) => {
 
 				return reloadedNotification
 			}
+		},
+		instanceMethods: {
+			async emitNotificationMessage (ioUsers, io) {
+				let User = sequelize.models.User
+				let user = await User.findById(this.UserId)
+				
+				if(ioUsers[user.username]) {
+					console.log(ioUsers)
+					io.to(ioUsers[user.username])
+					  .emit('notification', this.toJSON())
+				}
+			}
 		}
 	})
 

+ 26 - 1
models/post.js

@@ -1,4 +1,5 @@
 let marked = require('marked')
+const Errors = require('../lib/errors')
 
 marked.setOptions({
 	highlight: function (code) {
@@ -13,8 +14,14 @@ module.exports = (sequelize, DataTypes) => {
 		content: {
 			type: DataTypes.TEXT,
 			set (val) {
+				if(!val) throw Errors.sequelizeValidation(sequelize, {
+					error: 'content must be a string',
+					path: 'content'
+				})
+
 				this.setDataValue('content', marked(val))
-			}
+			},
+			allowNull: false
 		},
 		postNumber: DataTypes.INTEGER,
 		replyingToUsername: DataTypes.STRING,
@@ -52,6 +59,24 @@ module.exports = (sequelize, DataTypes) => {
 						[{ model: models.User, attributes: ['username', 'id', 'color'] }]	
 					}
 				]
+			},
+			async getReplyingToPost (id, thread) {
+				let { Thread, User } = sequelize.models
+				let replyingToPost = await Post.findById(
+					id,
+					{ include: [Thread, { model: User, attributes: ['username'] }] }
+				)
+
+				if(!replyingToPost) {
+					throw Errors.invalidParameter('replyingToId', 'post does not exist')
+				} else if(replyingToPost.Thread.id !== thread.id) {
+					throw Errors.invalidParameter('replyingToId', 'replies must be in same thread')
+				} else if (replyingToPost.removed) {
+					throw Errors.postRemoved
+				} else {
+					return replyingToPost
+				}
+
 			}
 		}
 	})

+ 76 - 103
routes/post.js

@@ -2,20 +2,21 @@ let express = require('express')
 let router = express.Router()
 
 const Errors = require('../lib/errors')
-let { User, Thread, Post, Notification } = require('../models')
+let { User, Thread, Post, Notification, Sequelize, sequelize } = require('../models')
 
 router.get('/:post_id', async (req, res) => {
 	try {
 		let post = await Post.findById(req.params.post_id, { include: Post.includeOptions() })
-		if(!post) throw Errors.invalidParameter('id', 'post does not exist')
+		if(!post) throw Errors.sequelizeValidation(Sequelize, {
+			error: 'post does not exist',
+			path: 'id'
+		})
 
 		res.json(post.toJSON())
 	} catch (e) {
-		if(e.name === 'invalidParameter') {
+		if(e instanceof Sequelize.ValidationError) {
 			res.status(400)
-			res.json({
-				errors: [e]
-			})
+			res.json(e)
 		} else {
 			res.status(500)
 			res.json({
@@ -36,35 +37,6 @@ router.all('*', (req, res, next) => {
 	}
 })
 
-router.delete('/:post_id', async (req, res) => {
-	try {
-		if(!req.session.admin) {
-			throw Errors.requestNotAuthorized
-		} else {
-			let post = await Post.findById(req.params.post_id)
-			if(!post) throw Errors.invalidParameter('postId', 'post does not exist')
-
-			await post.update({ content: '[This post has been removed by an administrator]', removed: true })
-
-			res.json({ success: true })
-		}
-	} catch (e) {
-		if(e.name === 'requestNotAuthorized') {
-			res.status(401)
-			res.json({ errors: [e] })
-		} else if(e.name === 'invalidParameter') {
-			res.status(400)
-			res.json({ errors: [e] })
-		} else {
-			console.log(e)
-
-			res.status(500)
-			res.json({
-				errors: [Errors.unknown]
-			})
-		}
-	}
-})
 
 router.put('/:post_id/like', async (req, res) => {
 	try {
@@ -125,31 +97,13 @@ router.delete('/:post_id/like', async (req, res) => {
 
 router.post('/', async (req, res) => {
 	let validationErrors = []
-	let thread, replyingToPost, post
+	let thread, replyingToPost, post, uniqueMentions = []
 
 	try {
-		if(req.body.content === undefined) {
-			validationErrors.push(Errors.missingParameter('content'))
-		} else if(typeof req.body.content !== 'string') {
-			validationErrors.push(Errors.invalidParameterType('content', 'string'))
-		} if (req.body.threadId === undefined) {
-			validationErrors.push(Errors.missingParameter('threadId'))
-		} else if(!Number.isInteger(req.body.threadId)) {
-			validationErrors.push(Errors.invalidParameterType('threadId', 'integer'))
-		} if(req.body.replyingToId !== undefined && !Number.isInteger(req.body.replyingToId)) {
-			validationErrors.push(Errors.invalidParameterType('replyingToId', 'integer'))
-		} if(req.body.mentions !== undefined) {
-			if(Array.isArray(req.body.mentions)) {
-				if(req.body.mentions.some(m => typeof m !== 'string')) {
-					validationErrors.push(Errors.invalidParameterType('mention', 'string'))
-				}
-			} else {
-				validationErrors.push(Errors.invalidParameterType('mentions', 'array'))
-			}
+		if(req.body.mentions) {
+			uniqueMentions = Notification.filterMentions(req.body.mentions)
 		}
 
-		if(validationErrors.length) throw Errors.VALIDATION_ERROR
-
 		thread = await Thread.findOne({ where: {
 			id: req.body.threadId
 		}})
@@ -157,42 +111,32 @@ router.post('/', async (req, res) => {
 			username: req.session.username
 		}})
 
-		if(!thread) throw Errors.invalidParameter('threadId', 'thread does not exist')
+		if(!thread) throw Errors.sequelizeValidation(Sequelize, {
+			error: 'thread does not exist',
+			path: 'id'
+		})
 		if(thread.locked) throw Errors.threadLocked
 
 		if(req.body.replyingToId) {
-			replyingToPost = await Post.findById(
-				req.body.replyingToId,
-				{ include: [Thread, { model: User, attributes: ['username'] }] }
+			replyingToPost = await Post.getReplyingToPost(
+				req.body.replyingToId, thread
 			)
 
-			if(!replyingToPost) {
-				throw Errors.invalidParameter('replyingToId', 'post does not exist')
-			} else if(replyingToPost.Thread.id !== thread.id) {
-				throw Errors.invalidParameter('replyingToId', 'replies must be in same thread')
-			} else if (replyingToPost.removed) {
-				throw Errors.postRemoved
-			} else {
-				post = await Post.create({ content: req.body.content, postNumber: thread.postsCount })
-
-				await post.setReplyingTo(replyingToPost)
-				await replyingToPost.addReplies(post)
+			post = await Post.create({ content: req.body.content, postNumber: thread.postsCount })
 
-				let replyNotification = await Notification.createPostNotification({
-					usernameTo: replyingToPost.User.username,
-					userFrom: user,
-					type: 'reply',
-					post: post
-				})
+			await post.setReplyingTo(replyingToPost)
+			await replyingToPost.addReplies(post)
 
-				let ioUsers = req.app.get('io-users')
-				if(ioUsers[replyingToPost.User.username]) {
-					req.app
-						.get('io')
-						.to(ioUsers[replyingToPost.User.username])
-						.emit('notification', replyNotification.toJSON())
-				}
-			}
+			let replyNotification = await Notification.createPostNotification({
+				usernameTo: replyingToPost.User.username,
+				userFrom: user,
+				type: 'reply',
+				post: post
+			})
+			await replyNotification.emitNotificationMessage(
+				req.app.get('io-users'),
+				req.app.get('io')
+			)
 		} else {
 			post = await Post.create({ content: req.body.content, postNumber: thread.postsCount })
 		}
@@ -202,26 +146,20 @@ router.post('/', async (req, res) => {
 
 		await thread.increment('postsCount')
 
-		if(req.body.mentions) {
-			let uniqueMentions = req.body.mentions.filter((mention, pos, self) => {
-				return self.indexOf(mention) === pos
-			})
-
-			for(var i = 0; i < uniqueMentions.length; i++) {
-				let mention = uniqueMentions[i]
-				let ioUsers = req.app.get('io-users')
+		if(uniqueMentions.length) {
+			let ioUsers = req.app.get('io-users')
+			let io = req.app.get('io')
 
+			uniqueMentions.forEach(async mention => {
 				let mentionNotification = await Notification.createPostNotification({
 					usernameTo: mention,
 					userFrom: user,
 					type: 'mention',
 					post
 				})
-				
-				if(ioUsers[mention]) {
-					req.app.get('io').to(ioUsers[mention]).emit('notification', mentionNotification.toJSON())
-				}
-			}
+
+				await mentionNotification.emitNotificationMessage(ioUsers, io)
+			})
 		}
 
 		res.json(await post.reload({
@@ -233,12 +171,10 @@ router.post('/', async (req, res) => {
 		})
 
 	} catch (e) {
-		if(e === Errors.VALIDATION_ERROR) {
+		if(e instanceof Sequelize.ValidationError) {
 			res.status(400)
-			res.json({
-				errors: validationErrors
-			})
-		} else if(['invalidParameter', 'threadLocked', 'postRemoved'].indexOf(e.name) > -1) {
+			res.json(e)
+		} else if(e.name in Errors) {
 			res.status(400)
 			res.json({
 				errors: [e]
@@ -253,4 +189,41 @@ router.post('/', async (req, res) => {
 	}
 })
 
+router.all('*', (req, res, next) => {
+	if(!req.session.admin) {
+		res.status(401)
+		res.json({
+			errors: [Errors.requestNotAuthorized]
+		})
+	} else {
+		next()
+	}
+})
+
+router.delete('/:post_id', async (req, res) => {
+	try {
+		let post = await Post.findById(req.params.post_id)
+		if(!post) throw Errors.sequelizeValidation(Sequelize, {
+			error: 'post does not exist',
+			path: 'id'
+		})
+
+		await post.update({ content: '[This post has been removed by an administrator]', removed: true })
+
+		res.json({ success: true })
+	} catch (e) {
+		if(e instanceof Sequelize.ValidationError) {
+			res.status(400)
+			res.json(e)
+		} else {
+			console.log(e)
+
+			res.status(500)
+			res.json({
+				errors: [Errors.unknown]
+			})
+		}
+	}
+})
+
 module.exports = router

+ 71 - 57
test/thread_post.js

@@ -362,64 +362,78 @@ describe('Thread and post', () => {
 				JSON.parse(res.response.text).errors.should.contain.something.that.deep.equals(Errors.requestNotAuthorized)
 			}
 		})
-		it('should return an error if missing parameters', async () => {
-			try {
-				let res = await userAgent
-					.post('/api/v1/post')
-
-				res.should.be.json
-				res.should.have.status(400)
-				res.body.errors.should.contain.something.that.deep.equals(Errors.missingParameter('content'))
-				res.body.errors.should.contain.something.that.deep.equals(Errors.missingParameter('threadId'))
-			} catch (res) {
-				let body = JSON.parse(res.response.text)
-				res.should.have.status(400)
-				body.errors.should.contain.something.that.deep.equals(Errors.missingParameter('content'))
-				body.errors.should.contain.something.that.deep.equals(Errors.missingParameter('threadId'))
-			}
-		})
-		it('should return an error if invalid types', async () => {
-			try {
-				let res = await userAgent
+		it('should return an error if missing content', done => {
+				userAgent
 					.post('/api/v1/post')
-					.set('content-type', 'application/json')
-					.send({
-						content: 123,
-						threadId: 'string',
-						replyingToId: 'string'
+					.send({		
+						threadId: 1
 					})
+					.end((err, res) => {
+						res.should.be.json
+						res.should.have.status(400)
+						res.body.errors.should.contain.something.that.has.property('message', 'content must be a string')
 
-				res.should.be.json
-				res.should.have.status(400)
-				res.body.errors.should.contain.something.that.deep.equals(Errors.invalidParameterType('content', 'string'))
-				res.body.errors.should.contain.something.that.deep.equals(Errors.invalidParameterType('threadId', 'integer'))
-				res.body.errors.should.contain.something.that.deep.equals(Errors.invalidParameterType('replyingToId', 'integer'))
-			} catch (res) {
-				let body = JSON.parse(res.response.text)
-				res.should.have.status(400)
-				body.errors.should.contain.something.that.deep.equals(Errors.invalidParameterType('content', 'string'))
-				body.errors.should.contain.something.that.deep.equals(Errors.invalidParameterType('threadId', 'integer'))
-				body.errors.should.contain.something.that.deep.equals(Errors.invalidParameterType('replyingToId', 'integer'))
-			}
-		})
-		it('should return an error if thread id does not exist', async () => {
-			try {
-				let res = await userAgent
-					.post('/api/v1/post')
-					.set('content-type', 'application/json')
-					.send({
-						content: 'content',
-						threadId: 10
+						done()
 					})
-
-				res.should.be.json
-				res.should.have.status(400)
-				res.body.errors.should.include.something.that.deep.equals(Errors.invalidParameter('threadId', 'thread does not exist'))
-			} catch (res) {
-				let body = JSON.parse(res.response.text)
-				res.should.have.status(400)
-				body.errors.should.include.something.that.deep.equals(Errors.invalidParameter('threadId', 'thread does not exist'))
-			}
+		})
+		it('should return an error if missing threadId', done => {
+			userAgent
+				.post('/api/v1/post')
+				.send({
+					content: 'content'
+				})
+				.end((err, res) => {
+					res.should.be.json
+					res.should.have.status(400)
+					res.body.errors.should.contain.something.that.has.property('message', 'thread does not exist')
+					
+					done()
+				})
+		})
+		it('should return an error if thread id does not exist', done => {
+			userAgent
+				.post('/api/v1/post')
+				.set('content-type', 'application/json')
+				.send({
+					content: 'content',
+					threadId: 10
+				})
+				.end((err, res) => {
+					res.should.be.json
+					res.should.have.status(400)
+					res.body.errors.should.contain.something.that.has.property('message', 'thread does not exist')
+					done()
+				})
+		})
+		it('should return an error if mentions are invalid type', done => {
+			userAgent
+				.post('/api/v1/post')
+				.set('content-type', 'application/json')
+				.send({
+					content: 'content',
+					threadId: 1,
+					mentions: 'string'
+				})
+				.end((err, res) => {
+					res.should.be.json
+					res.should.have.status(400)
+					res.body.errors.should.contain.something.that.has.property('message', 'mentions must be an array of strings')
+					
+					userAgent
+						.post('/api/v1/post')
+						.set('content-type', 'application/json')
+						.send({
+							content: 'content',
+							threadId: 1,
+							mentions: ['string', false, 3]
+						})
+						.end((err, res) => {
+							res.should.be.json
+							res.should.have.status(400)
+							res.body.errors.should.contain.something.that.has.property('message', 'mentions must be an array of strings')
+							done()
+						})
+				})
 		})
 		it('should be able to reply to a post', async () => {
 			await replyAgent
@@ -651,12 +665,12 @@ describe('Thread and post', () => {
 				let res = await chai.request(server).get('/api/v1/post/invalid')
 
 				res.should.have.status(400)
-				res.body.errors.should.contain.something.that.deep.equals(Errors.invalidParameter('id', 'post does not exist'))
+				res.body.errors.should.contain.something.that.has.property('message', 'post does not exist')
 			} catch (res) {
 				let body = JSON.parse(res.response.text)
 
 				res.should.have.status(400)
-				body.errors.should.contain.something.that.deep.equals(Errors.invalidParameter('id', 'post does not exist'))
+				body.errors.should.contain.something.that.has.property('message', 'post does not exist')
 			}
 		})
 	})
@@ -735,7 +749,7 @@ describe('Thread and post', () => {
 				.end((err, res) => {
 					res.should.be.json
 					res.should.have.status(400)
-					res.body.errors.should.include.something.that.deep.equals(Errors.invalidParameter('postId', 'post does not exist'))
+					res.body.errors.should.include.something.that.has.property('message', 'post does not exist')
 
 					done()
 				})