Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0db90dfaa6 | ||
| b7a1294ae6 | |||
|
|
be652f9790 | ||
| 1a3e6a9ab5 | |||
|
|
262396ddd0 | ||
| c542796048 | |||
| 5b8f309ad8 |
@@ -51,7 +51,14 @@ jobs:
|
||||
|
||||
# Bump the patch version number. This creates a new commit and a new tag.
|
||||
# The commit message includes [skip ci] to prevent this push from triggering another workflow run.
|
||||
npm version patch -m "ci: Bump version to %s [skip ci]"
|
||||
# If the tag already exists (e.g. re-running a failed job), we skip the conflicting version.
|
||||
if ! npm version patch -m "ci: Bump version to %s [skip ci]"; then
|
||||
echo "⚠️ Version bump failed (likely tag exists). Attempting to skip to next version..."
|
||||
# Bump package.json to the conflicting version without git tagging
|
||||
npm version patch --no-git-tag-version > /dev/null
|
||||
# Bump again to the next version, forcing it because the directory is now dirty
|
||||
npm version patch -m "ci: Bump version to %s [skip ci]" --force
|
||||
fi
|
||||
|
||||
# Push the new commit and the new tag back to the main branch.
|
||||
git push --follow-tags
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.7",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.message).toBe('User registered successfully!');
|
||||
expect(response.body.user.email).toBe(newUserEmail);
|
||||
expect(response.body.userprofile.user.email).toBe(newUserEmail);
|
||||
expect(response.body.token).toBeTypeOf('string'); // This was a duplicate, fixed.
|
||||
expect(db.userRepo.createUser).toHaveBeenCalled();
|
||||
});
|
||||
@@ -295,7 +295,7 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
// The API now returns a nested UserProfile object
|
||||
expect(response.body.user).toEqual(
|
||||
expect(response.body.userprofile).toEqual(
|
||||
expect.objectContaining({
|
||||
user_id: 'user-123',
|
||||
user: expect.objectContaining({
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
// src/routes/system.routes.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import { exec, type ExecException, type ExecOptions } from 'child_process'; // Keep this for mocking
|
||||
import { geocodingService } from '../services/geocodingService.server';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
|
||||
// FIX: Use the simple factory pattern for child_process to avoid default export issues
|
||||
// =============================================================================
|
||||
// MOCKS CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
// 1. Mock child_process
|
||||
// FIX: Use the simple factory pattern for child_process to avoid default export issues.
|
||||
// We also add logging here to catch any un-mocked usages immediately.
|
||||
vi.mock('child_process', () => {
|
||||
const mockExec = vi.fn((command, callback) => {
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, 'PM2 OK', '');
|
||||
const defaultMockExec = (command: string, ...args: any[]) => {
|
||||
console.log(`[MOCK:child_process] Global exec hit for command: "${command}"`);
|
||||
const callback = args.find((arg) => typeof arg === 'function');
|
||||
if (callback) {
|
||||
// Defensive: Run callback async to prevent Zalgo and stack overflows
|
||||
process.nextTick(() => {
|
||||
callback(null, 'PM2 OK', '');
|
||||
});
|
||||
}
|
||||
return { unref: () => {} };
|
||||
});
|
||||
return {
|
||||
unref: () => {
|
||||
/* no-op */
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
default: { exec: mockExec },
|
||||
exec: mockExec,
|
||||
default: { exec: defaultMockExec },
|
||||
exec: defaultMockExec,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -36,196 +51,156 @@ vi.mock('../services/logger.server', () => ({
|
||||
// Import the router AFTER all mocks are defined.
|
||||
import systemRouter from './system.routes';
|
||||
|
||||
// =============================================================================
|
||||
// TEST SUITE
|
||||
// =============================================================================
|
||||
|
||||
describe('System Routes (/api/system)', () => {
|
||||
console.log('[TEST SUITE] initializing System Routes tests...');
|
||||
|
||||
const app = createTestApp({ router: systemRouter, basePath: '/api/system' });
|
||||
|
||||
// Add a basic error handler to capture errors passed to next(err) and return JSON.
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
console.error('[TEST SUITE] Error caught in test app error handler:', err.message);
|
||||
res.status(err.status || 500).json({ message: err.message, errors: err.errors });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// We cast here to get type-safe access to mock functions like .mockImplementation
|
||||
vi.clearAllMocks();
|
||||
console.log('[TEST SUITE] Mocks cleared.');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks();
|
||||
console.log('[TEST SUITE] Mocks restored. Suite finished.');
|
||||
});
|
||||
|
||||
describe('GET /pm2-status', () => {
|
||||
it('should return success: true when pm2 process is online', async () => {
|
||||
// Arrange: Simulate a successful `pm2 describe` output for an online process.
|
||||
const pm2OnlineOutput = `
|
||||
// Helper function to set up the mock for `child_process.exec` for each test case.
|
||||
// This avoids repeating the complex mock implementation in every test.
|
||||
const setupExecMock = (error: ExecException | null, stdout: string, stderr: string) => {
|
||||
console.log('[TEST SETUP] Configuring exec mock implementation');
|
||||
|
||||
vi.mocked(exec).mockImplementation(
|
||||
(
|
||||
command: string,
|
||||
optionsOrCb?:
|
||||
| ExecOptions
|
||||
| ((error: ExecException | null, stdout: string, stderr: string) => void)
|
||||
| null,
|
||||
cb?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
|
||||
) => {
|
||||
console.log(`[MOCK EXEC] Command received: "${command}"`);
|
||||
|
||||
// Normalize arguments: options is optional
|
||||
let callback:
|
||||
| ((error: ExecException | null, stdout: string, stderr: string) => void)
|
||||
| undefined;
|
||||
|
||||
if (typeof optionsOrCb === 'function') {
|
||||
callback = optionsOrCb;
|
||||
} else if (typeof cb === 'function') {
|
||||
callback = cb;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
const safeCallback = callback;
|
||||
// Defensive: Execute callback asynchronously (nextTick) to simulate real I/O
|
||||
// This prevents the "synchronous callback" issue which can confuse test runners and async/await
|
||||
process.nextTick(() => {
|
||||
console.log(
|
||||
`[MOCK EXEC] Invoking callback for "${command}" | Error: ${!!error} | Stdout len: ${stdout.length}`,
|
||||
);
|
||||
try {
|
||||
safeCallback(error, stdout, stderr);
|
||||
} catch (err) {
|
||||
console.error('[MOCK EXEC] CRITICAL: Error inside exec callback:', err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn(`[MOCK EXEC] No callback provided for "${command}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
unref: () => {
|
||||
/* no-op */
|
||||
},
|
||||
} as ReturnType<typeof exec>;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
description: 'should return success: true when pm2 process is online',
|
||||
mock: {
|
||||
error: null,
|
||||
stdout: `
|
||||
┌─ PM2 info ────────────────┐
|
||||
│ status │ online │
|
||||
└───────────┴───────────┘
|
||||
`;
|
||||
|
||||
type ExecCallback = (error: ExecException | null, stdout: string, stderr: string) => void;
|
||||
|
||||
// A robust mock for `exec` that handles its multiple overloads.
|
||||
// This avoids the complex and error-prone `...args` signature.
|
||||
vi.mocked(exec).mockImplementation(
|
||||
(
|
||||
command: string,
|
||||
options?: ExecOptions | ExecCallback | null,
|
||||
callback?: ExecCallback | null,
|
||||
) => {
|
||||
// The actual callback can be the second or third argument.
|
||||
const actualCallback = (
|
||||
typeof options === 'function' ? options : callback
|
||||
) as ExecCallback;
|
||||
if (actualCallback) {
|
||||
actualCallback(null, pm2OnlineOutput, '');
|
||||
}
|
||||
// Return a minimal object that satisfies the ChildProcess type for .unref()
|
||||
return { unref: () => {} } as ReturnType<typeof exec>;
|
||||
`,
|
||||
stderr: '',
|
||||
},
|
||||
);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/system/pm2-status');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
success: true,
|
||||
message: 'Application is online and running under PM2.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return success: false when pm2 process is stopped or errored', async () => {
|
||||
const pm2StoppedOutput = `│ status │ stopped │`;
|
||||
|
||||
vi.mocked(exec).mockImplementation(
|
||||
(
|
||||
command: string,
|
||||
options?:
|
||||
| ExecOptions
|
||||
| ((error: ExecException | null, stdout: string, stderr: string) => void)
|
||||
| null,
|
||||
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
|
||||
) => {
|
||||
const actualCallback = (typeof options === 'function' ? options : callback) as (
|
||||
error: ExecException | null,
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
) => void;
|
||||
if (actualCallback) {
|
||||
actualCallback(null, pm2StoppedOutput, '');
|
||||
}
|
||||
return { unref: () => {} } as ReturnType<typeof exec>;
|
||||
expectedStatus: 200,
|
||||
expectedBody: { success: true, message: 'Application is online and running under PM2.' },
|
||||
},
|
||||
{
|
||||
description: 'should return success: false when pm2 process is stopped',
|
||||
mock: { error: null, stdout: '│ status │ stopped │', stderr: '' },
|
||||
expectedStatus: 200,
|
||||
expectedBody: { success: false, message: 'Application process exists but is not online.' },
|
||||
},
|
||||
{
|
||||
description: 'should return success: false when pm2 process does not exist',
|
||||
mock: {
|
||||
error: Object.assign(new Error('Command failed'), { code: 1 }),
|
||||
stdout: "[PM2][ERROR] Process or Namespace flyer-crawler-api doesn't exist",
|
||||
stderr: '',
|
||||
},
|
||||
);
|
||||
expectedStatus: 200,
|
||||
expectedBody: { success: false, message: 'Application process is not running under PM2.' },
|
||||
},
|
||||
{
|
||||
description: 'should return 500 if pm2 command produces stderr output',
|
||||
mock: { error: null, stdout: 'Some stdout', stderr: 'A non-fatal warning occurred.' },
|
||||
expectedStatus: 500,
|
||||
expectedBody: { message: 'PM2 command produced an error: A non-fatal warning occurred.' },
|
||||
},
|
||||
{
|
||||
description: 'should return 500 on a generic exec error',
|
||||
mock: { error: new Error('System error'), stdout: '', stderr: 'stderr output' },
|
||||
expectedStatus: 500,
|
||||
expectedBody: { message: 'System error' },
|
||||
},
|
||||
];
|
||||
|
||||
const response = await supertest(app).get('/api/system/pm2-status');
|
||||
it.each(testCases)(
|
||||
'$description',
|
||||
async ({ mock, expectedStatus, expectedBody, description }) => {
|
||||
console.log(`[TEST CASE] Starting: ${description}`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
// Arrange
|
||||
setupExecMock(mock.error as ExecException | null, mock.stdout, mock.stderr);
|
||||
|
||||
it('should return success: false when pm2 process does not exist', async () => {
|
||||
// Arrange: Simulate `pm2 describe` failing because the process isn't found.
|
||||
const processNotFoundOutput =
|
||||
"[PM2][ERROR] Process or Namespace flyer-crawler-api doesn't exist";
|
||||
const processNotFoundError = new Error(
|
||||
'Command failed: pm2 describe flyer-crawler-api',
|
||||
) as ExecException;
|
||||
processNotFoundError.code = 1;
|
||||
// Act
|
||||
console.log('[TEST CASE] Sending request...');
|
||||
const response = await supertest(app).get('/api/system/pm2-status');
|
||||
console.log(`[TEST CASE] Response received: status=${response.status}`);
|
||||
|
||||
vi.mocked(exec).mockImplementation(
|
||||
(
|
||||
command: string,
|
||||
options?:
|
||||
| ExecOptions
|
||||
| ((error: ExecException | null, stdout: string, stderr: string) => void)
|
||||
| null,
|
||||
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
|
||||
) => {
|
||||
const actualCallback = (typeof options === 'function' ? options : callback) as (
|
||||
error: ExecException | null,
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
) => void;
|
||||
if (actualCallback) {
|
||||
actualCallback(processNotFoundError, processNotFoundOutput, '');
|
||||
}
|
||||
return { unref: () => {} } as ReturnType<typeof exec>;
|
||||
},
|
||||
);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/system/pm2-status');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
success: false,
|
||||
message: 'Application process is not running under PM2.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 500 if pm2 command produces stderr output', async () => {
|
||||
// Arrange: Simulate a successful exit code but with content in stderr.
|
||||
const stderrOutput = 'A non-fatal warning occurred.';
|
||||
|
||||
vi.mocked(exec).mockImplementation(
|
||||
(
|
||||
command: string,
|
||||
options?:
|
||||
| ExecOptions
|
||||
| ((error: ExecException | null, stdout: string, stderr: string) => void)
|
||||
| null,
|
||||
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
|
||||
) => {
|
||||
const actualCallback = (typeof options === 'function' ? options : callback) as (
|
||||
error: ExecException | null,
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
) => void;
|
||||
if (actualCallback) {
|
||||
actualCallback(null, 'Some stdout', stderrOutput);
|
||||
}
|
||||
return { unref: () => {} } as ReturnType<typeof exec>;
|
||||
},
|
||||
);
|
||||
|
||||
const response = await supertest(app).get('/api/system/pm2-status');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe(`PM2 command produced an error: ${stderrOutput}`);
|
||||
});
|
||||
|
||||
it('should return 500 on a generic exec error', async () => {
|
||||
vi.mocked(exec).mockImplementation(
|
||||
(
|
||||
command: string,
|
||||
options?:
|
||||
| ExecOptions
|
||||
| ((error: ExecException | null, stdout: string, stderr: string) => void)
|
||||
| null,
|
||||
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
|
||||
) => {
|
||||
const actualCallback = (typeof options === 'function' ? options : callback) as (
|
||||
error: ExecException | null,
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
) => void;
|
||||
if (actualCallback) {
|
||||
actualCallback(new Error('System error') as ExecException, '', 'stderr output');
|
||||
}
|
||||
return { unref: () => {} } as ReturnType<typeof exec>;
|
||||
},
|
||||
);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/system/pm2-status');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('System error');
|
||||
});
|
||||
// Assert
|
||||
expect(response.status).toBe(expectedStatus);
|
||||
expect(response.body).toEqual(expectedBody);
|
||||
console.log(`[TEST CASE] Completed: ${description}`);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('POST /geocode', () => {
|
||||
it('should return geocoded coordinates for a valid address', async () => {
|
||||
console.log('[TEST CASE] POST /geocode - valid address');
|
||||
// Arrange
|
||||
const mockCoordinates = { lat: 48.4284, lng: -123.3656 };
|
||||
vi.mocked(geocodingService.geocodeAddress).mockResolvedValue(mockCoordinates);
|
||||
@@ -241,6 +216,7 @@ describe('System Routes (/api/system)', () => {
|
||||
});
|
||||
|
||||
it('should return 404 if the address cannot be geocoded', async () => {
|
||||
console.log('[TEST CASE] POST /geocode - address not found');
|
||||
vi.mocked(geocodingService.geocodeAddress).mockResolvedValue(null);
|
||||
const response = await supertest(app)
|
||||
.post('/api/system/geocode')
|
||||
@@ -250,6 +226,7 @@ describe('System Routes (/api/system)', () => {
|
||||
});
|
||||
|
||||
it('should return 500 if the geocoding service throws an error', async () => {
|
||||
console.log('[TEST CASE] POST /geocode - service error');
|
||||
const geocodeError = new Error('Geocoding service unavailable');
|
||||
vi.mocked(geocodingService.geocodeAddress).mockRejectedValue(geocodeError);
|
||||
const response = await supertest(app)
|
||||
@@ -259,6 +236,7 @@ describe('System Routes (/api/system)', () => {
|
||||
});
|
||||
|
||||
it('should return 400 if the address is missing from the body', async () => {
|
||||
console.log('[TEST CASE] POST /geocode - validation error');
|
||||
const response = await supertest(app)
|
||||
.post('/api/system/geocode')
|
||||
.send({ not_address: 'Victoria, BC' });
|
||||
|
||||
@@ -29,12 +29,24 @@ router.get(
|
||||
'/pm2-status',
|
||||
validateRequest(emptySchema),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
// DEBUG: Add logging to trace entry into the route
|
||||
console.log(`[API /pm2-status] Received request. Executing PM2 check...`);
|
||||
|
||||
// The name 'flyer-crawler-api' comes from your ecosystem.config.cjs file.
|
||||
exec('pm2 describe flyer-crawler-api', (error, stdout, stderr) => {
|
||||
// DEBUG: Log callback entry
|
||||
console.log(
|
||||
`[API /pm2-status] PM2 check complete. Error: ${!!error}, Stderr Len: ${stderr?.length}`,
|
||||
);
|
||||
|
||||
// The "doesn't exist" message can appear in stdout or stderr depending on PM2 version and context.
|
||||
const processNotFound =
|
||||
stdout?.includes("doesn't exist") || stderr?.includes("doesn't exist");
|
||||
|
||||
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 && stdout.includes("doesn't exist")) {
|
||||
if (processNotFound) {
|
||||
logger.warn('[API /pm2-status] PM2 process "flyer-crawler-api" not found.');
|
||||
return res.json({
|
||||
success: false,
|
||||
@@ -45,6 +57,8 @@ router.get(
|
||||
{ error: stderr || error.message },
|
||||
'[API /pm2-status] Error executing pm2 describe:',
|
||||
);
|
||||
// DEBUG: Explicit log for test debugging
|
||||
console.error('[API /pm2-status] Exec failed:', error);
|
||||
return next(error);
|
||||
}
|
||||
|
||||
@@ -52,6 +66,7 @@ router.get(
|
||||
// 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({ stderr }, '[API /pm2-status] PM2 executed but produced stderr:');
|
||||
console.error('[API /pm2-status] STDERR present:', stderr);
|
||||
return next(new Error(`PM2 command produced an error: ${stderr}`));
|
||||
}
|
||||
|
||||
@@ -60,6 +75,8 @@ router.get(
|
||||
const message = isOnline
|
||||
? 'Application is online and running under PM2.'
|
||||
: 'Application process exists but is not online.';
|
||||
|
||||
console.log(`[API /pm2-status] Success. Online: ${isOnline}`);
|
||||
res.json({ success: isOnline, message });
|
||||
});
|
||||
},
|
||||
@@ -79,16 +96,22 @@ router.post(
|
||||
body: { address },
|
||||
} = req as unknown as GeocodeRequest;
|
||||
|
||||
// DEBUG
|
||||
console.log(`[API /geocode] Request for address: "${address}"`);
|
||||
|
||||
try {
|
||||
const coordinates = await geocodingService.geocodeAddress(address, req.log);
|
||||
|
||||
if (!coordinates) {
|
||||
console.log('[API /geocode] Address not found.');
|
||||
// This check remains, but now it only fails if BOTH services fail.
|
||||
return res.status(404).json({ message: 'Could not geocode the provided address.' });
|
||||
}
|
||||
|
||||
console.log('[API /geocode] Success:', coordinates);
|
||||
res.json(coordinates);
|
||||
} catch (error) {
|
||||
console.error('[API /geocode] Error:', error);
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -128,7 +128,7 @@ export class AIService {
|
||||
// This preserves the dependency injection pattern used throughout the class.
|
||||
this.aiClient = genAI
|
||||
? {
|
||||
generateContent: (request) => {
|
||||
generateContent: async (request) => {
|
||||
// The model name is now injected here, into every call, as the new SDK requires.
|
||||
// Architectural guard clause: All requests from this service must have content.
|
||||
// This prevents sending invalid requests to the API and satisfies TypeScript's strictness.
|
||||
|
||||
@@ -52,7 +52,10 @@ export class UserRepository {
|
||||
);
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
logger.error({ err: error, email }, 'Database error in findUserByEmail');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, email },
|
||||
'Database error in findUserByEmail',
|
||||
);
|
||||
throw new Error('Failed to retrieve user from database.');
|
||||
}
|
||||
}
|
||||
@@ -127,7 +130,10 @@ export class UserRepository {
|
||||
throw new UniqueConstraintError('A user with this email address already exists.');
|
||||
}
|
||||
// The withTransaction helper logs the rollback, so we just log the context here.
|
||||
logger.error({ err: error, email }, 'Error during createUser transaction');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, email },
|
||||
'Error during createUser transaction',
|
||||
);
|
||||
throw new Error('Failed to create user in database.');
|
||||
});
|
||||
}
|
||||
@@ -182,7 +188,10 @@ export class UserRepository {
|
||||
|
||||
return authableProfile;
|
||||
} catch (error) {
|
||||
logger.error({ err: error, email }, 'Database error in findUserWithProfileByEmail');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, email },
|
||||
'Database error in findUserWithProfileByEmail',
|
||||
);
|
||||
throw new Error('Failed to retrieve user with profile from database.');
|
||||
}
|
||||
}
|
||||
@@ -205,7 +214,10 @@ export class UserRepository {
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) throw error;
|
||||
logger.error({ err: error, userId }, 'Database error in findUserById');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId },
|
||||
'Database error in findUserById',
|
||||
);
|
||||
throw new Error('Failed to retrieve user by ID from database.');
|
||||
}
|
||||
}
|
||||
@@ -229,7 +241,10 @@ export class UserRepository {
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) throw error;
|
||||
logger.error({ err: error, userId }, 'Database error in findUserWithPasswordHashById');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId },
|
||||
'Database error in findUserWithPasswordHashById',
|
||||
);
|
||||
throw new Error('Failed to retrieve user with sensitive data by ID from database.');
|
||||
}
|
||||
}
|
||||
@@ -275,7 +290,10 @@ export class UserRepository {
|
||||
if (error instanceof NotFoundError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error({ err: error, userId }, 'Database error in findUserProfileById');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId },
|
||||
'Database error in findUserProfileById',
|
||||
);
|
||||
throw new Error('Failed to retrieve user profile from database.');
|
||||
}
|
||||
}
|
||||
@@ -321,7 +339,10 @@ export class UserRepository {
|
||||
if (error instanceof NotFoundError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error({ err: error, userId, profileData }, 'Database error in updateUserProfile');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId, profileData },
|
||||
'Database error in updateUserProfile',
|
||||
);
|
||||
throw new Error('Failed to update user profile in database.');
|
||||
}
|
||||
}
|
||||
@@ -350,7 +371,10 @@ export class UserRepository {
|
||||
if (error instanceof NotFoundError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error({ err: error, userId, preferences }, 'Database error in updateUserPreferences');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId, preferences },
|
||||
'Database error in updateUserPreferences',
|
||||
);
|
||||
throw new Error('Failed to update user preferences in database.');
|
||||
}
|
||||
}
|
||||
@@ -368,7 +392,10 @@ export class UserRepository {
|
||||
[passwordHash, userId]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error({ err: error, userId }, 'Database error in updateUserPassword');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId },
|
||||
'Database error in updateUserPassword',
|
||||
);
|
||||
throw new Error('Failed to update user password in database.');
|
||||
}
|
||||
}
|
||||
@@ -382,7 +409,10 @@ export class UserRepository {
|
||||
try {
|
||||
await this.db.query('DELETE FROM public.users WHERE user_id = $1', [userId]);
|
||||
} catch (error) {
|
||||
logger.error({ err: error, userId }, 'Database error in deleteUserById');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId },
|
||||
'Database error in deleteUserById',
|
||||
);
|
||||
throw new Error('Failed to delete user from database.');
|
||||
}
|
||||
}
|
||||
@@ -400,7 +430,10 @@ export class UserRepository {
|
||||
[refreshToken, userId]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error({ err: error, userId }, 'Database error in saveRefreshToken');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId },
|
||||
'Database error in saveRefreshToken',
|
||||
);
|
||||
throw new Error('Failed to save refresh token.');
|
||||
}
|
||||
}
|
||||
@@ -423,7 +456,10 @@ export class UserRepository {
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) throw error;
|
||||
logger.error({ err: error }, 'Database error in findUserByRefreshToken');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error },
|
||||
'Database error in findUserByRefreshToken',
|
||||
);
|
||||
throw new Error('Failed to find user by refresh token.'); // Generic error for other failures
|
||||
}
|
||||
}
|
||||
@@ -438,7 +474,10 @@ export class UserRepository {
|
||||
refreshToken,
|
||||
]);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Database error in deleteRefreshToken');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error },
|
||||
'Database error in deleteRefreshToken',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +500,10 @@ export class UserRepository {
|
||||
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
||||
throw new ForeignKeyConstraintError('The specified user does not exist.');
|
||||
}
|
||||
logger.error({ err: error, userId }, 'Database error in createPasswordResetToken');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId },
|
||||
'Database error in createPasswordResetToken',
|
||||
);
|
||||
throw new Error('Failed to create password reset token.');
|
||||
}
|
||||
}
|
||||
@@ -478,7 +520,10 @@ export class UserRepository {
|
||||
);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Database error in getValidResetTokens');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error },
|
||||
'Database error in getValidResetTokens',
|
||||
);
|
||||
throw new Error('Failed to retrieve valid reset tokens.');
|
||||
}
|
||||
}
|
||||
@@ -492,7 +537,10 @@ export class UserRepository {
|
||||
try {
|
||||
await this.db.query('DELETE FROM public.password_reset_tokens WHERE token_hash = $1', [tokenHash]);
|
||||
} catch (error) {
|
||||
logger.error({ err: error, tokenHash }, 'Database error in deleteResetToken');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, tokenHash },
|
||||
'Database error in deleteResetToken',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +559,10 @@ export class UserRepository {
|
||||
);
|
||||
return res.rowCount ?? 0;
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Database error in deleteExpiredResetTokens');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error },
|
||||
'Database error in deleteExpiredResetTokens',
|
||||
);
|
||||
throw new Error('Failed to delete expired password reset tokens.');
|
||||
}
|
||||
}
|
||||
@@ -530,7 +581,10 @@ export class UserRepository {
|
||||
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
||||
throw new ForeignKeyConstraintError('One or both users do not exist.');
|
||||
}
|
||||
logger.error({ err: error, followerId, followingId }, 'Database error in followUser');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, followerId, followingId },
|
||||
'Database error in followUser',
|
||||
);
|
||||
throw new Error('Failed to follow user.');
|
||||
}
|
||||
}
|
||||
@@ -547,7 +601,10 @@ export class UserRepository {
|
||||
[followerId, followingId],
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error({ err: error, followerId, followingId }, 'Database error in unfollowUser');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, followerId, followingId },
|
||||
'Database error in unfollowUser',
|
||||
);
|
||||
throw new Error('Failed to unfollow user.');
|
||||
}
|
||||
}
|
||||
@@ -578,7 +635,10 @@ export class UserRepository {
|
||||
const res = await this.db.query<ActivityLogItem>(query, [userId, limit, offset]);
|
||||
return res.rows;
|
||||
} catch (error) {
|
||||
logger.error({ err: error, userId, limit, offset }, 'Database error in getUserFeed');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId, limit, offset },
|
||||
'Database error in getUserFeed',
|
||||
);
|
||||
throw new Error('Failed to retrieve user feed.');
|
||||
}
|
||||
}
|
||||
@@ -600,7 +660,10 @@ export class UserRepository {
|
||||
);
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
logger.error({ err: error, queryData }, 'Database error in logSearchQuery');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, queryData },
|
||||
'Database error in logSearchQuery',
|
||||
);
|
||||
throw new Error('Failed to log search query.');
|
||||
}
|
||||
}
|
||||
@@ -634,7 +697,10 @@ export async function exportUserData(userId: string, logger: Logger): Promise<{
|
||||
return { profile, watchedItems, shoppingLists };
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ err: error, userId }, 'Database error in exportUserData');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error, userId },
|
||||
'Database error in exportUserData',
|
||||
);
|
||||
throw new Error('Failed to export user data.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('Geocoding Service', () => {
|
||||
// Assert
|
||||
expect(result).toEqual(coordinates);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: expect.any(Error), cacheKey: expect.any(String) },
|
||||
{ err: 'Redis down', cacheKey: expect.any(String) },
|
||||
'Redis GET or JSON.parse command failed. Proceeding without cache.',
|
||||
);
|
||||
expect(mockGoogleService.geocode).toHaveBeenCalled(); // Should still proceed to fetch
|
||||
@@ -107,7 +107,7 @@ describe('Geocoding Service', () => {
|
||||
expect(mocks.mockRedis.get).toHaveBeenCalledWith(cacheKey);
|
||||
// The service should log the JSON parsing error and continue
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: expect.any(SyntaxError), cacheKey: expect.any(String) },
|
||||
{ err: expect.any(String), cacheKey: expect.any(String) },
|
||||
'Redis GET or JSON.parse command failed. Proceeding without cache.',
|
||||
);
|
||||
expect(mockGoogleService.geocode).toHaveBeenCalledTimes(1);
|
||||
@@ -185,7 +185,7 @@ describe('Geocoding Service', () => {
|
||||
// Assert
|
||||
expect(result).toEqual(coordinates);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: expect.any(Error) },
|
||||
{ err: 'Network Error' },
|
||||
expect.stringContaining('An error occurred while calling the Google Maps Geocoding API'),
|
||||
);
|
||||
expect(mockNominatimService.geocode).toHaveBeenCalledWith(address, logger);
|
||||
@@ -223,7 +223,7 @@ describe('Geocoding Service', () => {
|
||||
expect(mockGoogleService.geocode).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.mockRedis.set).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: expect.any(Error), cacheKey: expect.any(String) },
|
||||
{ err: 'Redis SET failed', cacheKey: expect.any(String) },
|
||||
'Redis SET command failed. Result will not be cached.',
|
||||
);
|
||||
});
|
||||
@@ -271,7 +271,7 @@ describe('Geocoding Service', () => {
|
||||
// Act & Assert
|
||||
await expect(geocodingService.clearGeocodeCache(logger)).rejects.toThrow(redisError);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: redisError },
|
||||
{ err: redisError.message },
|
||||
'Failed to clear geocode cache from Redis.',
|
||||
);
|
||||
expect(mocks.mockRedis.del).not.toHaveBeenCalled();
|
||||
|
||||
@@ -26,7 +26,7 @@ export class GeocodingService {
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error, cacheKey },
|
||||
{ err: error instanceof Error ? error.message : error, cacheKey },
|
||||
'Redis GET or JSON.parse command failed. Proceeding without cache.',
|
||||
);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class GeocodingService {
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error },
|
||||
{ err: error instanceof Error ? error.message : error },
|
||||
'An error occurred while calling the Google Maps Geocoding API. Falling back to Nominatim.',
|
||||
);
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export class GeocodingService {
|
||||
await redis.set(cacheKey, JSON.stringify(result), 'EX', 60 * 60 * 24 * 30); // Cache for 30 days
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error, cacheKey },
|
||||
{ err: error instanceof Error ? error.message : error, cacheKey },
|
||||
'Redis SET command failed. Result will not be cached.',
|
||||
);
|
||||
}
|
||||
@@ -98,7 +98,10 @@ export class GeocodingService {
|
||||
logger.info(`Successfully deleted ${totalDeleted} geocode cache entries.`);
|
||||
return totalDeleted;
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Failed to clear geocode cache from Redis.');
|
||||
logger.error(
|
||||
{ err: error instanceof Error ? error.message : error },
|
||||
'Failed to clear geocode cache from Redis.',
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class GoogleGeocodingService {
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error, address },
|
||||
{ err: error instanceof Error ? error.message : error, address },
|
||||
'[GoogleGeocodingService] An error occurred while calling the Google Maps API.',
|
||||
);
|
||||
throw error; // Re-throw to allow the calling service to handle the failure (e.g., by falling back).
|
||||
|
||||
Reference in New Issue
Block a user