diff --git a/__tests__/controllers/CartController.test.ts b/__tests__/controllers/CartController.test.ts new file mode 100644 index 0000000..a7e39f8 --- /dev/null +++ b/__tests__/controllers/CartController.test.ts @@ -0,0 +1,75 @@ +import { Request, Response } from 'express'; +import { Cart, ICart, Order } from '../mongoose/Schema.test'; +import { config } from 'dotenv'; + +config(); + +export async function addToCart(req: Request, res: Response) { + try { + const { userId, productId } = req.body; + if (!productId) { + res.status(400).json({ error: 'Product id is required.' }); + return; + } + let cart: ICart | null = await Cart.findOne({ userId }); + if (!cart) { + cart = await Cart.create({ + userId, + products: { [productId]: 1 }, + }); + } else { + const currentQuantity = cart.products[productId]; + if (currentQuantity === undefined) { + cart.products[productId] = 1; + } else { + cart.products[productId] += 1; + } + cart.markModified('products'); + } + + await cart.save(); + res.status(200).json(cart); + } catch (error) { + console.error('Error adding product to cart:', error); + res.status(500).json({ error: 'An error occurred while adding the product to the cart.' }); + } +} + +export async function listCart(req: Request, res: Response) { + try { + const { userId } = req.body; + const cart = await Cart.findOne({ userId }); + if (!cart) { + res.status(404).json({ error: 'Cart not found.' }); + return; + } + res.status(200).json(cart.products); + } catch (error) { + console.error('Error listing cart:', error); + res.status(500).json({ error: 'An error occurred while listing the cart.' }); + } +} + +export async function checkout(req: Request, res: Response) { + + const { userId } = req.body; + const usersCart = await Cart.findOne({ userId }); + + if (!usersCart) { + res.status(404).json({ error: 'Cart not found.' }); + return; + } + + const order = await Order.create({ + userId, + products: usersCart.products, + }); + + await removeCart(userId); + // sendEmailasync(order.id, userId); + res.status(200).json(order); +} + +async function removeCart(userId: string) { + await Cart.deleteOne({ userId }); +} \ No newline at end of file diff --git a/__tests__/controllers/ProductController.test.ts b/__tests__/controllers/ProductController.test.ts new file mode 100644 index 0000000..37a17f1 --- /dev/null +++ b/__tests__/controllers/ProductController.test.ts @@ -0,0 +1,58 @@ +import { Request, Response } from 'express'; +import { Product, IProduct } from '../mongoose/Schema'; + +export async function createProduct(req: Request, res: Response) { + try { + const { name, description, price, userId } = req.body; + if(!name || !description || !price || !userId) { + res.status(400).json({ error: 'Name, description, price are required.' }); + return; + } + + const productExists = await Product.exists({ name, userId }); + if(productExists) { + res.status(400).json({ error: 'Product already exists.' }); + return; + } + const product: IProduct = await Product.create({ + name, + description, + price, + userId: userId, + }); + + res.status(201).json(product); + } catch (error) { + console.error('Error creating product:', error); + res.status(500).json({ error: 'An error occurred while creating the product.' }); + } +} + +export async function listProducts(req: Request, res: Response) { + try { + const { page, limit } = req.query; + const dbPage = Number(page) || 0; + const dbLimit = Number(limit) || 50; + const products = await Product.find().sort({ price: 1 }).skip(Number(dbPage) * Number(dbLimit)).limit(Number(dbLimit)); + + res.json(products); + } catch (error) { + console.error('Error listing products:', error); + res.status(500).json({ error: 'An error occurred while listing the products.' }); + } +} + +export async function getProduct(req: Request, res: Response) { + const { id } = req.params; + try { + const product = await Product.findById(id); + if(!product) { + res.status(404).json({ error: 'Product not found.' }); + return; + } + res.json(product); + } catch (error) { + console.error('Error getting product:', error); + res.status(404).json({ error: 'Product not found.' }); + } +} diff --git a/__tests__/controllers/UserController.test.ts b/__tests__/controllers/UserController.test.ts new file mode 100644 index 0000000..634002b --- /dev/null +++ b/__tests__/controllers/UserController.test.ts @@ -0,0 +1,87 @@ +import { Request, Response } from 'express'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import { User, IUser } from '../mongoose/Schema'; +import { clearJwtCookie, setJwtCookie } from '../middlewares/checkAuth.test'; +import validate from 'deep-email-validator'; + +export async function createUser(req: Request, res: Response) { + try { + const { email, password, address } = req.body; + const isValidEmail = await validate(email); + if (!isValidEmail.valid) { + console.error('Email is invalid:', isValidEmail.validators); + return res.status(400).json({ error: 'Email is invalid' }); + } + + if (!(password && address)) { + return res.status(400).json({ error: 'All inputs are required' }); + } + // checkIfUserExists return true if the user exists + const userExists = await User.exists({ email }); + if(userExists) { + return res.status(400).json({ error: 'User already exists, Try login :)' }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + const user: IUser = await User.create({ + email, + password: hashedPassword, + address, + }); + + res.status(200).json({ + massage: 'User created successfully' + }); + } catch (error) { + console.error('Error creating user:', error); + res.status(500).json({ error: 'An error occurred while creating the user.' }); + } +} + +export async function login(req: Request, res: Response) { + try { + const { email, password } = req.body; + + // Check if the user exists + const user: IUser | null = await User.findOne({ email }); + if (!user) { + console.error('User not found'); + return res.status(401).json({ error: 'Invalid email or password' }); + } + + // Compare the provided password with the stored password + const isPasswordCorrect = await bcrypt.compare(password, user.password); + if (!isPasswordCorrect) { + console.error('Invalid password'); + return res.status(401).json({ error: 'Invalid email or password' }); + } + + const payload = { + userId: user._id + } + // Generate a JWT + const token = jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: '1d' }); + + setJwtCookie(res, token); + + // Send the JWT as the response + res.status(200).json({ + token + }); + } catch (error) { + console.error('Error during login:', error); + res.status(500).json({ error: 'An error occurred during login' }); + } +} + +export async function logout(req: Request, res: Response) { + try { + clearJwtCookie(res); + res.status(200).json({ message: 'Logout successful' }); + } catch (error) { + console.error('Error during logout:', error); + res.status(500).json({ error: 'An error occurred during logout' }); + } +} \ No newline at end of file diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts new file mode 100644 index 0000000..ec1dc24 --- /dev/null +++ b/__tests__/index.test.ts @@ -0,0 +1,39 @@ +import express from 'express'; +import mongoose from 'mongoose'; +import cookieParser from 'cookie-parser'; + +import userRouter from './routes/user'; +import productRouter from './routes/product'; +import cartRouter from './routes/cart'; + +const env = require('dotenv').config().parsed; + +const app = express(); +const PORT = env.PORT || 3000; + +app.use(express.json()); +app.use(cookieParser()) + +// Connect to MongoDB using Mongoose +mongoose.connect(env.DATABASE_URL); + +const db = mongoose.connection; + +// Check for DB connection +db.on('error', () => { + console.error.bind(console, 'MongoDB connection error:') + process.exit(1); +}); +db.once('open', () => { + console.log('Connected to MongoDB'); +}); + +// Routes +app.use('/users', userRouter); +app.use('/products', productRouter); +app.use('/cart', cartRouter); + +// Start server +app.listen(PORT, () => { + console.log(`Server started on port ${PORT}`); +}); diff --git a/__tests__/middlewares/checkAuth.test.ts b/__tests__/middlewares/checkAuth.test.ts new file mode 100644 index 0000000..9c62104 --- /dev/null +++ b/__tests__/middlewares/checkAuth.test.ts @@ -0,0 +1,33 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +interface AuthenticatedRequest extends Request { + userId?: string; +} + +// Middleware function to authenticate requests +export function authenticateToken(req: AuthenticatedRequest, res: Response, next: NextFunction) { + + const token = req.cookies.access_token; + if (!token) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + jwt.verify(token, process.env.JWT_SECRET as string, (err: any, decoded: { userId: any; }) => { + if (err) { + return res.status(401).json({ error: 'In Valid Token' }); + } + req.body.userId = decoded.userId; + next(); + }); +} + +// Set JWT as cookie in the response +export function setJwtCookie(res: Response, token: string) { + res.cookie('access_token', token, { httpOnly: true }); +} + +// Clear JWT cookie in the response +export function clearJwtCookie(res: Response) { + res.clearCookie('access_token'); +} diff --git a/__tests__/mongoose/Schema.test.ts b/__tests__/mongoose/Schema.test.ts new file mode 100644 index 0000000..0a1157b --- /dev/null +++ b/__tests__/mongoose/Schema.test.ts @@ -0,0 +1,74 @@ +import mongoose, { Schema, Document } from 'mongoose'; + +export interface IUser extends Document { + email: string; + password: string; + address: string; + createdAt: Date; + updatedAt: Date; +} + +export interface IProduct extends Document { + name: string; + description: string; + price: number; + userId: string; + createdAt: Date; + updatedAt: Date; +} + +export interface ICart extends Document { + userId: string; + products: { [itemId: string]: number }; + createdAt: Date; + updatedAt: Date; +} + +export interface IOrder extends Document { + userId: string; + products: { [itemId: string]: number }; + emailSent: boolean; + createdAt: Date; + updatedAt: Date; +} + +const UserSchema: Schema = new Schema({ + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + address: { type: String, required: true }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +const ProductSchema: Schema = new Schema({ + name: { type: String, required: true }, + description: { type: String, required: true }, + price: { type: Number, required: true }, + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +const CartSchema: Schema = new Schema({ + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, unique: true }, + products: { type: Schema.Types.Mixed, default: {} }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +const OrderSchema: Schema = new Schema({ + userId: { type: Schema.Types.ObjectId, ref: 'User', required: true }, + products: { type: Schema.Types.Mixed, default: {} }, + emailSent: { type: Boolean, default: false }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +ProductSchema.index({ name: 1, userId: 1 }, { unique: true }); + +const User = mongoose.model('User', UserSchema); +const Product = mongoose.model('Product', ProductSchema); +const Cart = mongoose.model('Cart', CartSchema); +const Order = mongoose.model('Order', OrderSchema); + +export { User, Product, Cart, Order }; diff --git a/__tests__/routes/cart.test.ts b/__tests__/routes/cart.test.ts new file mode 100644 index 0000000..10f68ef --- /dev/null +++ b/__tests__/routes/cart.test.ts @@ -0,0 +1,14 @@ +import express from 'express'; +import { authenticateToken } from '../middlewares/checkAuth.test'; +import { addToCart, listCart, checkout } from '../controllers/CartController.test'; + + + +const cartRouter = express.Router(); + +cartRouter.post('/', authenticateToken, addToCart); +cartRouter.get('/', authenticateToken, listCart); + +cartRouter.post('/checkout', authenticateToken, checkout); + +export default cartRouter; \ No newline at end of file diff --git a/__tests__/routes/product.test.ts b/__tests__/routes/product.test.ts new file mode 100644 index 0000000..43d344b --- /dev/null +++ b/__tests__/routes/product.test.ts @@ -0,0 +1,12 @@ +import express from 'express'; +import { authenticateToken } from '../middlewares/checkAuth.test'; +import { createProduct, listProducts, getProduct } from '../controllers/ProductController.test'; + +const productRouter = express.Router(); + +productRouter.post('/', authenticateToken, createProduct) +productRouter.get('/', listProducts); +productRouter.get('/:id', getProduct); + + +export default productRouter; \ No newline at end of file diff --git a/__tests__/routes/user.test.ts b/__tests__/routes/user.test.ts new file mode 100644 index 0000000..efb5204 --- /dev/null +++ b/__tests__/routes/user.test.ts @@ -0,0 +1,10 @@ +import express from 'express'; +import { createUser, login, logout } from '../controllers/UserController.test'; + +const userRouter = express.Router(); + +userRouter.post('/', createUser); +userRouter.post('/login', login); +userRouter.post('/logout', logout); + +export default userRouter; \ No newline at end of file diff --git a/__tests__/services/sendGrid.test.ts b/__tests__/services/sendGrid.test.ts new file mode 100644 index 0000000..a5ae023 --- /dev/null +++ b/__tests__/services/sendGrid.test.ts @@ -0,0 +1,26 @@ +// import { config } from "dotenv"; +// import { User, Order } from "../mongoose/Schema"; +// import client from '@sendgrid/mail'; +// config(); + +// export async function sendEmailasync(orderId:string, userId: string) { +// console.log(`Sending email for order ${orderId} to user ${userId}`); +// try { +// client.setApiKey(process.env.SENDGRID_API_KEY); +// const msg = { +// to: await User.findOne({ _id: userId }).select('email'), +// from: process.env.EMAIL_FROM, +// subject: 'Order Confirmation', +// text: 'Your order has been placed', +// html: 'Thank you for shopping with us!' +// }; +// await client.send(msg); +// console.log(`Email sent for order ${orderId} to user ${userId}`); +// // Update the order to indicate email has been sent +// await Order.findOne({ _id: orderId }).updateOne({ emailSent: true }); +// console.log(`Updated order ${orderId} to indicate email has been sent`); +// return; +// } catch (error) { +// console.error('Error sending email:', error); +// } +// }; \ No newline at end of file