diff --git a/src/__test__/auth.test.ts b/src/__test__/auth.test.ts new file mode 100644 index 0000000..229fab4 --- /dev/null +++ b/src/__test__/auth.test.ts @@ -0,0 +1,154 @@ +import request from 'supertest'; +import express, { Request, Response } from 'express'; +import { + userVerificationService, + userRegistrationService, + userLoginService, + userEnableTwoFactorAuth, + userDisableTwoFactorAuth, + userValidateOTP, + userResendOtpService, + logoutService, +} from '../services'; +import { userPasswordResetService } from '../services/userServices/userPasswordResetService'; +import { sendPasswordResetLinkService } from '../services/userServices/sendResetPasswordLinkService'; +import { activateUserService } from '../services/updateUserStatus/activateUserService'; +import { deactivateUserService } from '../services/updateUserStatus/deactivateUserService'; +import { userProfileUpdateServices } from '../services/userServices/userProfileUpdateServices'; +import { activateUser, disable2FA, disactivateUser, enable2FA, login, logout, resendOTP, sampleAPI, sendPasswordResetLink, userPasswordReset, userProfileUpdate, userRegistration, userVerification, verifyOTP } from '../controllers'; + +// Mock the services +jest.mock('../services', () => ({ + userVerificationService: jest.fn(), + userRegistrationService: jest.fn(), + userLoginService: jest.fn(), + userEnableTwoFactorAuth: jest.fn(), + userDisableTwoFactorAuth: jest.fn(), + userValidateOTP: jest.fn(), + userResendOtpService: jest.fn(), + logoutService: jest.fn(), +})); + +jest.mock('../services/userServices/userPasswordResetService', () => ({ + userPasswordResetService: jest.fn(), +})); + +jest.mock('../services/userServices/sendResetPasswordLinkService', () => ({ + sendPasswordResetLinkService: jest.fn(), +})); + +jest.mock('../services/updateUserStatus/activateUserService', () => ({ + activateUserService: jest.fn(), +})); + +jest.mock('../services/updateUserStatus/deactivateUserService', () => ({ + deactivateUserService: jest.fn(), +})); + +jest.mock('../services/userServices/userProfileUpdateServices', () => ({ + userProfileUpdateServices: jest.fn(), +})); + +const app = express(); +app.use(express.json()); + +app.post('/register', userRegistration); +app.post('/verify', userVerification); +app.post('/login', login); +app.post('/enable-2fa', enable2FA); +app.post('/disable-2fa', disable2FA); +app.post('/verify-otp', verifyOTP); +app.post('/resend-otp', resendOTP); +app.get('/sample', sampleAPI); +app.post('/reset-password', userPasswordReset); +app.post('/send-reset-link', sendPasswordResetLink); +app.post('/activate', activateUser); +app.post('/deactivate', disactivateUser); +app.post('/logout', logout); +app.put('/update-profile', userProfileUpdate); + +describe('User Controller', () => { + it('should call userRegistrationService on /register', async () => { + (userRegistrationService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(201).send()); + await request(app).post('/register').send({}); + expect(userRegistrationService).toHaveBeenCalled(); + }); + + it('should call userVerificationService on /verify', async () => { + (userVerificationService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/verify').send({}); + expect(userVerificationService).toHaveBeenCalled(); + }); + + it('should call userLoginService on /login', async () => { + (userLoginService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/login').send({}); + expect(userLoginService).toHaveBeenCalled(); + }); + + it('should call userEnableTwoFactorAuth on /enable-2fa', async () => { + (userEnableTwoFactorAuth as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/enable-2fa').send({}); + expect(userEnableTwoFactorAuth).toHaveBeenCalled(); + }); + + it('should call userDisableTwoFactorAuth on /disable-2fa', async () => { + (userDisableTwoFactorAuth as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/disable-2fa').send({}); + expect(userDisableTwoFactorAuth).toHaveBeenCalled(); + }); + + it('should call userValidateOTP on /verify-otp', async () => { + (userValidateOTP as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/verify-otp').send({}); + expect(userValidateOTP).toHaveBeenCalled(); + }); + + it('should call userResendOtpService on /resend-otp', async () => { + (userResendOtpService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/resend-otp').send({}); + expect(userResendOtpService).toHaveBeenCalled(); + }); + + it('should return 200 on /sample', async () => { + const response = await request(app).get('/sample'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ message: 'Token is valid' }); + }); + + it('should call userPasswordResetService on /reset-password', async () => { + (userPasswordResetService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/reset-password').send({}); + expect(userPasswordResetService).toHaveBeenCalled(); + }); + + it('should call sendPasswordResetLinkService on /send-reset-link', async () => { + (sendPasswordResetLinkService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/send-reset-link').send({}); + expect(sendPasswordResetLinkService).toHaveBeenCalled(); + }); + + it('should call activateUserService on /activate', async () => { + (activateUserService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/activate').send({}); + expect(activateUserService).toHaveBeenCalled(); + }); + + it('should call deactivateUserService on /deactivate', async () => { + (deactivateUserService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/deactivate').send({}); + expect(deactivateUserService).toHaveBeenCalled(); + }); + + it('should call logoutService on /logout', async () => { + (logoutService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/logout').send({}); + expect(logoutService).toHaveBeenCalled(); + }); + + it('should call userProfileUpdateServices on /update-profile', async () => { + (userProfileUpdateServices as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).put('/update-profile').send({}); + expect(userProfileUpdateServices).toHaveBeenCalled(); + }); +}); diff --git a/src/__test__/index.test.ts b/src/__test__/index.test.ts new file mode 100644 index 0000000..f4f2285 --- /dev/null +++ b/src/__test__/index.test.ts @@ -0,0 +1,34 @@ +import request from 'supertest'; +import { app, server } from '../../src/index'; + +describe('Express App', () => { + afterAll((done) => { + server.close(); + done(); + }); + + it('should respond with 404 for unknown routes', async () => { + const response = await request(app).get('/unknown-route'); + expect(response.status).toBe(404); + expect(response.body.message).toBe(`Can't find /unknown-route on the server!`); + }); + + it('should have CORS enabled', async () => { + const response = await request(app).get('/'); + expect(response.headers['access-control-allow-origin']).toBe('*'); + }); + + it('should have JSON parsing enabled', async () => { + const response = await request(app) + .post('/some-endpoint') + .send({ name: 'test' }) + .set('Content-Type', 'application/json'); + expect(response.status).not.toBe(404); + }); + + it('should respond to a valid route', async () => { + const response = await request(app).get('/valid-route'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ message: 'Success' }); + }); +}); diff --git a/src/__test__/index.utils.test.ts b/src/__test__/index.utils.test.ts new file mode 100644 index 0000000..5c0d16c --- /dev/null +++ b/src/__test__/index.utils.test.ts @@ -0,0 +1,34 @@ +import { formatMoney, formatDate } from '../utils/index'; + +describe('Utility Functions', () => { + describe('formatMoney', () => { + it('should format a number as currency with default currency RWF', () => { + expect(formatMoney(1234.56)).toBe('RWF1,234.56'); + }); + + it('should format a number as currency with specified currency', () => { + expect(formatMoney(1234.56, 'USD')).toBe('$1,234.56'); + expect(formatMoney(1234.56, 'EUR')).toBe('€1,234.56'); + }); + + it('should format a number with no cents if amount is a whole number', () => { + expect(formatMoney(1234)).toBe('RWF1,234.00'); + }); + }); + + describe('formatDate', () => { + it('should format a date string into a more readable format', () => { + const date = new Date('2024-05-28'); + expect(formatDate(date)).toBe('May 28, 2024'); + }); + + it('should format another date correctly', () => { + const date = new Date('2020-01-01'); + expect(formatDate(date)).toBe('January 1, 2020'); + }); + + it('should handle invalid date strings gracefully', () => { + expect(formatDate(new Date('invalid-date'))).toBe('Invalid Date'); + }); + }); +}); diff --git a/src/__test__/logger.test.ts b/src/__test__/logger.test.ts new file mode 100644 index 0000000..1893ffa --- /dev/null +++ b/src/__test__/logger.test.ts @@ -0,0 +1,68 @@ +import logger from '../utils/logger'; +import winston from 'winston'; + +console.log = jest.fn(); +console.error = jest.fn(); +console.warn = jest.fn(); +console.info = jest.fn(); +console.debug = jest.fn(); + +describe('Logger', () => { + it('should create a logger with the correct configuration', () => { + expect(winston.createLogger).toHaveBeenCalledWith(expect.objectContaining({ + level: 'info', + levels: expect.any(Object), + format: expect.anything(), + transports: expect.any(Array), + })); + }); + + it('should log messages with the correct level and format', () => { + const testMessage = 'Test log message'; + const testLevel = 'info'; + + logger.log(testLevel, testMessage); + + expect(logger.log).toHaveBeenCalledWith(testLevel, testMessage); + }); + + it('should correctly handle info level logs', () => { + const testMessage = 'Test info message'; + + logger.info(testMessage); + + expect(logger.info).toHaveBeenCalledWith(testMessage); + }); + + it('should correctly handle warn level logs', () => { + const testMessage = 'Test warn message'; + + logger.warn(testMessage); + + expect(logger.warn).toHaveBeenCalledWith(testMessage); + }); + + it('should correctly handle error level logs', () => { + const testMessage = 'Test error message'; + + logger.error(testMessage); + + expect(logger.error).toHaveBeenCalledWith(testMessage); + }); + + it('should correctly handle debug level logs', () => { + const testMessage = 'Test debug message'; + + logger.debug(testMessage); + + expect(logger.debug).toHaveBeenCalledWith(testMessage); + }); + + it('should correctly handle http level logs', () => { + const testMessage = 'Test http message'; + + logger.http(testMessage); + + expect(logger.http).toHaveBeenCalledWith(testMessage); + }); +}); diff --git a/src/__test__/product.entities.test.ts b/src/__test__/product.entities.test.ts new file mode 100644 index 0000000..ffca6f6 --- /dev/null +++ b/src/__test__/product.entities.test.ts @@ -0,0 +1,127 @@ +import { validate } from 'class-validator'; +import { createConnection, getConnection, Repository } from 'typeorm'; +import { Product } from '../entities/Product'; +import { User } from '../entities/User'; +import { Category } from '../entities/Category'; +import { OrderItem } from '../entities/OrderItem'; +import { Coupon } from '../entities/coupon'; + +describe('Product Entity', () => { + let productRepository: Repository; + let userRepository: Repository; + let categoryRepository: Repository; + let couponRepository: Repository; + + beforeAll(async () => { + const connection = await createConnection({ + type: 'sqlite', + database: ':memory:', + dropSchema: true, + entities: [Product, User, Category, OrderItem, Coupon], + synchronize: true, + logging: false, + }); + + productRepository = connection.getRepository(Product); + userRepository = connection.getRepository(User); + categoryRepository = connection.getRepository(Category); + couponRepository = connection.getRepository(Coupon); + }); + + afterAll(async () => { + await getConnection().close(); + }); + + it('should validate a valid product', async () => { + const user = userRepository.create({ /* ...user data... */ }); + await userRepository.save(user); + + const category = categoryRepository.create({ /* ...category data... */ }); + await categoryRepository.save(category); + + const coupon = couponRepository.create({ /* ...coupon data... */ }); + await couponRepository.save(coupon); + + const product = productRepository.create({ + vendor: user, + name: 'Sample Product', + description: 'This is a sample product', + images: ['image1.jpg', 'image2.jpg'], + newPrice: 100.0, + quantity: 10, + isAvailable: true, + categories: [category], + coupons: coupon, + }); + + const errors = await validate(product); + expect(errors.length).toBe(0); + + const savedProduct = await productRepository.save(product); + expect(savedProduct.id).toBeDefined(); + expect(savedProduct.createdAt).toBeDefined(); + expect(savedProduct.updatedAt).toBeDefined(); + }); + + it('should not validate a product with missing required fields', async () => { + const product = new Product(); + + const errors = await validate(product); + expect(errors.length).toBeGreaterThan(0); + }); + + it('should enforce array constraints on images', async () => { + const user = userRepository.create({ /* ...user data... */ }); + await userRepository.save(user); + + const product = productRepository.create({ + vendor: user, + name: 'Sample Product', + description: 'This is a sample product', + images: [], + newPrice: 100.0, + quantity: 10, + isAvailable: true, + }); + + const errors = await validate(product); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints?.arrayNotEmpty).toBeDefined(); + }); + + it('should handle relationships correctly', async () => { + const user = userRepository.create({ /* ...user data... */ }); + await userRepository.save(user); + + const category1 = categoryRepository.create({ /* ...category data... */ }); + const category2 = categoryRepository.create({ /* ...category data... */ }); + await categoryRepository.save([category1, category2]); + + const coupon = couponRepository.create({ /* ...coupon data... */ }); + await couponRepository.save(coupon); + + const product = productRepository.create({ + vendor: user, + name: 'Sample Product', + description: 'This is a sample product', + images: ['image1.jpg', 'image2.jpg'], + newPrice: 100.0, + quantity: 10, + isAvailable: true, + categories: [category1, category2], + coupons: coupon, + }); + + await productRepository.save(product); + + const savedProduct = await productRepository.findOne({ + where: { id: product.id }, + relations: ['vendor', 'categories', 'coupons'], + }); + + expect(savedProduct).toBeDefined(); + expect(savedProduct?.vendor).toBeDefined(); + expect(savedProduct?.categories.length).toBe(2); + expect(savedProduct?.coupons).toBeDefined(); + }); +}); diff --git a/src/__test__/user.entity.test.ts b/src/__test__/user.entity.test.ts new file mode 100644 index 0000000..df0c26f --- /dev/null +++ b/src/__test__/user.entity.test.ts @@ -0,0 +1,112 @@ +import { validate } from 'class-validator'; +import { createConnection, getConnection, Repository } from 'typeorm'; +import { User } from '../entities/User'; +import { Order } from '../entities/Order'; +import { Transaction } from '../entities/transaction'; + +describe('User Entity', () => { + let userRepository: Repository; + let orderRepository: Repository; + let transactionRepository: Repository; + + beforeAll(async () => { + const connection = await createConnection({ + type: 'sqlite', + database: ':memory:', + dropSchema: true, + entities: [User, Order, Transaction], + synchronize: true, + logging: false, + }); + + userRepository = connection.getRepository(User); + orderRepository = connection.getRepository(Order); + transactionRepository = connection.getRepository(Transaction); + }); + + afterAll(async () => { + await getConnection().close(); + }); + + it('should validate a valid user', async () => { + const user = userRepository.create({ + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + password: 'password123', + gender: 'male', + phoneNumber: '1234567890', + verified: true, + status: 'active', + userType: 'Buyer', + twoFactorEnabled: false, + accountBalance: 0.0, + }); + + const errors = await validate(user); + expect(errors.length).toBe(0); + + const savedUser = await userRepository.save(user); + expect(savedUser.id).toBeDefined(); + expect(savedUser.createdAt).toBeDefined(); + expect(savedUser.updatedAt).toBeDefined(); + }); + + it('should not validate a user with missing required fields', async () => { + const user = new User(); + + const errors = await validate(user); + expect(errors.length).toBeGreaterThan(0); + }); + + it('should set the role based on userType', async () => { + const user = userRepository.create({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + password: 'password123', + gender: 'female', + phoneNumber: '0987654321', + userType: 'Vendor', + }); + + await userRepository.save(user); + + expect(user.role).toBe('VendorRole'); + }); + + it('should handle relationships correctly', async () => { + const user = userRepository.create({ + firstName: 'Alice', + lastName: 'Smith', + email: 'alice.smith@example.com', + password: 'password123', + gender: 'female', + phoneNumber: '1122334455', + userType: 'Buyer', + }); + + const savedUser = await userRepository.save(user); + + const order = orderRepository.create({ + buyer: savedUser, + }); + + await orderRepository.save(order); + + const transaction = transactionRepository.create({ + user: savedUser, + }); + + await transactionRepository.save(transaction); + + const foundUser = await userRepository.findOne({ + where: { id: savedUser.id }, + relations: ['orders', 'transactions'], + }); + + expect(foundUser).toBeDefined(); + expect(foundUser?.orders.length).toBe(1); + expect(foundUser?.transactions.length).toBe(1); + }); +}); diff --git a/src/entities/User.ts b/src/entities/User.ts index eebd104..6606935 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -33,6 +33,12 @@ export interface UserInterface { @Entity() @Unique(['email']) export class User { + static lastName(lastName: any) { + throw new Error('Method not implemented.'); + } + static firstName(firstName: any) { + throw new Error('Method not implemented.'); + } @PrimaryGeneratedColumn('uuid') @IsNotEmpty() id!: string;