From a492ff12ccacce9a765d52e3a6920e0084a83a6f Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:40:38 +0000 Subject: [PATCH 01/33] Add test setup file for Prisma client with database connection and cleanup --- backend/src/tests/setup/testSetup.ts | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 backend/src/tests/setup/testSetup.ts diff --git a/backend/src/tests/setup/testSetup.ts b/backend/src/tests/setup/testSetup.ts new file mode 100644 index 0000000..d1c9eb6 --- /dev/null +++ b/backend/src/tests/setup/testSetup.ts @@ -0,0 +1,34 @@ +import { PrismaClient } from '@prisma/client'; +import { beforeAll, afterAll, beforeEach } from '@jest/globals'; + +const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL_TEST ?? process.env.DATABASE_URL, + }, + }, +}); + +beforeAll(async () => { + // Connect to test database + await prisma.$connect(); +}); + +afterAll(async () => { + // Clean up and disconnect + await prisma.$disconnect(); +}); + +beforeEach(async () => { + // Clean up database before each test + await prisma.booking.deleteMany(); + await prisma.waitlist.deleteMany(); + await prisma.timeSlot.deleteMany(); + await prisma.lab.deleteMany(); + await prisma.user.deleteMany(); + await prisma.admin.deleteMany(); + await prisma.superAdmin.deleteMany(); + await prisma.organization.deleteMany(); +}); + +export { prisma }; \ No newline at end of file From 624949fa2263fc15d5d5450a0e238041ad59ab1b Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:40:51 +0000 Subject: [PATCH 02/33] Add integration tests for authentication routes --- backend/src/tests/routes/auth.routes.test.ts | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 backend/src/tests/routes/auth.routes.test.ts diff --git a/backend/src/tests/routes/auth.routes.test.ts b/backend/src/tests/routes/auth.routes.test.ts new file mode 100644 index 0000000..e4fd180 --- /dev/null +++ b/backend/src/tests/routes/auth.routes.test.ts @@ -0,0 +1,113 @@ +import { describe, test, expect } from '@jest/globals'; +import request from 'supertest'; +import express from 'express'; +import authRoutes from '../../routes/auth.routes'; +import { createTestUser, createTestOrganization } from '../utils/authTestUtils'; +import { UserRole } from '@prisma/client'; + +const app = express(); +app.use(express.json()); +app.use('/auth', authRoutes); + +describe('Auth Routes Integration Tests', () => { + describe('POST /auth/register', () => { + test('should register new user successfully', async () => { + const organization = await createTestOrganization(); + + const userData = { + user_name: 'Test User', + user_email: 'newuser@example.com', + user_password: 'password123', + organizationId: organization.id, + }; + + const response = await request(app) + .post('/auth/register') + .send(userData) + .expect(201); + + expect(response.body.user).toBeDefined(); + expect(response.body.token).toBeDefined(); + expect(response.body.user.user_email).toBe(userData.user_email); + }); + + test('should reject registration with invalid email', async () => { + const userData = { + user_name: 'Test User', + user_email: 'invalid-email', + user_password: 'password123', + }; + + await request(app) + .post('/auth/register') + .send(userData) + .expect(400); + }); + }); + + describe('POST /auth/login', () => { + test('should login with valid credentials', async () => { + const testUser = await createTestUser({ + user_email: 'login@example.com', + user_role: UserRole.USER, + }); + + const loginData = { + email: testUser.user_email, + password: 'testpassword123', + }; + + const response = await request(app) + .post('/auth/login') + .send(loginData) + .expect(200); + + expect(response.body.user).toBeDefined(); + expect(response.body.token).toBeDefined(); + expect(response.body.user.id).toBe(testUser.id); + }); + + test('should reject login with invalid credentials', async () => { + const loginData = { + email: 'nonexistent@example.com', + password: 'wrongpassword', + }; + + await request(app) + .post('/auth/login') + .send(loginData) + .expect(401); + }); + }); + + describe('POST /auth/logout', () => { + test('should logout authenticated user', async () => { + const testUser = await createTestUser({ + user_email: 'logout@example.com', + user_role: UserRole.USER, + }); + + // First login to get token + const loginResponse = await request(app) + .post('/auth/login') + .send({ + email: testUser.user_email, + password: 'testpassword123', + }); + + const token = loginResponse.body.token; + + // Then logout + await request(app) + .post('/auth/logout') + .set('Authorization', `Bearer ${token}`) + .expect(200); + }); + + test('should reject logout without authentication', async () => { + await request(app) + .post('/auth/logout') + .expect(401); + }); + }); +}); \ No newline at end of file From 0d5f0ee98232aaad1b161fe3d050526a2254d72c Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:40:59 +0000 Subject: [PATCH 03/33] Add utility functions for testing authentication and user management --- backend/src/tests/utils/authTestUtils.ts | 80 ++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 backend/src/tests/utils/authTestUtils.ts diff --git a/backend/src/tests/utils/authTestUtils.ts b/backend/src/tests/utils/authTestUtils.ts new file mode 100644 index 0000000..3009753 --- /dev/null +++ b/backend/src/tests/utils/authTestUtils.ts @@ -0,0 +1,80 @@ +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcrypt'; +import { UserRole, PrismaClient } from '@prisma/client'; +import { Request } from 'express'; + +const prisma = new PrismaClient(); + +export interface TestUser { + id: string; + user_name: string; + user_email: string; + user_role: UserRole; + organizationId?: string | null; // Changed to allow null +} + +export const createTestUser = async ( + userData: Partial & { user_email: string }, +): Promise => { + const hashedPassword = await bcrypt.hash('testpassword123', 10); + + const user = await prisma.user.create({ + data: { + user_name: userData.user_name ?? 'Test User', + user_email: userData.user_email, + user_password: hashedPassword, + user_role: userData.user_role ?? UserRole.USER, + organizationId: userData.organizationId, + }, + }); + + return user; +}; + +export const createTestOrganization = async (name: string = 'Test Organization') => { + return await prisma.organization.create({ + data: { + org_name: name, + org_type: 'Educational', + org_location: 'Default Location', // Added missing required field + }, + }); +}; + +export const generateTestToken = (user: TestUser): string => { + return jwt.sign( + { + userId: user.id, + email: user.user_email, + role: user.user_role, + organizationId: user.organizationId, + }, + process.env.JWT_SECRET ?? 'test-secret', + { expiresIn: '1h' } + ); +}; + +export const createAuthenticatedRequest = (user: TestUser): Partial => { + const token = generateTestToken(user); + return { + headers: { + authorization: `Bearer ${token}`, + }, + user: { + id: user.id, + email: user.user_email, + role: user.user_role, + organizationId: user.organizationId, + }, + }; +}; + +export const createMockResponse = () => { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.send = jest.fn().mockReturnValue(res); + return res; +}; + +export const createMockNext = () => jest.fn(); \ No newline at end of file From c724f4415b305bcb20753a4ad25e8038dc26617b Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:41:07 +0000 Subject: [PATCH 04/33] Add tests for RBAC middleware functionality --- .../tests/middleware/rbacMiddleware.test.ts | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 backend/src/tests/middleware/rbacMiddleware.test.ts diff --git a/backend/src/tests/middleware/rbacMiddleware.test.ts b/backend/src/tests/middleware/rbacMiddleware.test.ts new file mode 100644 index 0000000..b63db0a --- /dev/null +++ b/backend/src/tests/middleware/rbacMiddleware.test.ts @@ -0,0 +1,180 @@ +import { describe, test, expect, beforeEach } from '@jest/globals'; +import { Request, Response } from 'express'; +import { + requirePermissions, + requireSameOrganization +} from '../../authorization/middleware/rbacMiddleware'; +import { + createTestUser, + createTestOrganization, + createMockResponse, + createMockNext +} from '../utils/authTestUtils'; +import { UserRole } from '@prisma/client'; + +describe('RBAC Middleware', () => { + let mockReq: Partial; + let mockRes: Response; + let mockNext: jest.Mock; + + beforeEach(() => { + mockReq = {}; + mockRes = createMockResponse(); + mockNext = createMockNext(); + }); + + describe('requirePermissions middleware', () => { + test('should allow access with required permissions', async () => { + const testUser = await createTestUser({ + user_email: 'admin@example.com', + user_role: UserRole.ADMIN, + organizationId: 'org1', + }); + + mockReq.user = { + id: testUser.id, + role: testUser.user_role, + organizationId: testUser.organizationId, + }; + + const permissionMiddleware = requirePermissions(['READ_USERS']); + permissionMiddleware(mockReq as Request, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + }); + + test('should deny access without required permissions', async () => { + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + organizationId: 'org1', + }); + + mockReq.user = { + id: testUser.id, + role: testUser.user_role, + organizationId: testUser.organizationId, + }; + + const permissionMiddleware = requirePermissions(['READ_USERS']); + permissionMiddleware(mockReq as Request, mockRes, mockNext); + + // Assuming HttpException calls mockRes.status().json() or similar + // For this test, we check if next was called with an error + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); // Or more specific HttpException + // expect(mockRes.status).toHaveBeenCalledWith(403); // This depends on how HttpException interacts with mockRes + }); + }); + + describe('requireSameOrganization middleware', () => { + let organization: { id: string }; + + beforeEach(async () => { + organization = await createTestOrganization(); + }); + + test('should allow access if user is in the same organization as target in params', async () => { + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + organizationId: organization.id, + }); + + mockReq.user = { + id: testUser.id, + role: testUser.user_role, + organizationId: testUser.organizationId, + }; + mockReq.params = { organizationId: organization.id }; // Target organizationId from params + + const orgMiddleware = requireSameOrganization(); // Uses default 'organizationId' key + orgMiddleware(mockReq as Request, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledTimes(1); + expect(mockNext).not.toHaveBeenCalledWith(expect.any(Error)); + }); + + test('should deny access if user is in a different organization', async () => { + const otherOrganization = await createTestOrganization(); // Different org + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + organizationId: organization.id, // User belongs to 'organization' + }); + + mockReq.user = { + id: testUser.id, + role: testUser.user_role, + organizationId: testUser.organizationId, + }; + mockReq.params = { organizationId: otherOrganization.id }; // Target is 'otherOrganization' + + const orgMiddleware = requireSameOrganization(); + orgMiddleware(mockReq as Request, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ + status: 403, + message: 'User does not belong to the target organization', + })); + }); + + test('should deny access if target organizationId is missing from params', async () => { + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + organizationId: organization.id, + }); + + mockReq.user = { + id: testUser.id, + role: testUser.user_role, + organizationId: testUser.organizationId, + }; + mockReq.params = {}; // No organizationId in params + + const orgMiddleware = requireSameOrganization(); + orgMiddleware(mockReq as Request, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ + status: 400, + message: "Target organization ID not found in request parameters using key: 'organizationId'", + })); + }); + + test('should deny access if authenticated user has no organizationId', async () => { + const testUser = await createTestUser({ // Assume createTestUser can create a user without orgId + user_email: 'user@example.com', + user_role: UserRole.USER, + organizationId: undefined, // Explicitly null or undefined + }); + + mockReq.user = { + id: testUser.id, + role: testUser.user_role, + organizationId: null, // User has no organization + }; + mockReq.params = { organizationId: organization.id }; + + const orgMiddleware = requireSameOrganization(); + orgMiddleware(mockReq as Request, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ + status: 403, + message: 'User does not belong to any organization', + })); + }); + + test('should deny access if user is not authenticated', async () => { + mockReq.user = undefined; // No user on request + mockReq.params = { organizationId: organization.id }; + + const orgMiddleware = requireSameOrganization(); + orgMiddleware(mockReq as Request, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ + status: 401, + message: 'Authentication required', + })); + }); + }); +}); \ No newline at end of file From ef6563e170a5158fe0bb1df2fdf93fab6737fc3d Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:41:15 +0000 Subject: [PATCH 05/33] Add tests for authentication and role-checking middleware --- .../tests/middleware/auth.middleware.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 backend/src/tests/middleware/auth.middleware.test.ts diff --git a/backend/src/tests/middleware/auth.middleware.test.ts b/backend/src/tests/middleware/auth.middleware.test.ts new file mode 100644 index 0000000..1e74f68 --- /dev/null +++ b/backend/src/tests/middleware/auth.middleware.test.ts @@ -0,0 +1,99 @@ +import { describe, test, expect, beforeEach } from '@jest/globals'; +import { Request, Response } from 'express'; +import { authenticate, checkRole } from '../../middleware/auth.middleware'; +import { + createTestUser, + generateTestToken, + createMockResponse, + createMockNext +} from '../utils/authTestUtils'; +import { UserRole } from '@prisma/client'; + +describe('Authentication Middleware', () => { + let mockReq: Partial; + let mockRes: Response; + let mockNext: jest.Mock; + + beforeEach(() => { + mockReq = {}; + mockRes = createMockResponse(); + mockNext = createMockNext(); + }); + + describe('authenticate middleware', () => { + test('should authenticate valid JWT token', async () => { + const testUser = await createTestUser({ + user_email: 'test@example.com', + user_role: UserRole.USER, + }); + + const token = generateTestToken(testUser); + mockReq.headers = { authorization: `Bearer ${token}` }; + + await authenticate(mockReq as Request, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(mockReq.user).toBeDefined(); + expect(mockReq.user?.id).toBe(testUser.id); + }); + + test('should reject request without token', async () => { + mockReq.headers = {}; + + await authenticate(mockReq as Request, mockRes, mockNext); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockNext).not.toHaveBeenCalled(); + }); + + test('should reject invalid token', async () => { + mockReq.headers = { authorization: 'Bearer invalid-token' }; + + await authenticate(mockReq as Request, mockRes, mockNext); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); + + describe('checkRole middleware', () => { + test('should allow access for correct role', async () => { + const testUser = await createTestUser({ + user_email: 'admin@example.com', + user_role: UserRole.ADMIN, + }); + + mockReq.user = { + id: testUser.id, + email: testUser.user_email, + role: testUser.user_role, + organizationId: testUser.organizationId, + }; + + const roleMiddleware = checkRole([UserRole.ADMIN]); + roleMiddleware(mockReq as Request, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + }); + + test('should reject access for incorrect role', async () => { + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + }); + + mockReq.user = { + id: testUser.id, + email: testUser.user_email, + role: testUser.user_role, + organizationId: testUser.organizationId, + }; + + const roleMiddleware = checkRole([UserRole.ADMIN]); + roleMiddleware(mockReq as Request, mockRes, mockNext); + + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file From f401cc9ec4136ebbe9609028dea3b974985ab048 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:41:27 +0000 Subject: [PATCH 06/33] Add tests for permission checking functionality --- .../authorization/permissionChecker.test.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 backend/src/tests/authorization/permissionChecker.test.ts diff --git a/backend/src/tests/authorization/permissionChecker.test.ts b/backend/src/tests/authorization/permissionChecker.test.ts new file mode 100644 index 0000000..532d749 --- /dev/null +++ b/backend/src/tests/authorization/permissionChecker.test.ts @@ -0,0 +1,93 @@ +import { describe, test, expect } from '@jest/globals'; +import { + hasPermission, + hasAnyPermission, + getUserPermissions +} from '../../authorization/utils/permissionChecker'; +import { createTestUser } from '../utils/authTestUtils'; +import { UserRole } from '@prisma/client'; + +describe('Permission Checker', () => { + describe('hasPermission', () => { + test('should return true for valid user permission', async () => { + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + }); + + const authUser = { + ...testUser, + user_role: testUser.user_role, + }; + + const result = hasPermission(authUser, 'READ_SELF'); + expect(result).toBe(true); + }); + + test('should return false for invalid user permission', async () => { + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + }); + + const authUser = { + ...testUser, + user_role: testUser.user_role, + }; + + const result = hasPermission(authUser, 'DELETE_USERS'); + expect(result).toBe(false); + }); + + test('should return true for admin permission', async () => { + const testUser = await createTestUser({ + user_email: 'admin@example.com', + user_role: UserRole.ADMIN, + }); + + const authUser = { + ...testUser, + user_role: testUser.user_role, + }; + + const result = hasPermission(authUser, 'READ_USERS'); + expect(result).toBe(true); + }); + }); + + describe('hasAnyPermission', () => { + test('should return true if user has any of the permissions', async () => { + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + }); + + const authUser = { + ...testUser, + user_role: testUser.user_role, + }; + + const result = hasAnyPermission(authUser, ['READ_SELF', 'DELETE_USERS']); + expect(result).toBe(true); + }); + }); + + describe('getUserPermissions', () => { + test('should return correct permissions for user role', async () => { + const testUser = await createTestUser({ + user_email: 'user@example.com', + user_role: UserRole.USER, + }); + + const authUser = { + ...testUser, + user_role: testUser.user_role, + }; + + const permissions = getUserPermissions(authUser); + expect(permissions).toContain('READ_SELF'); + expect(permissions).toContain('CREATE_BOOKING'); + expect(permissions).not.toContain('DELETE_USERS'); + }); + }); +}); \ No newline at end of file From d4bfe15673884f21fc6d2bb53a220b858517e6dc Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:41:40 +0000 Subject: [PATCH 07/33] Add email and organizationId to user object in authentication middleware --- backend/src/middleware/auth.middleware.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/middleware/auth.middleware.ts b/backend/src/middleware/auth.middleware.ts index 5856cb4..b27dbfd 100644 --- a/backend/src/middleware/auth.middleware.ts +++ b/backend/src/middleware/auth.middleware.ts @@ -15,6 +15,8 @@ declare module 'express' { user?: { id: string; role: UserRole; + email?: string; // Added email + organizationId?: string | null; // Added organizationId }; } } @@ -38,7 +40,7 @@ export const authenticate = async ( // Check if user exists const user = await prisma.user.findUnique({ where: { id: decoded.userId }, - select: { id: true, user_role: true }, + select: { id: true, user_role: true, user_email: true, organizationId: true }, // Added user_email and organizationId to select }); if (!user) { @@ -49,6 +51,8 @@ export const authenticate = async ( req.user = { id: user.id, role: user.user_role, + email: user.user_email, // Added email + organizationId: user.organizationId, // Added organizationId }; next(); From 86b2e63c08ab048fa2cb600e89f65eb8a88b37cd Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:41:55 +0000 Subject: [PATCH 08/33] Add requireSameOrganization middleware and getUserPermissions utility function --- .../middleware/rbacMiddleware.ts | 30 +++++++++++++++++++ .../authorization/utils/permissionChecker.ts | 9 ++++++ 2 files changed, 39 insertions(+) diff --git a/backend/src/authorization/middleware/rbacMiddleware.ts b/backend/src/authorization/middleware/rbacMiddleware.ts index f21a663..0182d24 100644 --- a/backend/src/authorization/middleware/rbacMiddleware.ts +++ b/backend/src/authorization/middleware/rbacMiddleware.ts @@ -138,3 +138,33 @@ export const requireAny = (permissionsOrRoles: (Permission | UserRole)[]) => { next(); }; }; + +/** + * Middleware to check if the authenticated user belongs to the same organization as a target entity. + * The target entity's organization ID is expected in req.params. + * @param paramKeyForTargetOrgId The key in req.params that holds the target organization ID. Defaults to 'organizationId'. + * @returns Express middleware + */ +export const requireSameOrganization = (paramKeyForTargetOrgId: string = 'organizationId') => { + return (req: Request, res: Response, next: NextFunction): void => { + if (!req.user) { + return next(new HttpException(401, 'Authentication required')); + } + + if (!req.user.organizationId) { + return next(new HttpException(403, 'User does not belong to any organization')); + } + + const targetOrganizationId = req.params[paramKeyForTargetOrgId]; + + if (!targetOrganizationId) { + return next(new HttpException(400, `Target organization ID not found in request parameters using key: '${paramKeyForTargetOrgId}'`)); + } + + if (req.user.organizationId !== targetOrganizationId) { + return next(new HttpException(403, 'User does not belong to the target organization')); + } + + next(); + }; +}; diff --git a/backend/src/authorization/utils/permissionChecker.ts b/backend/src/authorization/utils/permissionChecker.ts index 8377379..6e5ddcb 100644 --- a/backend/src/authorization/utils/permissionChecker.ts +++ b/backend/src/authorization/utils/permissionChecker.ts @@ -45,6 +45,15 @@ export const hasAllPermissions = (user: AuthUser, permissions: Permission[]): bo return permissions.every(permission => hasPermission(user, permission)); }; +/** + * Get all permissions for a given user's role + * @param user The authenticated user + * @returns Array of permissions + */ +export const getUserPermissions = (user: AuthUser): Permission[] => { + return getPermissionsForRole(user.user_role); +}; + /** * Check if a user can access a specific resource based on ownership and permissions * @param user The authenticated user From 8c9202e96c6928eb0d8f0b64b003d73e2a039854 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:42:04 +0000 Subject: [PATCH 09/33] Refactor Jest configuration for improved test matching and coverage collection --- backend/jest.config.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index 3586e72..d872a6c 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -1,13 +1,19 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - roots: ['/src'], - transform: { - '^.+\\.tsx?$': ['ts-jest', { - tsconfig: 'tsconfig.json', - }], - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', - collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'], + setupFilesAfterEnv: ['/src/tests/setup/testSetup.ts'], + testMatch: [ + '/src/**/__tests__/**/*.test.ts', + '/src/tests/**/*.test.ts' + ], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/tests/**', + '!src/**/__tests__/**' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + modulePathIgnorePatterns: ['/dist/'], + testTimeout: 10000, }; \ No newline at end of file From 6ba475ee18c342276e1ae452f928a5b45972159e Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:42:15 +0000 Subject: [PATCH 10/33] Add supertest and its type definitions for enhanced testing capabilities --- backend/package.json | 2 + package-lock.json | 171 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/backend/package.json b/backend/package.json index df6d20e..53e017e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -44,6 +44,7 @@ "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.12.12", + "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.56.0", @@ -54,6 +55,7 @@ "jest-mock-extended": "^4.0.0-beta1", "prettier": "^3.2.5", "prisma": "^6.8.2", + "supertest": "^7.1.1", "ts-jest": "^29.1.4", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", diff --git a/package-lock.json b/package-lock.json index 65e5843..293d2ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.12.12", + "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.56.0", @@ -61,6 +62,7 @@ "jest-mock-extended": "^4.0.0-beta1", "prettier": "^3.2.5", "prisma": "^6.8.2", + "supertest": "^7.1.1", "ts-jest": "^29.1.4", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", @@ -2870,6 +2872,19 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2908,6 +2923,16 @@ "node": ">= 8" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4028,6 +4053,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -4166,6 +4198,13 @@ "@types/node": "*" } }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4292,6 +4331,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -5163,6 +5226,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -6100,6 +6170,16 @@ "node": ">=18" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6183,6 +6263,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -6581,6 +6668,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -7844,6 +7942,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -8075,6 +8180,24 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13854,6 +13977,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", + "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", + "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", From f7341c8f868d415a5797b684874df30e26472a60 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:07:46 +0000 Subject: [PATCH 11/33] Fix missing newline at end of test setup file --- backend/src/tests/setup/testSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/tests/setup/testSetup.ts b/backend/src/tests/setup/testSetup.ts index d1c9eb6..1d6bea8 100644 --- a/backend/src/tests/setup/testSetup.ts +++ b/backend/src/tests/setup/testSetup.ts @@ -31,4 +31,4 @@ beforeEach(async () => { await prisma.organization.deleteMany(); }); -export { prisma }; \ No newline at end of file +export { prisma }; From 07cf029b2a2bda840d0b034954b034d7d509911a Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:07:53 +0000 Subject: [PATCH 12/33] Format error message in requireSameOrganization middleware for improved readability --- backend/src/authorization/middleware/rbacMiddleware.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/authorization/middleware/rbacMiddleware.ts b/backend/src/authorization/middleware/rbacMiddleware.ts index 0182d24..00c9a97 100644 --- a/backend/src/authorization/middleware/rbacMiddleware.ts +++ b/backend/src/authorization/middleware/rbacMiddleware.ts @@ -158,7 +158,12 @@ export const requireSameOrganization = (paramKeyForTargetOrgId: string = 'organi const targetOrganizationId = req.params[paramKeyForTargetOrgId]; if (!targetOrganizationId) { - return next(new HttpException(400, `Target organization ID not found in request parameters using key: '${paramKeyForTargetOrgId}'`)); + return next( + new HttpException( + 400, + `Target organization ID not found in request parameters using key: '${paramKeyForTargetOrgId}'`, + ), + ); } if (req.user.organizationId !== targetOrganizationId) { From 3ad9c15f6a1ab7768996b214dd51c20945a418c1 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:07:59 +0000 Subject: [PATCH 13/33] Fix missing comma in import statement and add newline at end of permissionChecker test file --- backend/src/tests/authorization/permissionChecker.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/tests/authorization/permissionChecker.test.ts b/backend/src/tests/authorization/permissionChecker.test.ts index 532d749..77684db 100644 --- a/backend/src/tests/authorization/permissionChecker.test.ts +++ b/backend/src/tests/authorization/permissionChecker.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect } from '@jest/globals'; import { hasPermission, hasAnyPermission, - getUserPermissions + getUserPermissions, } from '../../authorization/utils/permissionChecker'; import { createTestUser } from '../utils/authTestUtils'; import { UserRole } from '@prisma/client'; @@ -90,4 +90,4 @@ describe('Permission Checker', () => { expect(permissions).not.toContain('DELETE_USERS'); }); }); -}); \ No newline at end of file +}); From a23828f19f06e0a33adde1ddb6d54529244c2922 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:08:06 +0000 Subject: [PATCH 14/33] Refactor auth middleware tests to use MockResponse type and improve type casting for middleware calls --- .../tests/middleware/auth.middleware.test.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/backend/src/tests/middleware/auth.middleware.test.ts b/backend/src/tests/middleware/auth.middleware.test.ts index 1e74f68..95b7aae 100644 --- a/backend/src/tests/middleware/auth.middleware.test.ts +++ b/backend/src/tests/middleware/auth.middleware.test.ts @@ -1,22 +1,23 @@ import { describe, test, expect, beforeEach } from '@jest/globals'; -import { Request, Response } from 'express'; +import { Request, Response as ExpressResponse } from 'express'; // Alias ExpressResponse import { authenticate, checkRole } from '../../middleware/auth.middleware'; import { createTestUser, generateTestToken, createMockResponse, - createMockNext + createMockNext, + MockResponse, // Import your MockResponse type } from '../utils/authTestUtils'; import { UserRole } from '@prisma/client'; describe('Authentication Middleware', () => { let mockReq: Partial; - let mockRes: Response; + let mockRes: MockResponse; // Use MockResponse type let mockNext: jest.Mock; beforeEach(() => { mockReq = {}; - mockRes = createMockResponse(); + mockRes = createMockResponse(); // Assigns MockResponse to MockResponse mockNext = createMockNext(); }); @@ -30,7 +31,8 @@ describe('Authentication Middleware', () => { const token = generateTestToken(testUser); mockReq.headers = { authorization: `Bearer ${token}` }; - await authenticate(mockReq as Request, mockRes, mockNext); + // Cast mockRes when passing to the middleware + await authenticate(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); expect(mockNext).toHaveBeenCalled(); expect(mockReq.user).toBeDefined(); @@ -40,18 +42,18 @@ describe('Authentication Middleware', () => { test('should reject request without token', async () => { mockReq.headers = {}; - await authenticate(mockReq as Request, mockRes, mockNext); + await authenticate(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); - expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.status).toHaveBeenCalledWith(401); // Assert on MockResponse expect(mockNext).not.toHaveBeenCalled(); }); test('should reject invalid token', async () => { mockReq.headers = { authorization: 'Bearer invalid-token' }; - await authenticate(mockReq as Request, mockRes, mockNext); + await authenticate(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); - expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.status).toHaveBeenCalledWith(401); // Assert on MockResponse expect(mockNext).not.toHaveBeenCalled(); }); }); @@ -71,7 +73,7 @@ describe('Authentication Middleware', () => { }; const roleMiddleware = checkRole([UserRole.ADMIN]); - roleMiddleware(mockReq as Request, mockRes, mockNext); + roleMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); expect(mockNext).toHaveBeenCalled(); }); @@ -90,10 +92,10 @@ describe('Authentication Middleware', () => { }; const roleMiddleware = checkRole([UserRole.ADMIN]); - roleMiddleware(mockReq as Request, mockRes, mockNext); + roleMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); - expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockRes.status).toHaveBeenCalledWith(403); // Assert on MockResponse expect(mockNext).not.toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); From a0663132259b83b7a0b0909326767aac494510d1 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:08:19 +0000 Subject: [PATCH 15/33] Refactor RBAC middleware tests to use MockResponse type and improve type casting for middleware calls --- .../tests/middleware/rbacMiddleware.test.ts | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/backend/src/tests/middleware/rbacMiddleware.test.ts b/backend/src/tests/middleware/rbacMiddleware.test.ts index b63db0a..89b277d 100644 --- a/backend/src/tests/middleware/rbacMiddleware.test.ts +++ b/backend/src/tests/middleware/rbacMiddleware.test.ts @@ -1,25 +1,28 @@ import { describe, test, expect, beforeEach } from '@jest/globals'; -import { Request, Response } from 'express'; +import { Request, Response as ExpressResponse } from 'express'; // Alias ExpressResponse import { requirePermissions, - requireSameOrganization + requireSameOrganization, } from '../../authorization/middleware/rbacMiddleware'; import { createTestUser, createTestOrganization, createMockResponse, - createMockNext + createMockNext, + MockResponse, // Import your MockResponse type } from '../utils/authTestUtils'; import { UserRole } from '@prisma/client'; +// Import ALL_PERMISSIONS for runtime values, Permission can remain for type annotations if needed +import { ALL_PERMISSIONS } from '../../authorization/constants/permissions'; describe('RBAC Middleware', () => { let mockReq: Partial; - let mockRes: Response; + let mockRes: MockResponse; // Use MockResponse type let mockNext: jest.Mock; - beforeEach(() => { + beforeEach(async () => { mockReq = {}; - mockRes = createMockResponse(); + mockRes = createMockResponse(); // Assigns MockResponse to MockResponse mockNext = createMockNext(); }); @@ -37,8 +40,10 @@ describe('RBAC Middleware', () => { organizationId: testUser.organizationId, }; - const permissionMiddleware = requirePermissions(['READ_USERS']); - permissionMiddleware(mockReq as Request, mockRes, mockNext); + // Use ALL_PERMISSIONS for the actual permission string value + const permissionMiddleware = requirePermissions([ALL_PERMISSIONS.READ_USERS]); + // Cast mockRes when passing to the middleware + permissionMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); expect(mockNext).toHaveBeenCalled(); }); @@ -56,13 +61,12 @@ describe('RBAC Middleware', () => { organizationId: testUser.organizationId, }; - const permissionMiddleware = requirePermissions(['READ_USERS']); - permissionMiddleware(mockReq as Request, mockRes, mockNext); + // Use ALL_PERMISSIONS for the actual permission string value + const permissionMiddleware = requirePermissions([ALL_PERMISSIONS.READ_USERS]); + // Cast mockRes when passing to the middleware + permissionMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); - // Assuming HttpException calls mockRes.status().json() or similar - // For this test, we check if next was called with an error - expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); // Or more specific HttpException - // expect(mockRes.status).toHaveBeenCalledWith(403); // This depends on how HttpException interacts with mockRes + expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); }); }); @@ -85,21 +89,22 @@ describe('RBAC Middleware', () => { role: testUser.user_role, organizationId: testUser.organizationId, }; - mockReq.params = { organizationId: organization.id }; // Target organizationId from params + mockReq.params = { organizationId: organization.id }; - const orgMiddleware = requireSameOrganization(); // Uses default 'organizationId' key - orgMiddleware(mockReq as Request, mockRes, mockNext); + const orgMiddleware = requireSameOrganization(); + // Cast mockRes when passing to the middleware + orgMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); expect(mockNext).toHaveBeenCalledTimes(1); expect(mockNext).not.toHaveBeenCalledWith(expect.any(Error)); }); test('should deny access if user is in a different organization', async () => { - const otherOrganization = await createTestOrganization(); // Different org + const otherOrganization = await createTestOrganization('Other Org'); const testUser = await createTestUser({ user_email: 'user@example.com', user_role: UserRole.USER, - organizationId: organization.id, // User belongs to 'organization' + organizationId: organization.id, }); mockReq.user = { @@ -107,15 +112,18 @@ describe('RBAC Middleware', () => { role: testUser.user_role, organizationId: testUser.organizationId, }; - mockReq.params = { organizationId: otherOrganization.id }; // Target is 'otherOrganization' + mockReq.params = { organizationId: otherOrganization.id }; const orgMiddleware = requireSameOrganization(); - orgMiddleware(mockReq as Request, mockRes, mockNext); - - expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ - status: 403, - message: 'User does not belong to the target organization', - })); + // Cast mockRes when passing to the middleware + orgMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); + + expect(mockNext).toHaveBeenCalledWith( + expect.objectContaining({ + status: 403, + message: 'User does not belong to the target organization', + }), + ); }); test('should deny access if target organizationId is missing from params', async () => { @@ -130,51 +138,61 @@ describe('RBAC Middleware', () => { role: testUser.user_role, organizationId: testUser.organizationId, }; - mockReq.params = {}; // No organizationId in params + mockReq.params = {}; const orgMiddleware = requireSameOrganization(); - orgMiddleware(mockReq as Request, mockRes, mockNext); - - expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ - status: 400, - message: "Target organization ID not found in request parameters using key: 'organizationId'", - })); + // Cast mockRes when passing to the middleware + orgMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); + + expect(mockNext).toHaveBeenCalledWith( + expect.objectContaining({ + status: 400, + message: + "Target organization ID not found in request parameters using key: 'organizationId'", + }), + ); }); test('should deny access if authenticated user has no organizationId', async () => { - const testUser = await createTestUser({ // Assume createTestUser can create a user without orgId + const testUser = await createTestUser({ user_email: 'user@example.com', user_role: UserRole.USER, - organizationId: undefined, // Explicitly null or undefined + organizationId: undefined, }); mockReq.user = { id: testUser.id, role: testUser.user_role, - organizationId: null, // User has no organization + organizationId: null, }; mockReq.params = { organizationId: organization.id }; const orgMiddleware = requireSameOrganization(); - orgMiddleware(mockReq as Request, mockRes, mockNext); - - expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ - status: 403, - message: 'User does not belong to any organization', - })); + // Cast mockRes when passing to the middleware + orgMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); + + expect(mockNext).toHaveBeenCalledWith( + expect.objectContaining({ + status: 403, + message: 'User does not belong to any organization', + }), + ); }); test('should deny access if user is not authenticated', async () => { - mockReq.user = undefined; // No user on request + mockReq.user = undefined; mockReq.params = { organizationId: organization.id }; const orgMiddleware = requireSameOrganization(); - orgMiddleware(mockReq as Request, mockRes, mockNext); - - expect(mockNext).toHaveBeenCalledWith(expect.objectContaining({ - status: 401, - message: 'Authentication required', - })); + // Cast mockRes when passing to the middleware + orgMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); + + expect(mockNext).toHaveBeenCalledWith( + expect.objectContaining({ + status: 401, + message: 'Authentication required', + }), + ); }); }); -}); \ No newline at end of file +}); From db59a961efd776e84a93ce925a908a8bc58f9c73 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:08:26 +0000 Subject: [PATCH 16/33] Refactor auth routes tests for improved readability by consolidating request chaining --- backend/src/tests/routes/auth.routes.test.ts | 41 ++++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/backend/src/tests/routes/auth.routes.test.ts b/backend/src/tests/routes/auth.routes.test.ts index e4fd180..21e14c9 100644 --- a/backend/src/tests/routes/auth.routes.test.ts +++ b/backend/src/tests/routes/auth.routes.test.ts @@ -21,10 +21,7 @@ describe('Auth Routes Integration Tests', () => { organizationId: organization.id, }; - const response = await request(app) - .post('/auth/register') - .send(userData) - .expect(201); + const response = await request(app).post('/auth/register').send(userData).expect(201); expect(response.body.user).toBeDefined(); expect(response.body.token).toBeDefined(); @@ -38,10 +35,7 @@ describe('Auth Routes Integration Tests', () => { user_password: 'password123', }; - await request(app) - .post('/auth/register') - .send(userData) - .expect(400); + await request(app).post('/auth/register').send(userData).expect(400); }); }); @@ -57,10 +51,7 @@ describe('Auth Routes Integration Tests', () => { password: 'testpassword123', }; - const response = await request(app) - .post('/auth/login') - .send(loginData) - .expect(200); + const response = await request(app).post('/auth/login').send(loginData).expect(200); expect(response.body.user).toBeDefined(); expect(response.body.token).toBeDefined(); @@ -73,10 +64,7 @@ describe('Auth Routes Integration Tests', () => { password: 'wrongpassword', }; - await request(app) - .post('/auth/login') - .send(loginData) - .expect(401); + await request(app).post('/auth/login').send(loginData).expect(401); }); }); @@ -88,26 +76,19 @@ describe('Auth Routes Integration Tests', () => { }); // First login to get token - const loginResponse = await request(app) - .post('/auth/login') - .send({ - email: testUser.user_email, - password: 'testpassword123', - }); + const loginResponse = await request(app).post('/auth/login').send({ + email: testUser.user_email, + password: 'testpassword123', + }); const token = loginResponse.body.token; // Then logout - await request(app) - .post('/auth/logout') - .set('Authorization', `Bearer ${token}`) - .expect(200); + await request(app).post('/auth/logout').set('Authorization', `Bearer ${token}`).expect(200); }); test('should reject logout without authentication', async () => { - await request(app) - .post('/auth/logout') - .expect(401); + await request(app).post('/auth/logout').expect(401); }); }); -}); \ No newline at end of file +}); From 75400144228e5c77d32f22309b406dd1d5ae5994 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:08:39 +0000 Subject: [PATCH 17/33] Refactor authTestUtils to enhance type safety and improve mock response implementation --- backend/src/tests/utils/authTestUtils.ts | 72 ++++++++++++++++++++---- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/backend/src/tests/utils/authTestUtils.ts b/backend/src/tests/utils/authTestUtils.ts index 3009753..30ea79b 100644 --- a/backend/src/tests/utils/authTestUtils.ts +++ b/backend/src/tests/utils/authTestUtils.ts @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; -import { UserRole, PrismaClient } from '@prisma/client'; -import { Request } from 'express'; +import { UserRole, PrismaClient, Organization } from '@prisma/client'; +import { Request, CookieOptions } from 'express'; // Alias ExpressResponse, Import CookieOptions const prisma = new PrismaClient(); @@ -10,7 +10,15 @@ export interface TestUser { user_name: string; user_email: string; user_role: UserRole; - organizationId?: string | null; // Changed to allow null + organizationId?: string | null; +} + +// Interface for the user object attached to the Express Request +interface AuthenticatedUser { + id: string; + role: UserRole; + email: string; + organizationId?: string | null; } export const createTestUser = async ( @@ -31,12 +39,14 @@ export const createTestUser = async ( return user; }; -export const createTestOrganization = async (name: string = 'Test Organization') => { +export const createTestOrganization = async ( + name: string = 'Test Organization', +): Promise => { return await prisma.organization.create({ data: { org_name: name, org_type: 'Educational', - org_location: 'Default Location', // Added missing required field + org_location: 'Default Location', }, }); }; @@ -50,7 +60,7 @@ export const generateTestToken = (user: TestUser): string => { organizationId: user.organizationId, }, process.env.JWT_SECRET ?? 'test-secret', - { expiresIn: '1h' } + { expiresIn: '1h' }, ); }; @@ -65,16 +75,56 @@ export const createAuthenticatedRequest = (user: TestUser): Partial => email: user.user_email, role: user.user_role, organizationId: user.organizationId, - }, + } as AuthenticatedUser, }; }; -export const createMockResponse = () => { - const res: any = {}; +// Define a more specific type for the mock response +// It should implement the parts of Express.Response that are actually used in tests. +export interface MockResponse { + status: jest.Mock; + json: jest.Mock; + send: jest.Mock; + sendStatus: jest.Mock; + // Add other Response properties/methods if your tests use them + // For example, if you use res.locals, res.cookie, etc. + locals: Record; // Changed any to unknown + cookie: jest.Mock; // Used CookieOptions + clearCookie: jest.Mock; // Used CookieOptions + setHeader: jest.Mock; // Example + getHeader: jest.Mock; // Example + // Ensure all methods from Express.Response that are used are explicitly defined + // or ensure that the methods return `this` for chaining if that's the pattern. +} + +export const createMockResponse = (): MockResponse => { + const res = { + locals: {}, // Initialize if part of the interface + _headers: {}, // Initialize if getHeader mock relies on it and you want to set headers + } as MockResponse & { _headers?: Record }; + res.status = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); res.send = jest.fn().mockReturnValue(res); - return res; + res.sendStatus = jest.fn().mockReturnValue(res); + res.cookie = jest.fn().mockReturnValue(res); // Initialize if part of the interface + res.clearCookie = jest.fn().mockReturnValue(res); // Initialize if part of the interface + res.setHeader = jest.fn().mockImplementation(function ( + this: typeof res, + key: string, + value: string | string[], + ) { + if (this._headers) { + this._headers[key.toLowerCase()] = value; + } + return this; + }); + res.getHeader = jest.fn().mockImplementation(function (this: typeof res, key: string) { + // Accessing _headers which is intentionally not part of the public MockResponse type + return this._headers ? this._headers[key.toLowerCase()] : undefined; + }); + + return res as MockResponse; // Ensure the final return type is MockResponse }; -export const createMockNext = () => jest.fn(); \ No newline at end of file +export const createMockNext = (): jest.Mock => jest.fn(); From b288a5d76c659edf4eb95a50f91453e3fa28e2c3 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:38:31 +0000 Subject: [PATCH 18/33] Fix formatting in rbacMiddleware test by removing unnecessary newline --- .../authorization/middleware/__tests__/rbacMiddleware.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/authorization/middleware/__tests__/rbacMiddleware.test.ts b/backend/src/authorization/middleware/__tests__/rbacMiddleware.test.ts index ab7f33d..4400cff 100644 --- a/backend/src/authorization/middleware/__tests__/rbacMiddleware.test.ts +++ b/backend/src/authorization/middleware/__tests__/rbacMiddleware.test.ts @@ -28,7 +28,7 @@ describe('RBAC Middleware', () => { id: '123', role: UserRole.ADMIN, // Uses 'role' as per RequestUser }) as Request; - + const res = mockResponse() as Response; const middleware = requirePermissions([ADMIN_PERMISSIONS.READ_USERS]); From 6ae15b3e3b830cd30d78ea27a0b77867370adbbf Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:38:38 +0000 Subject: [PATCH 19/33] Update JWT_SECRET defaults in environment config to match authTestUtils --- backend/src/config/environment.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/config/environment.ts b/backend/src/config/environment.ts index a5e14fb..156c9b7 100644 --- a/backend/src/config/environment.ts +++ b/backend/src/config/environment.ts @@ -19,7 +19,7 @@ const envSchema = z.object({ : z.string(), JWT_SECRET: process.env.NODE_ENV === 'test' - ? z.string().optional().default('test_jwt_secret_schema_default') + ? z.string().optional().default('test-secret') // Changed to match authTestUtils.ts : z.string(), JWT_EXPIRES_IN: z.string().default('15m'), REFRESH_TOKEN_SECRET: @@ -54,10 +54,10 @@ export const config = env.success DATABASE_URL: process.env.DATABASE_URL ?? 'postgresql://postgres:password@localhost:5432/test_db_fallback', - JWT_SECRET: process.env.JWT_SECRET ?? 'test_jwt_secret_fallback', + JWT_SECRET: process.env.JWT_SECRET ?? 'test-secret', // Changed fallback to match JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '15m', REFRESH_TOKEN_SECRET: process.env.REFRESH_TOKEN_SECRET ?? 'test_refresh_secret_fallback', REFRESH_TOKEN_EXPIRES_IN: process.env.REFRESH_TOKEN_EXPIRES_IN ?? '7d', - REDIS_URL: process.env.REDIS_URL ?? undefined, // Ensure REDIS_URL is in the fallback + REDIS_URL: process.env.REDIS_URL ?? undefined, CORS_ORIGIN: process.env.CORS_ORIGIN ?? '*', }; From c6d87b542133312f3e838d78734e81b9110bda5a Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:38:44 +0000 Subject: [PATCH 20/33] Enhance validation and error handling in AuthController; add min length for password, required fields, and optional organizationId in registerSchema --- backend/src/controllers/auth.controller.ts | 35 +++++++++++++--------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 0287d85..455e2bb 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -4,14 +4,15 @@ import { Request, Response, NextFunction } from 'express'; import { z } from 'zod'; import authService from '../services/auth.service'; import { sendSuccess, sendError } from '../utils/response'; -import { errorTypes } from 'src/utils/errors'; +import { errorTypes } from '../utils/errors'; // Changed from 'src/utils/errors' // Validation schemas const registerSchema = z.object({ email: z.string().email(), - password: z.string(), - firstName: z.string(), - lastName: z.string(), + password: z.string().min(8), // Added min length for password + firstName: z.string().min(1), + lastName: z.string().min(1), + organizationId: z.string().uuid().optional(), // Added organizationId, assuming UUID }); const loginSchema = z.object({ @@ -37,20 +38,22 @@ export class AuthController { user_email: validatedData.email, user_password: validatedData.password, user_name: `${validatedData.firstName} ${validatedData.lastName}`, + organizationId: validatedData.organizationId, }; + const user = await authService.register(userData); - sendSuccess(res, user, 201); + sendSuccess(res, { user }, 201); } catch (error) { - if (error instanceof z.ZodError) { + if (error instanceof z.ZodError) { // Handle ZodError specifically return sendError( res, 'Validation error', errorTypes.BAD_REQUEST, 'VALIDATION_ERROR', - error.errors, + error.errors, // Pass Zod issues for detailed error response ); } - next(error); + next(error); // Pass other errors to the global error handler } } @@ -58,13 +61,17 @@ export class AuthController { async login(req: Request, res: Response, next: NextFunction): Promise { try { const { email, password } = loginSchema.parse(req.body); - const { accessToken, user } = await authService.login(email, password); - - // Set refresh token as HttpOnly cookie - //const refreshToken = req.cookies?.refreshToken; + const { accessToken, user, refreshToken } = await authService.login(email, password); - // Send access token in response body - sendSuccess(res, { accessToken, user }); + if (refreshToken) { + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + path: '/api/auth/refresh-token', // Ensure this path is correct + // maxAge: parseDurationToSeconds(config.REFRESH_TOKEN_EXPIRES_IN) * 1000, // Use your helper + }); + } + sendSuccess(res, { user, accessToken }); // Send user and accessToken } catch (error) { if (error instanceof z.ZodError) { return sendError( From 6ce3f65ad8059e1c6ffc1c615c380bfd3272bab8 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:38:53 +0000 Subject: [PATCH 21/33] Enhance error handling in authentication middleware; include specific error codes for authentication required, user not found, and JWT errors --- backend/src/middleware/auth.middleware.ts | 28 +++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/backend/src/middleware/auth.middleware.ts b/backend/src/middleware/auth.middleware.ts index b27dbfd..1905309 100644 --- a/backend/src/middleware/auth.middleware.ts +++ b/backend/src/middleware/auth.middleware.ts @@ -6,6 +6,7 @@ import { verifyAccessToken, extractTokenFromHeader } from '../utils/jwt'; import { AppError, errorTypes } from '../utils/errors'; import { sendError } from '../utils/response'; import rateLimit from 'express-rate-limit'; +import jwt from 'jsonwebtoken'; // Ensure jwt is imported for specific error types const prisma = new PrismaClient(); @@ -31,20 +32,22 @@ export const authenticate = async ( const authHeader = req.headers.authorization; if (!authHeader) { - return sendError(res, 'Authentication required', errorTypes.UNAUTHORIZED); + // Corrected: Pass the errorCode 'AUTH_REQUIRED' + return sendError(res, 'Authentication required', errorTypes.UNAUTHORIZED, 'AUTH_REQUIRED'); } - const token = extractTokenFromHeader(authHeader); - const decoded = verifyAccessToken(token); + const token = extractTokenFromHeader(authHeader); // This might throw if header is malformed + const decoded = verifyAccessToken(token); // This will throw if token is invalid/expired // Check if user exists const user = await prisma.user.findUnique({ where: { id: decoded.userId }, - select: { id: true, user_role: true, user_email: true, organizationId: true }, // Added user_email and organizationId to select + select: { id: true, user_role: true, user_email: true, organizationId: true }, }); if (!user) { - return sendError(res, 'User not found', errorTypes.UNAUTHORIZED); + // Corrected: Pass the errorCode 'USER_NOT_FOUND' + return sendError(res, 'User not found', errorTypes.UNAUTHORIZED, 'USER_NOT_FOUND'); } // Attach user to request @@ -57,10 +60,21 @@ export const authenticate = async ( next(); } catch (error) { + // Log the error for debugging purposes + // console.error('Authentication error details:', error); + if (error instanceof AppError) { - return sendError(res, error.message, error.statusCode); + return sendError(res, error.message, error.statusCode, error.errorCode); + } + // Handle specific JWT errors to provide clearer messages + if (error instanceof jwt.JsonWebTokenError) { + return sendError(res, 'Invalid token', errorTypes.UNAUTHORIZED, 'INVALID_TOKEN'); + } + if (error instanceof jwt.TokenExpiredError) { + return sendError(res, 'Token expired', errorTypes.UNAUTHORIZED, 'TOKEN_EXPIRED'); } - return sendError(res, 'Authentication failed', errorTypes.UNAUTHORIZED); + // Fallback for other unexpected errors + return sendError(res, 'Authentication failed due to an unexpected server error', errorTypes.INTERNAL_SERVER, 'AUTH_UNEXPECTED_ERROR'); } }; From 009cb962677049560348e1b612d9e418a6834973 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:00 +0000 Subject: [PATCH 22/33] Add error handling middleware to manage application errors and log unexpected errors --- backend/src/middleware/error.middleware.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 backend/src/middleware/error.middleware.ts diff --git a/backend/src/middleware/error.middleware.ts b/backend/src/middleware/error.middleware.ts new file mode 100644 index 0000000..999ed9b --- /dev/null +++ b/backend/src/middleware/error.middleware.ts @@ -0,0 +1,22 @@ +// Example of what your src/middleware/error.middleware.ts might contain: +import { Request, Response, NextFunction } from 'express'; +import { AppError } from '../utils/errors'; +import { sendError } from '../utils/response'; // Assuming sendError is in utils/response.ts +import { logger } from '../utils/logger'; // Assuming logger is in utils/logger.ts + +export const errorHandler = ( + err: Error | AppError, + req: Request, + res: Response, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + next: NextFunction, // Express needs this signature for error handlers +): void => { + if (err instanceof AppError) { + logger.warn(`AppError: ${err.statusCode} - ${err.message} - errorCode: ${err.errorCode ?? 'N/A'}`); + sendError(res, err.message, err.statusCode, err.errorCode); + } else { + // Log unexpected errors + logger.error('Unexpected error:', err); + sendError(res, 'An unexpected internal server error occurred.', 500, 'INTERNAL_SERVER_ERROR'); + } +}; \ No newline at end of file From 5458f718d12912ec71e671fabde23e71c54a44e9 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:06 +0000 Subject: [PATCH 23/33] Refactor AuthService to include organizationId in registration and login; add refreshToken to LoginResponse and improve duration parsing logic --- backend/src/services/auth.service.ts | 63 ++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts index 34270db..0f7617b 100644 --- a/backend/src/services/auth.service.ts +++ b/backend/src/services/auth.service.ts @@ -13,20 +13,52 @@ const prisma = new PrismaClient(); const redis = config.REDIS_URL ? new Redis(config.REDIS_URL) : null; interface LoginResponse { - accessToken: string; user: Omit; + accessToken: string; // Corrected: was 'token' which caused an error, should be accessToken + refreshToken: string; // Added refreshToken } +// Ensure RegisterData interface is defined correctly, possibly in a shared types file or here interface RegisterData { + user_name: string; user_email: string; user_password: string; - user_name: string; + organizationId?: string | null; // Added organizationId +} + +// Helper function to parse duration string to seconds +function parseDurationToSeconds(durationStr: string): number { + const regex = /^(\d+)([smhd])$/; + const match = regex.exec(durationStr); + if (!match) { + // Default to 7 days in seconds if parsing fails or format is unexpected + logger.warn(`Invalid duration string: ${durationStr}. Defaulting to 7 days.`); + return 7 * 24 * 60 * 60; + } + + const value = parseInt(match[1]); + const unit = match[2]; + + switch (unit) { + case 's': + return value; + case 'm': + return value * 60; + case 'h': + return value * 60 * 60; + case 'd': + return value * 24 * 60 * 60; + default: + // Should not happen due to regex, but as a fallback + logger.warn(`Unknown duration unit: ${unit} in ${durationStr}. Defaulting to 7 days.`); + return 7 * 24 * 60 * 60; + } } export class AuthService { // User registration async register(userData: RegisterData): Promise> { - const { user_email, user_password, user_name } = userData; + const { user_email, user_password, user_name, organizationId } = userData; // Check if user already exists const existingUser = await prisma.user.findFirst({ @@ -40,6 +72,15 @@ export class AuthService { // Hash password const hashedPassword = await hashPassword(user_password); + if (organizationId) { + const organization = await prisma.organization.findUnique({ + where: { id: organizationId }, + }); + if (!organization) { + throw new AppError('Organization not found', errorTypes.BAD_REQUEST); + } + } + // Create new user const newUser = await prisma.user.create({ data: { @@ -47,6 +88,7 @@ export class AuthService { user_password: hashedPassword, user_name, user_role: 'USER', // Default role + organizationId: organizationId ?? undefined, // Use undefined if schema expects optional, or null if nullable }, }); @@ -75,18 +117,19 @@ export class AuthService { } // Generate tokens - const tokenPayload = { userId: user.id, role: user.user_role }; + const tokenPayload = { userId: user.id, role: user.user_role, organizationId: user.organizationId }; const accessToken = generateAccessToken(tokenPayload); - const refreshToken = generateRefreshToken(tokenPayload); + const refreshTokenValue = generateRefreshToken(tokenPayload); // Renamed to avoid conflict // Store refresh token in Redis if available if (redis) { const tokenId = uuidv4(); + const expirySeconds = parseDurationToSeconds(config.REFRESH_TOKEN_EXPIRES_IN); await redis.set( `refresh_token:${user.id}:${tokenId}`, - refreshToken, + refreshTokenValue, 'EX', - 60 * 60 * 24 * 7, // 7 days + expirySeconds, ); } @@ -94,7 +137,7 @@ export class AuthService { const { user_password: _, ...userWithoutPassword } = user; logger.info(`User logged in: ${email}`); - return { accessToken, user: userWithoutPassword }; + return { user: userWithoutPassword, accessToken: accessToken, refreshToken: refreshTokenValue }; } // Refresh access token @@ -130,11 +173,13 @@ export class AuthService { async logout(userId: string, refreshToken: string): Promise { if (redis) { // Add refresh token to blacklist until its expiration + // Use the same parsing logic for consistency if REFRESH_TOKEN_EXPIRES_IN is used for blacklist duration + const blacklistExpirySeconds = parseDurationToSeconds(config.REFRESH_TOKEN_EXPIRES_IN); await redis.set( `blacklist:${refreshToken}`, '1', 'EX', - 60 * 60 * 24 * 7, // 7 days + blacklistExpirySeconds, ); // Remove all refresh tokens for this user (optional, for complete logout) From b699b79a23cf9a4ba5c5c5b264baba3264814339 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:13 +0000 Subject: [PATCH 24/33] Refactor permission tests to use constants for permissions and improve organization handling in admin tests --- .../authorization/permissionChecker.test.ts | 64 ++++++------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/backend/src/tests/authorization/permissionChecker.test.ts b/backend/src/tests/authorization/permissionChecker.test.ts index 77684db..6db74fc 100644 --- a/backend/src/tests/authorization/permissionChecker.test.ts +++ b/backend/src/tests/authorization/permissionChecker.test.ts @@ -4,53 +4,41 @@ import { hasAnyPermission, getUserPermissions, } from '../../authorization/utils/permissionChecker'; -import { createTestUser } from '../utils/authTestUtils'; +import { createTestUser, createTestOrganization } from '../utils/authTestUtils'; +import { USER_PERMISSIONS, ADMIN_PERMISSIONS } from '../../authorization/constants/permissions'; // Import constants import { UserRole } from '@prisma/client'; describe('Permission Checker', () => { describe('hasPermission', () => { test('should return true for valid user permission', async () => { const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-perm-${Date.now()}@example.com`, user_role: UserRole.USER, }); - - const authUser = { - ...testUser, - user_role: testUser.user_role, - }; - - const result = hasPermission(authUser, 'READ_SELF'); + const authUser = { ...testUser, user_role: testUser.user_role }; + const result = hasPermission(authUser, USER_PERMISSIONS.READ_SELF); expect(result).toBe(true); }); test('should return false for invalid user permission', async () => { const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-invalid-perm-${Date.now()}@example.com`, user_role: UserRole.USER, }); - - const authUser = { - ...testUser, - user_role: testUser.user_role, - }; - - const result = hasPermission(authUser, 'DELETE_USERS'); + const authUser = { ...testUser, user_role: testUser.user_role }; + const result = hasPermission(authUser, ADMIN_PERMISSIONS.DELETE_USERS); expect(result).toBe(false); }); test('should return true for admin permission', async () => { + const org = await createTestOrganization(`Org-AdminPerm-${Date.now()}`); const testUser = await createTestUser({ - user_email: 'admin@example.com', + user_email: `admin-perm-${Date.now()}@example.com`, user_role: UserRole.ADMIN, + organizationId: org.id, }); - - const authUser = { - ...testUser, - user_role: testUser.user_role, - }; - - const result = hasPermission(authUser, 'READ_USERS'); + const authUser = { ...testUser, user_role: testUser.user_role, organizationId: org.id }; + const result = hasPermission(authUser, ADMIN_PERMISSIONS.READ_USERS); expect(result).toBe(true); }); }); @@ -58,16 +46,11 @@ describe('Permission Checker', () => { describe('hasAnyPermission', () => { test('should return true if user has any of the permissions', async () => { const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-anyperm-${Date.now()}@example.com`, user_role: UserRole.USER, }); - - const authUser = { - ...testUser, - user_role: testUser.user_role, - }; - - const result = hasAnyPermission(authUser, ['READ_SELF', 'DELETE_USERS']); + const authUser = { ...testUser, user_role: testUser.user_role }; + const result = hasAnyPermission(authUser, [USER_PERMISSIONS.READ_SELF, ADMIN_PERMISSIONS.DELETE_USERS]); expect(result).toBe(true); }); }); @@ -75,19 +58,14 @@ describe('Permission Checker', () => { describe('getUserPermissions', () => { test('should return correct permissions for user role', async () => { const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-getperm-${Date.now()}@example.com`, user_role: UserRole.USER, }); - - const authUser = { - ...testUser, - user_role: testUser.user_role, - }; - + const authUser = { ...testUser, user_role: testUser.user_role }; const permissions = getUserPermissions(authUser); - expect(permissions).toContain('READ_SELF'); - expect(permissions).toContain('CREATE_BOOKING'); - expect(permissions).not.toContain('DELETE_USERS'); + expect(permissions).toContain(USER_PERMISSIONS.READ_SELF); + expect(permissions).toContain(USER_PERMISSIONS.CREATE_BOOKING); + expect(permissions).not.toContain(ADMIN_PERMISSIONS.DELETE_USERS); }); }); }); From 48ac3606c3dbfcd67e8b3a88104e62c1466551c1 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:19 +0000 Subject: [PATCH 25/33] Refactor authentication middleware tests to improve organization handling and add user not found scenario --- .../tests/middleware/auth.middleware.test.ts | 86 +++++++++++-------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/backend/src/tests/middleware/auth.middleware.test.ts b/backend/src/tests/middleware/auth.middleware.test.ts index 95b7aae..2dbabb9 100644 --- a/backend/src/tests/middleware/auth.middleware.test.ts +++ b/backend/src/tests/middleware/auth.middleware.test.ts @@ -6,40 +6,44 @@ import { generateTestToken, createMockResponse, createMockNext, - MockResponse, // Import your MockResponse type + MockResponse, + createTestOrganization // Import your MockResponse type } from '../utils/authTestUtils'; -import { UserRole } from '@prisma/client'; +import { UserRole, Organization } from '@prisma/client'; // Import Organization describe('Authentication Middleware', () => { let mockReq: Partial; - let mockRes: MockResponse; // Use MockResponse type + let mockRes: MockResponse; let mockNext: jest.Mock; + let testOrg: Organization; // To hold created organization - beforeEach(() => { + beforeEach(async () => { mockReq = {}; - mockRes = createMockResponse(); // Assigns MockResponse to MockResponse + mockRes = createMockResponse(); mockNext = createMockNext(); + // Create a common organization for tests in this suite + testOrg = await createTestOrganization(`AuthMiddlewareTestOrg-${Date.now()}`); }); describe('authenticate middleware', () => { test('should authenticate valid JWT token', async () => { const testUser = await createTestUser({ - user_email: 'test@example.com', + user_email: `auth-mid-user-${Date.now()}@example.com`, user_role: UserRole.USER, + organizationId: testOrg.id, // Provide organizationId }); - const token = generateTestToken(testUser); mockReq.headers = { authorization: `Bearer ${token}` }; - // Cast mockRes when passing to the middleware await authenticate(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); expect(mockNext).toHaveBeenCalled(); expect(mockReq.user).toBeDefined(); expect(mockReq.user?.id).toBe(testUser.id); + expect(mockReq.user?.organizationId).toBe(testOrg.id); }); - test('should reject request without token', async () => { + test('should return 401 if no token is provided', async () => { mockReq.headers = {}; await authenticate(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); @@ -48,7 +52,7 @@ describe('Authentication Middleware', () => { expect(mockNext).not.toHaveBeenCalled(); }); - test('should reject invalid token', async () => { + test('should return 401 for invalid token', async () => { mockReq.headers = { authorization: 'Bearer invalid-token' }; await authenticate(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); @@ -56,45 +60,57 @@ describe('Authentication Middleware', () => { expect(mockRes.status).toHaveBeenCalledWith(401); // Assert on MockResponse expect(mockNext).not.toHaveBeenCalled(); }); + + test('should return 401 if user not found', async () => { + // Create a user payload for a token, but don't create the user in DB + const nonExistentUserPayload = { + id: 'non-existent-user-id', + user_email: `ghost-${Date.now()}@example.com`, + user_role: UserRole.USER, + organizationId: testOrg.id, // Still need a valid orgId for token structure if it's included + }; + const token = generateTestToken(nonExistentUserPayload as any); // Cast if TestUser type is strict + mockReq.headers = { authorization: `Bearer ${token}` }; + + await authenticate(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); + expect(mockNext).not.toHaveBeenCalled(); + expect(mockRes.status).toHaveBeenCalledWith(401); + // Adjust to match the actual error response structure from sendError + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: expect.objectContaining({ + message: 'User not found', + code: 'USER_NOT_FOUND', + }), + }); + }); }); describe('checkRole middleware', () => { test('should allow access for correct role', async () => { - const testUser = await createTestUser({ - user_email: 'admin@example.com', + const adminUser = await createTestUser({ + user_email: `auth-mid-admin-${Date.now()}@example.com`, user_role: UserRole.ADMIN, + organizationId: testOrg.id, // Provide organizationId }); + mockReq.user = { id: adminUser.id, role: adminUser.user_role, organizationId: adminUser.organizationId }; + const adminOnlyMiddleware = checkRole([UserRole.ADMIN]); - mockReq.user = { - id: testUser.id, - email: testUser.user_email, - role: testUser.user_role, - organizationId: testUser.organizationId, - }; - - const roleMiddleware = checkRole([UserRole.ADMIN]); - roleMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); - + adminOnlyMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); expect(mockNext).toHaveBeenCalled(); }); test('should reject access for incorrect role', async () => { - const testUser = await createTestUser({ - user_email: 'user@example.com', + const regularUser = await createTestUser({ + user_email: `auth-mid-norole-${Date.now()}@example.com`, user_role: UserRole.USER, + organizationId: testOrg.id, // Provide organizationId }); + mockReq.user = { id: regularUser.id, role: regularUser.user_role, organizationId: regularUser.organizationId }; + const adminOnlyMiddleware = checkRole([UserRole.ADMIN]); - mockReq.user = { - id: testUser.id, - email: testUser.user_email, - role: testUser.user_role, - organizationId: testUser.organizationId, - }; - - const roleMiddleware = checkRole([UserRole.ADMIN]); - roleMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); - - expect(mockRes.status).toHaveBeenCalledWith(403); // Assert on MockResponse + adminOnlyMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); + expect(mockRes.status).toHaveBeenCalledWith(403); // Or 403 for Forbidden expect(mockNext).not.toHaveBeenCalled(); }); }); From 13c203d7750068833be48cda7237e65ce7811ab8 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:26 +0000 Subject: [PATCH 26/33] Refactor RBAC middleware tests to improve organization handling and ensure unique user emails --- .../tests/middleware/rbacMiddleware.test.ts | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/backend/src/tests/middleware/rbacMiddleware.test.ts b/backend/src/tests/middleware/rbacMiddleware.test.ts index 89b277d..16cb7d3 100644 --- a/backend/src/tests/middleware/rbacMiddleware.test.ts +++ b/backend/src/tests/middleware/rbacMiddleware.test.ts @@ -11,27 +11,32 @@ import { createMockNext, MockResponse, // Import your MockResponse type } from '../utils/authTestUtils'; -import { UserRole } from '@prisma/client'; -// Import ALL_PERMISSIONS for runtime values, Permission can remain for type annotations if needed +import { UserRole, Organization } from '@prisma/client'; // Add Organization type import { ALL_PERMISSIONS } from '../../authorization/constants/permissions'; describe('RBAC Middleware', () => { let mockReq: Partial; - let mockRes: MockResponse; // Use MockResponse type + let mockRes: MockResponse; let mockNext: jest.Mock; - - beforeEach(async () => { + // Outer beforeEach for common mocks + beforeEach(() => { mockReq = {}; - mockRes = createMockResponse(); // Assigns MockResponse to MockResponse + mockRes = createMockResponse(); mockNext = createMockNext(); }); describe('requirePermissions middleware', () => { + let org: Organization; // Define org here + + beforeEach(async () => { // This beforeEach is specific to this describe block + org = await createTestOrganization('Test Org For Permissions'); + }); + test('should allow access with required permissions', async () => { const testUser = await createTestUser({ - user_email: 'admin@example.com', + user_email: `admin-permissions-${Date.now()}@example.com`, // Ensure unique email user_role: UserRole.ADMIN, - organizationId: 'org1', + organizationId: org.id, // Use the created org's ID }); mockReq.user = { @@ -40,19 +45,18 @@ describe('RBAC Middleware', () => { organizationId: testUser.organizationId, }; - // Use ALL_PERMISSIONS for the actual permission string value const permissionMiddleware = requirePermissions([ALL_PERMISSIONS.READ_USERS]); - // Cast mockRes when passing to the middleware permissionMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); expect(mockNext).toHaveBeenCalled(); + expect(mockNext).not.toHaveBeenCalledWith(expect.any(Error)); }); test('should deny access without required permissions', async () => { const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-permissions-${Date.now()}@example.com`, // Ensure unique email user_role: UserRole.USER, - organizationId: 'org1', + organizationId: org.id, // Use the created org's ID }); mockReq.user = { @@ -61,9 +65,7 @@ describe('RBAC Middleware', () => { organizationId: testUser.organizationId, }; - // Use ALL_PERMISSIONS for the actual permission string value const permissionMiddleware = requirePermissions([ALL_PERMISSIONS.READ_USERS]); - // Cast mockRes when passing to the middleware permissionMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); expect(mockNext).toHaveBeenCalledWith(expect.any(Error)); @@ -71,15 +73,16 @@ describe('RBAC Middleware', () => { }); describe('requireSameOrganization middleware', () => { - let organization: { id: string }; + let organization: Organization; // Use Organization type beforeEach(async () => { - organization = await createTestOrganization(); + // This organization is created for each test within this describe block + organization = await createTestOrganization(`Test Org Same Org ${Date.now()}`); }); test('should allow access if user is in the same organization as target in params', async () => { const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-same-org-${Date.now()}@example.com`, user_role: UserRole.USER, organizationId: organization.id, }); @@ -100,11 +103,11 @@ describe('RBAC Middleware', () => { }); test('should deny access if user is in a different organization', async () => { - const otherOrganization = await createTestOrganization('Other Org'); + const otherOrganization = await createTestOrganization(`Other Org ${Date.now()}`); const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-diff-org-${Date.now()}@example.com`, user_role: UserRole.USER, - organizationId: organization.id, + organizationId: organization.id, // User belongs to 'organization' }); mockReq.user = { @@ -128,7 +131,7 @@ describe('RBAC Middleware', () => { test('should deny access if target organizationId is missing from params', async () => { const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-no-param-org-${Date.now()}@example.com`, user_role: UserRole.USER, organizationId: organization.id, }); @@ -154,10 +157,11 @@ describe('RBAC Middleware', () => { }); test('should deny access if authenticated user has no organizationId', async () => { + // Assuming User.organizationId is nullable. If not, this test needs adjustment. const testUser = await createTestUser({ - user_email: 'user@example.com', + user_email: `user-no-orgid-${Date.now()}@example.com`, user_role: UserRole.USER, - organizationId: undefined, + organizationId: null, // Explicitly set to null if schema allows }); mockReq.user = { From 23841776b5fefc4f09d673d03f1813247ab18e34 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:33 +0000 Subject: [PATCH 27/33] Refactor auth routes tests to improve organization handling, enhance user data structure, and ensure proper error handling for registration and login processes. --- backend/src/tests/routes/auth.routes.test.ts | 110 +++++++++++++------ 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/backend/src/tests/routes/auth.routes.test.ts b/backend/src/tests/routes/auth.routes.test.ts index 21e14c9..a3715d9 100644 --- a/backend/src/tests/routes/auth.routes.test.ts +++ b/backend/src/tests/routes/auth.routes.test.ts @@ -2,37 +2,40 @@ import { describe, test, expect } from '@jest/globals'; import request from 'supertest'; import express from 'express'; import authRoutes from '../../routes/auth.routes'; -import { createTestUser, createTestOrganization } from '../utils/authTestUtils'; -import { UserRole } from '@prisma/client'; +import { createTestOrganization } from '../utils/authTestUtils'; +import { errorHandler } from '../../middleware/error.middleware'; // Assuming this is your error handler const app = express(); app.use(express.json()); app.use('/auth', authRoutes); +app.use(errorHandler); // Use your imported error handling middleware describe('Auth Routes Integration Tests', () => { + const complexPassword = 'Password123!'; // Meets complexity + describe('POST /auth/register', () => { test('should register new user successfully', async () => { - const organization = await createTestOrganization(); - + const organization = await createTestOrganization(`Org-Reg-Route-${Date.now()}`); const userData = { - user_name: 'Test User', - user_email: 'newuser@example.com', - user_password: 'password123', + firstName: 'Test', + lastName: 'User Route', + email: `newuser-route-${Date.now()}@example.com`, + password: complexPassword, // Use complex password organizationId: organization.id, }; const response = await request(app).post('/auth/register').send(userData).expect(201); - expect(response.body.user).toBeDefined(); - expect(response.body.token).toBeDefined(); - expect(response.body.user.user_email).toBe(userData.user_email); + expect(response.body.data.user).toBeDefined(); + expect(response.body.data.user.user_email).toBe(userData.email); }); test('should reject registration with invalid email', async () => { const userData = { - user_name: 'Test User', - user_email: 'invalid-email', - user_password: 'password123', + firstName: 'Test', + lastName: 'User', + email: 'invalid-email', // This will be caught by Zod + password: complexPassword, // Use complex password }; await request(app).post('/auth/register').send(userData).expect(400); @@ -41,27 +44,52 @@ describe('Auth Routes Integration Tests', () => { describe('POST /auth/login', () => { test('should login with valid credentials', async () => { - const testUser = await createTestUser({ - user_email: 'login@example.com', - user_role: UserRole.USER, - }); + const org = await createTestOrganization(`Org-Login-Route-${Date.now()}`); + const userEmail = `login-route-${Date.now()}@example.com`; + // const userPassword = 'password123Secure'; // Old password + + // Register the user first + await request(app).post('/auth/register').send({ + firstName: 'Login', + lastName: 'Test', + email: userEmail, + password: complexPassword, // Use complex password + organizationId: org.id, + }).expect(201); + const loginData = { - email: testUser.user_email, - password: 'testpassword123', + email: userEmail, + password: complexPassword, // Use complex password }; const response = await request(app).post('/auth/login').send(loginData).expect(200); - expect(response.body.user).toBeDefined(); - expect(response.body.token).toBeDefined(); - expect(response.body.user.id).toBe(testUser.id); + expect(response.body.data.user).toBeDefined(); + expect(response.body.data.accessToken).toBeDefined(); + expect(response.body.data.user.user_email).toBe(loginData.email); + // Check for HttpOnly cookie for refreshToken + const setCookieHeader = response.headers['set-cookie']; + expect(setCookieHeader).toBeDefined(); + + let cookieArray: string[]; + if (typeof setCookieHeader === 'string') { + cookieArray = [setCookieHeader]; + } else if (Array.isArray(setCookieHeader)) { + cookieArray = setCookieHeader; + } else { + // This case should ideally not be reached if expect(setCookieHeader).toBeDefined() passes + // and set-cookie is either string or string[] + cookieArray = []; + } + + expect(cookieArray.some((cookie: string) => cookie.startsWith('refreshToken='))).toBe(true); }); test('should reject login with invalid credentials', async () => { const loginData = { email: 'nonexistent@example.com', - password: 'wrongpassword', + password: 'WrongPassword1!', // Use a complex but wrong password }; await request(app).post('/auth/login').send(loginData).expect(401); @@ -70,21 +98,37 @@ describe('Auth Routes Integration Tests', () => { describe('POST /auth/logout', () => { test('should logout authenticated user', async () => { - const testUser = await createTestUser({ - user_email: 'logout@example.com', - user_role: UserRole.USER, - }); + const org = await createTestOrganization(`Org-Logout-Route-${Date.now()}`); + const userEmail = `logout-route-${Date.now()}@example.com`; + // const userPassword = 'password123Secure'; // Old password + + // Register user + await request(app).post('/auth/register').send({ + firstName: 'Logout', + lastName: 'Test', + email: userEmail, + password: complexPassword, // Use complex password + organizationId: org.id, + }).expect(201); - // First login to get token + + // Login to get token and cookies const loginResponse = await request(app).post('/auth/login').send({ - email: testUser.user_email, - password: 'testpassword123', + email: userEmail, + password: complexPassword, // Use complex password }); - const token = loginResponse.body.token; + const token = loginResponse.body.data.accessToken; + expect(token).toBeDefined(); + + const agent = request.agent(app); + // Login with agent to establish session and cookies for the agent + await agent.post('/auth/login').send({ email: userEmail, password: complexPassword }); - // Then logout - await request(app).post('/auth/logout').set('Authorization', `Bearer ${token}`).expect(200); + await agent + .post('/auth/logout') + .set('Authorization', `Bearer ${token}`) + .expect(200); }); test('should reject logout without authentication', async () => { From 27a9a484e8273510cb9f7a0f3a25f3bab62913c7 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:38 +0000 Subject: [PATCH 28/33] Refactor test setup to improve logger handling, enhance database connection logic, and ensure proper cleanup of test data. --- backend/src/tests/setup/testSetup.ts | 78 +++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/backend/src/tests/setup/testSetup.ts b/backend/src/tests/setup/testSetup.ts index 1d6bea8..1a76eac 100644 --- a/backend/src/tests/setup/testSetup.ts +++ b/backend/src/tests/setup/testSetup.ts @@ -1,5 +1,20 @@ import { PrismaClient } from '@prisma/client'; import { beforeAll, afterAll, beforeEach } from '@jest/globals'; +import originalLogger from '../../utils/logger'; // Assuming logger is available + +// Attempt to create a logger mock/fallback if the real one isn't test-friendly or available +let logger = originalLogger; +if (!logger || typeof logger.info !== 'function') { + console.warn("Original logger not found or not standard in testSetup.ts, using console fallback."); + logger = { + info: console.log, + warn: console.warn, + error: console.error, + debug: console.log, + // Add other methods if your logger interface has them and they are called + } as any; // Use 'as any' for simplicity if logger structure varies +} + const prisma = new PrismaClient({ datasources: { @@ -7,28 +22,65 @@ const prisma = new PrismaClient({ url: process.env.DATABASE_URL_TEST ?? process.env.DATABASE_URL, }, }, + // log: ['query', 'info', 'warn', 'error'], // Optional: for more detailed logs during tests }); +let isConnected = false; + beforeAll(async () => { - // Connect to test database - await prisma.$connect(); + // Only attempt to connect if DATABASE_URL_TEST is explicitly set, + // indicating a dedicated test database environment is intended. + if (process.env.DATABASE_URL_TEST) { + try { + await prisma.$connect(); + isConnected = true; + logger.info('Successfully connected to test database (DATABASE_URL_TEST).'); + } catch (error) { + isConnected = false; + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error( + `Failed to connect to test database (DATABASE_URL_TEST) in global setup: ${errorMessage}. Integration tests requiring DB will likely fail.` + ); + // Do not rethrow here, to allow tests that mock Prisma to run. + } + } else { + logger.warn( + 'DATABASE_URL_TEST is not set. Global Prisma client in testSetup.ts will not attempt to connect. ' + + 'Database-dependent integration tests should ensure their own DB setup or use mocks. ' + + 'Operations in authTestUtils using this global prisma instance may fail if they expect a connection.' + ); + } }); afterAll(async () => { - // Clean up and disconnect - await prisma.$disconnect(); + if (isConnected) { + await prisma.$disconnect(); + isConnected = false; // Reset status + logger.info('Disconnected from test database.'); + } }); beforeEach(async () => { - // Clean up database before each test - await prisma.booking.deleteMany(); - await prisma.waitlist.deleteMany(); - await prisma.timeSlot.deleteMany(); - await prisma.lab.deleteMany(); - await prisma.user.deleteMany(); - await prisma.admin.deleteMany(); - await prisma.superAdmin.deleteMany(); - await prisma.organization.deleteMany(); + if (isConnected) { + // Clean up database before each test only if connected to a test DB + try { + // Order of deletion matters due to foreign key constraints + // Adjust order as per your actual schema dependencies + await prisma.booking.deleteMany({}); + await prisma.waitlist.deleteMany({}); + await prisma.notification.deleteMany({}); + await prisma.timeSlot.deleteMany({}); + await prisma.lab.deleteMany({}); + await prisma.admin.deleteMany({}); + await prisma.superAdmin.deleteMany({}); + await prisma.user.deleteMany({}); + await prisma.organizationNotification.deleteMany({}); + await prisma.organization.deleteMany({}); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Failed to clean test database in beforeEach: ${errorMessage}`); + } + } }); export { prisma }; From bf4c7fc40089bc157dd4a765e5ea0a4fa21e533e Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:49 +0000 Subject: [PATCH 29/33] Refactor auth test utilities to remove direct PrismaClient instantiation, enhance organization of mock response, and ensure all required fields are present in test data. --- backend/src/tests/utils/authTestUtils.ts | 46 ++++++++++-------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/backend/src/tests/utils/authTestUtils.ts b/backend/src/tests/utils/authTestUtils.ts index 30ea79b..790f129 100644 --- a/backend/src/tests/utils/authTestUtils.ts +++ b/backend/src/tests/utils/authTestUtils.ts @@ -1,9 +1,8 @@ import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; -import { UserRole, PrismaClient, Organization } from '@prisma/client'; -import { Request, CookieOptions } from 'express'; // Alias ExpressResponse, Import CookieOptions - -const prisma = new PrismaClient(); +import { UserRole, Organization } from '@prisma/client'; // PrismaClient removed +import { Request, CookieOptions } from 'express'; // Import CookieOptions +import { prisma } from '../setup/testSetup'; // Import the shared prisma instance export interface TestUser { id: string; @@ -26,6 +25,7 @@ export const createTestUser = async ( ): Promise => { const hashedPassword = await bcrypt.hash('testpassword123', 10); + // Use the imported prisma client const user = await prisma.user.create({ data: { user_name: userData.user_name ?? 'Test User', @@ -35,18 +35,18 @@ export const createTestUser = async ( organizationId: userData.organizationId, }, }); - return user; }; export const createTestOrganization = async ( name: string = 'Test Organization', ): Promise => { + // Use the imported prisma client return await prisma.organization.create({ data: { org_name: name, org_type: 'Educational', - org_location: 'Default Location', + org_location: 'Default Location', // Ensure all required fields are present }, }); }; @@ -79,52 +79,42 @@ export const createAuthenticatedRequest = (user: TestUser): Partial => }; }; -// Define a more specific type for the mock response -// It should implement the parts of Express.Response that are actually used in tests. export interface MockResponse { status: jest.Mock; json: jest.Mock; send: jest.Mock; sendStatus: jest.Mock; - // Add other Response properties/methods if your tests use them - // For example, if you use res.locals, res.cookie, etc. - locals: Record; // Changed any to unknown - cookie: jest.Mock; // Used CookieOptions - clearCookie: jest.Mock; // Used CookieOptions - setHeader: jest.Mock; // Example - getHeader: jest.Mock; // Example - // Ensure all methods from Express.Response that are used are explicitly defined - // or ensure that the methods return `this` for chaining if that's the pattern. + locals: Record; + cookie: jest.Mock; + clearCookie: jest.Mock; + setHeader: jest.Mock; + getHeader: jest.Mock; } export const createMockResponse = (): MockResponse => { const res = { - locals: {}, // Initialize if part of the interface - _headers: {}, // Initialize if getHeader mock relies on it and you want to set headers + locals: {}, + _headers: {}, } as MockResponse & { _headers?: Record }; res.status = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); res.send = jest.fn().mockReturnValue(res); res.sendStatus = jest.fn().mockReturnValue(res); - res.cookie = jest.fn().mockReturnValue(res); // Initialize if part of the interface - res.clearCookie = jest.fn().mockReturnValue(res); // Initialize if part of the interface - res.setHeader = jest.fn().mockImplementation(function ( - this: typeof res, - key: string, - value: string | string[], - ) { + res.cookie = jest.fn().mockReturnValue(res); + res.clearCookie = jest.fn().mockReturnValue(res); + res.setHeader = jest.fn().mockImplementation(function (this: typeof res, key: string, value: string | string[]) { if (this._headers) { this._headers[key.toLowerCase()] = value; } return this; }); res.getHeader = jest.fn().mockImplementation(function (this: typeof res, key: string) { - // Accessing _headers which is intentionally not part of the public MockResponse type + // @ts-ignore: Accessing _headers which is intentionally not part of the public MockResponse type return this._headers ? this._headers[key.toLowerCase()] : undefined; }); - return res as MockResponse; // Ensure the final return type is MockResponse + return res as MockResponse; }; export const createMockNext = (): jest.Mock => jest.fn(); From a2fd7f3c537b54cdf068aea8a5e84347f09940d6 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:39:56 +0000 Subject: [PATCH 30/33] Enhance error handling by adding optional errorCode parameter to AppError class and export logger instance for improved accessibility --- backend/src/utils/errors.ts | 4 +++- backend/src/utils/logger.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index a13e184..49387d1 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -2,11 +2,13 @@ export class AppError extends Error { statusCode: number; isOperational: boolean; + errorCode?: string; // Added errorCode - constructor(message: string, statusCode: number) { + constructor(message: string, statusCode: number, errorCode?: string) { // Added errorCode super(message); this.statusCode = statusCode; this.isOperational = true; + this.errorCode = errorCode; // Store errorCode Error.captureStackTrace(this, this.constructor); } diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index 6c1de3b..1500b72 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -33,7 +33,7 @@ if (config.NODE_ENV !== 'test') { } } -const logger = winston.createLogger({ +export const logger = winston.createLogger({ level: config.LOG_LEVEL, format: winston.format.combine( winston.format.timestamp(), From 3db41eb0de8f4033ee362d88e24688742c6e6fe9 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:51:34 +0000 Subject: [PATCH 31/33] Refactor error handling and improve organization in middleware and tests; add errorCode to AppError class for enhanced error reporting --- .../__tests__/rbacMiddleware.test.ts | 2 +- .../middleware/resourceAccessMiddleware.ts | 150 ++++++++---------- backend/src/controllers/auth.controller.ts | 3 +- backend/src/middleware/auth.middleware.ts | 7 +- backend/src/middleware/error.middleware.ts | 6 +- backend/src/services/auth.service.ts | 13 +- .../authorization/permissionChecker.test.ts | 5 +- .../tests/middleware/auth.middleware.test.ts | 23 ++- .../tests/middleware/rbacMiddleware.test.ts | 3 +- backend/src/tests/routes/auth.routes.test.ts | 41 ++--- backend/src/tests/setup/testSetup.ts | 36 +++-- backend/src/tests/utils/authTestUtils.ts | 7 +- backend/src/utils/errors.ts | 3 +- 13 files changed, 165 insertions(+), 134 deletions(-) diff --git a/backend/src/authorization/middleware/__tests__/rbacMiddleware.test.ts b/backend/src/authorization/middleware/__tests__/rbacMiddleware.test.ts index 4400cff..ab7f33d 100644 --- a/backend/src/authorization/middleware/__tests__/rbacMiddleware.test.ts +++ b/backend/src/authorization/middleware/__tests__/rbacMiddleware.test.ts @@ -28,7 +28,7 @@ describe('RBAC Middleware', () => { id: '123', role: UserRole.ADMIN, // Uses 'role' as per RequestUser }) as Request; - + const res = mockResponse() as Response; const middleware = requirePermissions([ADMIN_PERMISSIONS.READ_USERS]); diff --git a/backend/src/authorization/middleware/resourceAccessMiddleware.ts b/backend/src/authorization/middleware/resourceAccessMiddleware.ts index 5f398fb..2b895fa 100644 --- a/backend/src/authorization/middleware/resourceAccessMiddleware.ts +++ b/backend/src/authorization/middleware/resourceAccessMiddleware.ts @@ -93,13 +93,77 @@ export const requireResourceAccess = ( }; }; +// Helper function to get organization ID for a resource +const getResourceOrganizationId = async ( + resourceType: ResourceType, + resourceId: string, +): Promise => { + switch (resourceType) { + case ResourceType.USER: { + const resourceUser = await prisma.user.findUnique({ where: { id: resourceId } }); + return resourceUser?.organizationId; + } + case ResourceType.ADMIN: { + const admin = await prisma.admin.findUnique({ where: { id: resourceId } }); + // Assuming Admin model might have an optional organizationId. + // Adjust if Admin model definitely doesn't have it or has a different relation. + return admin && 'organizationId' in admin + ? (admin as Admin & { organizationId?: string | null }).organizationId + : null; // Or undefined, depending on how you want to treat admins without orgs + } + case ResourceType.LAB: { + const lab = await prisma.lab.findUnique({ where: { id: resourceId } }); + return lab?.organizationId; + } + case ResourceType.TIME_SLOT: { + const timeSlot = await prisma.timeSlot.findUnique({ + where: { id: resourceId }, + include: { lab: true }, + }); + return timeSlot?.lab?.organizationId; + } + case ResourceType.BOOKING: { + const booking = await prisma.booking.findUnique({ + where: { id: resourceId }, + include: { timeSlot: { include: { lab: true } } }, + }); + return booking?.timeSlot?.lab?.organizationId; + } + case ResourceType.WAITLIST: { + const waitlist = await prisma.waitlist.findUnique({ + where: { id: resourceId }, + include: { timeSlot: { include: { lab: true } } }, + }); + return waitlist?.timeSlot?.lab?.organizationId; + } + case ResourceType.NOTIFICATION: { + const notification = await prisma.notification.findUnique({ + where: { id: resourceId }, + include: { user: true }, + }); + return notification?.user?.organizationId; + } + case ResourceType.ORGANIZATION: { + return resourceId; // The resourceId itself is the organizationId + } + default: { + // This should be caught by TypeScript if all ResourceType cases are handled. + // If new types are added without updating this, it will throw at runtime. + const exhaustiveCheck: never = resourceType; + throw new HttpException( + 500, + `Organization check not implemented for resource type: ${exhaustiveCheck}`, + ); + } + } +}; + export const requireSameOrganization = ( resourceType: ResourceType, options: ResourceAccessOptions = {}, ) => { return async (req: Request, res: Response, next: NextFunction): Promise => { - // Added return type Promise - const user = req.user as RequestUser | undefined; // Cast is okay here after check + const user = req.user as RequestUser | undefined; if (!user) { return next(new HttpException(401, 'Authentication required')); @@ -126,84 +190,10 @@ export const requireSameOrganization = ( } try { - let resourceOrganizationId: string | null | undefined = null; - - switch (resourceType) { - case ResourceType.USER: { - const resourceUser = await prisma.user.findUnique({ where: { id: resourceId } }); - resourceOrganizationId = resourceUser?.organizationId; - break; - } - case ResourceType.ADMIN: { - const admin: Admin | null = await prisma.admin.findUnique({ where: { id: resourceId } }); - // Assuming Admin model might have an optional organizationId or a relation to it. - // If Admin model *definitely* doesn't have it, this case needs rethinking. - // For now, let's assume it *could* have it, making it type-safe. - // You'll need to define organizationId on the Admin model in schema.prisma for this to be truly effective. - if (admin && 'organizationId' in admin) { - resourceOrganizationId = (admin as Admin & { organizationId?: string | null }) - .organizationId; - } else { - // Handle case where admin is found but has no organizationId property, - // or if Admins are not organization-specific. - // This might mean access is allowed, or denied, or this check is not applicable. - // For now, if no organizationId, it won't match userOrganizationId unless both are null/undefined. - } - break; - } - case ResourceType.LAB: { - const lab = await prisma.lab.findUnique({ where: { id: resourceId } }); - resourceOrganizationId = lab?.organizationId; - break; - } - case ResourceType.TIME_SLOT: { - const timeSlot = await prisma.timeSlot.findUnique({ - where: { id: resourceId }, - include: { lab: true }, - }); - resourceOrganizationId = timeSlot?.lab?.organizationId; - break; - } - case ResourceType.BOOKING: { - const booking = await prisma.booking.findUnique({ - where: { id: resourceId }, - include: { timeSlot: { include: { lab: true } } }, - }); - resourceOrganizationId = booking?.timeSlot?.lab?.organizationId; - break; - } - case ResourceType.WAITLIST: { - const waitlist = await prisma.waitlist.findUnique({ - where: { id: resourceId }, - include: { timeSlot: { include: { lab: true } } }, - }); - resourceOrganizationId = waitlist?.timeSlot?.lab?.organizationId; - break; - } - case ResourceType.NOTIFICATION: { - const notification = await prisma.notification.findUnique({ - where: { id: resourceId }, - include: { user: true }, - }); - resourceOrganizationId = notification?.user?.organizationId; - break; - } - case ResourceType.ORGANIZATION: { - resourceOrganizationId = resourceId; - break; - } - default: { - const exhaustiveCheck: never = resourceType; - return next( - new HttpException( - 500, - `Organization check not implemented for resource type: ${exhaustiveCheck}`, - ), - ); - } - } + const resourceOrganizationId = await getResourceOrganizationId(resourceType, resourceId); - if (!resourceOrganizationId) { + if (resourceOrganizationId === undefined || resourceOrganizationId === null) { + // Check for both undefined and null return next( new HttpException(404, 'Resource not found or not associated with an organization'), ); @@ -215,7 +205,7 @@ export const requireSameOrganization = ( next(); } catch (error) { - console.error('Organization access check failed:', error); + console.error('Organization access check failed:', error); // Keep console.error for critical middleware errors if (error instanceof HttpException) { next(error); } else { diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 455e2bb..60e5bb9 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -44,7 +44,8 @@ export class AuthController { const user = await authService.register(userData); sendSuccess(res, { user }, 201); } catch (error) { - if (error instanceof z.ZodError) { // Handle ZodError specifically + if (error instanceof z.ZodError) { + // Handle ZodError specifically return sendError( res, 'Validation error', diff --git a/backend/src/middleware/auth.middleware.ts b/backend/src/middleware/auth.middleware.ts index 1905309..b35825c 100644 --- a/backend/src/middleware/auth.middleware.ts +++ b/backend/src/middleware/auth.middleware.ts @@ -74,7 +74,12 @@ export const authenticate = async ( return sendError(res, 'Token expired', errorTypes.UNAUTHORIZED, 'TOKEN_EXPIRED'); } // Fallback for other unexpected errors - return sendError(res, 'Authentication failed due to an unexpected server error', errorTypes.INTERNAL_SERVER, 'AUTH_UNEXPECTED_ERROR'); + return sendError( + res, + 'Authentication failed due to an unexpected server error', + errorTypes.INTERNAL_SERVER, + 'AUTH_UNEXPECTED_ERROR', + ); } }; diff --git a/backend/src/middleware/error.middleware.ts b/backend/src/middleware/error.middleware.ts index 999ed9b..6807424 100644 --- a/backend/src/middleware/error.middleware.ts +++ b/backend/src/middleware/error.middleware.ts @@ -12,11 +12,13 @@ export const errorHandler = ( next: NextFunction, // Express needs this signature for error handlers ): void => { if (err instanceof AppError) { - logger.warn(`AppError: ${err.statusCode} - ${err.message} - errorCode: ${err.errorCode ?? 'N/A'}`); + logger.warn( + `AppError: ${err.statusCode} - ${err.message} - errorCode: ${err.errorCode ?? 'N/A'}`, + ); sendError(res, err.message, err.statusCode, err.errorCode); } else { // Log unexpected errors logger.error('Unexpected error:', err); sendError(res, 'An unexpected internal server error occurred.', 500, 'INTERNAL_SERVER_ERROR'); } -}; \ No newline at end of file +}; diff --git a/backend/src/services/auth.service.ts b/backend/src/services/auth.service.ts index 0f7617b..4a3f89c 100644 --- a/backend/src/services/auth.service.ts +++ b/backend/src/services/auth.service.ts @@ -117,7 +117,11 @@ export class AuthService { } // Generate tokens - const tokenPayload = { userId: user.id, role: user.user_role, organizationId: user.organizationId }; + const tokenPayload = { + userId: user.id, + role: user.user_role, + organizationId: user.organizationId, + }; const accessToken = generateAccessToken(tokenPayload); const refreshTokenValue = generateRefreshToken(tokenPayload); // Renamed to avoid conflict @@ -175,12 +179,7 @@ export class AuthService { // Add refresh token to blacklist until its expiration // Use the same parsing logic for consistency if REFRESH_TOKEN_EXPIRES_IN is used for blacklist duration const blacklistExpirySeconds = parseDurationToSeconds(config.REFRESH_TOKEN_EXPIRES_IN); - await redis.set( - `blacklist:${refreshToken}`, - '1', - 'EX', - blacklistExpirySeconds, - ); + await redis.set(`blacklist:${refreshToken}`, '1', 'EX', blacklistExpirySeconds); // Remove all refresh tokens for this user (optional, for complete logout) const keys = await redis.keys(`refresh_token:${userId}:*`); diff --git a/backend/src/tests/authorization/permissionChecker.test.ts b/backend/src/tests/authorization/permissionChecker.test.ts index 6db74fc..312f7c9 100644 --- a/backend/src/tests/authorization/permissionChecker.test.ts +++ b/backend/src/tests/authorization/permissionChecker.test.ts @@ -50,7 +50,10 @@ describe('Permission Checker', () => { user_role: UserRole.USER, }); const authUser = { ...testUser, user_role: testUser.user_role }; - const result = hasAnyPermission(authUser, [USER_PERMISSIONS.READ_SELF, ADMIN_PERMISSIONS.DELETE_USERS]); + const result = hasAnyPermission(authUser, [ + USER_PERMISSIONS.READ_SELF, + ADMIN_PERMISSIONS.DELETE_USERS, + ]); expect(result).toBe(true); }); }); diff --git a/backend/src/tests/middleware/auth.middleware.test.ts b/backend/src/tests/middleware/auth.middleware.test.ts index 2dbabb9..5f02b13 100644 --- a/backend/src/tests/middleware/auth.middleware.test.ts +++ b/backend/src/tests/middleware/auth.middleware.test.ts @@ -7,7 +7,8 @@ import { createMockResponse, createMockNext, MockResponse, - createTestOrganization // Import your MockResponse type + createTestOrganization, + TestUser, // Import TestUser type } from '../utils/authTestUtils'; import { UserRole, Organization } from '@prisma/client'; // Import Organization @@ -63,13 +64,15 @@ describe('Authentication Middleware', () => { test('should return 401 if user not found', async () => { // Create a user payload for a token, but don't create the user in DB - const nonExistentUserPayload = { + const nonExistentUserPayload: TestUser = { + // Use TestUser type id: 'non-existent-user-id', + user_name: 'Ghost User', // Add user_name as it's part of TestUser user_email: `ghost-${Date.now()}@example.com`, user_role: UserRole.USER, - organizationId: testOrg.id, // Still need a valid orgId for token structure if it's included + organizationId: testOrg.id, }; - const token = generateTestToken(nonExistentUserPayload as any); // Cast if TestUser type is strict + const token = generateTestToken(nonExistentUserPayload); // No 'as any' needed mockReq.headers = { authorization: `Bearer ${token}` }; await authenticate(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); @@ -93,7 +96,11 @@ describe('Authentication Middleware', () => { user_role: UserRole.ADMIN, organizationId: testOrg.id, // Provide organizationId }); - mockReq.user = { id: adminUser.id, role: adminUser.user_role, organizationId: adminUser.organizationId }; + mockReq.user = { + id: adminUser.id, + role: adminUser.user_role, + organizationId: adminUser.organizationId, + }; const adminOnlyMiddleware = checkRole([UserRole.ADMIN]); adminOnlyMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); @@ -106,7 +113,11 @@ describe('Authentication Middleware', () => { user_role: UserRole.USER, organizationId: testOrg.id, // Provide organizationId }); - mockReq.user = { id: regularUser.id, role: regularUser.user_role, organizationId: regularUser.organizationId }; + mockReq.user = { + id: regularUser.id, + role: regularUser.user_role, + organizationId: regularUser.organizationId, + }; const adminOnlyMiddleware = checkRole([UserRole.ADMIN]); adminOnlyMiddleware(mockReq as Request, mockRes as unknown as ExpressResponse, mockNext); diff --git a/backend/src/tests/middleware/rbacMiddleware.test.ts b/backend/src/tests/middleware/rbacMiddleware.test.ts index 16cb7d3..c68f61a 100644 --- a/backend/src/tests/middleware/rbacMiddleware.test.ts +++ b/backend/src/tests/middleware/rbacMiddleware.test.ts @@ -28,7 +28,8 @@ describe('RBAC Middleware', () => { describe('requirePermissions middleware', () => { let org: Organization; // Define org here - beforeEach(async () => { // This beforeEach is specific to this describe block + beforeEach(async () => { + // This beforeEach is specific to this describe block org = await createTestOrganization('Test Org For Permissions'); }); diff --git a/backend/src/tests/routes/auth.routes.test.ts b/backend/src/tests/routes/auth.routes.test.ts index a3715d9..8fd73a1 100644 --- a/backend/src/tests/routes/auth.routes.test.ts +++ b/backend/src/tests/routes/auth.routes.test.ts @@ -49,14 +49,16 @@ describe('Auth Routes Integration Tests', () => { // const userPassword = 'password123Secure'; // Old password // Register the user first - await request(app).post('/auth/register').send({ - firstName: 'Login', - lastName: 'Test', - email: userEmail, - password: complexPassword, // Use complex password - organizationId: org.id, - }).expect(201); - + await request(app) + .post('/auth/register') + .send({ + firstName: 'Login', + lastName: 'Test', + email: userEmail, + password: complexPassword, // Use complex password + organizationId: org.id, + }) + .expect(201); const loginData = { email: userEmail, @@ -103,14 +105,16 @@ describe('Auth Routes Integration Tests', () => { // const userPassword = 'password123Secure'; // Old password // Register user - await request(app).post('/auth/register').send({ - firstName: 'Logout', - lastName: 'Test', - email: userEmail, - password: complexPassword, // Use complex password - organizationId: org.id, - }).expect(201); - + await request(app) + .post('/auth/register') + .send({ + firstName: 'Logout', + lastName: 'Test', + email: userEmail, + password: complexPassword, // Use complex password + organizationId: org.id, + }) + .expect(201); // Login to get token and cookies const loginResponse = await request(app).post('/auth/login').send({ @@ -125,10 +129,7 @@ describe('Auth Routes Integration Tests', () => { // Login with agent to establish session and cookies for the agent await agent.post('/auth/login').send({ email: userEmail, password: complexPassword }); - await agent - .post('/auth/logout') - .set('Authorization', `Bearer ${token}`) - .expect(200); + await agent.post('/auth/logout').set('Authorization', `Bearer ${token}`).expect(200); }); test('should reject logout without authentication', async () => { diff --git a/backend/src/tests/setup/testSetup.ts b/backend/src/tests/setup/testSetup.ts index 1a76eac..2112120 100644 --- a/backend/src/tests/setup/testSetup.ts +++ b/backend/src/tests/setup/testSetup.ts @@ -4,18 +4,32 @@ import originalLogger from '../../utils/logger'; // Assuming logger is available // Attempt to create a logger mock/fallback if the real one isn't test-friendly or available let logger = originalLogger; + +interface SimpleLogger { + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; + debug: (...args: unknown[]) => void; +} + if (!logger || typeof logger.info !== 'function') { - console.warn("Original logger not found or not standard in testSetup.ts, using console fallback."); + // Use console.warn directly here, as 'logger' might be null/undefined or not have a .warn method + // eslint-disable-next-line no-console + console.warn( + 'Original logger not found or not standard in testSetup.ts, using console fallback.', + ); logger = { - info: console.log, - warn: console.warn, - error: console.error, - debug: console.log, - // Add other methods if your logger interface has them and they are called - } as any; // Use 'as any' for simplicity if logger structure varies + // eslint-disable-next-line no-console + info: (...args: unknown[]) => console.log(...args), // Keep console for fallback + // eslint-disable-next-line no-console + warn: (...args: unknown[]) => console.warn(...args), + // eslint-disable-next-line no-console + error: (...args: unknown[]) => console.error(...args), + // eslint-disable-next-line no-console + debug: (...args: unknown[]) => console.log(...args), + } as SimpleLogger; } - const prisma = new PrismaClient({ datasources: { db: { @@ -39,15 +53,15 @@ beforeAll(async () => { isConnected = false; const errorMessage = error instanceof Error ? error.message : String(error); logger.error( - `Failed to connect to test database (DATABASE_URL_TEST) in global setup: ${errorMessage}. Integration tests requiring DB will likely fail.` + `Failed to connect to test database (DATABASE_URL_TEST) in global setup: ${errorMessage}. Integration tests requiring DB will likely fail.`, ); // Do not rethrow here, to allow tests that mock Prisma to run. } } else { logger.warn( 'DATABASE_URL_TEST is not set. Global Prisma client in testSetup.ts will not attempt to connect. ' + - 'Database-dependent integration tests should ensure their own DB setup or use mocks. ' + - 'Operations in authTestUtils using this global prisma instance may fail if they expect a connection.' + 'Database-dependent integration tests should ensure their own DB setup or use mocks. ' + + 'Operations in authTestUtils using this global prisma instance may fail if they expect a connection.', ); } }); diff --git a/backend/src/tests/utils/authTestUtils.ts b/backend/src/tests/utils/authTestUtils.ts index 790f129..8a2ed54 100644 --- a/backend/src/tests/utils/authTestUtils.ts +++ b/backend/src/tests/utils/authTestUtils.ts @@ -103,14 +103,17 @@ export const createMockResponse = (): MockResponse => { res.sendStatus = jest.fn().mockReturnValue(res); res.cookie = jest.fn().mockReturnValue(res); res.clearCookie = jest.fn().mockReturnValue(res); - res.setHeader = jest.fn().mockImplementation(function (this: typeof res, key: string, value: string | string[]) { + res.setHeader = jest.fn().mockImplementation(function ( + this: typeof res, + key: string, + value: string | string[], + ) { if (this._headers) { this._headers[key.toLowerCase()] = value; } return this; }); res.getHeader = jest.fn().mockImplementation(function (this: typeof res, key: string) { - // @ts-ignore: Accessing _headers which is intentionally not part of the public MockResponse type return this._headers ? this._headers[key.toLowerCase()] : undefined; }); diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 49387d1..39acd4b 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -4,7 +4,8 @@ export class AppError extends Error { isOperational: boolean; errorCode?: string; // Added errorCode - constructor(message: string, statusCode: number, errorCode?: string) { // Added errorCode + constructor(message: string, statusCode: number, errorCode?: string) { + // Added errorCode super(message); this.statusCode = statusCode; this.isOperational = true; From 88cb2ad5f6a7c1bc20411891183a10226876b556 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:55:34 +0000 Subject: [PATCH 32/33] Testsetup passes linting and formatting checks --- backend/src/tests/setup/testSetup.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/backend/src/tests/setup/testSetup.ts b/backend/src/tests/setup/testSetup.ts index 2112120..604f9ca 100644 --- a/backend/src/tests/setup/testSetup.ts +++ b/backend/src/tests/setup/testSetup.ts @@ -1,33 +1,37 @@ import { PrismaClient } from '@prisma/client'; import { beforeAll, afterAll, beforeEach } from '@jest/globals'; -import originalLogger from '../../utils/logger'; // Assuming logger is available +import originalLoggerInstance from '../../utils/logger'; // Import the actual logger instance -// Attempt to create a logger mock/fallback if the real one isn't test-friendly or available -let logger = originalLogger; - -interface SimpleLogger { +// Define an interface for the logger methods used in this file. +// This ensures that both the original Winston logger and the fallback are compatible. +interface TestSetupLogger { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; - debug: (...args: unknown[]) => void; + debug: (...args: unknown[]) => void; // Included for consistency with the fallback } +// Type 'logger' with TestSetupLogger. +// The originalLoggerInstance (Winston logger) is compatible because it has these methods. +let logger: TestSetupLogger = originalLoggerInstance; + if (!logger || typeof logger.info !== 'function') { // Use console.warn directly here, as 'logger' might be null/undefined or not have a .warn method // eslint-disable-next-line no-console console.warn( 'Original logger not found or not standard in testSetup.ts, using console fallback.', ); + // The fallback logger also conforms to TestSetupLogger. logger = { // eslint-disable-next-line no-console - info: (...args: unknown[]) => console.log(...args), // Keep console for fallback + info: (...args: unknown[]): void => console.log(...args), // eslint-disable-next-line no-console - warn: (...args: unknown[]) => console.warn(...args), + warn: (...args: unknown[]): void => console.warn(...args), // eslint-disable-next-line no-console - error: (...args: unknown[]) => console.error(...args), + error: (...args: unknown[]): void => console.error(...args), // eslint-disable-next-line no-console - debug: (...args: unknown[]) => console.log(...args), - } as SimpleLogger; + debug: (...args: unknown[]): void => console.log(...args), + }; } const prisma = new PrismaClient({ From 28836e6466dd604e515580dc858136136c248d42 Mon Sep 17 00:00:00 2001 From: Aditya Rathore <153427299+AdityaDRathore@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:58:07 +0000 Subject: [PATCH 33/33] Complete tasks for Day 3: Authorization Utilities & Integration in the backend implementation plan --- .../Development Plan Complete.md | 8 ++--- .../Implementation/Develpment Plan Backend.md | 32 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/Implementation/Development Plan Complete.md b/docs/Implementation/Development Plan Complete.md index 9f64682..07e14a5 100644 --- a/docs/Implementation/Development Plan Complete.md +++ b/docs/Implementation/Development Plan Complete.md @@ -116,10 +116,10 @@ Corresponds to Backend Plan - PHASE 1 (Day 3 Completion), PHASE 2, PHASE 3 (Day * [x] Task 2.3: Context-Aware Authorization * [x] Task 2.4: Testing Authorization Logic * **Day 3: Authorization Utilities & Integration** - * [ ] Task 3.1: Permission Checking Utilities - * [ ] Task 3.2: Audit Logging - * [ ] Task 3.3: Route Protection Implementation - * [ ] Task 3.4: Testing & Documentation + * [x] Task 3.1: Permission Checking Utilities + * [x] Task 3.2: Audit Logging + * [x] Task 3.3: Route Protection Implementation + * [x] Task 3.4: Testing & Documentation * **Base Service Layer & API Infrastructure** * **Base Repository Pattern (Backend PHASE 3 - Day 1)** * [ ] Task 1.1: Generic Repository Interface diff --git a/docs/Implementation/Develpment Plan Backend.md b/docs/Implementation/Develpment Plan Backend.md index 16ef005..d9a5d3d 100644 --- a/docs/Implementation/Develpment Plan Backend.md +++ b/docs/Implementation/Develpment Plan Backend.md @@ -101,22 +101,22 @@ **Day 3: Authorization Utilities & Integration** -* [ ] Task 3.1: Permission Checking Utilities - * [ ] Create reusable permission checking functions. - * [ ] Implement helper methods for common permission patterns. - * [ ] Add caching for frequently checked permissions. -* [ ] Task 3.2: Audit Logging - * [ ] Implement audit logging for sensitive operations. - * [ ] Create structured log format for security events. - * [ ] Ensure PII protection in logs. -* [ ] Task 3.3: Route Protection Implementation - * [ ] Apply role requirements to all defined routes. - * [ ] Group routes by required permission level. - * [ ] Implement resource-specific access controls. -* [ ] Task 3.4: Testing & Documentation - * [ ] Create comprehensive tests for authorization system. - * [ ] Document authorization patterns for developers. - * [ ] Update API documentation with permission requirements. +* [x] Task 3.1: Permission Checking Utilities + * [x] Create reusable permission checking functions. + * [x] Implement helper methods for common permission patterns. + * [x] Add caching for frequently checked permissions. +* [x] Task 3.2: Audit Logging + * [x] Implement audit logging for sensitive operations. + * [x] Create structured log format for security events. + * [x] Ensure PII protection in logs. +* [x] Task 3.3: Route Protection Implementation + * [x] Apply role requirements to all defined routes. + * [x] Group routes by required permission level. + * [x] Implement resource-specific access controls. +* [x] Task 3.4: Testing & Documentation + * [x] Create comprehensive tests for authorization system. + * [x] Document authorization patterns for developers. + * [x] Update API documentation with permission requirements. ## PHASE 3: PRISMA SERVICE LAYER (7 DAYS)