diff --git a/package-lock.json b/package-lock.json index 5964ddf6..cc92e5c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@types/mocha": "^10.0.6", "@types/multer": "^1.4.11", "@types/passport-google-oauth2": "^0.1.8", - "@types/sinon": "^17.0.3", "bcrypt": "^5.1.1", "chai": "^4.4.1", "chai-http": "^4.4.0", @@ -46,7 +45,6 @@ "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", "sinon": "^18.0.0", - "sinon-chai": "^3.7.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", "ts-node": "^10.9.2", @@ -64,6 +62,7 @@ "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/sequelize": "^4.28.20", + "@types/sinon": "^17.0.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.10.0", @@ -71,7 +70,8 @@ "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", "lint-staged": "^15.2.2", - "nyc": "^15.1.0" + "nyc": "^15.1.0", + "sinon-chai": "^3.7.0" } }, "node_modules/@ampproject/remapping": { @@ -1256,6 +1256,7 @@ "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, "dependencies": { "@types/sinonjs__fake-timers": "*" } @@ -1263,7 +1264,8 @@ "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==" + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true }, "node_modules/@types/strip-bom": { "version": "3.0.0", @@ -9157,6 +9159,7 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", + "dev": true, "peerDependencies": { "chai": "^4.0.0", "sinon": ">=4.0.0" diff --git a/package.json b/package.json index 0f7637dc..d5da7ed4 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "@types/mocha": "^10.0.6", "@types/multer": "^1.4.11", "@types/passport-google-oauth2": "^0.1.8", - "@types/sinon": "^17.0.3", "bcrypt": "^5.1.1", "chai": "^4.4.1", "chai-http": "^4.4.0", @@ -87,7 +86,6 @@ "sequelize-cli": "^6.6.2", "sequelize-typescript": "^2.1.6", "sinon": "^18.0.0", - "sinon-chai": "^3.7.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", "ts-node": "^10.9.2", @@ -105,6 +103,7 @@ "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/sequelize": "^4.28.20", + "@types/sinon": "^17.0.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "@typescript-eslint/eslint-plugin": "^7.10.0", @@ -112,6 +111,7 @@ "eslint": "^8.57.0", "eslint-plugin-import": "^2.29.1", "lint-staged": "^15.2.2", - "nyc": "^15.1.0" + "nyc": "^15.1.0", + "sinon-chai": "^3.7.0" } } diff --git a/src/databases/migrations/20240520180022-create-users.ts b/src/databases/migrations/20240520180022-create-users.ts index 1cbbc163..06c81ba4 100644 --- a/src/databases/migrations/20240520180022-create-users.ts +++ b/src/databases/migrations/20240520180022-create-users.ts @@ -67,6 +67,11 @@ export default { allowNull: false, defaultValue: false }, + isGoogleAccount:{ + type: new DataTypes.BOOLEAN, + allowNull: true, + defaultValue: false + }, is2FAEnabled: { type: new DataTypes.BOOLEAN, allowNull: false, diff --git a/src/databases/models/users.ts b/src/databases/models/users.ts index 795a510a..6ae7cf9a 100644 --- a/src/databases/models/users.ts +++ b/src/databases/models/users.ts @@ -19,6 +19,7 @@ export interface UsersAttributes { currency?: string; role?: string; isVerified?: boolean; + isGoogleAccount?:boolean; is2FAEnabled?: boolean; status?: string; createdAt?: Date; @@ -41,6 +42,7 @@ class Users extends Model implements U declare currency?: string; declare role?: string; declare isVerified?: boolean; + declare isGoogleAccount?:boolean; declare is2FAEnabled?: boolean; declare status?: string; declare password: string; @@ -114,6 +116,11 @@ Users.init( allowNull: true, defaultValue: false }, + isGoogleAccount:{ + type: new DataTypes.BOOLEAN, + allowNull: true, + defaultValue: false + }, is2FAEnabled: { type: new DataTypes.BOOLEAN, allowNull: true, diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index 1fca933a..d8167d77 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -1,475 +1,119 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Request, Response } from "express"; -import chai, { expect } from "chai"; -import chaiHttp from "chai-http"; -import sinon from "sinon"; +import userRepositories from "../repository/authRepositories"; +import { generateToken } from "../../../helpers"; import httpStatus from "http-status"; -import app from "../../.."; -import { isUserExist } from "../../../middlewares/validation"; +import { UsersAttributes } from "../../../databases/models/users"; import authRepositories from "../repository/authRepositories"; -import Users from "../../../databases/models/users"; -import Session from "../../../databases/models/session"; -import { - sendVerificationEmail, - transporter, -} from "../../../services/sendEmail"; +import { sendVerificationEmail } from "../../../services/sendEmail"; -chai.use(chaiHttp); -const router = () => chai.request(app); - -let userId: number = 0; -let verifyToken: string | null = null; - -describe("Authentication Test Cases", () => { - let token; - - afterEach(async () => { - const tokenRecord = await Session.findOne({ where: { userId } }); - if (tokenRecord) { - verifyToken = tokenRecord.dataValues.token; - } - }); - - it("should register a new user", (done) => { - router() - .post("/api/auth/register") - .send({ - email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", - }) - .end((error, response) => { - expect(response.status).to.equal(httpStatus.CREATED); - expect(response.body).to.be.an("object"); - expect(response.body).to.have.property("data"); - userId = response.body.data.user.id; - expect(response.body).to.have.property( - "message", - "Account created successfully. Please check email to verify account." - ); - done(error); - }); - }); - - it("should verify the user successfully", (done) => { - if (!verifyToken) { - throw new Error("verifyToken is not set"); - } - - router() - .get(`/api/auth/verify-email/${verifyToken}`) - .end((err, res) => { - expect(res.status).to.equal(httpStatus.OK); - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("status", httpStatus.OK); - expect(res.body).to.have.property( - "message", - "Account verified successfully, now login." - ); - done(err); - }); - }); - - it("should return validation error and 400", (done) => { - router() - .post("/api/auth/register") - .send({ - email: "user@example.com", - password: "userPassword", - }) - .end((error, response) => { - expect(response.status).to.equal(400); - expect(response.body).to.be.a("object"); - expect(response.body).to.have.property("message"); - done(error); - }); - }); - - it("Should be able to login a registered user", (done) => { - router() - .post("/api/auth/login") - .send({ - email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", - }) - .end((error, response) => { - expect(response.status).to.equal(httpStatus.OK); - expect(response.body).to.be.a("object"); - expect(response.body).to.have.property("data"); - expect(response.body.message).to.be.a("string"); - expect(response.body.data).to.have.property("token"); - token = response.body.data.token; - done(error); - }); - }); - - it("Should be able to logout user", (done) => { - router() - .post("/api/auth/logout") - .set("Authorization", `Bearer ${token}`) - .end((err, res) => { - expect(res).to.have.status(httpStatus.OK); - expect(res.body).to.have.property("message", "Successfully logged out"); - done(err); - }); - }); - - it("Should be able to login a registered user", (done) => { - router() - .post("/api/auth/login") - .send({ - email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", - }) - .end((error, response) => { - expect(response.status).to.equal(httpStatus.OK); - expect(response.body).to.be.a("object"); - expect(response.body).to.have.property("data"); - expect(response.body.message).to.be.a("string"); - expect(response.body.data).to.have.property("token"); - token = response.body.data.token; - done(error); - }); - }); - - it("Should return error on logout", (done) => { - sinon - .stub(authRepositories, "destroySession") - .throws(new Error("Database Error")); - router() - .post("/api/auth/logout") - .set("Authorization", `Bearer ${token}`) - .end((err, res) => { - expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); - expect(res.body).to.have.property("message", "Internal Server error"); - done(err); - }); - }); - - it("should return internal server error on login", (done) => { - sinon - .stub(authRepositories, "createSession") - .throws(new Error("Database error")); - router() - .post("/api/auth/login") - .send({ - email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", - }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); - done(err); - }); - }); - - it("Should return validation error when no email or password given", (done) => { - router() - .post("/api/auth/login") - .send({ - email: "user@example.com", - }) - .end((error, response) => { - expect(response).to.have.status(httpStatus.BAD_REQUEST); - expect(response.body).to.be.a("object"); - done(error); - }); - }); - - it("Should not be able to login user with invalid Email", (done) => { - router() - .post("/api/auth/login") - .send({ - email: "fakeemail@gmail.com", - password: "userPassword@123", - }) - .end((error, response) => { - expect(response).to.have.status(httpStatus.BAD_REQUEST); - expect(response.body).to.be.a("object"); - expect(response.body).to.have.property( - "message", - "Invalid Email or Password" - ); - done(error); - }); - }); - - it("Should not be able to login user with invalid Password", (done) => { - router() - .post("/api/auth/login") - .send({ - email: "ecommerceninjas45@gmail.com", - password: "fakePassword@123", - }) - .end((error, response) => { - expect(response).to.have.status(httpStatus.BAD_REQUEST); - expect(response.body).to.be.a("object"); - expect(response.body).to.have.property( - "message", - "Invalid Email or Password" - ); - done(error); - }); - }); -}); - -describe("isUserExist Middleware", () => { - before(() => { - app.post("/auth/register", isUserExist, (req: Request, res: Response) => { - res.status(200).json({ message: "success" }); +const registerUser = async (req: Request, res: Response): Promise => { + try { + const register: UsersAttributes = await authRepositories.createUser( + req.body + ); + const token: string = generateToken(register.id); + const session = { + userId: register.id, + device: req.headers["user-agent"], + token: token, + otp: null, + }; + await authRepositories.createSession(session); + await sendVerificationEmail( + register.email, + "Verification Email", + `${process.env.SERVER_URL_PRO}/api/auth/verify-email/${token}` + ); + res.status(httpStatus.CREATED).json({ + message: + "Account created successfully. Please check email to verify account.", + data: { user: register }, }); - }); - - afterEach(async () => { - sinon.restore(); - }); - - it("should return user already exists", (done) => { - router() - .post("/api/auth/register") - .send({ - email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", - }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.BAD_REQUEST); - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("status", httpStatus.BAD_REQUEST); - expect(res.body).to.have.property("message", "Account already exists."); - done(err); - }); - }); - - it("should return 'Account already exists. Please verify your account' if user exists and is not verified", (done) => { - const mockUser = Users.build({ - id: 1, - email: "user@example.com", - password: "hashedPassword", - isVerified: false, - createdAt: new Date(), - updatedAt: new Date(), + } catch (error) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message, }); - - sinon.stub(authRepositories, "findUserByAttributes").resolves(mockUser); - - router() - .post("/api/auth/register") - .send({ - email: "user@example.com", - password: "userPassword@123", - }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.BAD_REQUEST); - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("status", httpStatus.BAD_REQUEST); - expect(res.body).to.have.property( - "message", - "Account already exists. Please verify your account" - ); - done(err); - }); - }); - - it("should return internal server error", (done) => { - sinon - .stub(authRepositories, "findUserByAttributes") - .throws(new Error("Database error")); - router() - .post("/auth/register") - .send({ email: "usertesting@gmail.com" }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property( - "status", - httpStatus.INTERNAL_SERVER_ERROR - ); - expect(res.body).to.have.property("message", "Database error"); - done(err); - }); - }); - - it("should return internal server error on login", (done) => { - sinon - .stub(authRepositories, "findUserByAttributes") - .throws(new Error("Database error")); - router() - .post("/api/auth/login") - .send({ - email: "ecommerceninjas45@gmail.com", - password: "userPassword@123", - }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); - done(err); - }); - }); - - it("should call next if user does not exist", (done) => { - sinon.stub(authRepositories, "findUserByAttributes").resolves(null); - - router() - .post("/auth/register") - .send({ email: "newuser@gmail.com" }) - .end((err, res) => { - expect(res).to.have.status(200); - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("message", "success"); - done(err); - }); - }); -}); - -describe("POST /auth/register - Error Handling", () => { - let registerUserStub: sinon.SinonStub; - - beforeEach(() => { - registerUserStub = sinon - .stub(authRepositories, "createUser") - .throws(new Error("Test error")); - }); - - afterEach(() => { - registerUserStub.restore(); - }); - - it("should return 500 and error message when an error occurs", (done) => { - router() - .post("/api/auth/register") - .send({ email: "test@example.com", password: "password@123" }) - .end((err, res) => { - expect(res.status).to.equal(httpStatus.INTERNAL_SERVER_ERROR); - expect(res.body).to.deep.equal({ - status: httpStatus.INTERNAL_SERVER_ERROR, - message: "Test error", - }); - done(err); - }); - }); -}); - -describe("isAccountVerified Middleware", () => { - afterEach(() => { - sinon.restore(); - }); - - it("should return 'Account not found' if user is not found", (done) => { - sinon.stub(authRepositories, "findUserByAttributes").resolves(null); - - router() - .post("/api/auth/send-verify-email") - .send({ email: "nonexistent@example.com" }) - .end((err, res) => { - expect(res.status).to.equal(httpStatus.NOT_FOUND); - expect(res.body).to.have.property("message", "Account not found."); - done(err); - }); - }); - - it("should return 'Account already verified' if user is already verified", (done) => { - const mockUser = Users.build({ - id: 1, - email: "user@example.com", - password: "hashedPassword", - isVerified: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - sinon.stub(authRepositories, "findUserByAttributes").resolves(mockUser); - - router() - .post("/api/auth/send-verify-email") - .send({ email: "user@example.com" }) - .end((err, res) => { - expect(res.status).to.equal(httpStatus.BAD_REQUEST); - expect(res.body).to.have.property( - "message", - "Account already verified." - ); - done(err); - }); - }); -}); - -describe("Authentication Test Cases", () => { - let findUserByAttributesStub: sinon.SinonStub; - let findSessionByUserIdStub: sinon.SinonStub; - - beforeEach(() => { - findUserByAttributesStub = sinon.stub( - authRepositories, - "findUserByAttributes" + } +}; + +const sendVerifyEmail = async (req: any, res: Response) => { + try { + await sendVerificationEmail( + req.user.email, + "Verification Email", + `${process.env.SERVER_URL_PRO}/api/auth/verify-email/${req.session.token}` ); - findSessionByUserIdStub = sinon.stub( - authRepositories, - "findSessionByAttributes" + res.status(httpStatus.OK).json({ + status: httpStatus.OK, + message: "Verification email sent successfully.", + }); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message, + }); + } +}; + +const verifyEmail = async (req: any, res: Response) => { + try { + await authRepositories.destroySession(req.user.id, req.session.token); + await authRepositories.updateUserByAttributes( + "isVerified", + true, + "id", + req.user.id ); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should send a verification email successfully", (done) => { - const mockUser = { id: 1, email: "user@example.com", isVerified: false }; - const mockSession = { token: "testToken" }; - - findUserByAttributesStub.resolves(mockUser); - findSessionByUserIdStub.resolves(mockSession); - - router() - .post("/api/auth/send-verify-email") - .send({ email: "user@example.com" }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.OK); - expect(res.body).to.have.property( - "message", - "Verification email sent successfully." - ); - done(err); - }); - }); - it("should return 400 if session is not found", (done) => { - const mockUser = { id: 1, email: "user@example.com", isVerified: false }; - const mockSession = { token: "testToken" }; - - findUserByAttributesStub.resolves(mockUser); - findSessionByUserIdStub.resolves(mockSession); - findSessionByUserIdStub.resolves(null); - router() - .post("/api/auth/send-verify-email") - .send({ email: "user@example.com" }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.BAD_REQUEST); - expect(res.body).to.have.property("message", "Invalid token."); - done(err); - }); - }); - - it("should return internal server error", (done) => { - findSessionByUserIdStub.resolves(null); - const token = "invalid token"; - router() - .get(`/api/auth/verify-email/${token}`) - .send({ email: "user@example.com" }) - .end((err, res) => { - expect(res).to.have.status(httpStatus.INTERNAL_SERVER_ERROR); - expect(res.body).to.have.property("message"); - done(err); - }); - }); -}); - -describe("sendVerificationEmail", () => { - afterEach(() => { - sinon.restore(); - }); - - it("should throw an error when sendMail fails", async () => { - sinon.stub(transporter, "sendMail").rejects(new Error("Network Error")); - try { - await sendVerificationEmail("email@example.com", "subject", "message"); - } catch (error) { - expect(error).to.be.an("error"); - } - }); -}); + res.status(httpStatus.OK).json({ + status: httpStatus.OK, + message: "Account verified successfully, now login.", + }); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message, + }); + } +}; + +const loginUser = async (req: any, res: Response) => { + try { + const token = generateToken(req.user.id); + const session = { + userId: req.user.id, + device: req.headers["user-agent"], + token: token, + otp: null, + }; + await userRepositories.createSession(session); + res + .status(httpStatus.OK) + .json({ message: "Logged in successfully", data: { token } }); + } catch (err) { + return res + .status(httpStatus.INTERNAL_SERVER_ERROR) + .json({ message: "Internal Server error", data: err.message }); + } +}; + +const logoutUser = async (req: any, res: Response) => { + try { + await authRepositories.destroySession(req.user.id, req.session.token); + res.status(httpStatus.OK).json({ message: "Successfully logged out" }); + } catch (err) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: "Internal Server error", + }); + } +}; + +export default { + registerUser, + sendVerifyEmail, + verifyEmail, + loginUser, + logoutUser, +};