testing routes
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 2m8s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 2m8s
This commit is contained in:
470
src/routes/admin.test.ts
Normal file
470
src/routes/admin.test.ts
Normal file
@@ -0,0 +1,470 @@
|
||||
// src/routes/admin.test.ts
|
||||
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 adminRouter from './admin';
|
||||
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 middleware will be controlled by the `isAdmin` mock below.
|
||||
// In a real scenario, you might attach a user to `req.user` here if needed.
|
||||
return 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 Mocked<any>;
|
||||
|
||||
// Create a minimal Express app to host our router
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/admin', adminRouter);
|
||||
|
||||
describe('Admin Routes (/api/admin)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
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' } as UserProfile;
|
||||
next(); // Grant access
|
||||
});
|
||||
});
|
||||
|
||||
it('GET /corrections should return corrections data', async () => {
|
||||
// Arrange
|
||||
const mockCorrections = [{ correction_id: 1, suggested_value: 'New Price' }];
|
||||
mockedDb.getSuggestedCorrections.mockResolvedValue(mockCorrections as any);
|
||||
|
||||
// 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 = [
|
||||
{ brand_id: 1, name: 'Brand A', logo_url: '/path/a.png' },
|
||||
{ brand_id: 2, name: 'Brand B', logo_url: '/path/b.png' },
|
||||
];
|
||||
mockedDb.getAllBrands.mockResolvedValue(mockBrands as any);
|
||||
|
||||
// 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 },
|
||||
];
|
||||
mockedDb.getDailyStatsForLast30Days.mockResolvedValue(mockDailyStats as any);
|
||||
|
||||
// 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 = [
|
||||
{ flyer_item_id: 1, raw_item_description: 'Ketchup Chips', price_display: '$3.00' },
|
||||
{ flyer_item_id: 2, raw_item_description: 'Mystery Soda', price_display: '2 for $4.00' },
|
||||
];
|
||||
mockedDb.getUnmatchedFlyerItems.mockResolvedValue(mockUnmatchedItems as any);
|
||||
|
||||
// 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(); // 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /corrections/:id/reject', () => {
|
||||
it('should reject a correction and return a success message', async () => {
|
||||
// Arrange
|
||||
const correctionId = 789;
|
||||
mockedDb.rejectCorrection.mockResolvedValue(); // 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 = { correction_id: correctionId, ...requestBody };
|
||||
mockedDb.updateSuggestedCorrection.mockResolvedValue(mockUpdatedCorrection as any);
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /brands/:id/logo', () => {
|
||||
it('should upload a logo and update the brand', async () => {
|
||||
// Arrange
|
||||
const brandId = 55;
|
||||
mockedDb.updateBrandLogo.mockResolvedValue(); // Mock the DB call
|
||||
|
||||
// Create a dummy file path. The file doesn't need to exist for this test,
|
||||
// as multer and supertest handle the stream.
|
||||
const dummyFilePath = path.resolve(__dirname, 'test-logo.png');
|
||||
|
||||
// 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/'));
|
||||
});
|
||||
|
||||
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' };
|
||||
const mockUpdatedRecipe = { recipe_id: recipeId, status: 'public', name: 'Test Recipe' };
|
||||
mockedDb.updateRecipeStatus.mockResolvedValue(mockUpdatedRecipe as any);
|
||||
|
||||
// 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' };
|
||||
const mockUpdatedComment = { comment_id: commentId, status: 'hidden', content: 'Test Comment' };
|
||||
mockedDb.updateRecipeCommentStatus.mockResolvedValue(mockUpdatedComment as any);
|
||||
|
||||
// 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 = [
|
||||
{ user_id: '1', email: 'user1@test.com', role: 'user' },
|
||||
{ user_id: '2', email: 'user2@test.com', role: 'admin' },
|
||||
];
|
||||
mockedDb.getAllUsers.mockResolvedValue(mockUsers as any);
|
||||
|
||||
// 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 () => {
|
||||
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 = [{ log_id: 1, action: 'user_login', user_id: '1' }];
|
||||
mockedDb.getActivityLog.mockResolvedValue(mockLogs as any);
|
||||
|
||||
// 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
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -160,4 +160,26 @@ router.put('/comments/:id/status', async (req: Request, res: Response, next: Nex
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/users', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const users = await db.getAllUsers();
|
||||
res.json(users);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching users in /api/admin/users:', { error });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/activity-log', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const limit = parseInt(req.query.limit as string, 10) || 50;
|
||||
const offset = parseInt(req.query.offset as string, 10) || 0;
|
||||
try {
|
||||
const logs = await db.getActivityLog(limit, offset);
|
||||
res.json(logs);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching activity log in /api/admin/activity-log:', { error });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
81
src/routes/ai.test.ts
Normal file
81
src/routes/ai.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
// src/routes/ai.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import aiRouter from './ai';
|
||||
import * as aiService from '../services/aiService.server';
|
||||
|
||||
// Mock the AI service to avoid making real AI calls
|
||||
vi.mock('../services/aiService.server');
|
||||
const mockedAiService = aiService as Mocked<typeof aiService>;
|
||||
|
||||
// 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 passport's optionalAuth to simplify testing.
|
||||
// We just need it to call next() so the route handler can run.
|
||||
vi.mock('./passport', () => ({
|
||||
optionalAuth: vi.fn((req, res, next) => next()),
|
||||
}));
|
||||
|
||||
// Create a minimal Express app to host our router
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/ai', aiRouter);
|
||||
|
||||
describe('AI Routes (/api/ai)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('POST /process-flyer', () => {
|
||||
it('should process an uploaded flyer image and return extracted data', async () => {
|
||||
// Arrange:
|
||||
// 1. Define the mock data the AI service will "extract".
|
||||
const mockExtractedData = {
|
||||
store_name: 'Test Store',
|
||||
items: [{ item: 'Test Item', price_display: '$1.99' }],
|
||||
};
|
||||
mockedAiService.extractCoreDataFromFlyerImage.mockResolvedValue(mockExtractedData as any);
|
||||
|
||||
// 2. Define the other form fields that are sent along with the file.
|
||||
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');
|
||||
|
||||
// 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
|
||||
|
||||
// Assert:
|
||||
// 1. Check for a successful HTTP status.
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// 2. Verify the AI service was called correctly by the route handler.
|
||||
// Multer adds properties like 'path' to the file object. We check that the service
|
||||
// received an array of files with these properties.
|
||||
expect(mockedAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(1);
|
||||
expect(mockedAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([expect.objectContaining({ mimetype: 'text/plain' })]),
|
||||
mockMasterItems
|
||||
);
|
||||
|
||||
// 3. Ensure the response body contains the data we mocked.
|
||||
expect(response.body.data).toEqual(mockExtractedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
116
src/routes/auth.test.ts
Normal file
116
src/routes/auth.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// src/routes/auth.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express from 'express';
|
||||
import authRouter from './auth';
|
||||
import * as db from '../services/db';
|
||||
|
||||
// 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(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Create a minimal Express app to host our router
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/auth', authRouter);
|
||||
|
||||
describe('Auth Routes (/api/auth)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('POST /register', () => {
|
||||
const newUserEmail = 'newuser@test.com';
|
||||
const strongPassword = 'a-Very-Strong-Password-123!';
|
||||
|
||||
it('should successfully register a new user with a strong password', async () => {
|
||||
// Arrange:
|
||||
// 1. User does not exist
|
||||
mockedDb.findUserByEmail.mockResolvedValue(undefined);
|
||||
// 2. Mock the user creation and token saving
|
||||
const mockNewUser = { user_id: 'new-user-id', email: newUserEmail };
|
||||
mockedDb.createUser.mockResolvedValue(mockNewUser as any);
|
||||
mockedDb.saveRefreshToken.mockResolvedValue();
|
||||
mockedDb.logActivity.mockResolvedValue();
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
.post('/api/auth/register')
|
||||
.send({
|
||||
email: newUserEmail,
|
||||
password: strongPassword,
|
||||
full_name: 'Test User',
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.message).toBe('User registered successfully!');
|
||||
expect(response.body.user.email).toBe(newUserEmail);
|
||||
expect(response.body.token).toBeTypeOf('string');
|
||||
|
||||
// Verify that the correct DB functions were called
|
||||
expect(mockedDb.findUserByEmail).toHaveBeenCalledWith(newUserEmail);
|
||||
expect(mockedDb.createUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.saveRefreshToken).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.logActivity).toHaveBeenCalledWith(expect.objectContaining({ action: 'user_registered' }));
|
||||
});
|
||||
|
||||
it('should reject registration with a weak password', async () => {
|
||||
// Arrange
|
||||
const weakPassword = 'password';
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
.post('/api/auth/register')
|
||||
.send({
|
||||
email: 'anotheruser@test.com',
|
||||
password: weakPassword,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toContain('Password is too weak');
|
||||
|
||||
// Ensure no database operations were attempted
|
||||
expect(mockedDb.findUserByEmail).not.toHaveBeenCalled();
|
||||
expect(mockedDb.createUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject registration if the email already exists', async () => {
|
||||
// Arrange: Mock that the user already exists in the database
|
||||
const existingUser = { user_id: 'existing-id', email: newUserEmail, password_hash: 'somehash' };
|
||||
mockedDb.findUserByEmail.mockResolvedValue(existingUser as any);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
.post('/api/auth/register')
|
||||
.send({ email: newUserEmail, password: strongPassword });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(409); // 409 Conflict
|
||||
expect(response.body.message).toBe('User with that email already exists.');
|
||||
expect(mockedDb.createUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject registration if email or password are not provided', async () => {
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
.post('/api/auth/register')
|
||||
.send({ email: newUserEmail /* no password */ });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toBe('Email and password are required.');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user