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.
Files changed (217) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. 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
+ });