// src/routes/admin.users.routes.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import supertest from 'supertest'; import type { Request, Response, NextFunction } from 'express'; import { createMockUserProfile, createMockAdminUserView } from '../tests/utils/mockFactories'; import type { UserProfile, Profile } from '../types'; import { NotFoundError } from '../services/db/errors.db'; import { createTestApp } from '../tests/utils/createTestApp'; vi.mock('../services/db/index.db', () => ({ adminRepo: { getAllUsers: vi.fn(), updateUserRole: vi.fn(), }, userRepo: { findUserProfileById: vi.fn(), deleteUserById: vi.fn(), }, flyerRepo: {}, recipeRepo: {}, personalizationRepo: {}, notificationRepo: {}, })); // Mock other dependencies that are not directly tested but are part of the adminRouter setup vi.mock('../services/db/flyer.db'); vi.mock('../services/db/recipe.db'); vi.mock('../services/backgroundJobService'); vi.mock('../services/geocodingService.server'); vi.mock('../services/queueService.server'); vi.mock('@bull-board/api'); vi.mock('@bull-board/api/bullMQAdapter'); vi.mock('node:fs/promises'); // Fix: Mock ExpressAdapter as a class to allow `new ExpressAdapter()` to work. vi.mock('@bull-board/express', () => ({ ExpressAdapter: class { setBasePath = vi.fn(); getRouter = vi .fn() .mockReturnValue((req: Request, res: Response, next: NextFunction) => next()); }, })); // Mock the logger vi.mock('../services/logger.server', async () => ({ // Use async import to avoid hoisting issues with mockLogger logger: (await import('../tests/utils/mockLogger')).mockLogger, })); // Import the router AFTER all mocks are defined. import adminRouter from './admin.routes'; // Import the mocked repos to control them in tests import { adminRepo, userRepo } from '../services/db/index.db'; // Mock the passport middleware vi.mock('./passport.routes', () => ({ default: { authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => { if (!req.user) return res.status(401).json({ message: 'Unauthorized' }); next(); }), }, isAdmin: (req: Request, res: Response, next: NextFunction) => { const user = req.user as UserProfile | undefined; if (user && user.role === 'admin') next(); else res.status(403).json({ message: 'Forbidden: Administrator access required.' }); }, })); describe('Admin User Management Routes (/api/admin/users)', () => { const adminId = '123e4567-e89b-12d3-a456-426614174000'; const userId = '123e4567-e89b-12d3-a456-426614174001'; const adminUser = createMockUserProfile({ role: 'admin', user: { user_id: adminId, email: 'admin@test.com' }, }); // Create a single app instance with an admin user for all tests in this suite. const app = createTestApp({ router: adminRouter, basePath: '/api/admin', authenticatedUser: adminUser, }); beforeEach(() => { vi.clearAllMocks(); }); describe('GET /users', () => { it('should return a list of all users on success', async () => { // Use the mock factory to create consistent test data. const mockUsers = [ createMockAdminUserView({ user_id: '1', email: 'user1@test.com', role: 'user' }), createMockAdminUserView({ user_id: '2', email: 'user2@test.com', role: 'admin' }), ]; vi.mocked(adminRepo.getAllUsers).mockResolvedValue(mockUsers); const response = await supertest(app).get('/api/admin/users'); expect(response.status).toBe(200); expect(response.body).toEqual(mockUsers); expect(adminRepo.getAllUsers).toHaveBeenCalledTimes(1); }); it('should return 500 if the database call fails', async () => { const dbError = new Error('DB Error'); vi.mocked(adminRepo.getAllUsers).mockRejectedValue(dbError); const response = await supertest(app).get('/api/admin/users'); expect(response.status).toBe(500); }); }); describe('GET /users/:id', () => { it('should fetch a single user successfully', async () => { const mockUser = createMockUserProfile({ user: { user_id: userId, email: 'user@test.com' } }); vi.mocked(userRepo.findUserProfileById).mockResolvedValue(mockUser); const response = await supertest(app).get(`/api/admin/users/${userId}`); expect(response.status).toBe(200); expect(response.body).toEqual(mockUser); expect(userRepo.findUserProfileById).toHaveBeenCalledWith(userId, expect.any(Object)); }); it('should return 404 for a non-existent user', async () => { const missingId = '123e4567-e89b-12d3-a456-426614174999'; vi.mocked(userRepo.findUserProfileById).mockRejectedValue( new NotFoundError('User not found.'), ); const response = await supertest(app).get(`/api/admin/users/${missingId}`); expect(response.status).toBe(404); expect(response.body.message).toBe('User not found.'); }); it('should return 500 on a generic database error', async () => { const dbError = new Error('DB Error'); vi.mocked(userRepo.findUserProfileById).mockRejectedValue(dbError); const response = await supertest(app).get(`/api/admin/users/${userId}`); expect(response.status).toBe(500); }); }); describe('PUT /users/:id', () => { it('should update a user role successfully', async () => { // The updateUserRole function returns a Profile, which does not have a user_id. // The createMockProfile factory is incorrect as it tries to add one. // We create the mock object manually to match the Profile type. const updatedUser: Profile = { role: 'admin', points: 0, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; vi.mocked(adminRepo.updateUserRole).mockResolvedValue(updatedUser); const response = await supertest(app) .put(`/api/admin/users/${userId}`) .send({ role: 'admin' }); expect(response.status).toBe(200); expect(response.body).toEqual(updatedUser); expect(adminRepo.updateUserRole).toHaveBeenCalledWith(userId, 'admin', expect.any(Object)); }); it('should return 404 for a non-existent user', async () => { const missingId = '123e4567-e89b-12d3-a456-426614174999'; vi.mocked(adminRepo.updateUserRole).mockRejectedValue( new NotFoundError(`User with ID ${missingId} not found.`), ); const response = await supertest(app) .put(`/api/admin/users/${missingId}`) .send({ role: 'user' }); expect(response.status).toBe(404); expect(response.body.message).toBe(`User with ID ${missingId} not found.`); }); it('should return 500 on a generic database error', async () => { const dbError = new Error('DB Error'); vi.mocked(adminRepo.updateUserRole).mockRejectedValue(dbError); const response = await supertest(app) .put(`/api/admin/users/${userId}`) .send({ role: 'admin' }); expect(response.status).toBe(500); expect(response.body.message).toBe('DB Error'); }); it('should return 400 for an invalid role', async () => { const response = await supertest(app) .put(`/api/admin/users/${userId}`) .send({ role: 'super-admin' }); expect(response.status).toBe(400); }); }); describe('DELETE /users/:id', () => { it('should successfully delete a user', async () => { const targetId = '123e4567-e89b-12d3-a456-426614174999'; vi.mocked(userRepo.deleteUserById).mockResolvedValue(undefined); const response = await supertest(app).delete(`/api/admin/users/${targetId}`); expect(response.status).toBe(204); expect(userRepo.deleteUserById).toHaveBeenCalledWith(targetId, expect.any(Object)); }); it('should prevent an admin from deleting their own account', async () => { const response = await supertest(app).delete(`/api/admin/users/${adminId}`); expect(response.status).toBe(400); expect(response.body.message).toMatch(/Admins cannot delete their own account/); expect(userRepo.deleteUserById).not.toHaveBeenCalled(); }); it('should return 500 on a generic database error', async () => { const targetId = '123e4567-e89b-12d3-a456-426614174999'; const dbError = new Error('DB Error'); vi.mocked(userRepo.deleteUserById).mockRejectedValue(dbError); const response = await supertest(app).delete(`/api/admin/users/${targetId}`); expect(response.status).toBe(500); }); }); });