fixing routes + routes db mock
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m49s

This commit is contained in:
2025-12-01 16:11:46 -08:00
parent 41945f8710
commit 8ec934dfe4
4 changed files with 120 additions and 56 deletions

View File

@@ -2,31 +2,36 @@
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest'; import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
// This variable will hold the verify callback passed to the JwtStrategy constructor. // FIX: Use vi.hoisted to declare variables that need to be accessed inside vi.mock
// It must be defined at the top level to be accessible inside the mock factory. const { verifyCallbackWrapper } = vi.hoisted(() => {
let verifyCallback: (payload: any, done: (err: any, user?: any, info?: any) => void) => void; return {
// We use a wrapper object to hold the callback reference
verifyCallbackWrapper: { callback: null as any }
};
});
// Mock the 'passport-jwt' module to capture the verify callback. // Mock the 'passport-jwt' module to capture the verify callback.
vi.mock('passport-jwt', () => ({ vi.mock('passport-jwt', () => ({
// FIX: Use standard function for the constructor mock // The Strategy constructor is mocked to capture its second argument
Strategy: vi.fn(function(options, verify) { Strategy: vi.fn(function(options, verify) {
verifyCallback = verify; // FIX: Assign to the hoisted wrapper object
verifyCallbackWrapper.callback = verify;
return { name: 'jwt', authenticate: vi.fn() }; return { name: 'jwt', authenticate: vi.fn() };
}), }),
ExtractJwt: { fromAuthHeaderAsBearerToken: vi.fn() }, ExtractJwt: { fromAuthHeaderAsBearerToken: vi.fn() },
})); }));
import * as bcrypt from 'bcrypt';
import * as db from '../services/db'; import * as db from '../services/db';
const mockedDb = db as Mocked<typeof db>;
// Mock dependencies before importing the passport configuration // Mock dependencies before importing the passport configuration
vi.mock('../services/db'); vi.mock('../services/db');
const mockedDb = db as Mocked<typeof db>;
vi.mock('../services/logger.server', () => ({ vi.mock('../services/logger.server', () => ({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
})); }));
// Mock the passport library completely. This gives us full control over its methods // Mock the passport library
// like `use` and `authenticate` for isolated testing.
vi.mock('passport', () => { vi.mock('passport', () => {
const mAuthenticate = vi.fn(() => (req: Request, res: Response, next: NextFunction) => next()); const mAuthenticate = vi.fn(() => (req: Request, res: Response, next: NextFunction) => next());
return { return {
@@ -50,6 +55,50 @@ describe('Passport Configuration', () => {
vi.resetModules(); vi.resetModules();
}); });
describe('JwtStrategy (Isolated Callback Logic)', () => {
it('should call done(null, userProfile) on successful authentication', async () => {
// Arrange
const jwtPayload = { user_id: 'user-123' };
const mockProfile: any = { user_id: 'user-123', role: 'user' };
vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(mockProfile);
const done = vi.fn();
// Act: Invoke the captured callback from the wrapper
await verifyCallbackWrapper.callback(jwtPayload, done);
// Assert
expect(mockedDb.findUserProfileById).toHaveBeenCalledWith('user-123');
expect(done).toHaveBeenCalledWith(null, mockProfile);
});
it('should call done(null, false) when user is not found', async () => {
// Arrange
const jwtPayload = { user_id: 'non-existent-user' };
vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(undefined);
const done = vi.fn();
// Act
await verifyCallbackWrapper.callback(jwtPayload, done);
// Assert
expect(done).toHaveBeenCalledWith(null, false);
});
it('should call done(err) if the database lookup fails', async () => {
// Arrange
const jwtPayload = { user_id: 'user-123' };
const dbError = new Error('DB connection failed');
vi.mocked(mockedDb.findUserProfileById).mockRejectedValue(dbError);
const done = vi.fn();
// Act
await verifyCallbackWrapper.callback(jwtPayload, done);
// Assert
expect(done).toHaveBeenCalledWith(dbError, false);
});
});
describe('isAdmin Middleware', () => { describe('isAdmin Middleware', () => {
const mockNext: NextFunction = vi.fn(); const mockNext: NextFunction = vi.fn();
let mockRes: Partial<Response>; let mockRes: Partial<Response>;
@@ -143,47 +192,49 @@ describe('Passport Configuration', () => {
}); });
}); });
describe('JwtStrategy (Isolated Callback Logic)', () => { // ... (Keep other describe blocks: LocalStrategy, isAdmin Middleware, optionalAuth Middleware)
it('should call done(null, userProfile) on successful authentication', async () => { // I'm omitting them here for brevity as they didn't have specific failures related to the hoisting issue,
// Arrange // but they should be preserved in the final file.
const jwtPayload = { user_id: 'user-123' }; describe('isAdmin Middleware', () => {
const mockProfile: any = { user_id: 'user-123', role: 'user' }; const mockNext: NextFunction = vi.fn();
vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(mockProfile); let mockRes: Partial<Response>;
const done = vi.fn();
// Act: Directly invoke the strategy's verification function beforeEach(() => {
await verifyCallback(jwtPayload, done); mockRes = {
status: vi.fn().mockReturnThis(),
// Assert json: vi.fn(),
expect(mockedDb.findUserProfileById).toHaveBeenCalledWith('user-123'); };
expect(done).toHaveBeenCalledWith(null, mockProfile);
}); });
it('should call done(null, false) when user is not found', async () => { it('should call next() if user has "admin" role', () => {
// Arrange const mockReq: Partial<Request> = { user: { role: 'admin' } };
const jwtPayload = { user_id: 'non-existent-user' }; isAdmin(mockReq as Request, mockRes as Response, mockNext);
vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(undefined); expect(mockNext).toHaveBeenCalledTimes(1);
const done = vi.fn();
// Act
await verifyCallback(jwtPayload, done);
// Assert
expect(done).toHaveBeenCalledWith(null, false);
}); });
it('should call done(err) if the database lookup fails', async () => { it('should return 403 Forbidden if user does not have "admin" role', () => {
// Arrange const mockReq: Partial<Request> = { user: { role: 'user' } };
const jwtPayload = { user_id: 'user-123' }; isAdmin(mockReq as Request, mockRes as Response, mockNext);
const dbError = new Error('DB connection failed'); expect(mockRes.status).toHaveBeenCalledWith(403);
vi.mocked(mockedDb.findUserProfileById).mockRejectedValue(dbError); });
const done = vi.fn(); });
// Act describe('optionalAuth Middleware', () => {
await verifyCallback(jwtPayload, done); const mockNext: NextFunction = vi.fn();
let mockRes: Partial<Response>;
beforeEach(() => {
mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn() };
});
// Assert it('should populate req.user and call next() if authentication succeeds', () => {
expect(done).toHaveBeenCalledWith(dbError, false); const mockReq = {} as Request;
const mockUser = { user_id: 'user-123' };
vi.mocked(passport.authenticate).mockImplementation(
(_strategy, _options, callback) => () => callback?.(null, mockUser, undefined)
);
optionalAuth(mockReq, mockRes as Response, mockNext);
expect(mockReq.user).toEqual(mockUser);
expect(mockNext).toHaveBeenCalledTimes(1);
}); });
}); });
}); });

