Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev To Main Sync #2295

Merged
merged 2 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions controllers/stocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,42 @@ const fetchStocks = async (req, res) => {
* @param req {Object} - Express request object
* @param res {Object} - Express response object
*/
/**
* @deprecated
* WARNING: This API endpoint is being deprecated and will be removed in future.
* Please use the updated API endpoint: `/stocks/:userId` for retrieving user stocks details.
*
* This API is kept temporarily for backward compatibility.
*/
const getSelfStocks = async (req, res) => {
try {
const { id: userId } = req.userData;
const userStocks = await stocks.fetchUserStocks(userId);

res.set(
"X-Deprecation-Warning",
"WARNING: This endpoint is being deprecated and will be removed in the future. Please use `/stocks/:userId` route to get the user stocks details."
);
return res.json({
message: userStocks.length > 0 ? "User stocks returned successfully!" : "No stocks found",
userStocks,
});
} catch (err) {
logger.error(`Error while getting user stocks ${err}`);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
};

/**
* Fetches all the stocks of the authenticated user
*
* @param req {Object} - Express request object
* @param res {Object} - Express response object
*/
const getUserStocks = async (req, res) => {
try {
const userStocks = await stocks.fetchUserStocks(req.params.userId);

return res.json({
message: userStocks.length > 0 ? "User stocks returned successfully!" : "No stocks found",
userStocks,
Expand All @@ -62,4 +94,5 @@ module.exports = {
addNewStock,
fetchStocks,
getSelfStocks,
getUserStocks,
};
23 changes: 13 additions & 10 deletions controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ const discordDeveloperRoleId = config.get("discordDeveloperRoleId");

const verifyUser = async (req, res) => {
const userId = req.userData.id;
const devFeatureFlag = req.query.dev === "true";
try {
if (!req.userData?.profileURL) {
return res.boom.serverUnavailable("ProfileURL is Missing");
}
await userQuery.addOrUpdate({ profileStatus: "PENDING" }, userId);
await userQuery.addOrUpdate({ profileStatus: "PENDING" }, userId, devFeatureFlag);
} catch (error) {
logger.error(`Error while verifying user: ${error}`);
return res.boom.serverUnavailable(SOMETHING_WENT_WRONG);
Expand Down Expand Up @@ -500,7 +501,7 @@ const updateSelf = async (req, res) => {
const { roles } = discordMember;
if (roles && roles.includes(discordDeveloperRoleId)) {
if (req.body.disabledRoles && devFeatureFlag) {
const updatedUser = await userQuery.addOrUpdate({ disabled_roles: rolesToDisable }, userId);
const updatedUser = await userQuery.addOrUpdate({ disabled_roles: rolesToDisable }, userId, devFeatureFlag);
if (updatedUser) {
return res
.status(200)
Expand All @@ -514,7 +515,7 @@ const updateSelf = async (req, res) => {
}
}

const updatedUser = await userQuery.addOrUpdate(req.body, userId);
const updatedUser = await userQuery.addOrUpdate(req.body, userId, devFeatureFlag);

if (!updatedUser.isNewUser) {
// Success criteria, user finished the sign-up process.
Expand Down Expand Up @@ -711,9 +712,9 @@ const getUserImageForVerification = async (req, res) => {
const updateUser = async (req, res) => {
try {
const { id: profileDiffId, message } = req.body;
const devFeatureFlag = req.query.dev;
const devFeatureFlag = req.query.dev === "true";
let profileDiffData;
if (devFeatureFlag === "true") {
if (devFeatureFlag) {
profileDiffData = await profileDiffsQuery.fetchProfileDiffUnobfuscated(profileDiffId);
} else {
profileDiffData = await profileDiffsQuery.fetchProfileDiff(profileDiffId);
Expand All @@ -728,7 +729,7 @@ const updateUser = async (req, res) => {

await profileDiffsQuery.updateProfileDiff({ approval: profileDiffStatus.APPROVED }, profileDiffId);

await userQuery.addOrUpdate(profileDiff, userId);
await userQuery.addOrUpdate(profileDiff, userId, devFeatureFlag);

const meta = {
approvedBy: req.userData.id,
Expand All @@ -749,9 +750,9 @@ const updateUser = async (req, res) => {
const generateChaincode = async (req, res) => {
try {
const { id } = req.userData;

const devFeatureFlag = req.query.dev === "true";
const chaincode = await chaincodeQuery.storeChaincode(id);
await userQuery.addOrUpdate({ chaincode }, id);
await userQuery.addOrUpdate({ chaincode }, id, devFeatureFlag);
return res.json({
chaincode,
message: "Chaincode returned successfully",
Expand All @@ -766,7 +767,8 @@ const profileURL = async (req, res) => {
try {
const userId = req.userData.id;
const { profileURL } = req.body;
await userQuery.addOrUpdate({ profileURL }, userId);
const devFeatureFlag = req.query.dev === "true";
await userQuery.addOrUpdate({ profileURL }, userId, devFeatureFlag);
return res.json({
message: "updated profile URL!!",
});
Expand Down Expand Up @@ -958,6 +960,7 @@ const setInDiscordScript = async (req, res) => {
const updateRoles = async (req, res) => {
try {
const result = await dataAccess.retrieveUsers({ id: req.params.id });
const devFeatureFlag = req.query.dev === "true";
if (result?.userExists) {
const dataToUpdate = req.body;
const roles = req?.userData?.roles;
Expand All @@ -966,7 +969,7 @@ const updateRoles = async (req, res) => {

const response = await getRoleToUpdate(result.user, dataToUpdate);
if (response.updateRole) {
await userQuery.addOrUpdate(response.newUserRoles, result.user.id);
await userQuery.addOrUpdate(response.newUserRoles, result.user.id, devFeatureFlag);
if (dataToUpdate?.archived) {
const body = {
reason: reason || "",
Expand Down
9 changes: 9 additions & 0 deletions middlewares/userAuthorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NextFunction } from "express";
import { CustomRequest, CustomResponse } from "../types/global";

export const userAuthorization = (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
if (req.params.userId === req.userData.id) {
return next();
}
res.boom.forbidden("Unauthorized access");
};
2 changes: 1 addition & 1 deletion models/stocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const userStocksModel = firestore.collection("user-stocks");
* Adds Stocks
*
* @param stockData { Object }: stock data object to be stored in DB
* @return {Promise<{stockId: string}>}
* @return {Promise<{id: string, stockData: Object}>}
*/
const addStock = async (stockData) => {
try {
Expand Down
28 changes: 19 additions & 9 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const archiveUsers = async (usersData) => {
* @param userId { String }: User Id String to be used to update the user
* @return {Promise<{isNewUser: boolean, userId: string}|{isNewUser: boolean, userId: string}>}
*/
const addOrUpdate = async (userData, userId = null) => {
const addOrUpdate = async (userData, userId = null, devFeatureFlag) => {
try {
// userId exists Update user
if (userId !== null) {
Expand All @@ -96,14 +96,24 @@ const addOrUpdate = async (userData, userId = null) => {
if ("id" in userData) {
delete userData.id;
}
await userModel.doc(userId).set(
{
...user.data(),
...userData,
updated_at: Date.now(),
},
{ merge: true }
);
if (devFeatureFlag) {
await userModel.doc(userId).set(
{
...userData,
updated_at: Date.now(),
},
{ merge: true }
);
} else {
await userModel.doc(userId).set(
{
...user.data(),
...userData,
updated_at: Date.now(),
},
{ merge: true }
);
}

const logData = {
type: logType.USER_DETAILS_UPDATED,
Expand Down
7 changes: 5 additions & 2 deletions routes/stocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ const express = require("express");
const router = express.Router();
const authenticate = require("../middlewares/authenticate");
const authorizeRoles = require("../middlewares/authorizeRoles");
const { addNewStock, fetchStocks, getSelfStocks } = require("../controllers/stocks");
const { addNewStock, fetchStocks, getSelfStocks, getUserStocks } = require("../controllers/stocks");
const { createStock } = require("../middlewares/validators/stocks");
const { SUPERUSER } = require("../constants/roles");
const { devFlagMiddleware } = require("../middlewares/devFlag");
const { userAuthorization } = require("../middlewares/userAuthorization");

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

module.exports = router;
97 changes: 97 additions & 0 deletions test/integration/stocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import chai from "chai";
import chaiHttp from "chai-http";
import app from "../../server";
import authService from "../../services/authService";
import addUser from "../utils/addUser";
import cleanDb from "../utils/cleanDb";
import stocks from "../../models/stocks";
import sinon from "sinon";
import config from "config";

const cookieName: string = config.get("userToken.cookieName");
chai.use(chaiHttp);
const { expect } = chai;

describe("GET /stocks/:userId", function () {
let jwt: string;
let userId: string;
let userStock;
const stockData = { name: "EURO", quantity: 2, price: 10 };

beforeEach(async function () {
userId = await addUser();
jwt = authService.generateAuthToken({ userId });
const { id } = await stocks.addStock(stockData);
userStock = { stockId: id, stockName: "EURO", quantity: 1, orderValue: 10, initialStockValue: 2 };
});

afterEach(async function () {
await cleanDb();
sinon.restore();
});

it("Should return user stocks when stocks are available", async function () {
await stocks.updateUserStocks(userId, userStock);

const res = await chai.request(app).get(`/stocks/${userId}?dev=true`).set("cookie", `${cookieName}=${jwt}`);

expect(res).to.have.status(200);
expect(res.body).to.be.an("object");
expect(res.body.message).to.equal("User stocks returned successfully!");
expect(res.body.userStocks).to.be.an("array");
expect(res.body.userStocks.map(({ id, ...rest }) => rest)).to.deep.equal([{ ...userStock, userId }]);
});

it("Should return empty object when no stocks are found", function (done) {
chai
.request(app)
.get(`/stocks/${userId}?dev=true`)
.set("cookie", `${cookieName}=${jwt}`)
.end((err, res) => {
if (err) return done(err);

expect(res).to.have.status(200);
expect(res.body).to.be.an("object");
expect(res.body.message).to.equal("No stocks found");
expect(res.body.userStocks).to.be.an("array");

return done();
});
});

it("Should return 403 for unauthorized access", function (done) {
const userId = "anotherUser123";

chai
.request(app)
.get(`/stocks/${userId}?dev=true`)
.set("cookie", `${cookieName}=${jwt}`)
.end((err, res) => {
if (err) return done(err);

expect(res).to.have.status(403);
expect(res.body).to.be.an("object");
expect(res.body.message).to.equal("Unauthorized access");

return done();
});
});

it("Should return 500 when an internal server error occurs", function (done) {
sinon.stub(stocks, "fetchUserStocks").throws(new Error("Database error"));

chai
.request(app)
.get(`/stocks/${userId}?dev=true`)
.set("cookie", `${cookieName}=${jwt}`)
.end((err, res) => {
if (err) return done(err);

expect(res).to.have.status(500);
expect(res.body).to.be.an("object");
expect(res.body.message).to.equal("An internal server error occurred");

return done();
});
});
});
70 changes: 70 additions & 0 deletions test/unit/middlewares/userAuthorization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as sinon from "sinon";
import chai from "chai";
const { expect } = chai;
const { userAuthorization } = require("../../../middlewares/userAuthorization");

describe("userAuthorization Middleware", function () {
let req;
let res;
let next;

beforeEach(function () {
req = {
params: {},
userData: {},
};
res = {
boom: {
forbidden: sinon.spy((message) => {
res.status = 403;
res.message = message;
}),
},
};
next = sinon.spy();
});

it("should call next() if userId matches userData.id", function () {
req.params.userId = "123";
req.userData.id = "123";

userAuthorization(req, res, next);

expect(next.calledOnce).to.be.true;
expect(res.boom.forbidden.notCalled).to.be.true;
});

it("should call res.boom.forbidden() if userId does not match userData.id", function () {
req.params.userId = "123";
req.userData.id = "456";

userAuthorization(req, res, next);

expect(res.boom.forbidden.calledOnce).to.be.true;
expect(res.status).to.equal(403);
expect(res.message).to.equal("Unauthorized access");
expect(next.notCalled).to.be.true;
});

it("should call res.boom.forbidden() if userData.id is missing", function () {
req.params.userId = "123";

userAuthorization(req, res, next);

expect(res.boom.forbidden.calledOnce).to.be.true;
expect(res.status).to.equal(403);
expect(res.message).to.equal("Unauthorized access");
expect(next.notCalled).to.be.true;
});

it("should call res.boom.forbidden() if userId is missing", function () {
req.userData.id = "123";

userAuthorization(req, res, next);

expect(res.boom.forbidden.calledOnce).to.be.true;
expect(res.status).to.equal(403);
expect(res.message).to.equal("Unauthorized access");
expect(next.notCalled).to.be.true;
});
});
Loading
Loading