All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m24s
620 lines
26 KiB
TypeScript
620 lines
26 KiB
TypeScript
// src/routes/admin.test.ts
|
|
import { describe, it, expect, vi, beforeEach, type Mocked, type Mock } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import express, { Request, Response, NextFunction } from 'express';
|
|
import path from 'node:path';
|
|
import fs from 'node:fs/promises';
|
|
import adminRouter from './admin'; // Correctly imported
|
|
import * as db from '../services/db';
|
|
import { UserProfile } from '../types';
|
|
|
|
// Mock the entire db service
|
|
vi.mock('../services/db');
|
|
const mockedDb = db as Mocked<typeof db>;
|
|
|
|
// Mock the logger to keep test output clean
|
|
vi.mock('../services/logger.server', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
/**
|
|
* Mock the passport authentication middleware.
|
|
* This is the core of testing protected routes. We replace the real authentication
|
|
* with a flexible mock that we can control in each test.
|
|
*/
|
|
vi.mock('./passport', () => ({
|
|
// Mock the default export (the passport instance)
|
|
default: {
|
|
// The 'authenticate' method returns a middleware function. We mock that.
|
|
authenticate: vi.fn((strategy, options) => (req: Request, res: Response, next: NextFunction) => {
|
|
// This mock will be controlled by the isAdmin mock, but we ensure it calls next()
|
|
// to allow the isAdmin middleware to run.
|
|
next();
|
|
}),
|
|
},
|
|
// Mock the named export 'isAdmin'
|
|
isAdmin: vi.fn((_req: Request, res: Response, _next: NextFunction) => {
|
|
// The default behavior of this mock is to deny access.
|
|
// We will override this implementation in specific tests.
|
|
res.status(401).json({ message: 'Unauthorized' });
|
|
}),
|
|
}));
|
|
|
|
// We need to import the mocked 'isAdmin' so we can change its behavior in tests.
|
|
import { isAdmin } from './passport';
|
|
const mockedIsAdmin = isAdmin as Mock;
|
|
|
|
// Create a minimal Express app to host our router
|
|
const app = express();
|
|
app.use(express.json({ strict: false }));
|
|
app.use('/api/admin', adminRouter);
|
|
|
|
describe('Admin Routes (/api/admin)', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Reset the isAdmin mock to its default implementation before each test.
|
|
// This prevents mock configurations from one test leaking into another.
|
|
mockedIsAdmin.mockImplementation((_req: Request, res: Response, _next: NextFunction) => {
|
|
// The default behavior is to deny access, which is correct for unauthenticated tests.
|
|
res.status(401).json({ message: 'Unauthorized' });
|
|
});
|
|
});
|
|
|
|
it('should deny access if user is not an admin', async () => {
|
|
// Arrange: Configure the isAdmin mock to simulate a non-admin user.
|
|
// It will call next(), but since req.user.role is not 'admin', the real logic
|
|
// inside the original isAdmin would fail. Here, we just simulate the end result of a 403 Forbidden.
|
|
mockedIsAdmin.mockImplementation((_req: Request, res: Response, _next: NextFunction) => {
|
|
res.status(403).json({ message: 'Forbidden: Administrator access required.' });
|
|
});
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/corrections');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(403);
|
|
expect(response.body.message).toContain('Administrator access required');
|
|
});
|
|
|
|
it('should deny access if no user is authenticated', async () => {
|
|
// Arrange: The default mock behavior is to deny access, so no specific setup is needed.
|
|
// Let the default mock implementation run.
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/corrections');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(401);
|
|
expect(response.body.message).toBe('Unauthorized');
|
|
});
|
|
|
|
describe('when user is an admin', () => {
|
|
beforeEach(() => {
|
|
// Arrange: For all tests in this block, simulate a logged-in admin user.
|
|
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => {
|
|
// Attach a mock admin user profile to the request object.
|
|
// The UserProfile type has a nested `user` object.
|
|
req.user = {
|
|
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
|
role: 'admin',
|
|
// Add missing properties to align with the UserProfile type
|
|
points: 0,
|
|
} as UserProfile;
|
|
next(); // Grant access
|
|
});
|
|
});
|
|
|
|
it('GET /corrections should return corrections data', async () => {
|
|
// Arrange
|
|
const mockCorrections: Awaited<ReturnType<typeof db.getSuggestedCorrections>> = [{ suggested_correction_id: 1, flyer_item_id: 1, user_id: '1', correction_type: 'price', suggested_value: 'New Price', status: 'pending', created_at: new Date().toISOString() }];
|
|
vi.mocked(mockedDb.getSuggestedCorrections).mockResolvedValue(mockCorrections);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/corrections');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockCorrections);
|
|
expect(mockedDb.getSuggestedCorrections).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
describe('GET /brands', () => {
|
|
it('should return a list of all brands on success', async () => {
|
|
// Arrange
|
|
const mockBrands: Awaited<ReturnType<typeof db.getAllBrands>> = [
|
|
{ brand_id: 1, name: 'Brand A', logo_url: '/path/a.png' },
|
|
{ brand_id: 2, name: 'Brand B', logo_url: '/path/b.png' },
|
|
];
|
|
vi.mocked(mockedDb.getAllBrands).mockResolvedValue(mockBrands);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/brands');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockBrands);
|
|
expect(mockedDb.getAllBrands).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should return a 500 error if the database call fails', async () => {
|
|
mockedDb.getAllBrands.mockRejectedValue(new Error('Failed to fetch brands'));
|
|
const response = await supertest(app).get('/api/admin/brands');
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe('GET /stats', () => {
|
|
it('should return application stats on success', async () => {
|
|
// Arrange
|
|
const mockStats = {
|
|
flyerCount: 150,
|
|
userCount: 42,
|
|
flyerItemCount: 10000,
|
|
storeCount: 12,
|
|
pendingCorrectionCount: 5,
|
|
};
|
|
mockedDb.getApplicationStats.mockResolvedValue(mockStats);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/stats');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockStats);
|
|
expect(mockedDb.getApplicationStats).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should return a 500 error if the database call fails', async () => {
|
|
mockedDb.getApplicationStats.mockRejectedValue(new Error('Failed to fetch stats'));
|
|
const response = await supertest(app).get('/api/admin/stats');
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe('GET /stats/daily', () => {
|
|
it('should return daily stats on success', async () => {
|
|
// Arrange
|
|
const mockDailyStats = [
|
|
{ date: '2024-01-01', new_users: 5, new_flyers: 10 },
|
|
{ date: '2024-01-02', new_users: 3, new_flyers: 8 },
|
|
] as Awaited<ReturnType<typeof db.getDailyStatsForLast30Days>>;
|
|
vi.mocked(mockedDb.getDailyStatsForLast30Days).mockResolvedValue(mockDailyStats);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/stats/daily');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockDailyStats);
|
|
expect(mockedDb.getDailyStatsForLast30Days).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should return a 500 error if the database call fails', async () => {
|
|
mockedDb.getDailyStatsForLast30Days.mockRejectedValue(new Error('Failed to fetch daily stats'));
|
|
const response = await supertest(app).get('/api/admin/stats/daily');
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe('GET /unmatched-items', () => {
|
|
it('should return a list of unmatched items on success', async () => {
|
|
// Arrange
|
|
const mockUnmatchedItems: Awaited<ReturnType<typeof db.getUnmatchedFlyerItems>> = [
|
|
{ unmatched_flyer_item_id: 1, status: 'pending', created_at: new Date().toISOString(), flyer_item_id: 101, flyer_item_name: 'Ketchup Chips', price_display: '$3.00', flyer_id: 1, store_name: 'Test Store' },
|
|
{ unmatched_flyer_item_id: 2, status: 'pending', created_at: new Date().toISOString(), flyer_item_id: 102, flyer_item_name: 'Mystery Soda', price_display: '2 for $4.00', flyer_id: 1, store_name: 'Test Store' },
|
|
];
|
|
vi.mocked(mockedDb.getUnmatchedFlyerItems).mockResolvedValue(mockUnmatchedItems);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/unmatched-items');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockUnmatchedItems);
|
|
expect(mockedDb.getUnmatchedFlyerItems).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should return a 500 error if the database call fails', async () => {
|
|
mockedDb.getUnmatchedFlyerItems.mockRejectedValue(new Error('Failed to fetch unmatched items'));
|
|
const response = await supertest(app).get('/api/admin/unmatched-items');
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe('POST /corrections/:id/approve', () => {
|
|
it('should approve a correction and return a success message', async () => {
|
|
// Arrange
|
|
const correctionId = 123;
|
|
mockedDb.approveCorrection.mockResolvedValue(undefined); // Mock the DB call to succeed
|
|
|
|
// Act
|
|
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({ message: 'Correction approved successfully.' });
|
|
expect(mockedDb.approveCorrection).toHaveBeenCalledTimes(1);
|
|
expect(mockedDb.approveCorrection).toHaveBeenCalledWith(correctionId);
|
|
});
|
|
|
|
it('should return a 500 error if the database call fails', async () => {
|
|
// Arrange
|
|
const correctionId = 456;
|
|
mockedDb.approveCorrection.mockRejectedValue(new Error('Database failure'));
|
|
|
|
// Act
|
|
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(500);
|
|
});
|
|
|
|
it('should return a 400 error for a non-numeric correction ID', async () => {
|
|
// Act
|
|
const response = await supertest(app).post('/api/admin/corrections/abc/approve');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.message).toBe('Invalid correction ID provided.');
|
|
expect(mockedDb.approveCorrection).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('POST /corrections/:id/reject', () => {
|
|
it('should reject a correction and return a success message', async () => {
|
|
// Arrange
|
|
const correctionId = 789;
|
|
mockedDb.rejectCorrection.mockResolvedValue(undefined); // Mock the DB call to succeed
|
|
|
|
// Act
|
|
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({ message: 'Correction rejected successfully.' });
|
|
expect(mockedDb.rejectCorrection).toHaveBeenCalledTimes(1);
|
|
expect(mockedDb.rejectCorrection).toHaveBeenCalledWith(correctionId);
|
|
});
|
|
|
|
it('should return a 500 error if the database call fails', async () => {
|
|
// Arrange
|
|
const correctionId = 987;
|
|
mockedDb.rejectCorrection.mockRejectedValue(new Error('Database failure'));
|
|
|
|
// Act
|
|
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe('PUT /corrections/:id', () => {
|
|
it('should update a correction and return the updated data', async () => {
|
|
// Arrange
|
|
const correctionId = 101;
|
|
const requestBody = { suggested_value: 'A new corrected value' };
|
|
const mockUpdatedCorrection: Awaited<ReturnType<typeof db.updateSuggestedCorrection>> = { suggested_correction_id: correctionId, flyer_item_id: 1, user_id: '1', correction_type: 'price', status: 'pending', created_at: new Date().toISOString(), ...requestBody };
|
|
vi.mocked(mockedDb.updateSuggestedCorrection).mockResolvedValue(mockUpdatedCorrection);
|
|
|
|
// Act: Use .send() to include a request body
|
|
const response = await supertest(app)
|
|
.put(`/api/admin/corrections/${correctionId}`)
|
|
.send(requestBody);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockUpdatedCorrection);
|
|
expect(mockedDb.updateSuggestedCorrection).toHaveBeenCalledTimes(1);
|
|
expect(mockedDb.updateSuggestedCorrection).toHaveBeenCalledWith(correctionId, requestBody.suggested_value);
|
|
});
|
|
|
|
it('should return a 400 error if suggested_value is missing from the body', async () => {
|
|
// Arrange
|
|
const correctionId = 102;
|
|
|
|
// Act: Send an empty body
|
|
const response = await supertest(app)
|
|
.put(`/api/admin/corrections/${correctionId}`)
|
|
.send({});
|
|
|
|
// Assert
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.message).toBe('A new suggested_value is required.');
|
|
// Ensure the database was not called
|
|
expect(mockedDb.updateSuggestedCorrection).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return a 404 error if the correction to update is not found', async () => {
|
|
// Arrange
|
|
const correctionId = 999; // A non-existent ID
|
|
const requestBody = { suggested_value: 'This will fail' };
|
|
// Mock the DB function to throw a "not found" error, simulating the real DB behavior.
|
|
mockedDb.updateSuggestedCorrection.mockRejectedValue(new Error(`Correction with ID ${correctionId} not found.`));
|
|
|
|
// Act
|
|
const response = await supertest(app)
|
|
.put(`/api/admin/corrections/${correctionId}`)
|
|
.send(requestBody);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(404);
|
|
expect(response.body.message).toContain(`Correction with ID ${correctionId} not found.`);
|
|
});
|
|
});
|
|
|
|
describe('POST /brands/:id/logo', () => {
|
|
it('should upload a logo and update the brand', async () => {
|
|
// Arrange
|
|
const brandId = 55;
|
|
mockedDb.updateBrandLogo.mockResolvedValue(undefined); // Mock the DB call
|
|
|
|
// Create a dummy file for supertest to attach.
|
|
// supertest needs a real file path to stream from.
|
|
const dummyFilePath = path.resolve(__dirname, 'test-logo.png');
|
|
await fs.writeFile(dummyFilePath, 'dummy content');
|
|
|
|
// Act: Use .attach() to simulate a file upload.
|
|
// The first argument is the field name from upload.single('logoImage').
|
|
const response = await supertest(app)
|
|
.post(`/api/admin/brands/${brandId}/logo`)
|
|
.attach('logoImage', dummyFilePath);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.message).toBe('Brand logo updated successfully.');
|
|
expect(response.body.logoUrl).toMatch(/^\/assets\/logoImage-/); // Check for the generated URL format
|
|
|
|
// Verify the database was updated with the correct brand ID and a generated URL
|
|
expect(mockedDb.updateBrandLogo).toHaveBeenCalledTimes(1);
|
|
expect(mockedDb.updateBrandLogo).toHaveBeenCalledWith(brandId, expect.stringContaining('/assets/'));
|
|
|
|
// Clean up the dummy file
|
|
await fs.unlink(dummyFilePath);
|
|
});
|
|
|
|
// Add a cleanup hook to remove the file created on the server by multer
|
|
afterAll(async () => {
|
|
const uploadDir = path.resolve(__dirname, '../../../flyer-images');
|
|
try {
|
|
const files = await fs.readdir(uploadDir);
|
|
const testFiles = files.filter(f => f.startsWith('logoImage-'));
|
|
for (const file of testFiles) {
|
|
await fs.unlink(path.join(uploadDir, file));
|
|
}
|
|
} catch (error) { console.error('Error during admin test file cleanup:', error); }
|
|
});
|
|
|
|
it('should return a 400 error if no logo image is provided', async () => {
|
|
// Arrange
|
|
const brandId = 56;
|
|
|
|
// Act: Make the request without attaching a file
|
|
const response = await supertest(app).post(`/api/admin/brands/${brandId}/logo`);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.message).toBe('Logo image file is required.');
|
|
});
|
|
});
|
|
|
|
describe('PUT /recipes/:id/status', () => {
|
|
it('should update a recipe status and return the updated recipe', async () => {
|
|
// Arrange
|
|
const recipeId = 201;
|
|
const requestBody = { status: 'public' as const };
|
|
const mockUpdatedRecipe: Awaited<ReturnType<typeof db.updateRecipeStatus>> = { recipe_id: recipeId, status: 'public', name: 'Test Recipe', avg_rating: 0, rating_count: 0, fork_count: 0, created_at: new Date().toISOString() };
|
|
vi.mocked(mockedDb.updateRecipeStatus).mockResolvedValue(mockUpdatedRecipe);
|
|
|
|
// Act
|
|
const response = await supertest(app)
|
|
.put(`/api/admin/recipes/${recipeId}/status`)
|
|
.send(requestBody);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockUpdatedRecipe);
|
|
expect(mockedDb.updateRecipeStatus).toHaveBeenCalledTimes(1);
|
|
expect(mockedDb.updateRecipeStatus).toHaveBeenCalledWith(recipeId, 'public');
|
|
});
|
|
|
|
it('should return a 400 error for an invalid status value', async () => {
|
|
// Arrange
|
|
const recipeId = 202;
|
|
const requestBody = { status: 'not_a_valid_status' };
|
|
|
|
// Act
|
|
const response = await supertest(app)
|
|
.put(`/api/admin/recipes/${recipeId}/status`)
|
|
.send(requestBody);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.message).toBe('A valid status (private, pending_review, public, rejected) is required.');
|
|
expect(mockedDb.updateRecipeStatus).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('PUT /comments/:id/status', () => {
|
|
it('should update a comment status and return the updated comment', async () => {
|
|
// Arrange
|
|
const commentId = 301;
|
|
const requestBody = { status: 'hidden' as const };
|
|
const mockUpdatedComment: Awaited<ReturnType<typeof db.updateRecipeCommentStatus>> = { recipe_comment_id: commentId, recipe_id: 1, user_id: '1', status: 'hidden', content: 'Test Comment', created_at: new Date().toISOString() };
|
|
vi.mocked(mockedDb.updateRecipeCommentStatus).mockResolvedValue(mockUpdatedComment);
|
|
|
|
// Act
|
|
const response = await supertest(app)
|
|
.put(`/api/admin/comments/${commentId}/status`)
|
|
.send(requestBody);
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockUpdatedComment);
|
|
expect(mockedDb.updateRecipeCommentStatus).toHaveBeenCalledTimes(1);
|
|
expect(mockedDb.updateRecipeCommentStatus).toHaveBeenCalledWith(commentId, 'hidden');
|
|
});
|
|
|
|
it('should return a 400 error for an invalid status value', async () => {
|
|
const response = await supertest(app).put('/api/admin/comments/302/status').send({ status: 'invalid' });
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.message).toBe('A valid status (visible, hidden, reported) is required.');
|
|
expect(mockedDb.updateRecipeCommentStatus).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('GET /users', () => {
|
|
it('should return a list of all users on success', async () => {
|
|
// Arrange
|
|
const mockUsers: Awaited<ReturnType<typeof db.getAllUsers>> = [
|
|
{ user_id: '1', email: 'user1@test.com', role: 'user', created_at: new Date().toISOString(), full_name: 'User One', avatar_url: null },
|
|
{ user_id: '2', email: 'user2@test.com', role: 'admin', created_at: new Date().toISOString(), full_name: 'Admin Two', avatar_url: null },
|
|
];
|
|
vi.mocked(mockedDb.getAllUsers).mockResolvedValue(mockUsers);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/users');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockUsers);
|
|
expect(mockedDb.getAllUsers).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should return a 500 error if the database call fails', async () => {
|
|
vi.mocked(mockedDb.getAllUsers).mockRejectedValue(new Error('Failed to fetch users'));
|
|
const response = await supertest(app).get('/api/admin/users');
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe('GET /activity-log', () => {
|
|
it('should return a list of activity logs with default pagination', async () => {
|
|
// Arrange
|
|
const mockLogs: Awaited<ReturnType<typeof db.getActivityLog>> = [{ activity_log_id: 1, action: 'user_registered', display_text: 'test', created_at: new Date().toISOString(), user_id: '1', details: { full_name: 'test', user_avatar_url: 'test', user_full_name: 'test' } }];
|
|
vi.mocked(mockedDb.getActivityLog).mockResolvedValue(mockLogs);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/activity-log');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockLogs);
|
|
expect(mockedDb.getActivityLog).toHaveBeenCalledTimes(1);
|
|
// Check that default pagination values were used
|
|
// This makes the test more robust by verifying the correct parameters were passed.
|
|
expect(mockedDb.getActivityLog).toHaveBeenCalledWith(50, 0);
|
|
});
|
|
|
|
it('should use limit and offset query parameters when provided', async () => {
|
|
mockedDb.getActivityLog.mockResolvedValue([]);
|
|
|
|
await supertest(app).get('/api/admin/activity-log?limit=10&offset=20');
|
|
|
|
expect(mockedDb.getActivityLog).toHaveBeenCalledTimes(1);
|
|
expect(mockedDb.getActivityLog).toHaveBeenCalledWith(10, 20);
|
|
});
|
|
|
|
it('should handle invalid pagination parameters gracefully', async () => {
|
|
mockedDb.getActivityLog.mockResolvedValue([]);
|
|
|
|
// Act: Send non-numeric query parameters
|
|
await supertest(app).get('/api/admin/activity-log?limit=abc&offset=xyz');
|
|
|
|
// Assert: The route should fall back to the default values
|
|
expect(mockedDb.getActivityLog).toHaveBeenCalledWith(50, 0);
|
|
});
|
|
|
|
it('should return a 500 error if the database call fails', async () => {
|
|
// Arrange
|
|
mockedDb.getActivityLog.mockRejectedValue(new Error('DB connection error'));
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/activity-log');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe('GET /users/:id', () => {
|
|
it('should fetch a single user successfully', async () => {
|
|
// Arrange
|
|
const mockUser: Awaited<ReturnType<typeof db.findUserProfileById>> = { user_id: 'user-123', role: 'user', points: 0 };
|
|
vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(mockUser);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/users/user-123');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockUser);
|
|
expect(mockedDb.findUserProfileById).toHaveBeenCalledWith('user-123');
|
|
});
|
|
|
|
it('should return 404 for a non-existent user', async () => {
|
|
// Arrange
|
|
vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(undefined);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/users/non-existent-id');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(404);
|
|
expect(response.body.message).toBe('User not found.');
|
|
});
|
|
});
|
|
|
|
describe('PUT /users/:id', () => {
|
|
it('should update a user role successfully', async () => {
|
|
// Arrange
|
|
const updatedUser: Awaited<ReturnType<typeof db.updateUserRole>> = { user_id: 'user-to-update', email: 'test@test.com' };
|
|
vi.mocked(mockedDb.updateUserRole).mockResolvedValue(updatedUser);
|
|
|
|
// Act
|
|
const response = await supertest(app)
|
|
.put('/api/admin/users/user-to-update')
|
|
.send({ role: 'admin' });
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(updatedUser);
|
|
expect(mockedDb.updateUserRole).toHaveBeenCalledWith('user-to-update', 'admin');
|
|
});
|
|
|
|
it('should return 404 for a non-existent user', async () => {
|
|
vi.mocked(mockedDb.updateUserRole).mockRejectedValue(new Error('User with ID non-existent not found.'));
|
|
const response = await supertest(app).put('/api/admin/users/non-existent').send({ role: 'user' });
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should return 400 for an invalid role', async () => {
|
|
const response = await supertest(app).put('/api/admin/users/any-id').send({ role: 'invalid-role' });
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.message).toBe('A valid role ("user" or "admin") is required.');
|
|
});
|
|
});
|
|
|
|
describe('DELETE /users/:id', () => {
|
|
it('should successfully delete a user', async () => {
|
|
vi.mocked(mockedDb.deleteUserById).mockResolvedValue(undefined);
|
|
const response = await supertest(app).delete('/api/admin/users/user-to-delete');
|
|
expect(response.status).toBe(204);
|
|
expect(mockedDb.deleteUserById).toHaveBeenCalledWith('user-to-delete');
|
|
});
|
|
|
|
it('should prevent an admin from deleting their own account', async () => {
|
|
// The admin user ID is 'admin-user-id' from the beforeEach hook
|
|
const response = await supertest(app).delete('/api/admin/users/admin-user-id');
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.message).toBe('Admins cannot delete their own account.');
|
|
expect(mockedDb.deleteUserById).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
}); |