Compare commits

...

5 Commits

Author SHA1 Message Date
Gitea Actions
219de4a25c ci: Bump version to 0.1.16 [skip ci] 2025-12-26 22:53:31 +05:00
1540d5051f Merge branch 'main' of https://gitea.projectium.com/torbo/flyer-crawler.projectium.com
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m45s
2025-12-26 09:52:47 -08:00
9c978c26fa not sure why those errors got removed we'll see 2025-12-26 09:52:41 -08:00
Gitea Actions
adb109d8e9 ci: Bump version to 0.1.15 [skip ci] 2025-12-26 22:33:15 +05:00
c668c8785f not sure why those errors got removed we'll see
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m39s
2025-12-26 09:32:38 -08:00
6 changed files with 102 additions and 53 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.1.14",
"version": "0.1.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.1.14",
"version": "0.1.16",
"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.1.14",
"version": "0.1.16",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -161,10 +161,14 @@ describe('Health Routes (/api/health)', () => {
const response = await supertest(app).get('/api/health/db-schema');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB connection failed');
expect(logger.error).toHaveBeenCalledWith(
{ error: 'DB connection failed' },
'Error during DB schema check:',
expect(response.body.message).toBe('DB connection failed'); // This is the message from the original error
expect(response.body.stack).toBeDefined();
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
);
});
@@ -176,10 +180,13 @@ describe('Health Routes (/api/health)', () => {
const response = await supertest(app).get('/api/health/db-schema');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB connection failed');
expect(logger.error).toHaveBeenCalledWith(
{ error: dbError },
'Error during DB schema check:',
expect(response.body.message).toBe('DB connection failed'); // This is the message from the original error
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.objectContaining({ message: 'DB connection failed' }),
}),
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
);
});
});
@@ -209,9 +216,11 @@ describe('Health Routes (/api/health)', () => {
// Assert
expect(response.status).toBe(500);
expect(response.body.message).toContain('Storage check failed.');
expect(logger.error).toHaveBeenCalledWith(
{ error: 'EACCES: permission denied' },
expect.stringContaining('Storage check failed for path:'),
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
);
});
@@ -226,9 +235,11 @@ describe('Health Routes (/api/health)', () => {
// Assert
expect(response.status).toBe(500);
expect(response.body.message).toContain('Storage check failed.');
expect(logger.error).toHaveBeenCalledWith(
{ error: accessError },
expect.stringContaining('Storage check failed for path:'),
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
);
});
});
@@ -283,10 +294,13 @@ describe('Health Routes (/api/health)', () => {
const response = await supertest(app).get('/api/health/db-pool');
expect(response.status).toBe(500);
expect(response.body.message).toBe('Pool is not initialized');
expect(logger.error).toHaveBeenCalledWith(
{ error: 'Pool is not initialized' },
'Error during DB pool health check:',
expect(response.body.message).toBe('Pool is not initialized'); // This is the message from the original error
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
);
});
@@ -300,10 +314,51 @@ describe('Health Routes (/api/health)', () => {
const response = await supertest(app).get('/api/health/db-pool');
expect(response.status).toBe(500);
expect(response.body.message).toBe('Pool is not initialized');
expect(logger.error).toHaveBeenCalledWith(
{ error: poolError },
'Error during DB pool health check:',
expect(response.body.message).toBe('Pool is not initialized'); // This is the message from the original error
expect(response.body.stack).toBeDefined();
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.objectContaining({ message: 'Pool is not initialized' }),
}),
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
);
});
describe('GET /redis', () => {
it('should return 500 if Redis ping fails', async () => {
const redisError = new Error('Connection timed out');
mockedRedisConnection.ping.mockRejectedValue(redisError);
const response = await supertest(app).get('/api/health/redis');
expect(response.status).toBe(500);
expect(response.body.message).toBe('Connection timed out');
expect(response.body.stack).toBeDefined();
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
);
});
it('should return 500 if Redis ping returns an unexpected response', async () => {
mockedRedisConnection.ping.mockResolvedValue('OK'); // Not 'PONG'
const response = await supertest(app).get('/api/health/redis');
expect(response.status).toBe(500);
expect(response.body.message).toContain('Unexpected Redis ping response: OK');
expect(response.body.stack).toBeDefined();
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
);
});
});
});

