Compare commits

...

4 Commits

Author SHA1 Message Date
Gitea Actions
a77105316f ci: Bump version to 0.4.6 [skip ci] 2025-12-30 22:39:46 +05:00
cadacb63f5 fix unit tests
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m54s
2025-12-30 03:19:47 -08:00
Gitea Actions
62592f707e ci: Bump version to 0.4.5 [skip ci] 2025-12-30 15:32:34 +05:00
023e48d99a fix unit tests
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m27s
2025-12-30 02:32:02 -08:00
14 changed files with 108 additions and 23 deletions

4
package-lock.json generated
View File

@@ -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",

View File

@@ -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\"",

View File

@@ -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';

View File

@@ -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.
}
};

View File

@@ -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>>;

View File

@@ -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');
});
});

View File

@@ -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');

View File

@@ -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');

View File

@@ -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);

View File

@@ -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

View File

@@ -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 *`,

View File

@@ -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

View File

@@ -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;

View File

@@ -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 };
};