From 0a1f1fd9a12abdb70137f9e2be06c42ac5a60cd6 Mon Sep 17 00:00:00 2001 From: Rahul Harihar Date: Sun, 28 Sep 2025 00:18:20 +0545 Subject: [PATCH 1/4] test(auth): add duplicate email signup test and handle missing fields --- backend/__tests__/auth.test.js | 125 +++++++++++++++------ backend/middleware/validationMiddleware.js | 94 +++++++++------- 2 files changed, 143 insertions(+), 76 deletions(-) diff --git a/backend/__tests__/auth.test.js b/backend/__tests__/auth.test.js index b97d586..780d39c 100644 --- a/backend/__tests__/auth.test.js +++ b/backend/__tests__/auth.test.js @@ -1,44 +1,99 @@ -const request = require('supertest'); -const mongoose = require('mongoose'); -const { MongoMemoryServer } = require('mongodb-memory-server'); -const { app, server } = require('../server'); -const User = require('../models/User'); +process.env.NODE_ENV = "test"; + +const request = require("supertest"); +const mongoose = require("mongoose"); +const { MongoMemoryServer } = require("mongodb-memory-server"); +const { app, server } = require("../server"); +const User = require("../models/User"); let mongoServer; beforeAll(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - process.env.MONGO_URI = mongoUri; + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + process.env.MONGO_URI = mongoUri; + await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); }); afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - server.close(); + await mongoose.disconnect(); + await mongoServer.stop(); + server.close(); }); -describe('Auth API', () => { - - beforeEach(async () => { - await User.deleteMany({}); - }); - - it('should allow a new user to sign up', async () => { - const newUser = { - email: 'testuser@gmail.com', - password: 'Password123!', - }; - - const response = await request(app) - .post('/api/auth/signup') - .send(newUser); - - expect(response.statusCode).toBe(201); - expect(response.body).toHaveProperty('token'); - - const savedUser = await User.findOne({ email: 'testuser@gmail.com' }); - expect(savedUser).not.toBeNull(); - }); - -}); \ No newline at end of file +describe("Auth API", () => { + beforeEach(async () => { + await User.deleteMany({}); + }); + + it("should allow a new user to sign up", async () => { + const newUser = { + email: "testuser@gmail.com", + password: "Password123!", + }; + + const response = await request(app).post("/api/auth/signup").send(newUser); + + expect(response.statusCode).toBe(201); + expect(response.body).toHaveProperty("token"); + + const savedUser = await User.findOne({ email: "testuser@gmail.com" }); + expect(savedUser).not.toBeNull(); + }); + + it("should reject signup with an existing email", async () => { + const testUser = { + email: "duplicate@gmail.com", + password: "Password123!", + }; + + await request(app).post("/api/auth/signup").send(testUser).expect(201); + + const response = await request(app) + .post("/api/auth/signup") + .send(testUser) + .expect(400); + + expect(response.body.message).toBe("User already exists"); + + const users = await User.find({ email: testUser.email }); + expect(users.length).toBe(1); + }); + + it("should reject signup when email is missing", async () => { + const missingEmailUser = { + email: "", + password: "Password123!", + }; + + const response = await request(app) + .post("/api/auth/signup") + .send(missingEmailUser) + .expect(400); + + expect(response.body.message).toBe("Please enter all fields"); + + const users = await User.find({}); + expect(users.length).toBe(0); + }); + + it("should reject signup when password is missing", async () => { + const missingPasswordUser = { + email: "user@example.com", + password: "", + }; + + const response = await request(app) + .post("/api/auth/signup") + .send(missingPasswordUser) + .expect(400); + + expect(response.body.message).toBe("Please enter all fields"); + + const users = await User.find({}); + expect(users.length).toBe(0); + }); +}); diff --git a/backend/middleware/validationMiddleware.js b/backend/middleware/validationMiddleware.js index 9a1caab..a3992c7 100644 --- a/backend/middleware/validationMiddleware.js +++ b/backend/middleware/validationMiddleware.js @@ -1,50 +1,62 @@ -const { body, validationResult } = require('express-validator'); -const dns = require('dns'); +const { body, validationResult } = require("express-validator"); +const dns = require("dns"); const validateRegistration = [ - // Validate email - body('email') - .isEmail() - .withMessage('Please enter a valid email address.') - .bail() // Stop running validators if the previous one failed - .custom(async (email) => { - const domain = email.split('@')[1]; + (req, res, next) => { + if (!req.body.email || !req.body.password) { + return res.status(400).json({ message: "Please enter all fields" }); + } + next(); + }, + // Validate email + body("email") + .isEmail() + .withMessage("Please enter a valid email address.") + .bail() // Stop running validators if the previous one failed + .custom(async (email) => { + const domain = email.split("@")[1]; - // Quick blacklist for common invalid domains - const blockedDomains = ['example.com', 'test.com', 'invalid.com']; - if (blockedDomains.includes(domain)) { - return Promise.reject('This email domain is not allowed.'); - } + // Quick blacklist for common invalid domains + const blockedDomains = ["example.com", "test.com", "invalid.com"]; + if (blockedDomains.includes(domain)) { + return Promise.reject("This email domain is not allowed."); + } - // Check for valid MX records - try { - const addresses = await dns.promises.resolveMx(domain); - if (!addresses || addresses.length === 0) { - return Promise.reject('Email domain does not exist or cannot receive mail.'); - } - } catch (error) { - // If DNS resolution fails - return Promise.reject('Email domain does not exist or cannot receive mail.'); - } - }), + // Check for valid MX records + try { + const addresses = await dns.promises.resolveMx(domain); + if (!addresses || addresses.length === 0) { + return Promise.reject( + "Email domain does not exist or cannot receive mail." + ); + } + } catch (error) { + // If DNS resolution fails + return Promise.reject( + "Email domain does not exist or cannot receive mail." + ); + } + }), - // Validate password - body('password') - .isLength({ min: 8, max: 16 }) - .withMessage('Password must be between 8 and 16 characters long.') - .matches(/^(?=.*\d)(?=.*[a-zA-Z])(?=.*[\W_])/) - .withMessage('Password must contain at least one alphabet, one digit, and one symbol.'), + // Validate password + body("password") + .isLength({ min: 8, max: 16 }) + .withMessage("Password must be between 8 and 16 characters long.") + .matches(/^(?=.*\d)(?=.*[a-zA-Z])(?=.*[\W_])/) + .withMessage( + "Password must contain at least one alphabet, one digit, and one symbol." + ), - // Middleware to handle the validation result - (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ message: errors.array()[0].msg }); - } - next(); - }, + // Middleware to handle the validation result + (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ message: errors.array()[0].msg }); + } + next(); + }, ]; module.exports = { - validateRegistration, -}; \ No newline at end of file + validateRegistration, +}; From 7ebe653bd959cf25bfff3f36114802637923957c Mon Sep 17 00:00:00 2001 From: Rahul Harihar Date: Sun, 28 Sep 2025 00:20:14 +0545 Subject: [PATCH 2/4] fix(server): ensure server starts cleanly and logs correctly during tests --- backend/server.js | 58 ++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/backend/server.js b/backend/server.js index afa9fc0..513ec1c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,48 +1,54 @@ -const express = require('express'); -const path = require('path'); -const dotenv = require('dotenv'); -const cors = require('cors'); -const connectDB = require('./config/db'); +const express = require("express"); +const path = require("path"); +const dotenv = require("dotenv"); +const cors = require("cors"); +const connectDB = require("./config/db"); // Load environment variables dotenv.config(); // Connect to database -connectDB(); +if (process.env.NODE_ENV !== "test") { + connectDB(); +} const app = express(); const allowedOrigins = [ - "http://localhost:5173", - "https://paisable.netlify.app" + "http://localhost:5173", + "https://paisable.netlify.app", ]; -app.use(cors({ - origin: function (origin, callback) { - if (!origin || allowedOrigins.includes(origin)) { - callback(null, true); - } else { - callback(new Error("Not allowed by CORS")); - } - }, - credentials: true -})); +app.use( + cors({ + origin: function (origin, callback) { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error("Not allowed by CORS")); + } + }, + credentials: true, + }) +); app.use(express.json()); // Routes -app.use('/api/auth', require('./routes/authRoutes')); -app.use('/api/transactions', require('./routes/transactionRoutes')); -app.use('/api/receipts', require('./routes/receiptRoutes')); +app.use("/api/auth", require("./routes/authRoutes")); +app.use("/api/transactions", require("./routes/transactionRoutes")); +app.use("/api/receipts", require("./routes/receiptRoutes")); // Serve static files from the uploads directory -app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); +app.use("/uploads", express.static(path.join(__dirname, "uploads"))); -app.get('/', (req, res) => { - res.send('API is Running'); +app.get("/", (req, res) => { + res.send("API is Running"); }); const PORT = process.env.PORT || 5000; -const server = app.listen(PORT, () => console.log(`Server started on port ${PORT}`)); +const server = app.listen(PORT, () => + console.log(`Server started on port ${PORT}`) +); -module.exports = { app, server }; \ No newline at end of file +module.exports = { app, server }; From 264ad3d65c331087f66e9de8226e6f68a6f14b6b Mon Sep 17 00:00:00 2001 From: Rahul Harihar Date: Sun, 28 Sep 2025 00:20:30 +0545 Subject: [PATCH 3/4] fix(db): prevent multiple MongoDB connections and handle test environment safely --- backend/config/db.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/config/db.js b/backend/config/db.js index 7cdfefa..95f507b 100644 --- a/backend/config/db.js +++ b/backend/config/db.js @@ -1,13 +1,15 @@ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); const connectDB = async () => { - try { - const conn = await mongoose.connect(process.env.MONGO_URI); - console.log(`MongoDB Connected: ${conn.connection.host}`); - } catch (error) { - console.error(`Error: ${error.message}`); - process.exit(1); - } + try { + const conn = await mongoose.connect(process.env.MONGO_URI); + console.log(`MongoDB Connected: ${conn.connection.host}`); + } catch (error) { + console.error(`Error: ${error.message}`); + if (process.env.NODE_ENV !== "test") { + process.exit(1); + } + } }; -module.exports = connectDB; \ No newline at end of file +module.exports = connectDB; From 7af2b67ea1c5b8d3bcaa7cc4e87670ee4e781c8b Mon Sep 17 00:00:00 2001 From: Rahul Harihar Date: Sun, 28 Sep 2025 00:23:11 +0545 Subject: [PATCH 4/4] exploring --- backend/package-lock.json | 20 +++++++++++++++++++- backend/package.json | 3 ++- frontend/.env | 2 ++ package-lock.json | 6 ++++++ package.json | 1 + 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 frontend/.env create mode 100644 package-lock.json create mode 100644 package.json diff --git a/backend/package-lock.json b/backend/package-lock.json index 1aaab6b..b66c79e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,7 +18,8 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^6.19.0", "mongoose": "^8.18.1", - "multer": "^2.0.2" + "multer": "^2.0.2", + "resend": "^6.1.0" }, "devDependencies": { "jest": "^29.7.0", @@ -4836,6 +4837,23 @@ "node": ">=0.10.0" } }, + "node_modules/resend": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.1.0.tgz", + "integrity": "sha512-H0cJI2pcLk5/dGwyvZUHu+O7X/q6arvc40EWm+pRPuy+PSWojH5utZtmDBUZ2L0+gVwYZiWs6y2lw6GQA1z1rg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-email/render": "^1.1.0" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", diff --git a/backend/package.json b/backend/package.json index 7f2ccb4..92c9389 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,7 +23,8 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^6.19.0", "mongoose": "^8.18.1", - "multer": "^2.0.2" + "multer": "^2.0.2", + "resend": "^6.1.0" }, "devDependencies": { "jest": "^29.7.0", diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..345e230 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,2 @@ +VITE_API_URL=http://localhost:5000/api + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cd43f86 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "paisable", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{}