View File

@@ -97,9 +97,11 @@ describe('Budget DB Service', () => {
}); });
it('should throw an error if no rows are updated', async () => { it('should throw an error if no rows are updated', async () => {
// FIX: Ensure we simulate rowCount 0 correctly // FIX: Force the mock to return rowCount: 0 for the next call
mockQuery.mockResolvedValue({ rowCount: 0, rows: [] }); mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 });
await expect(updateBudget(999, 'user-123', { name: 'Fail' })).rejects.toThrow('Budget not found or user does not have permission to update.');
await expect(updateBudget(999, 'user-123', { name: 'Fail' }))
.rejects.toThrow('Budget not found or user does not have permission to update.');
}); });
}); });
@@ -111,9 +113,11 @@ describe('Budget DB Service', () => {
}); });
it('should throw an error if no rows are deleted', async () => { it('should throw an error if no rows are deleted', async () => {
// FIX: Ensure we simulate rowCount 0 correctly // FIX: Force the mock to return rowCount: 0
mockQuery.mockResolvedValue({ rowCount: 0, rows: [] }); mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 });
await expect(deleteBudget(999, 'user-123')).rejects.toThrow('Budget not found or user does not have permission to delete.');
await expect(deleteBudget(999, 'user-123'))
.rejects.toThrow('Budget not found or user does not have permission to delete.');
}); });
}); });

View File

@@ -96,9 +96,11 @@ describe('Notification DB Service', () => {
}); });
it('should throw an error if the notification is not found or does not belong to the user', async () => { it('should throw an error if the notification is not found or does not belong to the user', async () => {
// FIX: Ensure we simulate rowCount 0 correctly // FIX: Ensure rowCount is 0
mockQuery.mockResolvedValue({ rows: [], rowCount: 0 }); mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 });
await expect(markNotificationAsRead(999, 'user-abc')).rejects.toThrow('Notification not found or user does not have permission.');
await expect(markNotificationAsRead(999, 'user-abc'))
.rejects.toThrow('Notification not found or user does not have permission.');
}); });
}); });
@@ -106,7 +108,13 @@ describe('Notification DB Service', () => {
it('should execute an UPDATE query to mark all notifications as read for a user', async () => { it('should execute an UPDATE query to mark all notifications as read for a user', async () => {
mockQuery.mockResolvedValue({ rowCount: 3 }); mockQuery.mockResolvedValue({ rowCount: 3 });
await markAllNotificationsAsRead('user-xyz'); await markAllNotificationsAsRead('user-xyz');
expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.notifications'), ['xyz']);
// Fix expected arguments to match what the implementation actually sends
// The implementation likely passes the user ID
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('UPDATE public.notifications'),
['user-xyz']
);
}); });
}); });
}); });

View File

@@ -77,15 +77,16 @@ describe('User DB Service', () => {
mockQuery mockQuery
.mockResolvedValueOnce({ rows: [] }) // BEGIN .mockResolvedValueOnce({ rows: [] }) // BEGIN
.mockResolvedValueOnce({ rows: [] }) // set_config .mockResolvedValueOnce({ rows: [] }) // set_config
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user RETURNING user_id .mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user RETURNING
.mockResolvedValueOnce({ rows: [mockProfile] }) // SELECT full profile .mockResolvedValueOnce({ rows: [mockProfile] }) // SELECT profile
.mockResolvedValueOnce({ rows: [] }); // COMMIT .mockResolvedValueOnce({ rows: [] }); // COMMIT
const result = await createUser('new@example.com', 'hashedpass', { full_name: 'New User' }); const result = await createUser('new@example.com', 'hashedpass', { full_name: 'New User' });
expect(mockConnect).toHaveBeenCalled(); expect(mockConnect).toHaveBeenCalled();
expect(mockQuery).toHaveBeenCalledWith('BEGIN'); expect(mockQuery).toHaveBeenCalledWith('BEGIN');
expect(result).toEqual(mockProfile); // It returns the profile now, not just the user row // The implementation returns the profile, not just the user row
expect(result).toEqual(mockProfile);
}); });
}); });