Skip to content

Commit c976b31

Browse files
authored
Merge pull request #2292 from vikasosmium/deprecate-stocks-self-GET-route
Added New Route to fetch User Stocks
1 parent 70cfd78 commit c976b31

File tree

6 files changed

+215
-3
lines changed

6 files changed

+215
-3
lines changed

controllers/stocks.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,42 @@ const fetchStocks = async (req, res) => {
4444
* @param req {Object} - Express request object
4545
* @param res {Object} - Express response object
4646
*/
47+
/**
48+
* @deprecated
49+
* WARNING: This API endpoint is being deprecated and will be removed in future.
50+
* Please use the updated API endpoint: `/stocks/:userId` for retrieving user stocks details.
51+
*
52+
* This API is kept temporarily for backward compatibility.
53+
*/
4754
const getSelfStocks = async (req, res) => {
4855
try {
4956
const { id: userId } = req.userData;
5057
const userStocks = await stocks.fetchUserStocks(userId);
58+
59+
res.set(
60+
"X-Deprecation-Warning",
61+
"WARNING: This endpoint is being deprecated and will be removed in the future. Please use `/stocks/:userId` route to get the user stocks details."
62+
);
63+
return res.json({
64+
message: userStocks.length > 0 ? "User stocks returned successfully!" : "No stocks found",
65+
userStocks,
66+
});
67+
} catch (err) {
68+
logger.error(`Error while getting user stocks ${err}`);
69+
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
70+
}
71+
};
72+
73+
/**
74+
* Fetches all the stocks of the authenticated user
75+
*
76+
* @param req {Object} - Express request object
77+
* @param res {Object} - Express response object
78+
*/
79+
const getUserStocks = async (req, res) => {
80+
try {
81+
const userStocks = await stocks.fetchUserStocks(req.params.userId);
82+
5183
return res.json({
5284
message: userStocks.length > 0 ? "User stocks returned successfully!" : "No stocks found",
5385
userStocks,
@@ -62,4 +94,5 @@ module.exports = {
6294
addNewStock,
6395
fetchStocks,
6496
getSelfStocks,
97+
getUserStocks,
6598
};

middlewares/userAuthorization.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { NextFunction } from "express";
2+
import { CustomRequest, CustomResponse } from "../types/global";
3+
4+
export const userAuthorization = (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
5+
if (req.params.userId === req.userData.id) {
6+
return next();
7+
}
8+
res.boom.forbidden("Unauthorized access");
9+
};

models/stocks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const userStocksModel = firestore.collection("user-stocks");
66
* Adds Stocks
77
*
88
* @param stockData { Object }: stock data object to be stored in DB
9-
* @return {Promise<{stockId: string}>}
9+
* @return {Promise<{id: string, stockData: Object}>}
1010
*/
1111
const addStock = async (stockData) => {
1212
try {

routes/stocks.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ const express = require("express");
22
const router = express.Router();
33
const authenticate = require("../middlewares/authenticate");
44
const authorizeRoles = require("../middlewares/authorizeRoles");
5-
const { addNewStock, fetchStocks, getSelfStocks } = require("../controllers/stocks");
5+
const { addNewStock, fetchStocks, getSelfStocks, getUserStocks } = require("../controllers/stocks");
66
const { createStock } = require("../middlewares/validators/stocks");
77
const { SUPERUSER } = require("../constants/roles");
8+
const { devFlagMiddleware } = require("../middlewares/devFlag");
9+
const { userAuthorization } = require("../middlewares/userAuthorization");
810

911
router.get("/", fetchStocks);
1012
router.post("/", authenticate, authorizeRoles([SUPERUSER]), createStock, addNewStock);
11-
router.get("/user/self", authenticate, getSelfStocks);
13+
router.get("/user/self", authenticate, getSelfStocks); // this route will soon be deprecated, please use `/stocks/:userId` route.
14+
router.get("/:userId", devFlagMiddleware, authenticate, userAuthorization, getUserStocks);
1215

1316
module.exports = router;

test/integration/stocks.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import chai from "chai";
2+
import chaiHttp from "chai-http";
3+
import app from "../../server";
4+
import authService from "../../services/authService";
5+
import addUser from "../utils/addUser";
6+
import cleanDb from "../utils/cleanDb";
7+
import stocks from "../../models/stocks";
8+
import sinon from "sinon";
9+
import config from "config";
10+
11+
const cookieName: string = config.get("userToken.cookieName");
12+
chai.use(chaiHttp);
13+
const { expect } = chai;
14+
15+
describe("GET /stocks/:userId", function () {
16+
let jwt: string;
17+
let userId: string;
18+
let userStock;
19+
const stockData = { name: "EURO", quantity: 2, price: 10 };
20+
21+
beforeEach(async function () {
22+
userId = await addUser();
23+
jwt = authService.generateAuthToken({ userId });
24+
const { id } = await stocks.addStock(stockData);
25+
userStock = { stockId: id, stockName: "EURO", quantity: 1, orderValue: 10, initialStockValue: 2 };
26+
});
27+
28+
afterEach(async function () {
29+
await cleanDb();
30+
sinon.restore();
31+
});
32+
33+
it("Should return user stocks when stocks are available", async function () {
34+
await stocks.updateUserStocks(userId, userStock);
35+
36+
const res = await chai.request(app).get(`/stocks/${userId}?dev=true`).set("cookie", `${cookieName}=${jwt}`);
37+
38+
expect(res).to.have.status(200);
39+
expect(res.body).to.be.an("object");
40+
expect(res.body.message).to.equal("User stocks returned successfully!");
41+
expect(res.body.userStocks).to.be.an("array");
42+
expect(res.body.userStocks.map(({ id, ...rest }) => rest)).to.deep.equal([{ ...userStock, userId }]);
43+
});
44+
45+
it("Should return empty object when no stocks are found", function (done) {
46+
chai
47+
.request(app)
48+
.get(`/stocks/${userId}?dev=true`)
49+
.set("cookie", `${cookieName}=${jwt}`)
50+
.end((err, res) => {
51+
if (err) return done(err);
52+
53+
expect(res).to.have.status(200);
54+
expect(res.body).to.be.an("object");
55+
expect(res.body.message).to.equal("No stocks found");
56+
expect(res.body.userStocks).to.be.an("array");
57+
58+
return done();
59+
});
60+
});
61+
62+
it("Should return 403 for unauthorized access", function (done) {
63+
const userId = "anotherUser123";
64+
65+
chai
66+
.request(app)
67+
.get(`/stocks/${userId}?dev=true`)
68+
.set("cookie", `${cookieName}=${jwt}`)
69+
.end((err, res) => {
70+
if (err) return done(err);
71+
72+
expect(res).to.have.status(403);
73+
expect(res.body).to.be.an("object");
74+
expect(res.body.message).to.equal("Unauthorized access");
75+
76+
return done();
77+
});
78+
});
79+
80+
it("Should return 500 when an internal server error occurs", function (done) {
81+
sinon.stub(stocks, "fetchUserStocks").throws(new Error("Database error"));
82+
83+
chai
84+
.request(app)
85+
.get(`/stocks/${userId}?dev=true`)
86+
.set("cookie", `${cookieName}=${jwt}`)
87+
.end((err, res) => {
88+
if (err) return done(err);
89+
90+
expect(res).to.have.status(500);
91+
expect(res.body).to.be.an("object");
92+
expect(res.body.message).to.equal("An internal server error occurred");
93+
94+
return done();
95+
});
96+
});
97+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as sinon from "sinon";
2+
import chai from "chai";
3+
const { expect } = chai;
4+
const { userAuthorization } = require("../../../middlewares/userAuthorization");
5+
6+
describe("userAuthorization Middleware", function () {
7+
let req;
8+
let res;
9+
let next;
10+
11+
beforeEach(function () {
12+
req = {
13+
params: {},
14+
userData: {},
15+
};
16+
res = {
17+
boom: {
18+
forbidden: sinon.spy((message) => {
19+
res.status = 403;
20+
res.message = message;
21+
}),
22+
},
23+
};
24+
next = sinon.spy();
25+
});
26+
27+
it("should call next() if userId matches userData.id", function () {
28+
req.params.userId = "123";
29+
req.userData.id = "123";
30+
31+
userAuthorization(req, res, next);
32+
33+
expect(next.calledOnce).to.be.true;
34+
expect(res.boom.forbidden.notCalled).to.be.true;
35+
});
36+
37+
it("should call res.boom.forbidden() if userId does not match userData.id", function () {
38+
req.params.userId = "123";
39+
req.userData.id = "456";
40+
41+
userAuthorization(req, res, next);
42+
43+
expect(res.boom.forbidden.calledOnce).to.be.true;
44+
expect(res.status).to.equal(403);
45+
expect(res.message).to.equal("Unauthorized access");
46+
expect(next.notCalled).to.be.true;
47+
});
48+
49+
it("should call res.boom.forbidden() if userData.id is missing", function () {
50+
req.params.userId = "123";
51+
52+
userAuthorization(req, res, next);
53+
54+
expect(res.boom.forbidden.calledOnce).to.be.true;
55+
expect(res.status).to.equal(403);
56+
expect(res.message).to.equal("Unauthorized access");
57+
expect(next.notCalled).to.be.true;
58+
});
59+
60+
it("should call res.boom.forbidden() if userId is missing", function () {
61+
req.userData.id = "123";
62+
63+
userAuthorization(req, res, next);
64+
65+
expect(res.boom.forbidden.calledOnce).to.be.true;
66+
expect(res.status).to.equal(403);
67+
expect(res.message).to.equal("Unauthorized access");
68+
expect(next.notCalled).to.be.true;
69+
});
70+
});

0 commit comments

Comments
 (0)