Skip to content

Commit

Permalink
feat(bookmark-article): users should bookmark articles
Browse files Browse the repository at this point in the history
- add bookmarking functionality
- add tests for the endpoint
- document feature using swagger

[Delivers #166790012]
  • Loading branch information
IsaiahRn authored and dmithamo committed Jul 30, 2019
1 parent f4a5078 commit 73ec4cf
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 4 deletions.
123 changes: 123 additions & 0 deletions controllers/bookmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import models from '../models';

const { bookmark, articles, users } = models;
/**
* @description Controller for Bookmarking
*/
class Bookmark {
/**
* @param {object} req
* @param {object} res
* @param {string} string
* @returns {object} response
*/
static async bookmark(req, res) {
try {
const { slug } = req.params;
const articleId = await articles.findOne({
where: { slug }
});
if (!articleId) {
return res.status(404).json({
error: 'Article is not found.'
});
}
const loggedinUser = await users.findOne({ where: { username: req.decoded.username } });
const userId = loggedinUser.id;
const bookmarked = await bookmark.findOne({
where: {
userId,
articleId: articleId.dataValues.id
},
});
const { title } = articleId;
if (!bookmarked) {
await bookmark.create({
userId,
articleId: articleId.dataValues.id
});
return res.status(201).json({
message: 'Successfully bookmarked the article',
article: {
title
}
});
}
await bookmark.destroy({ where: { userId, articleId: articleId.dataValues.id } });
return res.status(204).json({
message: 'Successfully unbookmarked the article',
});
} catch (error) {
return res.status(500).json({ error: 'Failed to bookmark article, please try again' });
}
}

/**
* @param {object} req
* @param {object} res
* @param {string} string
* @returns {object} response
*/
static async unBookmark(req, res) {
try {
const { slug } = req.params;
const articleId = await articles.findOne({
where: { slug }
});
if (!articleId) {
return res.status(404).json({
error: 'Article is not found.'
});
}
const loggedinUser = await users.findOne({ where: { username: req.decoded.username } });
const userId = loggedinUser.id;
const bookmarked = await bookmark.findOne({
where: {
userId,
articleId: articleId.dataValues.id,
},
});
if (bookmarked) {
await bookmark.destroy({
where: {
userId,
articleId: articleId.dataValues.id
}
});
}
return res.status(204).json({
message: 'Successfully unbookmarked the article'
});
} catch (error) {
return res.status(500).json({ error: 'Failed to unbookmark article, please try again' });
}
}

/**
* @param {object} req
* @param {object} res
* @returns {object} response
*/
static async getBookmarks(req, res) {
try {
const loggedinUser = await users.findOne({ where: { username: req.decoded.username } });
const userId = loggedinUser.id;
const Bookmarks = await bookmark.findAll({
attributes: [],
where: {
userId,
},

include: [{ model: articles, as: 'article', attributes: ['title', 'description', 'body', 'tagList', 'image', 'createdAt', 'updatedAt'] }],
});
return res.status(200).json({
message: 'Article bookmarks retrieved!',
Bookmarks,
});
} catch (error) {
return res.status(500).json({ error: 'Failed to retrieve bookmarks, please try again' });
}
}
}

export default Bookmark;
34 changes: 34 additions & 0 deletions migrations/20190724205121-create-bookmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const up = (queryInterface, Sequelize) => queryInterface.createTable('bookmarks', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
allowNull: false,
primaryKey: true
},
userId: {
type: Sequelize.UUID,
allowNull: false,
refences: {
model: 'users',
key: 'id'
}
},
articleId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'articles',
key: 'id'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});

export const down = queryInterface => queryInterface.dropTable('bookmarks');
1 change: 1 addition & 0 deletions models/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default (sequelize, DataTypes) => {
articles.belongsTo(models.users, { as: 'author', foreignKey: 'authorId' });
articles.hasMany(models.ratings, { foreignKey: 'articleSlug', sourceKey: 'slug' });
articles.hasMany(models.likes, { foreignKey: 'articleSlug', sourceKey: 'slug' });
articles.hasMany(models.bookmark, { foreignKey: 'articleId' });
};
return articles;
};
27 changes: 27 additions & 0 deletions models/bookmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default (sequelize, DataTypes) => {
const Bookmark = sequelize.define('bookmark', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
allowNull: false,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
},
articleId: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
}
}, {});
Bookmark.associate = (models) => {
Bookmark.belongsTo(models.users, {
foreignKey: 'userId', targetKey: 'id', as: 'user'
});
Bookmark.belongsTo(models.articles, {
foreignKey: 'articleId', targetKey: 'id', as: 'article'
});
};
return Bookmark;
};
1 change: 1 addition & 0 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default (sequelize, DataTypes) => {
users.hasMany(models.articles, { foreignKey: 'authorId', allowNull: false });
users.hasMany(models.comments, { foreignKey: 'authorId', allowNull: false });
users.hasMany(models.Follow, { as: 'User', foreignKey: 'followed' });
users.hasMany(models.bookmark, { foreignKey: 'userId', allowNull: false });
};
return users;
};
72 changes: 72 additions & 0 deletions routes/api/articles.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from 'express';
import article from '../../controllers/article';
import bookmark from '../../controllers/bookmark';
import { createComment } from '../../controllers/comment';
import { checkArticleOwner } from '../../middlewares/checkResourceOwner';
import validate from '../../middlewares/validations/articleValidations';
Expand Down Expand Up @@ -230,4 +231,75 @@ router.post('/articles/:slug/comments', checkToken, createComment);
*/
router.post('/articles/:slug/share/:channel', article.shareArticle);

