servcraft 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2
|
+
import { QueueService } from '../../src/modules/queue/queue.service.js';
|
|
3
|
+
import type { Job, Worker } from '../../src/modules/queue/types.js';
|
|
4
|
+
|
|
5
|
+
describe('Queue Service with BullMQ Integration', () => {
|
|
6
|
+
let queueService: QueueService;
|
|
7
|
+
const testQueueName = 'test-queue';
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
// Create queue service with test Redis connection
|
|
11
|
+
queueService = new QueueService({
|
|
12
|
+
redis: {
|
|
13
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
14
|
+
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
|
15
|
+
db: 15, // Use separate DB for tests
|
|
16
|
+
},
|
|
17
|
+
prefix: 'test:queue',
|
|
18
|
+
metrics: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Wait for connection
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
// Clean up test queues
|
|
27
|
+
try {
|
|
28
|
+
const queues = await queueService.listQueues();
|
|
29
|
+
for (const queueName of queues) {
|
|
30
|
+
await queueService.obliterateQueue(queueName);
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Ignore cleanup errors
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Close service
|
|
37
|
+
await queueService.close();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
// Drain test queue before each test
|
|
42
|
+
try {
|
|
43
|
+
await queueService.drainQueue(testQueueName);
|
|
44
|
+
} catch {
|
|
45
|
+
// Queue might not exist yet
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Queue Creation', () => {
|
|
50
|
+
it('should create a queue', () => {
|
|
51
|
+
const queue = queueService.createQueue(testQueueName);
|
|
52
|
+
expect(queue).toBeDefined();
|
|
53
|
+
expect(queue.name).toBe(testQueueName);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return same queue instance on multiple calls', () => {
|
|
57
|
+
const queue1 = queueService.createQueue(testQueueName);
|
|
58
|
+
const queue2 = queueService.createQueue(testQueueName);
|
|
59
|
+
expect(queue1).toBe(queue2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should list created queues', async () => {
|
|
63
|
+
queueService.createQueue('queue-1');
|
|
64
|
+
queueService.createQueue('queue-2');
|
|
65
|
+
const queues = await queueService.listQueues();
|
|
66
|
+
expect(queues).toContain('queue-1');
|
|
67
|
+
expect(queues).toContain('queue-2');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Job Management', () => {
|
|
72
|
+
it('should add a job to queue', async () => {
|
|
73
|
+
const job = await queueService.addJob(testQueueName, 'test-job', {
|
|
74
|
+
message: 'Hello World',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(job).toBeDefined();
|
|
78
|
+
expect(job.id).toBeDefined();
|
|
79
|
+
expect(job.name).toBe('test-job');
|
|
80
|
+
expect(job.data).toEqual({ message: 'Hello World' });
|
|
81
|
+
expect(job.queueName).toBe(testQueueName);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should add job with priority', async () => {
|
|
85
|
+
const job = await queueService.addJob(
|
|
86
|
+
testQueueName,
|
|
87
|
+
'priority-job',
|
|
88
|
+
{ value: 1 },
|
|
89
|
+
{ priority: 'high' }
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(job.options.priority).toBe('high');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should add delayed job', async () => {
|
|
96
|
+
const job = await queueService.addJob(
|
|
97
|
+
testQueueName,
|
|
98
|
+
'delayed-job',
|
|
99
|
+
{ value: 1 },
|
|
100
|
+
{ delay: 10000 }
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(job.options.delay).toBe(10000);
|
|
104
|
+
expect(job.delayedUntil).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should add bulk jobs', async () => {
|
|
108
|
+
const jobs = await queueService.addBulkJobs(testQueueName, {
|
|
109
|
+
jobs: [
|
|
110
|
+
{ name: 'bulk-job-1', data: { index: 1 } },
|
|
111
|
+
{ name: 'bulk-job-2', data: { index: 2 } },
|
|
112
|
+
{ name: 'bulk-job-3', data: { index: 3 } },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(jobs).toHaveLength(3);
|
|
117
|
+
expect(jobs[0].name).toBe('bulk-job-1');
|
|
118
|
+
expect(jobs[1].name).toBe('bulk-job-2');
|
|
119
|
+
expect(jobs[2].name).toBe('bulk-job-3');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should get job by ID', async () => {
|
|
123
|
+
const addedJob = await queueService.addJob(testQueueName, 'get-job', { value: 42 });
|
|
124
|
+
const retrievedJob = await queueService.getJob(testQueueName, addedJob.id);
|
|
125
|
+
|
|
126
|
+
expect(retrievedJob.id).toBe(addedJob.id);
|
|
127
|
+
expect(retrievedJob.data).toEqual({ value: 42 });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should throw error for non-existent job', async () => {
|
|
131
|
+
queueService.createQueue(testQueueName);
|
|
132
|
+
|
|
133
|
+
await expect(queueService.getJob(testQueueName, 'non-existent-job')).rejects.toThrow(
|
|
134
|
+
'Job not found'
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should remove a waiting job', async () => {
|
|
139
|
+
const job = await queueService.addJob(testQueueName, 'remove-job', { value: 1 });
|
|
140
|
+
|
|
141
|
+
await queueService.removeJob(testQueueName, job.id);
|
|
142
|
+
|
|
143
|
+
await expect(queueService.getJob(testQueueName, job.id)).rejects.toThrow('Job not found');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Job Listing and Filtering', () => {
|
|
148
|
+
it('should list jobs with status filter', async () => {
|
|
149
|
+
await queueService.addJob(testQueueName, 'list-job-1', { index: 1 });
|
|
150
|
+
await queueService.addJob(testQueueName, 'list-job-2', { index: 2 });
|
|
151
|
+
|
|
152
|
+
const jobs = await queueService.listJobs(testQueueName, { status: 'waiting' });
|
|
153
|
+
|
|
154
|
+
expect(jobs.length).toBeGreaterThanOrEqual(2);
|
|
155
|
+
expect(jobs.every((j) => j.status === 'waiting')).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should list jobs with name filter', async () => {
|
|
159
|
+
await queueService.addJob(testQueueName, 'type-a', { index: 1 });
|
|
160
|
+
await queueService.addJob(testQueueName, 'type-b', { index: 2 });
|
|
161
|
+
await queueService.addJob(testQueueName, 'type-a', { index: 3 });
|
|
162
|
+
|
|
163
|
+
const jobs = await queueService.listJobs(testQueueName, { name: 'type-a' });
|
|
164
|
+
|
|
165
|
+
expect(jobs.every((j) => j.name === 'type-a')).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should list jobs with pagination', async () => {
|
|
169
|
+
for (let i = 0; i < 5; i++) {
|
|
170
|
+
await queueService.addJob(testQueueName, 'paginated-job', { index: i });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const jobs = await queueService.listJobs(testQueueName, { limit: 3 });
|
|
174
|
+
|
|
175
|
+
expect(jobs.length).toBeLessThanOrEqual(3);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('Queue Statistics', () => {
|
|
180
|
+
it('should get queue stats', async () => {
|
|
181
|
+
await queueService.addJob(testQueueName, 'stats-job-1', { value: 1 });
|
|
182
|
+
await queueService.addJob(testQueueName, 'stats-job-2', { value: 2 });
|
|
183
|
+
|
|
184
|
+
const stats = await queueService.getStats(testQueueName);
|
|
185
|
+
|
|
186
|
+
expect(stats.name).toBe(testQueueName);
|
|
187
|
+
expect(stats.waiting).toBeGreaterThanOrEqual(2);
|
|
188
|
+
expect(typeof stats.active).toBe('number');
|
|
189
|
+
expect(typeof stats.completed).toBe('number');
|
|
190
|
+
expect(typeof stats.failed).toBe('number');
|
|
191
|
+
expect(typeof stats.delayed).toBe('number');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should get queue metrics', async () => {
|
|
195
|
+
queueService.createQueue(testQueueName);
|
|
196
|
+
|
|
197
|
+
const metrics = await queueService.getMetrics(testQueueName);
|
|
198
|
+
|
|
199
|
+
expect(typeof metrics.totalProcessed).toBe('number');
|
|
200
|
+
expect(typeof metrics.totalFailed).toBe('number');
|
|
201
|
+
expect(typeof metrics.avgProcessingTime).toBe('number');
|
|
202
|
+
expect(typeof metrics.successRate).toBe('number');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('Worker Registration and Job Processing', () => {
|
|
207
|
+
it('should register a worker', async () => {
|
|
208
|
+
const worker: Worker = {
|
|
209
|
+
name: 'test-worker',
|
|
210
|
+
process: async (job: Job) => {
|
|
211
|
+
return { processed: job.data };
|
|
212
|
+
},
|
|
213
|
+
concurrency: 2,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
queueService.registerWorker(testQueueName, worker);
|
|
217
|
+
|
|
218
|
+
// Worker should be registered without errors
|
|
219
|
+
expect(true).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should process jobs with registered worker', async () => {
|
|
223
|
+
const processedJobs: string[] = [];
|
|
224
|
+
const processQueueName = 'process-test-queue';
|
|
225
|
+
|
|
226
|
+
const worker: Worker = {
|
|
227
|
+
name: 'process-worker',
|
|
228
|
+
process: async (job: Job) => {
|
|
229
|
+
processedJobs.push(job.id);
|
|
230
|
+
return { success: true };
|
|
231
|
+
},
|
|
232
|
+
concurrency: 1,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
queueService.registerWorker(processQueueName, worker);
|
|
236
|
+
|
|
237
|
+
const job = await queueService.addJob(processQueueName, 'process-worker', { value: 1 });
|
|
238
|
+
|
|
239
|
+
// Wait for job to be processed
|
|
240
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
241
|
+
|
|
242
|
+
expect(processedJobs).toContain(job.id);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle job failures and retries', async () => {
|
|
246
|
+
let attempts = 0;
|
|
247
|
+
const failQueueName = 'fail-test-queue';
|
|
248
|
+
|
|
249
|
+
const worker: Worker = {
|
|
250
|
+
name: 'fail-worker',
|
|
251
|
+
process: async () => {
|
|
252
|
+
attempts++;
|
|
253
|
+
if (attempts < 3) {
|
|
254
|
+
throw new Error('Intentional failure');
|
|
255
|
+
}
|
|
256
|
+
return { success: true };
|
|
257
|
+
},
|
|
258
|
+
concurrency: 1,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
queueService.registerWorker(failQueueName, worker);
|
|
262
|
+
|
|
263
|
+
await queueService.addJob(
|
|
264
|
+
failQueueName,
|
|
265
|
+
'fail-worker',
|
|
266
|
+
{ value: 1 },
|
|
267
|
+
{
|
|
268
|
+
attempts: 3,
|
|
269
|
+
backoff: { type: 'fixed', delay: 100 },
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Wait for retries
|
|
274
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
275
|
+
|
|
276
|
+
expect(attempts).toBeGreaterThanOrEqual(3);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('Queue Operations', () => {
|
|
281
|
+
it('should pause and resume queue', async () => {
|
|
282
|
+
queueService.createQueue(testQueueName);
|
|
283
|
+
|
|
284
|
+
await queueService.pauseQueue(testQueueName);
|
|
285
|
+
// Queue should be paused without errors
|
|
286
|
+
|
|
287
|
+
await queueService.resumeQueue(testQueueName);
|
|
288
|
+
// Queue should be resumed without errors
|
|
289
|
+
|
|
290
|
+
expect(true).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should drain queue', async () => {
|
|
294
|
+
await queueService.addJob(testQueueName, 'drain-job-1', { value: 1 });
|
|
295
|
+
await queueService.addJob(testQueueName, 'drain-job-2', { value: 2 });
|
|
296
|
+
|
|
297
|
+
await queueService.drainQueue(testQueueName);
|
|
298
|
+
|
|
299
|
+
const stats = await queueService.getStats(testQueueName);
|
|
300
|
+
expect(stats.waiting).toBe(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should clean old jobs', async () => {
|
|
304
|
+
// Add a job that completes
|
|
305
|
+
const cleanQueueName = 'clean-test-queue';
|
|
306
|
+
const worker: Worker = {
|
|
307
|
+
name: 'clean-worker',
|
|
308
|
+
process: async () => ({ done: true }),
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
queueService.registerWorker(cleanQueueName, worker);
|
|
312
|
+
|
|
313
|
+
await queueService.addJob(cleanQueueName, 'clean-worker', { value: 1 });
|
|
314
|
+
|
|
315
|
+
// Wait for job to complete
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
317
|
+
|
|
318
|
+
// Clean completed jobs older than 0ms (all)
|
|
319
|
+
const cleaned = await queueService.cleanJobs(cleanQueueName, 'completed', 0);
|
|
320
|
+
|
|
321
|
+
expect(typeof cleaned).toBe('number');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('Events', () => {
|
|
326
|
+
it('should emit job events', async () => {
|
|
327
|
+
const events: string[] = [];
|
|
328
|
+
|
|
329
|
+
queueService.on('added', () => events.push('added'));
|
|
330
|
+
queueService.on('job:event', () => events.push('job:event'));
|
|
331
|
+
|
|
332
|
+
await queueService.addJob(testQueueName, 'event-job', { value: 1 });
|
|
333
|
+
|
|
334
|
+
// Wait for events
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
336
|
+
|
|
337
|
+
expect(events).toContain('added');
|
|
338
|
+
expect(events).toContain('job:event');
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('Connection Management', () => {
|
|
343
|
+
it('should report connection status', () => {
|
|
344
|
+
const isConnected = queueService.isConnected();
|
|
345
|
+
expect(typeof isConnected).toBe('boolean');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should get connection info', () => {
|
|
349
|
+
const info = queueService.getConnectionInfo();
|
|
350
|
+
expect(info.host).toBeDefined();
|
|
351
|
+
expect(info.port).toBeDefined();
|
|
352
|
+
expect(info.status).toBeDefined();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('Error Handling', () => {
|
|
357
|
+
it('should throw error for non-existent queue operations', async () => {
|
|
358
|
+
await expect(queueService.getStats('non-existent-queue')).rejects.toThrow('Queue not found');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should throw error when pausing non-existent queue', async () => {
|
|
362
|
+
await expect(queueService.pauseQueue('non-existent-queue')).rejects.toThrow(
|
|
363
|
+
'Queue not found'
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should throw error when removing non-existent job', async () => {
|
|
368
|
+
queueService.createQueue(testQueueName);
|
|
369
|
+
await expect(queueService.removeJob(testQueueName, 'non-existent-job')).rejects.toThrow(
|
|
370
|
+
'Job not found'
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('Job Retry', () => {
|
|
376
|
+
it('should retry failed jobs', async () => {
|
|
377
|
+
const retryQueueName = 'retry-test-queue';
|
|
378
|
+
let callCount = 0;
|
|
379
|
+
|
|
380
|
+
const worker: Worker = {
|
|
381
|
+
name: 'retry-worker',
|
|
382
|
+
process: async () => {
|
|
383
|
+
callCount++;
|
|
384
|
+
throw new Error('Always fails');
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
queueService.registerWorker(retryQueueName, worker);
|
|
389
|
+
|
|
390
|
+
await queueService.addJob(
|
|
391
|
+
retryQueueName,
|
|
392
|
+
'retry-worker',
|
|
393
|
+
{ value: 1 },
|
|
394
|
+
{ attempts: 1, removeOnFail: false }
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Wait for job to fail
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
399
|
+
|
|
400
|
+
// Get failed jobs
|
|
401
|
+
const jobs = await queueService.listJobs(retryQueueName, { status: 'failed' });
|
|
402
|
+
|
|
403
|
+
if (jobs.length > 0) {
|
|
404
|
+
const failedJob = jobs[0];
|
|
405
|
+
callCount = 0;
|
|
406
|
+
|
|
407
|
+
// Retry the job
|
|
408
|
+
await queueService.retryJob(retryQueueName, failedJob.id);
|
|
409
|
+
|
|
410
|
+
// Wait for retry
|
|
411
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
412
|
+
|
|
413
|
+
expect(callCount).toBeGreaterThan(0);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
});
|