Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a77105316f | ||
| cadacb63f5 | |||
|
|
62592f707e | ||
| 023e48d99a |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.6",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/middleware/multer.middleware.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
||||
import multer from 'multer';
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { createUploadMiddleware, handleMulterError } from './multer.middleware';
|
||||
|
||||
@@ -71,7 +71,7 @@ const imageFileFilter = (req: Request, file: Express.Multer.File, cb: multer.Fil
|
||||
} else {
|
||||
// Reject the file with a specific error that can be caught by a middleware.
|
||||
const validationIssue = { path: ['file', file.fieldname], message: 'Only image files are allowed!' };
|
||||
const err = new ValidationError([validationIssue]);
|
||||
const err = new ValidationError([validationIssue], 'Only image files are allowed!');
|
||||
cb(err as Error); // Cast to Error to satisfy multer's type, though ValidationError extends Error.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,7 +84,11 @@ const emptySchema = z.object({});
|
||||
|
||||
const router = Router();
|
||||
|
||||
const upload = createUploadMiddleware({ storageType: 'flyer' });
|
||||
const brandLogoUpload = createUploadMiddleware({
|
||||
storageType: 'flyer', // Using flyer storage path is acceptable for brand logos.
|
||||
fileSize: 2 * 1024 * 1024, // 2MB limit for logos
|
||||
fileFilter: 'image',
|
||||
});
|
||||
|
||||
// --- Bull Board (Job Queue UI) Setup ---
|
||||
const serverAdapter = new ExpressAdapter();
|
||||
@@ -239,7 +243,7 @@ router.put(
|
||||
router.post(
|
||||
'/brands/:id/logo',
|
||||
validateRequest(numericIdParam('id')),
|
||||
upload.single('logoImage'),
|
||||
brandLogoUpload.single('logoImage'),
|
||||
requireFileUpload('logoImage'),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParam>>;
|
||||
|
||||
@@ -325,7 +325,7 @@ describe('AI API Client (Network Mocking with MSW)', () => {
|
||||
return HttpResponse.text('Gateway Timeout', { status: 504, statusText: 'Gateway Timeout' });
|
||||
}),
|
||||
);
|
||||
await expect(aiApiClient.getJobStatus(jobId)).rejects.toThrow('API Error: 504 Gateway Timeout');
|
||||
await expect(aiApiClient.getJobStatus(jobId)).rejects.toThrow('Gateway Timeout');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ describe('Admin API Routes Integration Tests', () => {
|
||||
email: `admin-integration-${Date.now()}@test.com`,
|
||||
role: 'admin',
|
||||
fullName: 'Admin Test User',
|
||||
request, // Pass supertest request to ensure user is created in the test DB
|
||||
}));
|
||||
({ user: regularUser, token: regularUserToken } = await createAndLoginUser({
|
||||
email: `regular-integration-${Date.now()}@test.com`,
|
||||
fullName: 'Regular User',
|
||||
request, // Pass supertest request
|
||||
}));
|
||||
|
||||
// Cleanup the created user after all tests in this file are done
|
||||
@@ -51,6 +53,10 @@ describe('Admin API Routes Integration Tests', () => {
|
||||
.get('/api/admin/stats')
|
||||
.set('Authorization', `Bearer ${adminToken}`);
|
||||
const stats = response.body;
|
||||
// DEBUG: Log response if it fails expectation
|
||||
if (response.status !== 200) {
|
||||
console.error('[DEBUG] GET /api/admin/stats failed:', response.status, response.body);
|
||||
}
|
||||
expect(stats).toBeDefined();
|
||||
expect(stats).toHaveProperty('flyerCount');
|
||||
expect(stats).toHaveProperty('userCount');
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('AI API Routes Integration Tests', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create and log in as a new user for authenticated tests.
|
||||
({ token: authToken } = await createAndLoginUser({ fullName: 'AI Tester' }));
|
||||
({ token: authToken } = await createAndLoginUser({ fullName: 'AI Tester', request }));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -83,6 +83,10 @@ describe('AI API Routes Integration Tests', () => {
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ items: [{ item: 'test' }] });
|
||||
const result = response.body;
|
||||
// DEBUG: Log response if it fails expectation
|
||||
if (response.status !== 404 || !result.text) {
|
||||
console.log('[DEBUG] POST /api/ai/quick-insights response:', response.status, response.body);
|
||||
}
|
||||
expect(response.status).toBe(404);
|
||||
expect(result.text).toBe('This is a server-generated quick insight: buy the cheap stuff!');
|
||||
});
|
||||
@@ -93,6 +97,10 @@ describe('AI API Routes Integration Tests', () => {
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ items: [{ item: 'test' }] });
|
||||
const result = response.body;
|
||||
// DEBUG: Log response if it fails expectation
|
||||
if (response.status !== 404 || !result.text) {
|
||||
console.log('[DEBUG] POST /api/ai/deep-dive response:', response.status, response.body);
|
||||
}
|
||||
expect(response.status).toBe(404);
|
||||
expect(result.text).toBe('This is a server-generated deep dive analysis. It is very detailed.');
|
||||
});
|
||||
@@ -103,6 +111,10 @@ describe('AI API Routes Integration Tests', () => {
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ query: 'test query' });
|
||||
const result = response.body;
|
||||
// DEBUG: Log response if it fails expectation
|
||||
if (response.status !== 404 || !result.text) {
|
||||
console.log('[DEBUG] POST /api/ai/search-web response:', response.status, response.body);
|
||||
}
|
||||
expect(response.status).toBe(404);
|
||||
expect(result).toEqual({ text: 'The web says this is good.', sources: [] });
|
||||
});
|
||||
@@ -141,6 +153,10 @@ describe('AI API Routes Integration Tests', () => {
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ items: [], store: mockStore, userLocation: mockLocation });
|
||||
// The service for this endpoint is disabled and throws an error, which results in a 500.
|
||||
// DEBUG: Log response if it fails expectation
|
||||
if (response.status !== 500) {
|
||||
console.log('[DEBUG] POST /api/ai/plan-trip response:', response.status, response.body);
|
||||
}
|
||||
expect(response.status).toBe(500);
|
||||
const errorResult = response.body;
|
||||
expect(errorResult.message).toContain('planTripWithMaps');
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('Authentication API Integration', () => {
|
||||
beforeAll(async () => {
|
||||
// Use a unique email for this test suite to prevent collisions with other tests.
|
||||
const email = `auth-integration-test-${Date.now()}@example.com`;
|
||||
({ user: testUser } = await createAndLoginUser({ email, fullName: 'Auth Test User' }));
|
||||
({ user: testUser } = await createAndLoginUser({ email, fullName: 'Auth Test User', request }));
|
||||
testUserEmail = testUser.user.email;
|
||||
});
|
||||
|
||||
@@ -43,6 +43,10 @@ describe('Authentication API Integration', () => {
|
||||
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: false });
|
||||
const data = response.body;
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.error('[DEBUG] Login failed:', response.status, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// Assert that the API returns the expected structure
|
||||
expect(data).toBeDefined();
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
@@ -101,6 +101,10 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
}
|
||||
|
||||
// Assert 2: Check that the job completed successfully.
|
||||
if (jobStatus?.state === 'failed') {
|
||||
console.error('[DEBUG] Job failed with reason:', jobStatus.failedReason);
|
||||
console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
|
||||
}
|
||||
expect(jobStatus?.state).toBe('completed');
|
||||
const flyerId = jobStatus?.returnValue?.flyerId;
|
||||
expect(flyerId).toBeTypeOf('number');
|
||||
@@ -132,6 +136,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
const { user: authUser, token } = await createAndLoginUser({
|
||||
email,
|
||||
fullName: 'Flyer Uploader',
|
||||
request,
|
||||
});
|
||||
createdUserIds.push(authUser.user.user_id); // Track for cleanup
|
||||
|
||||
|
||||
@@ -38,6 +38,15 @@ describe('Public API Routes Integration Tests', () => {
|
||||
fullName: 'Public Routes Test User',
|
||||
});
|
||||
testUser = createdUser;
|
||||
|
||||
// DEBUG: Verify user existence in DB
|
||||
console.log(`[DEBUG] createAndLoginUser returned user ID: ${testUser.user.user_id}`);
|
||||
const userCheck = await pool.query('SELECT user_id FROM public.users WHERE user_id = $1', [testUser.user.user_id]);
|
||||
console.log(`[DEBUG] DB check for user found ${userCheck.rowCount} rows.`);
|
||||
if (userCheck.rowCount === 0) {
|
||||
console.error(`[DEBUG] CRITICAL: User ${testUser.user.user_id} does not exist in public.users table!`);
|
||||
}
|
||||
|
||||
// Create a recipe
|
||||
const recipeRes = await pool.query(
|
||||
`INSERT INTO public.recipes (name, instructions, user_id, status) VALUES ('Public Test Recipe', 'Instructions here', $1, 'public') RETURNING *`,
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('User API Routes Integration Tests', () => {
|
||||
// The token will be used for all subsequent API calls in this test suite.
|
||||
beforeAll(async () => {
|
||||
const email = `user-test-${Date.now()}@example.com`;
|
||||
const { user, token } = await createAndLoginUser({ email, fullName: 'Test User' });
|
||||
const { user, token } = await createAndLoginUser({ email, fullName: 'Test User', request });
|
||||
testUser = user;
|
||||
authToken = token;
|
||||
});
|
||||
@@ -130,7 +130,7 @@ describe('User API Routes Integration Tests', () => {
|
||||
it('should allow a user to delete their own account and then fail to log in', async () => {
|
||||
// Arrange: Create a new, separate user just for this deletion test.
|
||||
const deletionEmail = `delete-me-${Date.now()}@example.com`;
|
||||
const { token: deletionToken } = await createAndLoginUser({ email: deletionEmail });
|
||||
const { token: deletionToken } = await createAndLoginUser({ email: deletionEmail, request });
|
||||
|
||||
// Act: Call the delete endpoint with the correct password and token.
|
||||
const response = await request
|
||||
@@ -155,7 +155,7 @@ describe('User API Routes Integration Tests', () => {
|
||||
it('should allow a user to reset their password and log in with the new one', async () => {
|
||||
// Arrange: Create a new user for the password reset flow.
|
||||
const resetEmail = `reset-me-${Date.now()}@example.com`;
|
||||
const { user: resetUser } = await createAndLoginUser({ email: resetEmail });
|
||||
const { user: resetUser } = await createAndLoginUser({ email: resetEmail, request });
|
||||
|
||||
// Act 1: Request a password reset. In our test environment, the token is returned in the response.
|
||||
const resetRequestRawResponse = await request
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('User Routes Integration Tests (/api/users)', () => {
|
||||
// Use the helper to create and log in a user in one step.
|
||||
const { user, token } = await createAndLoginUser({
|
||||
fullName: 'User Routes Test User',
|
||||
request,
|
||||
});
|
||||
testUser = user;
|
||||
authToken = token;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import { getPool } from '../../services/db/connection.db';
|
||||
import type { UserProfile } from '../../types';
|
||||
import supertest from 'supertest';
|
||||
|
||||
export const TEST_PASSWORD = 'a-much-stronger-password-for-testing-!@#$';
|
||||
|
||||
@@ -10,6 +11,8 @@ interface CreateUserOptions {
|
||||
password?: string;
|
||||
fullName?: string;
|
||||
role?: 'admin' | 'user';
|
||||
// Use ReturnType to match the actual return type of supertest(app) to avoid type mismatches (e.g. TestAgent vs SuperTest)
|
||||
request?: ReturnType<typeof supertest>;
|
||||
}
|
||||
|
||||
interface CreateUserResult {
|
||||
@@ -31,16 +34,53 @@ export const createAndLoginUser = async (
|
||||
const password = options.password || TEST_PASSWORD;
|
||||
const fullName = options.fullName || 'Test User';
|
||||
|
||||
await apiClient.registerUser(email, password, fullName);
|
||||
if (options.request) {
|
||||
// Use supertest for integration tests (hits the app instance directly)
|
||||
const registerRes = await options.request
|
||||
.post('/api/auth/register')
|
||||
.send({ email, password, full_name: fullName });
|
||||
|
||||
if (options.role === 'admin') {
|
||||
await getPool().query(
|
||||
`UPDATE public.profiles SET role = 'admin' FROM public.users WHERE public.profiles.user_id = public.users.user_id AND public.users.email = $1`,
|
||||
[email],
|
||||
);
|
||||
if (registerRes.status !== 201 && registerRes.status !== 200) {
|
||||
throw new Error(
|
||||
`Failed to register user via supertest: ${registerRes.status} ${JSON.stringify(registerRes.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.role === 'admin') {
|
||||
await getPool().query(
|
||||
`UPDATE public.profiles SET role = 'admin' FROM public.users WHERE public.profiles.user_id = public.users.user_id AND public.users.email = $1`,
|
||||
[email],
|
||||
);
|
||||
}
|
||||
|
||||
const loginRes = await options.request
|
||||
.post('/api/auth/login')
|
||||
.send({ email, password, rememberMe: false });
|
||||
|
||||
if (loginRes.status !== 200) {
|
||||
throw new Error(
|
||||
`Failed to login user via supertest: ${loginRes.status} ${JSON.stringify(loginRes.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { userprofile, token } = loginRes.body;
|
||||
return { user: userprofile, token };
|
||||
} else {
|
||||
// Use apiClient for E2E tests (hits the external URL via fetch)
|
||||
await apiClient.registerUser(email, password, fullName);
|
||||
|
||||
if (options.role === 'admin') {
|
||||
await getPool().query(
|
||||
`UPDATE public.profiles SET role = 'admin' FROM public.users WHERE public.profiles.user_id = public.users.user_id AND public.users.email = $1`,
|
||||
[email],
|
||||
);
|
||||
}
|
||||
|
||||
const loginResponse = await apiClient.loginUser(email, password, false);
|
||||
if (!loginResponse.ok) {
|
||||
throw new Error(`Failed to login user via apiClient: ${loginResponse.status}`);
|
||||
}
|
||||
const { userprofile, token } = await loginResponse.json();
|
||||
return { user: userprofile, token };
|
||||
}
|
||||
|
||||
const loginResponse = await apiClient.loginUser(email, password, false);
|
||||
const { userprofile, token } = await loginResponse.json();
|
||||
return { user: userprofile, token };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user