/**
* @swagger
* /articles/{slug}/bookmark:
* post:
* tags:
* - Articles
* summary: Bookmark an article
* description:
* Authentication required, returns a bookmarked article. No additional
* parameters required
* produces:
* - application/json
* parameters:
* - in: path
* name: slug
* schema:
* type: string
* required: true
* description: The artice will be bookmarked
* responses:
* 200:
* description: Successful operation
* 404:
* description: Article not found
*/
router.post('/articles/:slug/bookmark', checkToken, bookmark.bookmark);
/**
* @swagger
* /bookmark:
* get:
* tags:
* - Articles
* summary: View all the bookmarked articles
* description:
* Authentication required, returns the bookmarked articles of the authenticated
* user. No additional parameters required
* produces:
* - application/json
* responses:
* 200:
* description: Successful operation
* 401:
* description: Unauthorized access
*/
router.get('/bookmark', checkToken, bookmark.getBookmarks);
/**
* @swagger
* /articles/{slug}/bookmark:
* delete:
* tags:
* - Articles
* summary: Unbookmark an article
* description:
* Authentication required, returns an unbookmarked article. No additional
* parameters required
* produces:
* - application/json
* parameters:
* - in: path
* name: slug
* description: The article will be unbookmarked
* required: true
* type: string
* responses:
* 200:
* description: successful operation
* 404:
* description: Article not found
*/
router.delete('/articles/:slug/bookmark', checkToken, bookmark.unBookmark);

export default router;
92 changes: 92 additions & 0 deletions tests/bookmark.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import chai from 'chai';
import chaiHttp from 'chai-http';
import app from '../index';
import user from './index.test';

chai.use(chaiHttp);
const { expect } = chai;

let tokenGen1;
const slugArticle = 'the-basics-of-java';

describe('Article bookmark', () => {
it('signin a user for bookmark', (done) => {
chai.request(app)
.post('/api/users/login')
.send(user.adminLogin)
.end((req, res) => {
const { status, body } = res;
expect(status).to.equal(200);
expect(res.body).to.be.an('object');
expect(body).to.have.property('message');
expect(body).to.have.property('user');
expect(body.user).to.have.property('email');
expect(body.user).to.have.property('username');
expect(body.user).to.have.property('token');
expect(body.message).to.equals('logged in');
tokenGen1 = body.user.token;
done();
});
});

it('Should not bookmark the article', (done) => {
chai.request(app)
.post(`/api/articles/${slugArticle}/bookmark`)
.send({})
.end((err, res) => {
expect(res.status).to.equal(401);
done();
});
});
it('Should successfully bookmark an article', (done) => {
chai.request(app)
.post(`/api/articles/${slugArticle}/bookmark`)
.set('Content-Type', 'application/json')
.set('Authorization', tokenGen1)
.end((err, res) => {
expect(res.status).to.equal(201);
expect(res.body.message).to.equal('Successfully bookmarked the article');
expect(res.body).to.have.property('article');
expect(res.body.article).to.have.property('title');
done();
});
});
it('Should allow the user to view all the article bookmarked', (done) => {
chai.request(app)
.get('/api/bookmark')
.set('Authorization', tokenGen1)
.end((err, res) => {
expect(res.status).to.equal(200);
expect(res.body).to.have.property('Bookmarks').be.an('array');
done();
});
});

// Test of unfound bookmark

it('Should not find the article to bookmark', (done) => {
chai.request(app)
.post('/api/articles/non-existing-article/bookmark')
.set('Content-Type', 'application/json')
.set('Authorization', tokenGen1)
.end((err, res) => {
expect(res.status).to.equal(404);
expect(res.body.error).to.equal('Article is not found.');
done();
});
});

// Unbookmark the bookmarked article

it('Should successfully unbookmark the article', (done) => {
chai.request(app)
.delete(`/api/articles/${slugArticle}/bookmark`)
.set('Content-Type', 'application/json')
.set('Authorization', tokenGen1)
.end((err, res) => {
expect(res.status).to.equal(204);
expect(res.body).to.be.an('object');
done();
});
});
});
Loading

0 comments on commit 73ec4cf

Please sign in to comment.