From 6361ea0f998d2af9e5c975f22590a637f0ac8e66 Mon Sep 17 00:00:00 2001 From: dvynshuu Date: Thu, 30 Apr 2026 16:22:03 +0530 Subject: [PATCH] Migrate session.controller to TypeScript --- .../__tests__/session.controller.test.ts | 224 ++++++++++++++++++ server/controllers/session.controller.js | 51 ---- server/controllers/session.controller.ts | 71 ++++++ 3 files changed, 295 insertions(+), 51 deletions(-) create mode 100644 server/controllers/__tests__/session.controller.test.ts delete mode 100644 server/controllers/session.controller.js create mode 100644 server/controllers/session.controller.ts diff --git a/server/controllers/__tests__/session.controller.test.ts b/server/controllers/__tests__/session.controller.test.ts new file mode 100644 index 0000000000..4df890a6de --- /dev/null +++ b/server/controllers/__tests__/session.controller.test.ts @@ -0,0 +1,224 @@ +import { Request as MockRequest } from 'jest-express/lib/request'; +import { Response as MockResponse } from 'jest-express/lib/response'; +import { NextFunction as MockNext } from 'jest-express/lib/next'; +import { Request, Response } from 'express'; +import passport from 'passport'; +import { + createSession, + getSession, + destroySession +} from '../session.controller'; +import { userResponse } from '../user.controller'; + +jest.mock('passport', () => ({ + authenticate: jest.fn() +})); + +jest.mock('../user.controller', () => ({ + userResponse: jest.fn((user) => ({ id: user.id, username: user.username })) +})); + +describe('session.controller', () => { + let request: MockRequest; + let response: MockResponse; + let next: MockNext; + + beforeEach(() => { + request = new MockRequest(); + response = new MockResponse(); + next = jest.fn(); + + // Add missing properties that jest-express MockRequest doesn't have by default but Passport uses + (request as any).logIn = jest.fn(); + (request as any).logout = jest.fn(); + (request as any).session = { + destroy: jest.fn() + } as any; + }); + + afterEach(() => { + request.resetMocked(); + response.resetMocked(); + jest.clearAllMocks(); + }); + + describe('createSession', () => { + it('calls next with error if passport authentication fails', () => { + const error = new Error('Auth failed'); + (passport.authenticate as jest.Mock).mockImplementation( + (strategy, callback) => (mockReq: any, mockRes: any, mockNext: any) => + callback(error, null) + ); + + createSession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(next).toHaveBeenCalledWith(error); + }); + + it('returns 401 if user is not found', () => { + (passport.authenticate as jest.Mock).mockImplementation( + (strategy, callback) => (mockReq: any, mockRes: any, mockNext: any) => + callback(null, false) + ); + + createSession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(response.status).toHaveBeenCalledWith(401); + expect(response.json).toHaveBeenCalledWith({ + message: 'Invalid username or password.' + }); + }); + + it('calls next with error if req.logIn fails', () => { + const user = { id: '1', username: 'test' }; + const loginError = new Error('Login failed'); + + (passport.authenticate as jest.Mock).mockImplementation( + (strategy, callback) => (mockReq: any, mockRes: any, mockNext: any) => + callback(null, user) + ); + + ((request as any) + .logIn as jest.Mock).mockImplementation( + (mockUser: any, callback: any) => callback(loginError) + ); + + createSession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(next).toHaveBeenCalledWith(loginError); + }); + + it('returns user data on successful login', () => { + const user = { id: '1', username: 'test' }; + + (passport.authenticate as jest.Mock).mockImplementation( + (strategy, callback) => (mockReq: any, mockRes: any, mockNext: any) => + callback(null, user) + ); + + ((request as any).logIn as jest.Mock).mockImplementation( + (mockUser: any, callback: any) => { + request.user = user as any; + callback(null); + } + ); + + createSession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(response.json).toHaveBeenCalledWith({ id: '1', username: 'test' }); + }); + }); + + describe('getSession', () => { + it('returns null user if not authenticated', () => { + getSession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(response.status).toHaveBeenCalledWith(200); + expect(response.send).toHaveBeenCalledWith({ user: null }); + }); + + it('returns 403 if user is banned', () => { + request.user = { banned: true } as any; + + getSession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(response.status).toHaveBeenCalledWith(403); + expect(response.send).toHaveBeenCalledWith({ + message: 'Forbidden: User is banned.' + }); + }); + + it('returns user data if authenticated and not banned', () => { + request.user = { id: '1', username: 'test', banned: false } as any; + + getSession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(response.json).toHaveBeenCalledWith({ id: '1', username: 'test' }); + }); + }); + + describe('destroySession', () => { + it('calls next with error if logout fails', () => { + const logoutError = new Error('Logout failed'); + ((request as any) + .logout as jest.Mock).mockImplementation((callback: any) => + callback(logoutError) + ); + + destroySession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(next).toHaveBeenCalledWith(logoutError); + }); + + it('calls next with error if session destruction fails', () => { + const destroyError = new Error('Destroy failed'); + ((request as any) + .logout as jest.Mock).mockImplementation((callback: any) => + callback(null) + ); + ((request as any).session + .destroy as jest.Mock).mockImplementation((callback: any) => + callback(destroyError) + ); + + destroySession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(next).toHaveBeenCalledWith(destroyError); + }); + + it('returns success true if logout and session destruction succeed', () => { + ((request as any) + .logout as jest.Mock).mockImplementation((callback: any) => + callback(null) + ); + ((request as any).session + .destroy as jest.Mock).mockImplementation((callback: any) => + callback(null) + ); + + destroySession( + (request as unknown) as Request, + (response as unknown) as Response, + next + ); + + expect(response.json).toHaveBeenCalledWith({ success: true }); + }); + }); +}); diff --git a/server/controllers/session.controller.js b/server/controllers/session.controller.js deleted file mode 100644 index 173e9e9834..0000000000 --- a/server/controllers/session.controller.js +++ /dev/null @@ -1,51 +0,0 @@ -import passport from 'passport'; - -import { userResponse } from './user.controller'; - -export function createSession(req, res, next) { - passport.authenticate('local', (err, user) => { - if (err) { - next(err); - return; - } - if (!user) { - res.status(401).json({ message: 'Invalid username or password.' }); - return; - } - - req.logIn(user, (innerErr) => { - if (innerErr) { - next(innerErr); - return; - } - res.json(userResponse(req.user)); - }); - })(req, res, next); -} - -export function getSession(req, res) { - if (!req.user) { - return res.status(200).send({ user: null }); - } - if (req.user.banned) { - return res.status(403).send({ message: 'Forbidden: User is banned.' }); - } - - return res.json(userResponse(req.user)); -} - -export function destroySession(req, res, next) { - req.logout((err) => { - if (err) { - next(err); - return; - } - req.session.destroy((error) => { - if (error) { - next(error); - return; - } - res.json({ success: true }); - }); - }); -} diff --git a/server/controllers/session.controller.ts b/server/controllers/session.controller.ts new file mode 100644 index 0000000000..806e932cfa --- /dev/null +++ b/server/controllers/session.controller.ts @@ -0,0 +1,71 @@ +/* global Express */ +import passport from 'passport'; +import { RequestHandler } from 'express'; + +import { userResponse } from './user.controller'; + +/** + * - Id: `SessionController.createSession` + * + * Description: + * - Creates a new user session (Login) using Passport's local strategy. + */ +export const createSession: RequestHandler = (req, res, next) => { + passport.authenticate('local', (err: Error, user: Express.User) => { + if (err) { + next(err); + return; + } + if (!user) { + res.status(401).json({ message: 'Invalid username or password.' }); + return; + } + + req.logIn(user, (innerErr: Error) => { + if (innerErr) { + next(innerErr); + return; + } + res.json(userResponse(req.user as Express.User)); + }); + })(req, res, next); +}; + +/** + * - Id: `SessionController.getSession` + * + * Description: + * - Retrieves the current session user. Returns null if not authenticated. + */ +export const getSession: RequestHandler = (req, res) => { + if (!req.user) { + return res.status(200).send({ user: null }); + } + if (req.user.banned) { + return res.status(403).send({ message: 'Forbidden: User is banned.' }); + } + + return res.json(userResponse(req.user)); +}; + +/** + * - Id: `SessionController.destroySession` + * + * Description: + * - Destroys the current session (Logout). + */ +export const destroySession: RequestHandler = (req, res, next) => { + req.logout((err: Error) => { + if (err) { + next(err); + return; + } + (req as any).session.destroy((error: Error) => { + if (error) { + next(error); + return; + } + res.json({ success: true }); + }); + }); +};