View File

@@ -39,7 +39,6 @@ router.get('/db-schema', validateRequest(emptySchema), async (req, res, next: Ne
}
return res.status(200).json({ success: true, message: 'All required database tables exist.' });
} catch (error: unknown) {
logger.error({ error }, 'Error during DB schema check:');
next(error);
}
});
@@ -59,10 +58,6 @@ router.get('/storage', validateRequest(emptySchema), async (req, res, next: Next
message: `Storage directory '${storagePath}' is accessible and writable.`,
});
} catch (error: unknown) {
logger.error(
{ error: error instanceof Error ? error.message : error },
`Storage check failed for path: ${storagePath}`,
);
next(
new Error(
`Storage check failed. Ensure the directory '${storagePath}' exists and is writable by the application.`,
@@ -93,10 +88,6 @@ router.get(
.json({ success: false, message: `Pool may be under stress. ${message}` });
}
} catch (error: unknown) {
logger.error(
{ error: error instanceof Error ? error.message : error },
'Error during DB pool health check:',
);
next(error);
}
},
@@ -130,7 +121,6 @@ router.get(
}
throw new Error(`Unexpected Redis ping response: ${reply}`); // This will be caught below
} catch (error: unknown) {
logger.error({ error }, 'Error checking Redis health');
next(error);
}
},

View File

@@ -148,8 +148,8 @@ describe('User Routes (/api/users)', () => {
// Assert
expect(logger.error).toHaveBeenCalledWith(
'Failed to create avatar upload directory:',
mkdirError,
{ err: mkdirError },
'Failed to create avatar upload directory',
);
vi.doUnmock('node:fs/promises'); // Clean up
});

View File

@@ -86,20 +86,6 @@ vi.mock('./flyerDataTransformer', () => ({
},
}));
// Import the module under test AFTER the mocks are set up.
// This will trigger the instantiation of the workers.
import './queueService.server';
// Destructure the captured processors for easier use in tests.
const {
'flyer-processing': flyerProcessor,
'email-sending': emailProcessor,
'analytics-reporting': analyticsProcessor,
'file-cleanup': cleanupProcessor,
'weekly-analytics-reporting': weeklyAnalyticsProcessor,
'token-cleanup': tokenCleanupProcessor,
} = mocks.capturedProcessors;
// Helper to create a mock BullMQ Job object
const createMockJob = <T>(data: T): Job<T> => {
return {
@@ -116,14 +102,32 @@ const createMockJob = <T>(data: T): Job<T> => {
};
describe('Queue Workers', () => {
beforeEach(() => {
let flyerProcessor: (job: Job) => Promise<unknown>;
let emailProcessor: (job: Job) => Promise<unknown>;
let analyticsProcessor: (job: Job) => Promise<unknown>;
let cleanupProcessor: (job: Job) => Promise<unknown>;
let weeklyAnalyticsProcessor: (job: Job) => Promise<unknown>;
let tokenCleanupProcessor: (job: Job) => Promise<unknown>;
beforeEach(async () => {
vi.clearAllMocks();
vi.resetModules();
// Reset default mock implementations for hoisted mocks
mocks.sendEmail.mockResolvedValue(undefined);
mocks.unlink.mockResolvedValue(undefined);
mocks.processFlyerJob.mockResolvedValue({ flyerId: 123 }); // Default success for flyer processing
mocks.deleteExpiredResetTokens.mockResolvedValue(5);
await import('./workers.server');
flyerProcessor = mocks.capturedProcessors['flyer-processing'];
emailProcessor = mocks.capturedProcessors['email-sending'];
analyticsProcessor = mocks.capturedProcessors['analytics-reporting'];
cleanupProcessor = mocks.capturedProcessors['file-cleanup'];
weeklyAnalyticsProcessor = mocks.capturedProcessors['weekly-analytics-reporting'];
tokenCleanupProcessor = mocks.capturedProcessors['token-cleanup'];
});
mocks.deleteExpiredResetTokens.mockResolvedValue(5);
describe('flyerWorker', () => {
it('should call flyerProcessingService.processJob with the job data', async () => {