come on ai get it right
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 3m47s

This commit is contained in:
2025-12-01 18:39:10 -08:00
parent 3096e97616
commit 2ed623c199
4 changed files with 134 additions and 190 deletions

View File

@@ -1,26 +1,16 @@
// src/routes/auth.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express from 'express';
import authRouter from './auth';
import * as bcrypt from 'bcrypt';
import cookieParser from 'cookie-parser';
import * as bcrypt from 'bcrypt';
import authRouter from './auth';
import * as db from '../services/db';
import { UserProfile } from '../types';
// Mock the entire db service. We will use a test-local mock for getPool.
const mockQuery = vi.fn();
vi.mock('../services/db', async (importOriginal) => {
const actual = await importOriginal<typeof db>();
return {
...actual, // Import all actual functions
// Mock only the getPool function
getPool: vi.fn(() => ({
query: mockQuery,
connect: vi.fn().mockResolvedValue({ query: mockQuery, release: vi.fn() }),
})),
};
});
// 1. Mock the Service Layer directly.
// This decouples the route tests from the SQL implementation details.
vi.mock('../services/db');
// Mock the logger to keep test output clean
vi.mock('../services/logger.server', () => ({
@@ -43,6 +33,31 @@ vi.mock('bcrypt', async (importOriginal) => {
return { ...actual, compare: vi.fn() };
});
// Mock Passport middleware
vi.mock('./passport', () => ({
default: {
authenticate: (strategy: string, options: any, callback: any) => (req: any, res: any, next: any) => {
// Logic to simulate passport authentication outcome based on test input
if (req.body.password === 'wrong_password') {
// Simulate incorrect credentials
return callback(null, false, { message: 'Incorrect email or password.' });
}
if (req.body.email === 'locked@test.com') {
// Simulate locked account
return callback(null, false, { message: 'Account is temporarily locked.' });
}
if (req.body.email === 'notfound@test.com') {
// Simulate user not found
return callback(null, false, { message: 'Login failed' });
}
// Default success case
const user = { user_id: 'user-123', email: req.body.email };
callback(null, user, null);
},
},
}));
// Create a minimal Express app to host our router
const app = express();
app.use(express.json({ strict: false }));
@@ -65,15 +80,16 @@ describe('Auth Routes (/api/auth)', () => {
user: { user_id: 'new-user-id', email: newUserEmail },
role: 'user',
points: 0,
full_name: 'Test User',
avatar_url: null,
preferences: {}
};
// Mock the sequence of queries inside the createUser transaction
mockQuery
.mockResolvedValueOnce({ rows: [] }) // findUserByEmail
.mockResolvedValueOnce({ rows: [] }) // BEGIN
.mockResolvedValueOnce({ rows: [{ user_id: 'new-user-id' }] }) // INSERT user
.mockResolvedValueOnce({ rows: [mockNewUser] }) // SELECT profile
.mockResolvedValueOnce({ rows: [] }) // COMMIT
.mockResolvedValue({ rows: [] }); // All other calls (saveRefreshToken, logActivity)
vi.mocked(db.findUserByEmail).mockResolvedValue(undefined); // No existing user
// @ts-ignore
vi.mocked(db.createUser).mockResolvedValue(mockNewUser);
vi.mocked(db.saveRefreshToken).mockResolvedValue(undefined);
vi.mocked(db.logActivity).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
@@ -88,16 +104,12 @@ describe('Auth Routes (/api/auth)', () => {
expect(response.status).toBe(201);
expect(response.body.message).toBe('User registered successfully!');
expect(response.body.user.email).toBe(newUserEmail);
// Assert against more of the mocked data for a more precise test.
expect(response.body.user.user_id).toBe(mockNewUser.user_id);
expect(response.body.token).toBeTypeOf('string');
});
it('should reject registration with a weak password', async () => {
// Arrange
const weakPassword = 'password';
// Act
const response = await supertest(app)
.post('/api/auth/register')
.send({
@@ -105,22 +117,14 @@ describe('Auth Routes (/api/auth)', () => {
password: weakPassword,
});
// Assert
expect(response.status).toBe(400);
expect(response.body.message).toContain('Password is too weak');
});
it('should reject registration if the email already exists', async () => {
// Arrange: Mock that the user already exists in the database
const existingUser: Awaited<ReturnType<typeof db.findUserByEmail>> = {
user_id: 'existing-id',
email: newUserEmail,
password_hash: 'somehash',
failed_login_attempts: 0,
last_failed_login: null,
};
mockQuery.mockResolvedValue({ rows: [existingUser] });
// Arrange: Mock that the user exists
// @ts-ignore
vi.mocked(db.findUserByEmail).mockResolvedValue({ user_id: 'existing', email: newUserEmail });
// Act
const response = await supertest(app)
@@ -133,7 +137,6 @@ describe('Auth Routes (/api/auth)', () => {
});
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 */ });
@@ -145,20 +148,10 @@ describe('Auth Routes (/api/auth)', () => {
});
describe('POST /login', () => {
const loginCredentials = { email: 'test@test.com', password: 'password123' };
it('should successfully log in a user and return a token and cookie', async () => {
// Arrange:
// 1. Simulate passport successfully finding a user.
const mockUser = { user_id: 'user-123', email: 'test@test.com', password_hash: 'hashed' };
mockQuery.mockImplementation((query: string) => {
if (query.includes('FROM public.users WHERE email = $1')) {
return Promise.resolve({ rows: [mockUser] });
}
return Promise.resolve({ rows: [] }); // For saveRefreshToken
});
vi.mocked(bcrypt.compare).mockResolvedValue(true as never);
const loginCredentials = { email: 'test@test.com', password: 'password123' };
vi.mocked(db.saveRefreshToken).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
@@ -167,42 +160,34 @@ describe('Auth Routes (/api/auth)', () => {
// Assert
expect(response.status).toBe(200);
expect(response.body.user).toEqual({ user_id: 'user-123', email: 'test@test.com' });
expect(response.body.user).toEqual({ user_id: 'user-123', email: loginCredentials.email });
expect(response.body.token).toBeTypeOf('string');
// Check that the 'Set-Cookie' header was sent for the refresh token.
expect(response.headers['set-cookie']).toBeDefined();
expect(response.headers['set-cookie'][0]).toContain('refreshToken=');
});
it.each([
{ scenario: 'incorrect credentials', message: 'Incorrect email or password.', user: false },
{ scenario: 'a locked account', message: 'Account is temporarily locked.', user: false },
])('should reject login for $scenario', async ({ message, user }) => {
// Arrange: Simulate passport failing with a specific message.
const mockUser = { user_id: 'user-123', email: 'test@test.com', password_hash: 'hashed', failed_login_attempts: 10, last_failed_login: new Date().toISOString() };
mockQuery.mockResolvedValue({ rows: [mockUser] });
if (message.includes('Incorrect')) {
vi.mocked(bcrypt.compare).mockResolvedValue(false as never);
}
// Act
it('should reject login for incorrect credentials', async () => {
const response = await supertest(app)
.post('/api/auth/login')
.send(loginCredentials);
// Assert
.send({ email: 'test@test.com', password: 'wrong_password' });
expect(response.status).toBe(401);
expect(response.body.message).toBe(message);
expect(response.body.message).toBe('Incorrect email or password.');
});
it('should reject login for a locked account', async () => {
const response = await supertest(app)
.post('/api/auth/login')
.send({ email: 'locked@test.com', password: 'password123' });
expect(response.status).toBe(401);
expect(response.body.message).toBe('Account is temporarily locked.');
});
it('should return 401 if user is not found', async () => {
// Arrange:
// Force user lookup to fail
mockQuery.mockResolvedValue({ rows: [] });
const response = await supertest(app)
.post('/api/auth/login')
.send({ email: 'notfound@test.com', password: 'password123' });
// Act
const response = await supertest(app).post('/api/auth/login').send(loginCredentials);
// Assert
expect(response.status).toBe(401);
});
});
@@ -210,8 +195,9 @@ describe('Auth Routes (/api/auth)', () => {
describe('POST /forgot-password', () => {
it('should send a reset link if the user exists', async () => {
// Arrange
const mockUser = { user_id: 'user-123', email: 'test@test.com' };
mockQuery.mockResolvedValue({ rows: [mockUser] });
// @ts-ignore
vi.mocked(db.findUserByEmail).mockResolvedValue({ user_id: 'user-123', email: 'test@test.com' });
vi.mocked(db.createPasswordResetToken).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
@@ -221,13 +207,13 @@ describe('Auth Routes (/api/auth)', () => {
// Assert
expect(response.status).toBe(200);
expect(response.body.message).toContain('a password reset link has been sent');
// Since NODE_ENV is 'test', the token should be returned in the response.
// In test env, token is returned
expect(response.body.token).toBeTypeOf('string');
});
it('should return a generic success message even if the user does not exist', async () => {
// Arrange
mockQuery.mockResolvedValue({ rows: [] });
vi.mocked(db.findUserByEmail).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
@@ -243,9 +229,13 @@ describe('Auth Routes (/api/auth)', () => {
describe('POST /reset-password', () => {
it('should reset the password with a valid token and strong password', async () => {
// Arrange
const tokenRecord = { user_id: 'user-123', token_hash: 'hashed-token' };
mockQuery.mockResolvedValue({ rows: [tokenRecord] });
vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Mock that the token matches the hash
const tokenRecord = { user_id: 'user-123', token_hash: 'hashed-token', expires_at: new Date(Date.now() + 3600000).toISOString() };
// @ts-ignore
vi.mocked(db.getValidResetTokens).mockResolvedValue([tokenRecord]);
vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Token matches
vi.mocked(db.updateUserPassword).mockResolvedValue(undefined);
vi.mocked(db.deleteResetToken).mockResolvedValue(undefined);
vi.mocked(db.logActivity).mockResolvedValue(undefined);
// Act
const response = await supertest(app)
@@ -258,9 +248,8 @@ describe('Auth Routes (/api/auth)', () => {
});
it('should reject with an invalid or expired token', async () => {
// Arrange: Mock that the token does not match any valid hashes.
mockQuery.mockResolvedValue({ rows: [{ user_id: 'user-123', token_hash: 'hashed-token' }] });
vi.mocked(bcrypt.compare).mockResolvedValue(false as never);
// Arrange
vi.mocked(db.getValidResetTokens).mockResolvedValue([]); // No valid tokens found
// Act
const response = await supertest(app)
@@ -277,7 +266,8 @@ describe('Auth Routes (/api/auth)', () => {
it('should issue a new access token with a valid refresh token cookie', async () => {
// Arrange
const mockUser = { user_id: 'user-123', email: 'test@test.com' };
mockQuery.mockResolvedValue({ rows: [mockUser] });
// @ts-ignore
vi.mocked(db.findUserByRefreshToken).mockResolvedValue(mockUser);
// Act
const response = await supertest(app)
@@ -294,5 +284,15 @@ describe('Auth Routes (/api/auth)', () => {
expect(response.status).toBe(401);
expect(response.body.message).toBe('Refresh token not found.');
});
it('should return 403 if refresh token is invalid', async () => {
vi.mocked(db.findUserByRefreshToken).mockResolvedValue(undefined);
const response = await supertest(app)
.post('/api/auth/refresh-token')
.set('Cookie', 'refreshToken=invalid-token');
expect(response.status).toBe(403);
});
});
});

View File

@@ -1,23 +1,14 @@
// src/routes/budget.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express from 'express';
import express, { Request, Response, NextFunction } from 'express';
import budgetRouter from './budget';
import * as db from '../services/db';
import { UserProfile, Budget, SpendingByCategory } from '../types';
// Mock the db service, but specifically override getPool
const mockQuery = vi.fn();
vi.mock('../services/db', async (importOriginal) => {
const actual = await importOriginal<typeof db>();
return {
...actual,
getPool: vi.fn(() => ({
query: mockQuery,
connect: vi.fn().mockResolvedValue({ query: mockQuery, release: vi.fn() }),
})),
};
});
// 1. Mock the Service Layer directly.
// This decouples the route tests from the database logic.
vi.mock('../services/db');
// Mock the logger to keep test output clean
vi.mock('../services/logger.server', () => ({
@@ -30,34 +21,40 @@ vi.mock('../services/logger.server', () => ({
}));
// Mock the passport authentication middleware
let mockAuthMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => next();
let mockAuthMiddleware = (req: Request, res: Response, next: NextFunction) => next();
vi.mock('./passport', () => ({
default: {
authenticate: vi.fn(() => (req: express.Request, res: express.Response, next: express.NextFunction) => {
authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
mockAuthMiddleware(req, res, next);
}),
},
}));
import passport from './passport';
// Create a minimal Express app to host our router
const app = express();
app.use(express.json({ strict: false }));
app.use(express.json());
app.use('/api/budgets', budgetRouter);
// Add a basic error handler to return JSON errors instead of Express default HTML
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({ message: 'Internal Server Error' });
});
describe('Budget Routes (/api/budgets)', () => {
const mockUserProfile: UserProfile = {
user_id: 'user-123',
user: { user_id: 'user-123', email: 'test@test.com' },
role: 'user',
points: 100,
full_name: 'Test User',
avatar_url: null,
preferences: {}
};
beforeEach(() => {
vi.clearAllMocks();
mockQuery.mockReset();
mockAuthMiddleware = (req: express.Request, res: express.Response, _next: express.NextFunction) => {
// Default to unauthorized
mockAuthMiddleware = (req: Request, res: Response, _next: NextFunction) => {
res.status(401).json({ message: 'Unauthorized' });
};
});
@@ -91,7 +88,8 @@ describe('Budget Routes (/api/budgets)', () => {
describe('when user is authenticated', () => {
beforeEach(() => {
mockAuthMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
// Simulate logged-in user
mockAuthMiddleware = (req: Request, res: Response, next: NextFunction) => {
req.user = mockUserProfile;
next();
};
@@ -100,12 +98,14 @@ describe('Budget Routes (/api/budgets)', () => {
describe('GET /', () => {
it('should return a list of budgets for the user', async () => {
const mockBudgets: Budget[] = [{ budget_id: 1, user_id: 'user-123', name: 'Groceries', amount_cents: 50000, period: 'monthly', start_date: '2024-01-01' }];
mockQuery.mockResolvedValue({ rows: mockBudgets });
// Mock the service function directly
vi.mocked(db.getBudgetsForUser).mockResolvedValue(mockBudgets);
const response = await supertest(app).get('/api/budgets');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockBudgets);
expect(db.getBudgetsForUser).toHaveBeenCalledWith(mockUserProfile.user_id);
});
});
@@ -113,7 +113,8 @@ describe('Budget Routes (/api/budgets)', () => {
it('should create a new budget and return it', async () => {
const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' };
const mockCreatedBudget: Budget = { budget_id: 2, user_id: 'user-123', ...newBudgetData };
mockQuery.mockResolvedValue({ rows: [mockCreatedBudget] });
// Mock the service function
vi.mocked(db.createBudget).mockResolvedValue(mockCreatedBudget);
const response = await supertest(app).post('/api/budgets').send(newBudgetData);
@@ -126,7 +127,8 @@ describe('Budget Routes (/api/budgets)', () => {
it('should update an existing budget', async () => {
const budgetUpdates = { amount_cents: 60000 };
const mockUpdatedBudget: Budget = { budget_id: 1, user_id: 'user-123', name: 'Groceries', amount_cents: 60000, period: 'monthly', start_date: '2024-01-01' };
mockQuery.mockResolvedValue({ rows: [mockUpdatedBudget] });
// Mock the service function
vi.mocked(db.updateBudget).mockResolvedValue(mockUpdatedBudget);
const response = await supertest(app).put('/api/budgets/1').send(budgetUpdates);
@@ -137,18 +139,21 @@ describe('Budget Routes (/api/budgets)', () => {
describe('DELETE /:id', () => {
it('should delete a budget', async () => {
mockQuery.mockResolvedValue({ rowCount: 1 });
// Mock the service function to resolve (void)
vi.mocked(db.deleteBudget).mockResolvedValue(undefined);
const response = await supertest(app).delete('/api/budgets/1');
expect(response.status).toBe(204);
expect(db.deleteBudget).toHaveBeenCalledWith(1, mockUserProfile.user_id);
});
});
describe('GET /spending-analysis', () => {
it('should return spending analysis data for a valid date range', async () => {
const mockSpendingData: SpendingByCategory[] = [{ category_id: 1, category_name: 'Produce', total_spent_cents: 12345 }];
mockQuery.mockResolvedValue({ rows: mockSpendingData });
// Mock the service function
vi.mocked(db.getSpendingByCategory).mockResolvedValue(mockSpendingData);
const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31');
@@ -164,12 +169,12 @@ describe('Budget Routes (/api/budgets)', () => {
});
it('should return 500 if the database call fails', async () => {
mockQuery.mockRejectedValue(new Error('DB Error'));
// Mock the service function to throw
vi.mocked(db.getSpendingByCategory).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31');
expect(response.status).toBe(500);
expect(response.body.message).toBe('Failed to fetch spending analysis.');
});
});
});

View File

@@ -15,11 +15,18 @@ router.get('/pm2-status', (req: Request, res: Response) => {
if (error) {
// 'pm2 describe' exits with an error if the process is not found.
// We can treat this as a "fail" status for our check.
if (stdout.includes("doesn't exist")) {
if (stdout && stdout.includes("doesn't exist")) {
logger.warn('[API /pm2-status] PM2 process "flyer-crawler-api" not found.');
return res.json({ success: false, message: 'Application process is not running under PM2.' });
}
logger.error('[API /pm2-status] Error executing pm2 describe:', { error: stderr });
logger.error('[API /pm2-status] Error executing pm2 describe:', { error: stderr || error.message });
return res.status(500).json({ success: false, message: 'Failed to query PM2 status.' });
}
// Check if there was output to stderr, even if the exit code was 0 (success).
// This handles warnings or non-fatal errors that should arguably be treated as failures in this context.
if (stderr && stderr.trim().length > 0) {
logger.error('[API /pm2-status] PM2 executed but produced stderr:', { stderr });
return res.status(500).json({ success: false, message: 'Failed to query PM2 status.' });
}

View File

@@ -22,16 +22,6 @@ vi.mock('bcrypt', () => ({
}));
// Mock the logger
vi.mock('../services/logger.server', () => ({
logger: {
info: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
},
});
// Mock the logger to keep test output clean
vi.mock('../services/logger.server', () => ({
logger: {
info: vi.fn(),
@@ -41,8 +31,6 @@ vi.mock('../services/logger.server', () => ({
},
}));
// This is a more robust way to mock passport. We mock the entire module.
// The mock middleware can be controlled by a test-scoped variable.
let mockAuthMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => next();
vi.mock('./passport', () => ({
@@ -122,7 +110,7 @@ describe('User Routes (/api/users)', () => {
it('should return 404 if profile is not found in DB', async () => {
// Arrange
vi.mocked(db.findUserProfileById).mockResolvedValue(null);
vi.mocked(db.findUserProfileById).mockResolvedValue(undefined);
// Act
const response = await supertest(app).get('/api/users/profile');
@@ -362,7 +350,7 @@ describe('User Routes (/api/users)', () => {
const updatedProfile = {
...mockUserProfile,
preferences: { ...mockUserProfile.preferences, ...preferencesUpdate } };
mockQuery.mockResolvedValue({ rows: [updatedProfile] });
vi.mocked(db.updateUserPreferences).mockResolvedValue(updatedProfile);
// Act
const response = await supertest(app)
@@ -422,63 +410,7 @@ describe('User Routes (/api/users)', () => {
});
it('PUT should successfully set the appliances', async () => {
vi.mocked(db.setUserAppliances).mockResolvedValue(undefined);
const applianceIds = [2, 4, 6];
const response = await supertest(app).put('/api/users/me/appliances').send({ applianceIds });
expect(response.status).toBe(204);
});
});
});
});
});
const response = await supertest(app)
.put('/api/users/profile/preferences')
.set('Content-Type', 'application/json')
.send('"not-an-object"');
// Assert
expect(response.status).toBe(400);
expect(response.body.message).toBe('Invalid preferences format. Body must be a JSON object.');
});
});
describe('GET and PUT /users/me/dietary-restrictions', () => {
it('GET should return a list of restriction IDs', async () => {
const mockRestrictions: Awaited<ReturnType<typeof db.getUserDietaryRestrictions>> = [{ dietary_restriction_id: 1, name: 'Gluten-Free', type: 'diet' }];
mockQuery.mockResolvedValue({ rows: mockRestrictions });
const response = await supertest(app).get('/api/users/me/dietary-restrictions');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockRestrictions);
});
it('PUT should successfully set the restrictions', async () => {
mockQuery.mockResolvedValue({ rows: [] });
const restrictionIds = [1, 3, 5];
const response = await supertest(app)
.put('/api/users/me/dietary-restrictions')
.send({ restrictionIds });
expect(response.status).toBe(204);
});
});
describe('GET and PUT /users/me/appliances', () => {
it('GET should return a list of appliance IDs', async () => {
const mockAppliances: Awaited<ReturnType<typeof db.getUserAppliances>> = [{ appliance_id: 2, name: 'Air Fryer' }];
mockQuery.mockResolvedValue({ rows: mockAppliances });
const response = await supertest(app).get('/api/users/me/appliances');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockAppliances);
});
it('PUT should successfully set the appliances', async () => {
mockQuery.mockResolvedValue({ rows: [] });
vi.mocked(db.setUserAppliances).mockResolvedValue([]);
const applianceIds = [2, 4, 6];
const response = await supertest(app).put('/api/users/me/appliances').send({ applianceIds });
expect(response.status).toBe(204);