Skip to content

Commit 702a50b

Browse files
committed
feat(endpoint): User can rate an accomodation
- Creates rating model and migration - Creates seeders - Writes tests [finishes #167891606]
1 parent 28b335e commit 702a50b

9 files changed

Lines changed: 358 additions & 0 deletions

File tree

src/controllers/Rating.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import db from '../database/models';
2+
import Response from '../helpers/Response';
3+
4+
const { Accomodation, Rating } = db;
5+
6+
/** Rating Class */
7+
class Ratings {
8+
/**
9+
* @param {req} req
10+
* @param {res} res
11+
* @returns {object} object
12+
*/
13+
static async create(req, res) {
14+
try {
15+
const { payload } = req.payload;
16+
const { id: userId } = payload;
17+
18+
const {
19+
accomodationId, ratingValue
20+
} = req.body;
21+
22+
const accomodation = await Accomodation.findOne({ where: { id: accomodationId } });
23+
if (!accomodation) {
24+
const response = new Response(
25+
false,
26+
404,
27+
'This accomodation does not exist'
28+
);
29+
return res.status(response.code).json(response);
30+
}
31+
32+
const previousRating = await Rating.findOne(
33+
{
34+
where: { userId, accomodationId }
35+
}
36+
);
37+
38+
if (!previousRating) {
39+
const ratingDetails = await Rating.create({
40+
userId,
41+
accomodationId,
42+
ratingValue
43+
});
44+
const response = new Response(
45+
true,
46+
201,
47+
'Thank you for rating this accomodation',
48+
{ rating: ratingDetails }
49+
);
50+
return res.status(response.code).json(response);
51+
}
52+
const ratingDetails = await Rating.update(
53+
{ ratingValue },
54+
{
55+
where: { userId, accomodationId },
56+
returning: true,
57+
}
58+
);
59+
const response = new Response(
60+
true,
61+
200,
62+
'Thank you for rating this accomodation',
63+
{ rating: ratingDetails }
64+
);
65+
return res.status(response.code).json(response);
66+
} catch (err) {
67+
const response = new Response(
68+
false,
69+
500,
70+
'Server error, Please try again later',
71+
);
72+
return res.status(response.code).json(response);
73+
}
74+
}
75+
}
76+
77+
export default Ratings;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
module.exports = {
2+
up: (queryInterface, Sequelize) => {
3+
return queryInterface.createTable('Ratings', {
4+
id: {
5+
allowNull: false,
6+
primaryKey: true,
7+
type: Sequelize.UUID,
8+
defaultValue: Sequelize.literal('uuid_generate_v4()')
9+
},
10+
userId: {
11+
type: Sequelize.UUID,
12+
allowNull: false,
13+
references: {
14+
model: 'Users',
15+
key: 'id'
16+
},
17+
onUpdate: 'CASCADE',
18+
onDelete: 'CASCADE'
19+
},
20+
accomodationId: {
21+
type: Sequelize.UUID,
22+
allowNull: false,
23+
references: {
24+
model: 'Accomodations',
25+
key: 'id'
26+
},
27+
onUpdate: 'CASCADE',
28+
onDelete: 'CASCADE'
29+
},
30+
ratingValue: {
31+
type: Sequelize.INTEGER,
32+
allowNull: true,
33+
defaultValue: null
34+
},
35+
createdAt: {
36+
allowNull: false,
37+
type: Sequelize.DATE,
38+
defaultValue: Sequelize.fn('now')
39+
},
40+
updatedAt: {
41+
allowNull: false,
42+
type: Sequelize.DATE,
43+
defaultValue: Sequelize.fn('now')
44+
}
45+
});
46+
},
47+
down: queryInterface => queryInterface.dropTable('Ratings')
48+
};

src/database/models/rating.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = (sequelize, DataTypes) => {
2+
const Rating = sequelize.define('Rating', {
3+
userId: DataTypes.UUID,
4+
accomodationId: DataTypes.UUID,
5+
ratingValue: DataTypes.INTEGER,
6+
},
7+
{
8+
indexes: [
9+
{
10+
unique: true,
11+
fields: ['userId', 'accomodationId'],
12+
},
13+
],
14+
});
15+
Rating.associate = (models) => {
16+
Rating.belongsTo(models.Accomodation, {
17+
foreignKey: 'accomodationId',
18+
});
19+
Rating.belongsTo(models.User, {
20+
foreignKey: 'userId',
21+
});
22+
};
23+
return Rating;
24+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
up: (queryInterface) => queryInterface.bulkInsert('Ratings', [
3+
{
4+
userId: '91542e6f-94bc-4e80-a667-586fb0752f23',
5+
accomodationId: '3dd3b34a-7554-425e-a688-36afda199619',
6+
ratingValue: 3
7+
}
8+
]),
9+
down: queryInterface => queryInterface.bulkDelete('Ratings', null, {})
10+
};

src/routes/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import searchRoute from './search.routes';
88
import accomodationRoutes from './accomodation.routes';
99
import commentRoute from './comment.routes';
1010
import notificationOptRoute from './notificationOpt.routes';
11+
import ratingRoute from './rating.routes';
1112

1213
const router = Router();
1314

@@ -21,5 +22,6 @@ router.use('/admin/accomodation', accomodationRoutes);
2122
router.use('/user/accomodation', accomodationRoutes);
2223
router.use('/trips', commentRoute);
2324
router.use('/', adminRoute);
25+
router.use('/accomodation', ratingRoute);
2426

2527
export default router;

src/routes/rating.routes.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Router } from 'express';
2+
import Ratings from '../controllers/Rating';
3+
import ratingSchema from '../validation/ratingSchema';
4+
import validator from '../middlewares/validator';
5+
import middleware from '../middlewares/AuthMiddlewares';
6+
import tokenHelper from '../helpers/Token';
7+
8+
const ratingRoute = Router();
9+
10+
ratingRoute.post(
11+
'/ratings',
12+
tokenHelper.verifyToken,
13+
middleware.isUserVerified,
14+
validator(ratingSchema),
15+
Ratings.create
16+
);
17+
18+
export default ratingRoute;

