testing admin routes
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 3m49s

This commit is contained in:
2025-11-28 17:12:52 -08:00
parent c244844f2b
commit a7fe9e085b
6 changed files with 1407 additions and 7 deletions

1198
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,9 @@
"eslint-plugin-react-refresh": "^0.4.24",
"glob": "^13.0.0",
"globals": "16.5.0",
"istanbul-reports": "^3.2.0",
"jsdom": "^27.2.0",
"nyc": "^17.1.0",
"postcss": "^8.5.6",
"rimraf": "^6.1.2",
"supertest": "^7.1.4",

View File

@@ -251,6 +251,16 @@ describe('Admin Routes (/api/admin)', () => {
// 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', () => {
@@ -317,6 +327,23 @@ describe('Admin Routes (/api/admin)', () => {
// 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', () => {
@@ -477,6 +504,102 @@ describe('Admin Routes (/api/admin)', () => {
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 = { user_id: 'user-123', email: 'single@test.com', role: 'user' };
(mockedDb.findUserProfileById as Mocked<any>).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
(mockedDb.findUserProfileById as Mocked<any>).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 = { user_id: 'user-to-update', role: 'admin' };
(mockedDb.updateUserRole as Mocked<any>).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 () => {
(mockedDb.updateUserRole as Mocked<any>).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 () => {
(mockedDb.deleteUserById as Mocked<any>).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();
});
});
});
});

View File

@@ -69,6 +69,11 @@ router.get('/stats/daily', async (req: Request, res: Response, next: NextFunctio
router.post('/corrections/:id/approve', async (req: Request, res: Response, next: NextFunction) => {
const correctionId = parseInt(req.params.id, 10);
// Add validation to ensure the ID is a valid number.
if (isNaN(correctionId)) {
return res.status(400).json({ message: 'Invalid correction ID provided.' });
}
try {
await db.approveCorrection(correctionId);
res.status(200).json({ message: 'Correction approved successfully.' });
@@ -99,6 +104,10 @@ router.put('/corrections/:id', async (req: Request, res: Response, next: NextFun
const updatedCorrection = await db.updateSuggestedCorrection(correctionId, suggested_value);
res.status(200).json(updatedCorrection);
} catch (error) {
// Check if the error message indicates "not found" to return a 404
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ message: error.message });
}
next(error);
}
});
@@ -182,4 +191,50 @@ router.get('/activity-log', async (req: Request, res: Response, next: NextFuncti
}
});
router.get('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await db.findUserProfileById(req.params.id);
if (!user) {
return res.status(404).json({ message: 'User not found.' });
}
res.json(user);
} catch (error) {
logger.error(`Error fetching user ${req.params.id}:`, { error });
next(error);
}
});
router.put('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
const { role } = req.body;
if (!role || !['user', 'admin'].includes(role)) {
return res.status(400).json({ message: 'A valid role ("user" or "admin") is required.' });
}
try {
const updatedUser = await db.updateUserRole(req.params.id, role);
res.json(updatedUser);
} catch (error) {
// Check if the error message indicates "not found" to return a 404
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ message: error.message });
}
logger.error(`Error updating user ${req.params.id}:`, { error });
next(error);
}
});
router.delete('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile;
if (adminUser.user.user_id === req.params.id) {
return res.status(400).json({ message: 'Admins cannot delete their own account.' });
}
try {
await db.deleteUserById(req.params.id);
res.status(204).send();
} catch (error) {
logger.error(`Error deleting user ${req.params.id}:`, { error });
next(error);
}
});
export default router;

View File

@@ -2,7 +2,8 @@
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import path from 'path';
import path from 'node:path';
import fs from 'node:fs/promises';
import aiRouter from './ai';
import * as aiService from '../services/aiService.server';
@@ -55,16 +56,15 @@ describe('AI Routes (/api/ai)', () => {
const mockMasterItems = [{ master_item_id: 1, item_name: 'Milk' }];
// 3. Define the path to a dummy file to upload.
// This can be any file; its content doesn't matter since we're mocking the AI service.
// We create a dummy file path. A real file isn't needed for this mock.
const dummyFilePath = path.resolve(__dirname, 'test-asset.txt');
// The path is resolved from the current file's directory (`src/routes`) to the assets directory.
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
// Act:
// Use supertest to build the multipart/form-data request.
const response = await supertest(app)
.post('/api/ai/process-flyer')
.field('masterItems', JSON.stringify(mockMasterItems)) // Attach regular form fields
.attach('flyerImages', dummyFilePath); // Attach the file for upload
.attach('flyerImages', imagePath); // Attach the file for upload
// Assert:
// 1. Check for a successful HTTP status.
@@ -75,7 +75,7 @@ describe('AI Routes (/api/ai)', () => {
// received an array of files with these properties.
expect(mockedAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(1);
expect(mockedAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledWith(
expect.arrayContaining([expect.objectContaining({ mimetype: 'text/plain' })]),
expect.arrayContaining([expect.objectContaining({ mimetype: 'image/jpeg' })]),
mockMasterItems
);

View File

@@ -427,4 +427,26 @@ export const getAllUsers = async (): Promise<AdminUserView[]> => {
`;
const res = await pool.query<AdminUserView>(query);
return res.rows;
};
};
/**
* Updates the role of a specific user.
* @param userId The ID of the user to update.
* @param role The new role to assign ('user' or 'admin').
* @returns A promise that resolves to the updated Profile object.
*/
export async function updateUserRole(userId: string, role: 'user' | 'admin'): Promise<User> {
try {
const res = await getPool().query<User>(
'UPDATE public.profiles SET role = $1 WHERE user_id = $2 RETURNING *',
[role, userId]
);
if (res.rowCount === 0) {
throw new Error(`User with ID ${userId} not found.`);
}
return res.rows[0];
} catch (error) {
logger.error('Database error in updateUserRole:', { error, userId, role });
throw error; // Re-throw to be handled by the route
}
}