more unit test fixes + some integration
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h21m56s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h21m56s
This commit is contained in:
@@ -144,6 +144,13 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
expect(vi.mocked(mockedDb.adminRepo.approveCorrection)).toHaveBeenCalledWith(correctionId, expect.anything());
|
||||
});
|
||||
|
||||
it('POST /corrections/:id/approve should return 500 on DB error', async () => {
|
||||
const correctionId = 123;
|
||||
vi.mocked(mockedDb.adminRepo.approveCorrection).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`);
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('POST /corrections/:id/reject should reject a correction', async () => {
|
||||
const correctionId = 789;
|
||||
vi.mocked(mockedDb.adminRepo.rejectCorrection).mockResolvedValue(undefined);
|
||||
@@ -152,6 +159,13 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
expect(response.body).toEqual({ message: 'Correction rejected successfully.' });
|
||||
});
|
||||
|
||||
it('POST /corrections/:id/reject should return 500 on DB error', async () => {
|
||||
const correctionId = 789;
|
||||
vi.mocked(mockedDb.adminRepo.rejectCorrection).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`);
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('PUT /corrections/:id should update a correction', async () => {
|
||||
const correctionId = 101;
|
||||
const requestBody = { suggested_value: 'A new corrected value' };
|
||||
@@ -197,6 +211,13 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
expect(vi.mocked(mockedDb.adminRepo.updateBrandLogo)).toHaveBeenCalledWith(brandId, expect.stringContaining('/assets/'), expect.anything());
|
||||
});
|
||||
|
||||
it('POST /brands/:id/logo should return 500 on DB error', async () => {
|
||||
const brandId = 55;
|
||||
vi.mocked(mockedDb.adminRepo.updateBrandLogo).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).post(`/api/admin/brands/${brandId}/logo`).attach('logoImage', Buffer.from('dummy-logo-content'), 'test-logo.png');
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('POST /brands/:id/logo should return 400 if no file is uploaded', async () => {
|
||||
const response = await supertest(app).post('/api/admin/brands/55/logo');
|
||||
expect(response.status).toBe(400);
|
||||
@@ -225,6 +246,13 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('DELETE /recipes/:recipeId should return 500 on DB error', async () => {
|
||||
const recipeId = 300;
|
||||
vi.mocked(mockedDb.recipeRepo.deleteRecipe).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).delete(`/api/admin/recipes/${recipeId}`);
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('PUT /recipes/:id/status should update a recipe status', async () => {
|
||||
const recipeId = 201;
|
||||
const requestBody = { status: 'public' as const };
|
||||
@@ -242,6 +270,14 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('PUT /recipes/:id/status should return 500 on DB error', async () => {
|
||||
const recipeId = 201;
|
||||
const requestBody = { status: 'public' as const };
|
||||
vi.mocked(mockedDb.adminRepo.updateRecipeStatus).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).put(`/api/admin/recipes/${recipeId}/status`).send(requestBody);
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('PUT /comments/:id/status should update a comment status', async () => {
|
||||
const commentId = 301;
|
||||
const requestBody = { status: 'hidden' as const };
|
||||
@@ -259,6 +295,14 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('PUT /comments/:id/status should return 500 on DB error', async () => {
|
||||
const commentId = 301;
|
||||
const requestBody = { status: 'hidden' as const };
|
||||
vi.mocked(mockedDb.adminRepo.updateRecipeCommentStatus).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).put(`/api/admin/comments/${commentId}/status`).send(requestBody);
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Unmatched Items Route', () => {
|
||||
@@ -270,6 +314,12 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockUnmatchedItems);
|
||||
});
|
||||
|
||||
it('GET /unmatched-items should return 500 on DB error', async () => {
|
||||
vi.mocked(mockedDb.adminRepo.getUnmatchedFlyerItems).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).get('/api/admin/unmatched-items');
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Flyer Routes', () => {
|
||||
|
||||
@@ -97,6 +97,13 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
||||
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', () => {
|
||||
@@ -116,6 +123,13 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
||||
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', () => {
|
||||
@@ -141,6 +155,14 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
||||
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}`)
|
||||
@@ -164,5 +186,13 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// src/routes/ai.routes.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import fs from 'node:fs';
|
||||
import { type Request, type Response, type NextFunction } from 'express';
|
||||
import path from 'node:path';
|
||||
import type { Job } from 'bullmq';
|
||||
@@ -69,6 +70,30 @@ describe('AI Routes (/api/ai)', () => {
|
||||
});
|
||||
const app = createTestApp({ router: aiRouter, basePath: '/api/ai' });
|
||||
|
||||
describe('Module-level error handling', () => {
|
||||
it('should log an error if storage path creation fails', async () => {
|
||||
// Arrange
|
||||
const mkdirError = new Error('EACCES: permission denied');
|
||||
vi.resetModules(); // Reset modules to re-run top-level code
|
||||
vi.doMock('node:fs', () => ({
|
||||
default: {
|
||||
...fs, // Keep other fs functions
|
||||
mkdirSync: vi.fn().mockImplementation(() => {
|
||||
throw mkdirError;
|
||||
}),
|
||||
},
|
||||
}));
|
||||
const { logger } = await import('../services/logger.server');
|
||||
|
||||
// Act: Dynamically import the router to trigger the mkdirSync call
|
||||
await import('./ai.routes');
|
||||
|
||||
// Assert
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: 'EACCES: permission denied' }, `Failed to create storage path (/var/www/flyer-crawler.projectium.com/flyer-images). File uploads may fail.`);
|
||||
vi.doUnmock('node:fs'); // Cleanup
|
||||
});
|
||||
});
|
||||
|
||||
// Add a basic error handler to capture errors passed to next(err) and return JSON.
|
||||
// This prevents unhandled error crashes in tests and ensures we get the 500 response we expect.
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
@@ -314,6 +339,10 @@ describe('AI Routes (/api/ai)', () => {
|
||||
// verify the flyerData.store_name passed to DB was the fallback string
|
||||
const flyerDataArg = vi.mocked(mockedDb.createFlyerAndItems).mock.calls[0][0];
|
||||
expect(flyerDataArg.store_name).toContain('Unknown Store');
|
||||
// Also verify the warning was logged
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
'extractedData.store_name missing; using fallback store name to avoid DB constraint error.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a generic error during flyer creation', async () => {
|
||||
@@ -382,6 +411,14 @@ describe('AI Routes (/api/ai)', () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.is_flyer).toBe(true);
|
||||
});
|
||||
|
||||
it('should return 500 on a generic error', async () => {
|
||||
// To trigger the catch block, we can cause the middleware to fail.
|
||||
// A simple way is to mock the service to throw an error.
|
||||
vi.mocked(aiService.aiService.extractTextFromImageArea).mockRejectedValue(new Error('Generic Error')); // Not used by route, but triggers catch
|
||||
const response = await supertest(app).post('/api/ai/check-flyer').attach('image', Buffer.from('')); // Empty buffer might cause issues
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /rescan-area', () => {
|
||||
@@ -402,6 +439,14 @@ describe('AI Routes (/api/ai)', () => {
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.errors[0].message).toMatch(/cropArea must be a valid JSON string|Required/i);
|
||||
});
|
||||
|
||||
it('should return 400 if cropArea is malformed JSON', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/rescan-area')
|
||||
.attach('image', imagePath)
|
||||
.field('cropArea', '{ "x": 0, "y": 0, "width": 10, "height": 10'); // Malformed
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /extract-address', () => {
|
||||
@@ -416,6 +461,12 @@ describe('AI Routes (/api/ai)', () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.address).toBe('not identified');
|
||||
});
|
||||
|
||||
it('should return 500 on a generic error', async () => {
|
||||
// An empty buffer can sometimes cause underlying libraries to throw an error
|
||||
const response = await supertest(app).post('/api/ai/extract-address').attach('image', Buffer.from(''));
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /extract-logo', () => {
|
||||
@@ -430,6 +481,12 @@ describe('AI Routes (/api/ai)', () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.store_logo_base_64).toBeNull();
|
||||
});
|
||||
|
||||
it('should return 500 on a generic error', async () => {
|
||||
// An empty buffer can sometimes cause underlying libraries to throw an error
|
||||
const response = await supertest(app).post('/api/ai/extract-logo').attach('images', Buffer.from(''));
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /rescan-area (authenticated)', () => { // This was a duplicate, fixed.
|
||||
@@ -494,6 +551,15 @@ describe('AI Routes (/api/ai)', () => {
|
||||
expect(response.body.text).toContain('server-generated quick insight');
|
||||
});
|
||||
|
||||
it('POST /quick-insights should return 500 on a generic error', async () => {
|
||||
// To hit the catch block, we can simulate an error by making the logger throw.
|
||||
vi.mocked(mockLogger.info).mockImplementation(() => { throw new Error('Logging failed'); });
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/quick-insights')
|
||||
.send({ items: [] });
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('POST /deep-dive should return the stubbed response', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/deep-dive')
|
||||
|
||||
@@ -32,6 +32,8 @@ const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.s
|
||||
const uploadAndProcessSchema = z.object({
|
||||
body: z.object({
|
||||
checksum: requiredString('File checksum is required.'),
|
||||
// Potential improvement: If checksum is always a specific format (e.g., SHA-256),
|
||||
// you could add `.length(64).regex(/^[a-f0-9]+$/)` for stricter validation.
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -48,6 +50,13 @@ const errMsg = (e: unknown) => {
|
||||
return String(e || 'An unknown error occurred.');
|
||||
};
|
||||
|
||||
const cropAreaObjectSchema = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
width: z.number().positive('Crop area width must be positive.'),
|
||||
height: z.number().positive('Crop area height must be positive.'),
|
||||
});
|
||||
|
||||
const rescanAreaSchema = z.object({
|
||||
body: z.object({
|
||||
cropArea: requiredString('cropArea must be a valid JSON string.').transform((val, ctx) => {
|
||||
@@ -57,30 +66,37 @@ const rescanAreaSchema = z.object({
|
||||
logger.warn({ error: errMsg(err), receivedValue: val }, 'Failed to parse cropArea in rescanAreaSchema');
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'cropArea must be a valid JSON string.' }); return z.NEVER;
|
||||
}
|
||||
}).pipe(cropAreaObjectSchema), // Further validate the structure of the parsed object
|
||||
extractionType: z.enum(['store_name', 'dates', 'item_details'], { // This is the line with the error
|
||||
message: "extractionType must be one of 'store_name', 'dates', or 'item_details'."
|
||||
}),
|
||||
extractionType: requiredString('extractionType is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
const flyerItemForAnalysisSchema = z.object({
|
||||
name: requiredString("Item name is required."),
|
||||
// Allow other properties to pass through without validation
|
||||
}).passthrough();
|
||||
|
||||
const insightsSchema = z.object({
|
||||
body: z.object({
|
||||
items: z.array(z.any()), // Define more strictly if item structure is known
|
||||
items: z.array(flyerItemForAnalysisSchema).nonempty("The 'items' array cannot be empty."),
|
||||
}),
|
||||
});
|
||||
|
||||
const comparePricesSchema = z.object({
|
||||
body: z.object({
|
||||
items: z.array(z.any()), // Define more strictly if item structure is known
|
||||
items: z.array(flyerItemForAnalysisSchema).nonempty("The 'items' array cannot be empty."),
|
||||
}),
|
||||
});
|
||||
|
||||
const planTripSchema = z.object({
|
||||
body: z.object({
|
||||
items: z.array(z.any()),
|
||||
store: z.object({ name: z.string() }),
|
||||
items: z.array(flyerItemForAnalysisSchema),
|
||||
store: z.object({ name: requiredString("Store name is required.") }),
|
||||
userLocation: z.object({
|
||||
latitude: z.number(),
|
||||
longitude: z.number(),
|
||||
latitude: z.number().min(-90, 'Latitude must be between -90 and 90.').max(90, 'Latitude must be between -90 and 90.'),
|
||||
longitude: z.number().min(-180, 'Longitude must be between -180 and 180.').max(180, 'Longitude must be between -180 and 180.'),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -134,6 +134,13 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.errors).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should return 400 if required fields are missing', async () => {
|
||||
// This test covers the `val ?? ''` part of the `requiredString` helper
|
||||
const response = await supertest(app).post('/api/budgets').send({ amount_cents: 10000, period: 'monthly', start_date: '2024-01-01' });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.errors[0].message).toBe('Budget name is required.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /:id', () => {
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
||||
});
|
||||
|
||||
describe('POST /items/:itemId/track', () => {
|
||||
it('should return 202 Accepted and call the tracking function', async () => {
|
||||
it('should return 202 Accepted and call the tracking function for "click"', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/flyers/items/99/track')
|
||||
.send({ type: 'click' });
|
||||
@@ -232,6 +232,15 @@ describe('Flyer Routes (/api/flyers)', () => {
|
||||
expect(db.flyerRepo.trackFlyerItemInteraction).toHaveBeenCalledWith(99, 'click', expectLogger);
|
||||
});
|
||||
|
||||
it('should return 202 Accepted and call the tracking function for "view"', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/flyers/items/101/track')
|
||||
.send({ type: 'view' });
|
||||
|
||||
expect(response.status).toBe(202);
|
||||
expect(db.flyerRepo.trackFlyerItemInteraction).toHaveBeenCalledWith(101, 'view', expectLogger);
|
||||
});
|
||||
|
||||
it('should return 400 for an invalid item ID', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/flyers/items/abc/track')
|
||||
|
||||
@@ -201,6 +201,19 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
expect(response.body.errors).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should return 400 if userId or achievementName are missing', async () => {
|
||||
mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => { req.user = mockAdminProfile; next(); });
|
||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
||||
|
||||
const response1 = await supertest(adminApp).post('/api/achievements/award').send({ achievementName: 'Test Award' });
|
||||
expect(response1.status).toBe(400);
|
||||
expect(response1.body.errors[0].message).toBe('userId is required.');
|
||||
|
||||
const response2 = await supertest(adminApp).post('/api/achievements/award').send({ userId: 'user-789' });
|
||||
expect(response2.status).toBe(400);
|
||||
expect(response2.body.errors[0].message).toBe('achievementName is required.');
|
||||
});
|
||||
|
||||
it('should return 400 if awarding an achievement to a non-existent user', async () => {
|
||||
mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => {
|
||||
req.user = mockAdminProfile;
|
||||
|
||||
@@ -169,6 +169,18 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect(response.body.message).toBe('DB connection failed');
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: 'DB connection failed' }, 'Error during DB schema check:');
|
||||
});
|
||||
|
||||
it('should return 500 if the database check fails with a non-Error object', async () => {
|
||||
// Arrange: Mock the service function to reject with a non-error object
|
||||
const dbError = { message: 'DB connection failed' };
|
||||
mockedDbConnection.checkTablesExist.mockRejectedValue(dbError);
|
||||
|
||||
const response = await supertest(app).get('/api/health/db-schema');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB connection failed');
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: dbError }, 'Error during DB schema check:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /storage', () => {
|
||||
@@ -198,6 +210,20 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect(response.body.message).toContain('Storage check failed.');
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: 'EACCES: permission denied' }, expect.stringContaining('Storage check failed for path:'));
|
||||
});
|
||||
|
||||
it('should return 500 if storage check fails with a non-Error object', async () => {
|
||||
// Arrange: Mock fs.access to reject with a non-error object.
|
||||
const accessError = { message: 'EACCES: permission denied' };
|
||||
mockedFs.access.mockRejectedValue(accessError);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/health/storage');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toContain('Storage check failed.');
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: accessError }, expect.stringContaining('Storage check failed for path:'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /db-pool', () => {
|
||||
@@ -249,4 +275,16 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect(response.body.message).toBe('Pool is not initialized');
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: 'Pool is not initialized' }, 'Error during DB pool health check:');
|
||||
});
|
||||
|
||||
it('should return 500 if getPoolStatus throws a non-Error object', async () => {
|
||||
// Arrange: Mock getPoolStatus to throw a non-error object
|
||||
const poolError = { message: 'Pool is not initialized' };
|
||||
mockedDbConnection.getPoolStatus.mockImplementation(() => { throw poolError; });
|
||||
|
||||
const response = await supertest(app).get('/api/health/db-pool');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('Pool is not initialized');
|
||||
expect(logger.error).toHaveBeenCalledWith({ error: poolError }, 'Error during DB pool health check:');
|
||||
});
|
||||
});
|
||||
@@ -497,6 +497,23 @@ describe('Passport Configuration', () => {
|
||||
expect(mockNext).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should log info.toString() if info object has no message property', () => {
|
||||
// Arrange
|
||||
const mockReq = {} as Request;
|
||||
const mockInfo = { custom: 'some info' };
|
||||
// Mock passport.authenticate to call its callback with a custom info object
|
||||
vi.mocked(passport.authenticate).mockImplementation(
|
||||
(_strategy, _options, callback) => () => callback?.(null, false, mockInfo as any)
|
||||
);
|
||||
|
||||
// Act
|
||||
optionalAuth(mockReq, mockRes as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(logger.info).toHaveBeenCalledWith({ info: mockInfo.toString() }, 'Optional auth info:');
|
||||
expect(mockNext).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call next() and not populate user if passport returns an error', () => {
|
||||
// Arrange
|
||||
const mockReq = {} as Request;
|
||||
|
||||
@@ -109,6 +109,29 @@ const expectLogger = expect.objectContaining({
|
||||
error: expect.any(Function),
|
||||
});
|
||||
describe('User Routes (/api/users)', () => {
|
||||
// This test needs to be separate because the code it tests runs on module load.
|
||||
describe('Avatar Upload Directory Creation', () => {
|
||||
it('should log an error if avatar directory creation fails', async () => {
|
||||
// Arrange
|
||||
const mkdirError = new Error('EACCES: permission denied');
|
||||
// Reset modules to force re-import with a new mock implementation
|
||||
vi.resetModules();
|
||||
// Set up the mock *before* the module is re-imported
|
||||
vi.doMock('node:fs/promises', () => ({
|
||||
// We only need to mock mkdir for this test.
|
||||
mkdir: vi.fn().mockRejectedValue(mkdirError),
|
||||
}));
|
||||
const { logger } = await import('../services/logger.server');
|
||||
|
||||
// Act: Dynamically import the router to trigger the top-level fs.mkdir call
|
||||
await import('./user.routes');
|
||||
|
||||
// Assert
|
||||
expect(logger.error).toHaveBeenCalledWith('Failed to create avatar upload directory:', mkdirError);
|
||||
vi.doUnmock('node:fs/promises'); // Clean up
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -894,6 +917,19 @@ describe('User Routes (/api/users)', () => {
|
||||
expect(response.body.message).toBe('Only image files are allowed!');
|
||||
});
|
||||
|
||||
it('should return 400 if the uploaded file is too large', async () => {
|
||||
// This test relies on the `fileSize` limit set in the multer config in user.routes.ts
|
||||
const largeFile = Buffer.alloc(2 * 1024 * 1024, 'a'); // 2MB file, assuming limit is smaller
|
||||
const dummyImagePath = 'large-avatar.png';
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/users/profile/avatar')
|
||||
.attach('avatar', largeFile, dummyImagePath);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toContain('File too large');
|
||||
});
|
||||
|
||||
it('should return 400 if no file is uploaded', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/users/profile/avatar'); // No .attach() call
|
||||
|
||||
@@ -103,6 +103,7 @@ const avatarStorage = multer.diskStorage({
|
||||
|
||||
const avatarUpload = multer({
|
||||
storage: avatarStorage,
|
||||
limits: { fileSize: 1 * 1024 * 1024 }, // 1MB file size limit
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
|
||||
Reference in New Issue
Block a user