src/validation/ratingSchema.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { check } from 'express-validator';
2+
3+
const rating = [
4+
check('accomodationId')
5+
.exists().withMessage('Accomodation Id is required')
6+
.isUUID(4)
7+
.withMessage('Invalid Accomodation Id format'),
8+
9+
check('ratingValue')
10+
.exists().withMessage('Rating is required')
11+
.isInt()
12+
.withMessage('Ratings should be a number between 0 to 5')
13+
.custom((value, { req }) => {
14+
if (value > 5) {
15+
throw new Error('Ratings should be between the number 0 to 5');
16+
}
17+
return true;
18+
})
19+
];
20+
21+
export default rating;

test/mockData/mockRatings.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const accomodationRatings = [
2+
{
3+
accomodationId: '3dd3b34a-7554-425e-a688-36afda199615',
4+
ratingValue: 2
5+
},
6+
{
7+
accomodationId: '3dd3b34a-7554-425e-a688-36afda199619',
8+
ratingValue: 5
9+
},
10+
{
11+
accomodationId: '3dd3b34a-7554-425e-a688-36afda199617',
12+
ratingValue: 2
13+
},
14+
{
15+
ratingValue: 2
16+
},
17+
{
18+
accomodationId: '3dd3b34a-7554-425e-a688-36afda199619'
19+
},
20+
{
21+
accomodationId: '3dd3b34a-7554-425e-a688-36afda199619',
22+
ratingValue: 7
23+
}
24+
];
25+
26+
const userOne = {
27+
email: 'tjhakeemus1@gmail.com',
28+
password: '12345678',
29+
code: '4RHJHJJKSK'
30+
};
31+
32+
export default { accomodationRatings, userOne };

