All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 22m13s
264 lines
10 KiB
TypeScript
264 lines
10 KiB
TypeScript
// 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, ValidationError } 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: {},
|
|
}));
|
|
|
|
vi.mock('../services/userService', () => ({
|
|
userService: {
|
|
deleteUserAsAdmin: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// 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('../services/queues.server');
|
|
vi.mock('../services/workers.server');
|
|
vi.mock('../services/monitoringService.server');
|
|
vi.mock('../services/cacheService.server');
|
|
vi.mock('../services/brandService');
|
|
vi.mock('../services/receiptService.server');
|
|
vi.mock('../services/aiService.server');
|
|
vi.mock('../config/env', () => ({
|
|
config: {
|
|
database: { host: 'localhost', port: 5432, user: 'test', password: 'test', name: 'test' },
|
|
redis: { url: 'redis://localhost:6379' },
|
|
auth: { jwtSecret: 'test-secret' },
|
|
server: { port: 3000, host: 'localhost' },
|
|
featureFlags: {
|
|
bugsinkSync: false,
|
|
advancedRbac: false,
|
|
newDashboard: false,
|
|
betaRecipes: false,
|
|
experimentalAi: false,
|
|
debugMode: false,
|
|
},
|
|
},
|
|
isAiConfigured: vi.fn().mockReturnValue(false),
|
|
parseConfig: vi.fn(),
|
|
}));
|
|
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 () => {
|
|
const { mockLogger, createMockLogger } = await import('../tests/utils/mockLogger');
|
|
return {
|
|
logger: mockLogger,
|
|
createScopedLogger: vi.fn(() => createMockLogger()),
|
|
};
|
|
});
|
|
|
|
// 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';
|
|
import { userService } from '../services/userService';
|
|
|
|
// Mock the passport middleware
|
|
// Note: admin.routes.ts imports from '../config/passport', so we mock that path
|
|
vi.mock('../config/passport', () => ({
|
|
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/v1/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/v1/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({ users: mockUsers, total: 2 });
|
|
const response = await supertest(app).get('/api/v1/admin/users');
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toEqual({ users: mockUsers, total: 2 });
|
|
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/v1/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/v1/admin/users/${userId}`);
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).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/v1/admin/users/${missingId}`);
|
|
expect(response.status).toBe(404);
|
|
expect(response.body.error.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/v1/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/v1/admin/users/${userId}`)
|
|
.send({ role: 'admin' });
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).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/v1/admin/users/${missingId}`)
|
|
.send({ role: 'user' });
|
|
expect(response.status).toBe(404);
|
|
expect(response.body.error.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/v1/admin/users/${userId}`)
|
|
.send({ role: 'admin' });
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.error.message).toBe('DB Error');
|
|
});
|
|
|
|
it('should return 400 for an invalid role', async () => {
|
|
const response = await supertest(app)
|
|
.put(`/api/v1/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);
|
|
vi.mocked(userService.deleteUserAsAdmin).mockResolvedValue(undefined);
|
|
const response = await supertest(app).delete(`/api/v1/admin/users/${targetId}`);
|
|
expect(response.status).toBe(204);
|
|
expect(userService.deleteUserAsAdmin).toHaveBeenCalledWith(
|
|
adminId,
|
|
targetId,
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it('should prevent an admin from deleting their own account', async () => {
|
|
const validationError = new ValidationError([], 'Admins cannot delete their own account.');
|
|
vi.mocked(userService.deleteUserAsAdmin).mockRejectedValue(validationError);
|
|
const response = await supertest(app).delete(`/api/v1/admin/users/${adminId}`);
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error.message).toMatch(/Admins cannot delete their own account/);
|
|
expect(userRepo.deleteUserById).not.toHaveBeenCalled();
|
|
expect(userService.deleteUserAsAdmin).toHaveBeenCalledWith(
|
|
adminId,
|
|
adminId,
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
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);
|
|
vi.mocked(userService.deleteUserAsAdmin).mockRejectedValue(dbError);
|
|
const response = await supertest(app).delete(`/api/v1/admin/users/${targetId}`);
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
});
|