test/ratings.test.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import chai from 'chai';
2+
import chaiHttp from 'chai-http';
3+
import sinon from 'sinon';
4+
import app from '../src/index';
5+
import db from '../src/database/models';
6+
import users from './mockData/mockAuth';
7+
import ratings from './mockData/mockRatings';
8+
9+
const { expect } = chai;
10+
chai.use(chaiHttp);
11+
12+
const baseURL = '/api/v1';
13+
14+
const { Rating } = db;
15+
const { user10 } = users;
16+
const { accomodationRatings, userOne } = ratings;
17+
18+
let validToken;
19+
20+
// check successful post
21+
describe('Ratings', () => {
22+
describe('Users post a request to rate an accomodation', () => {
23+
it('should return a valid token', (done) => {
24+
chai
25+
.request(app)
26+
.post(`${baseURL}/auth/login`)
27+
.send(user10)
28+
.end((err, res) => {
29+
const { user } = res.body.data;
30+
// eslint-disable-next-line prefer-destructuring
31+
validToken = user.token;
32+
done();
33+
});
34+
});
35+
it('should succesfully update if previous rating is found for an accomodation', (done) => {
36+
chai
37+
.request(app)
38+
.post(`${baseURL}/accomodation/ratings`)
39+
.send(accomodationRatings[1])
40+
.set('authorization', validToken)
41+
.end((err, res) => {
42+
expect(res).to.have.status(200);
43+
expect(res.body.message).to.eq('Thank you for rating this accomodation');
44+
done();
45+
});
46+
});
47+
it('should succesfully rate an accomodation', (done) => {
48+
chai
49+
.request(app)
50+
.post(`${baseURL}/accomodation/ratings`)
51+
.send(accomodationRatings[0])
52+
.set('authorization', validToken)
53+
.end((err, res) => {
54+
expect(res).to.have.status(201);
55+
done();
56+
});
57+
});
58+
it('should return an error if accomodation does not exist', (done) => {
59+
chai
60+
.request(app)
61+
.post(`${baseURL}/accomodation/ratings`)
62+
.send(accomodationRatings[2])
63+
.set('authorization', validToken)
64+
.end((err, res) => {
65+
expect(res).to.have.status(404);
66+
expect(res.body).to.be.an('object');
67+
done(err);
68+
});
69+
});
70+
it('should return an error if accomodation Id is not supplied', (done) => {
71+
chai
72+
.request(app)
73+
.post(`${baseURL}/accomodation/ratings`)
74+
.send(accomodationRatings[3])
75+
.set('authorization', validToken)
76+
.end((err, res) => {
77+
expect(res).to.have.status(400);
78+
expect(res.body).to.be.an('object');
79+
expect(res.body.message).to.eql('Validation Error!');
80+
expect(res.body.data.accomodationId).to.eql('Accomodation Id is required');
81+
done(err);
82+
});
83+
});
84+
it('should return an error if rating value is not supplied', (done) => {
85+
chai
86+
.request(app)
87+
.post(`${baseURL}/accomodation/ratings`)
88+
.send(accomodationRatings[4])
89+
.set('authorization', validToken)
90+
.end((err, res) => {
91+
expect(res).to.have.status(400);
92+
expect(res.body).to.be.an('object');
93+
expect(res.body.message).to.eql('Validation Error!');
94+
expect(res.body.data.ratingValue).to.eql('Rating is required');
95+
done(err);
96+
});
97+
});
98+
it('should return an error if rating is greater than 5', (done) => {
99+
chai
100+
.request(app)
101+
.post(`${baseURL}/accomodation/ratings`)
102+
.send(accomodationRatings[5])
103+
.set('authorization', validToken)
104+
.end((err, res) => {
105+
expect(res).to.have.status(400);
106+
expect(res.body).to.be.an('object');
107+
expect(res.body.message).to.eql('Validation Error!');
108+
expect(res.body.data.ratingValue).to.eql('Ratings should be between the number 0 to 5');
109+
done(err);
110+
});
111+
});
112+
it('should return a 500 error when an error occurs on the server', (done) => {
113+
const stub = sinon.stub(Rating, 'update')
114+
.rejects(new Error('Server error, Please try again later'));
115+
chai.request(app)
116+
.post(`${baseURL}/accomodation/ratings`)
117+
.send(accomodationRatings[0])
118+
.set('authorization', validToken)
119+
.end((err, res) => {
120+
expect(res.status).to.equal(500);
121+
stub.restore();
122+
done();
123+
});
124+
});
125+
});
126+
});

0 commit comments

Comments
 (0)