servcraft 0.1.0 → 0.1.1

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 (216) hide show
  1. package/.claude/settings.local.json +29 -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/README.md +1070 -1
  9. package/dist/cli/index.cjs +2026 -2168
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +2026 -2168
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.cjs +595 -616
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +114 -52
  16. package/dist/index.d.ts +114 -52
  17. package/dist/index.js +595 -616
  18. package/dist/index.js.map +1 -1
  19. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  20. package/docs/DATABASE_MULTI_ORM.md +399 -0
  21. package/docs/PHASE1_BREAKDOWN.md +346 -0
  22. package/docs/PROGRESS.md +550 -0
  23. package/docs/modules/ANALYTICS.md +226 -0
  24. package/docs/modules/API-VERSIONING.md +252 -0
  25. package/docs/modules/AUDIT.md +192 -0
  26. package/docs/modules/AUTH.md +431 -0
  27. package/docs/modules/CACHE.md +346 -0
  28. package/docs/modules/EMAIL.md +254 -0
  29. package/docs/modules/FEATURE-FLAG.md +291 -0
  30. package/docs/modules/I18N.md +294 -0
  31. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  32. package/docs/modules/MFA.md +266 -0
  33. package/docs/modules/NOTIFICATION.md +311 -0
  34. package/docs/modules/OAUTH.md +237 -0
  35. package/docs/modules/PAYMENT.md +804 -0
  36. package/docs/modules/QUEUE.md +540 -0
  37. package/docs/modules/RATE-LIMIT.md +339 -0
  38. package/docs/modules/SEARCH.md +288 -0
  39. package/docs/modules/SECURITY.md +327 -0
  40. package/docs/modules/SESSION.md +382 -0
  41. package/docs/modules/SWAGGER.md +305 -0
  42. package/docs/modules/UPLOAD.md +296 -0
  43. package/docs/modules/USER.md +505 -0
  44. package/docs/modules/VALIDATION.md +294 -0
  45. package/docs/modules/WEBHOOK.md +270 -0
  46. package/docs/modules/WEBSOCKET.md +691 -0
  47. package/package.json +53 -38
  48. package/prisma/schema.prisma +395 -1
  49. package/src/cli/commands/add-module.ts +520 -87
  50. package/src/cli/commands/db.ts +3 -4
  51. package/src/cli/commands/docs.ts +256 -6
  52. package/src/cli/commands/generate.ts +12 -19
  53. package/src/cli/commands/init.ts +384 -214
  54. package/src/cli/index.ts +0 -4
  55. package/src/cli/templates/repository.ts +6 -1
  56. package/src/cli/templates/routes.ts +6 -21
  57. package/src/cli/utils/docs-generator.ts +6 -7
  58. package/src/cli/utils/env-manager.ts +717 -0
  59. package/src/cli/utils/field-parser.ts +16 -7
  60. package/src/cli/utils/interactive-prompt.ts +223 -0
  61. package/src/cli/utils/template-manager.ts +346 -0
  62. package/src/config/database.config.ts +183 -0
  63. package/src/config/env.ts +0 -10
  64. package/src/config/index.ts +0 -14
  65. package/src/core/server.ts +1 -1
  66. package/src/database/adapters/mongoose.adapter.ts +132 -0
  67. package/src/database/adapters/prisma.adapter.ts +118 -0
  68. package/src/database/connection.ts +190 -0
  69. package/src/database/interfaces/database.interface.ts +85 -0
  70. package/src/database/interfaces/index.ts +7 -0
  71. package/src/database/interfaces/repository.interface.ts +129 -0
  72. package/src/database/models/mongoose/index.ts +7 -0
  73. package/src/database/models/mongoose/payment.schema.ts +347 -0
  74. package/src/database/models/mongoose/user.schema.ts +154 -0
  75. package/src/database/prisma.ts +1 -4
  76. package/src/database/redis.ts +101 -0
  77. package/src/database/repositories/mongoose/index.ts +7 -0
  78. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  79. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  80. package/src/database/seed.ts +6 -1
  81. package/src/index.ts +9 -20
  82. package/src/middleware/security.ts +2 -6
  83. package/src/modules/analytics/analytics.routes.ts +80 -0
  84. package/src/modules/analytics/analytics.service.ts +364 -0
  85. package/src/modules/analytics/index.ts +18 -0
  86. package/src/modules/analytics/types.ts +180 -0
  87. package/src/modules/api-versioning/index.ts +15 -0
  88. package/src/modules/api-versioning/types.ts +86 -0
  89. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  90. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  91. package/src/modules/api-versioning/versioning.service.ts +189 -0
  92. package/src/modules/audit/audit.repository.ts +206 -0
  93. package/src/modules/audit/audit.service.ts +27 -59
  94. package/src/modules/auth/auth.controller.ts +2 -2
  95. package/src/modules/auth/auth.middleware.ts +3 -9
  96. package/src/modules/auth/auth.routes.ts +10 -107
  97. package/src/modules/auth/auth.service.ts +126 -23
  98. package/src/modules/auth/index.ts +3 -4
  99. package/src/modules/cache/cache.service.ts +367 -0
  100. package/src/modules/cache/index.ts +10 -0
  101. package/src/modules/cache/types.ts +44 -0
  102. package/src/modules/email/email.service.ts +3 -10
  103. package/src/modules/email/templates.ts +2 -8
  104. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  105. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  106. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  107. package/src/modules/feature-flag/index.ts +20 -0
  108. package/src/modules/feature-flag/types.ts +192 -0
  109. package/src/modules/i18n/i18n.middleware.ts +186 -0
  110. package/src/modules/i18n/i18n.routes.ts +191 -0
  111. package/src/modules/i18n/i18n.service.ts +456 -0
  112. package/src/modules/i18n/index.ts +18 -0
  113. package/src/modules/i18n/types.ts +118 -0
  114. package/src/modules/media-processing/index.ts +17 -0
  115. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  116. package/src/modules/media-processing/media-processing.service.ts +245 -0
  117. package/src/modules/media-processing/types.ts +156 -0
  118. package/src/modules/mfa/index.ts +20 -0
  119. package/src/modules/mfa/mfa.repository.ts +206 -0
  120. package/src/modules/mfa/mfa.routes.ts +595 -0
  121. package/src/modules/mfa/mfa.service.ts +572 -0
  122. package/src/modules/mfa/totp.ts +150 -0
  123. package/src/modules/mfa/types.ts +57 -0
  124. package/src/modules/notification/index.ts +20 -0
  125. package/src/modules/notification/notification.repository.ts +356 -0
  126. package/src/modules/notification/notification.service.ts +483 -0
  127. package/src/modules/notification/types.ts +119 -0
  128. package/src/modules/oauth/index.ts +20 -0
  129. package/src/modules/oauth/oauth.repository.ts +219 -0
  130. package/src/modules/oauth/oauth.routes.ts +446 -0
  131. package/src/modules/oauth/oauth.service.ts +293 -0
  132. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  133. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  134. package/src/modules/oauth/providers/github.provider.ts +248 -0
  135. package/src/modules/oauth/providers/google.provider.ts +189 -0
  136. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  137. package/src/modules/oauth/types.ts +94 -0
  138. package/src/modules/payment/index.ts +19 -0
  139. package/src/modules/payment/payment.repository.ts +733 -0
  140. package/src/modules/payment/payment.routes.ts +390 -0
  141. package/src/modules/payment/payment.service.ts +354 -0
  142. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  143. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  144. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  145. package/src/modules/payment/types.ts +140 -0
  146. package/src/modules/queue/cron.ts +438 -0
  147. package/src/modules/queue/index.ts +87 -0
  148. package/src/modules/queue/queue.routes.ts +600 -0
  149. package/src/modules/queue/queue.service.ts +842 -0
  150. package/src/modules/queue/types.ts +222 -0
  151. package/src/modules/queue/workers.ts +366 -0
  152. package/src/modules/rate-limit/index.ts +59 -0
  153. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  154. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  155. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  156. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  157. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  158. package/src/modules/rate-limit/types.ts +153 -0
  159. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  160. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  161. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  162. package/src/modules/search/index.ts +21 -0
  163. package/src/modules/search/search.service.ts +234 -0
  164. package/src/modules/search/types.ts +214 -0
  165. package/src/modules/security/index.ts +40 -0
  166. package/src/modules/security/sanitize.ts +223 -0
  167. package/src/modules/security/security-audit.service.ts +388 -0
  168. package/src/modules/security/security.middleware.ts +398 -0
  169. package/src/modules/session/index.ts +3 -0
  170. package/src/modules/session/session.repository.ts +159 -0
  171. package/src/modules/session/session.service.ts +340 -0
  172. package/src/modules/session/types.ts +38 -0
  173. package/src/modules/swagger/index.ts +7 -1
  174. package/src/modules/swagger/schema-builder.ts +16 -4
  175. package/src/modules/swagger/swagger.service.ts +9 -10
  176. package/src/modules/swagger/types.ts +0 -2
  177. package/src/modules/upload/index.ts +14 -0
  178. package/src/modules/upload/types.ts +83 -0
  179. package/src/modules/upload/upload.repository.ts +199 -0
  180. package/src/modules/upload/upload.routes.ts +311 -0
  181. package/src/modules/upload/upload.service.ts +448 -0
  182. package/src/modules/user/index.ts +3 -3
  183. package/src/modules/user/user.controller.ts +15 -9
  184. package/src/modules/user/user.repository.ts +237 -113
  185. package/src/modules/user/user.routes.ts +39 -164
  186. package/src/modules/user/user.service.ts +4 -3
  187. package/src/modules/validation/validator.ts +12 -17
  188. package/src/modules/webhook/index.ts +91 -0
  189. package/src/modules/webhook/retry.ts +196 -0
  190. package/src/modules/webhook/signature.ts +135 -0
  191. package/src/modules/webhook/types.ts +181 -0
  192. package/src/modules/webhook/webhook.repository.ts +358 -0
  193. package/src/modules/webhook/webhook.routes.ts +442 -0
  194. package/src/modules/webhook/webhook.service.ts +457 -0
  195. package/src/modules/websocket/features.ts +504 -0
  196. package/src/modules/websocket/index.ts +106 -0
  197. package/src/modules/websocket/middlewares.ts +298 -0
  198. package/src/modules/websocket/types.ts +181 -0
  199. package/src/modules/websocket/websocket.service.ts +692 -0
  200. package/src/utils/errors.ts +7 -0
  201. package/src/utils/pagination.ts +4 -1
  202. package/tests/helpers/db-check.ts +79 -0
  203. package/tests/integration/auth-redis.test.ts +94 -0
  204. package/tests/integration/cache-redis.test.ts +387 -0
  205. package/tests/integration/mongoose-repositories.test.ts +410 -0
  206. package/tests/integration/payment-prisma.test.ts +637 -0
  207. package/tests/integration/queue-bullmq.test.ts +417 -0
  208. package/tests/integration/user-prisma.test.ts +441 -0
  209. package/tests/integration/websocket-socketio.test.ts +552 -0
  210. package/tests/setup.ts +11 -9
  211. package/vitest.config.ts +3 -8
  212. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  213. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  216. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,552 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
2
+ import { createServer } from 'http';
3
+ import { WebSocketService } from '../../src/modules/websocket/websocket.service.js';
4
+ import { io as ioClient, Socket as ClientSocket } from 'socket.io-client';
5
+
6
+ // Helper to wait for socket connection
7
+ const waitForConnect = (client: ClientSocket): Promise<void> => {
8
+ return new Promise((resolve, reject) => {
9
+ const timeout = setTimeout(() => reject(new Error('Connection timeout')), 5000);
10
+ client.on('connect', () => {
11
+ clearTimeout(timeout);
12
+ resolve();
13
+ });
14
+ client.on('connect_error', (error) => {
15
+ clearTimeout(timeout);
16
+ reject(error);
17
+ });
18
+ });
19
+ };
20
+
21
+ // Helper to wait for event
22
+ const waitForEvent = <T>(client: ClientSocket, event: string, timeout = 2000): Promise<T> => {
23
+ return new Promise((resolve, reject) => {
24
+ const timer = setTimeout(() => reject(new Error(`Timeout waiting for ${event}`)), timeout);
25
+ client.once(event, (data: T) => {
26
+ clearTimeout(timer);
27
+ resolve(data);
28
+ });
29
+ });
30
+ };
31
+
32
+ // Helper to wait for ms
33
+ const wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
34
+
35
+ describe('WebSocketService - Socket.io Integration', () => {
36
+ let httpServer: ReturnType<typeof createServer>;
37
+ let wsService: WebSocketService;
38
+ let serverAddress: string;
39
+ const testPort = 3010;
40
+
41
+ beforeAll(async () => {
42
+ // Create HTTP server
43
+ httpServer = createServer();
44
+
45
+ // Initialize WebSocket service
46
+ wsService = new WebSocketService({
47
+ path: '/socket.io',
48
+ cors: {
49
+ origin: '*',
50
+ credentials: true,
51
+ },
52
+ pingTimeout: 60000,
53
+ pingInterval: 25000,
54
+ });
55
+
56
+ // Initialize Socket.io with HTTP server
57
+ wsService.initialize(httpServer);
58
+
59
+ // Start server
60
+ await new Promise<void>((resolve) => {
61
+ httpServer.listen(testPort, () => {
62
+ serverAddress = `http://localhost:${testPort}`;
63
+ resolve();
64
+ });
65
+ });
66
+ });
67
+
68
+ afterAll(async () => {
69
+ await wsService.shutdown();
70
+ httpServer.close();
71
+ });
72
+
73
+ beforeEach(async () => {
74
+ // Clean up any existing connections
75
+ const connectedUsers = wsService.getConnectedUsers();
76
+ for (const user of connectedUsers) {
77
+ await wsService.handleDisconnection(user.socketId);
78
+ }
79
+ });
80
+
81
+ // ==========================================
82
+ // CONNECTION TESTS
83
+ // ==========================================
84
+
85
+ describe('Connection Management', () => {
86
+ it('should accept client connection', async () => {
87
+ const client = ioClient(serverAddress, {
88
+ path: '/socket.io',
89
+ query: {
90
+ username: 'testuser',
91
+ email: 'test@example.com',
92
+ },
93
+ });
94
+
95
+ await waitForConnect(client);
96
+ expect(client.connected).toBe(true);
97
+ client.disconnect();
98
+ });
99
+
100
+ it('should track connected users', async () => {
101
+ const client = ioClient(serverAddress, {
102
+ query: {
103
+ username: 'user1',
104
+ email: 'user1@example.com',
105
+ },
106
+ });
107
+
108
+ await waitForConnect(client);
109
+ await wait(100);
110
+
111
+ const users = wsService.getConnectedUsers();
112
+ expect(users.length).toBeGreaterThan(0);
113
+ client.disconnect();
114
+ });
115
+
116
+ it('should handle multiple connections', async () => {
117
+ const client1 = ioClient(serverAddress, {
118
+ query: { username: 'user1', email: 'user1@example.com' },
119
+ });
120
+ const client2 = ioClient(serverAddress, {
121
+ query: { username: 'user2', email: 'user2@example.com' },
122
+ });
123
+
124
+ await Promise.all([waitForConnect(client1), waitForConnect(client2)]);
125
+ await wait(100);
126
+
127
+ const users = wsService.getConnectedUsers();
128
+ expect(users.length).toBeGreaterThanOrEqual(2);
129
+
130
+ client1.disconnect();
131
+ client2.disconnect();
132
+ });
133
+
134
+ it('should handle disconnection', async () => {
135
+ const client = ioClient(serverAddress, {
136
+ query: { username: 'disconnectuser', email: 'disconnect@example.com' },
137
+ });
138
+
139
+ await waitForConnect(client);
140
+ const socketId = client.id;
141
+ client.disconnect();
142
+
143
+ await wait(100);
144
+ const user = wsService.getUser(socketId);
145
+ expect(user).toBeUndefined();
146
+ });
147
+ });
148
+
149
+ // ==========================================
150
+ // ROOM TESTS
151
+ // ==========================================
152
+
153
+ describe('Room Management', () => {
154
+ let client: ClientSocket;
155
+
156
+ beforeEach(async () => {
157
+ client = ioClient(serverAddress, {
158
+ query: { username: 'roomuser', email: 'room@example.com' },
159
+ });
160
+ await waitForConnect(client);
161
+ });
162
+
163
+ afterEach(() => {
164
+ if (client.connected) {
165
+ client.disconnect();
166
+ }
167
+ });
168
+
169
+ it('should create a room', async () => {
170
+ const room = await wsService.createRoom('test-room', 'default');
171
+ expect(room.id).toBeDefined();
172
+ expect(room.name).toBe('test-room');
173
+ expect(room.namespace).toBe('default');
174
+ });
175
+
176
+ it('should join room', async () => {
177
+ const room = await wsService.createRoom('joinroom', 'default');
178
+ client.emit('room:join', { roomId: room.id });
179
+
180
+ await wait(100);
181
+ const members = wsService.getRoomMembers(room.id);
182
+ expect(members.length).toBeGreaterThan(0);
183
+ });
184
+
185
+ it('should leave room', async () => {
186
+ const room = await wsService.createRoom('leaveroom', 'default');
187
+ client.emit('room:join', { roomId: room.id });
188
+ await wait(100);
189
+
190
+ client.emit('room:leave', { roomId: room.id });
191
+ await wait(100);
192
+
193
+ const members = wsService.getRoomMembers(room.id);
194
+ expect(members.length).toBe(0);
195
+ });
196
+
197
+ it('should list rooms', async () => {
198
+ await wsService.createRoom('room1', 'default');
199
+ await wsService.createRoom('room2', 'custom');
200
+
201
+ const allRooms = wsService.listRooms();
202
+ expect(allRooms.length).toBeGreaterThanOrEqual(2);
203
+
204
+ const defaultRooms = wsService.listRooms('default');
205
+ expect(defaultRooms.length).toBeGreaterThanOrEqual(1);
206
+ });
207
+
208
+ it('should delete room', async () => {
209
+ const room = await wsService.createRoom('deleteroom', 'default');
210
+ await wsService.deleteRoom(room.id);
211
+
212
+ const deletedRoom = wsService.getRoom(room.id);
213
+ expect(deletedRoom).toBeUndefined();
214
+ });
215
+ });
216
+
217
+ // ==========================================
218
+ // MESSAGE TESTS
219
+ // ==========================================
220
+
221
+ describe('Messaging', () => {
222
+ let client: ClientSocket;
223
+ let roomId: string;
224
+
225
+ beforeEach(async () => {
226
+ client = ioClient(serverAddress, {
227
+ query: { username: 'msguser', email: 'msg@example.com' },
228
+ });
229
+
230
+ await waitForConnect(client);
231
+ const room = await wsService.createRoom('msgroom', 'default');
232
+ roomId = room.id;
233
+ client.emit('room:join', { roomId });
234
+ await wait(100);
235
+ });
236
+
237
+ afterEach(() => {
238
+ if (client.connected) {
239
+ client.disconnect();
240
+ }
241
+ });
242
+
243
+ it('should send message to room', async () => {
244
+ const messagePromise = waitForEvent<{ content: string; roomId: string }>(client, 'message');
245
+
246
+ await wait(100);
247
+ client.emit('message', {
248
+ roomId,
249
+ content: 'Hello room!',
250
+ type: 'text',
251
+ });
252
+
253
+ const message = await messagePromise;
254
+ expect(message.content).toBe('Hello room!');
255
+ expect(message.roomId).toBe(roomId);
256
+ });
257
+
258
+ it('should store message history', async () => {
259
+ await wait(200);
260
+ const user = wsService.getUser(client.id);
261
+ if (!user) {
262
+ throw new Error('User not found');
263
+ }
264
+
265
+ await wsService.sendMessage(roomId, user.id, 'Message 1', 'text');
266
+ await wsService.sendMessage(roomId, user.id, 'Message 2', 'text');
267
+
268
+ const messages = wsService.getRoomMessages(roomId);
269
+ expect(messages.length).toBeGreaterThanOrEqual(2);
270
+ });
271
+
272
+ it('should handle different message types', async () => {
273
+ await wait(200);
274
+ const user = wsService.getUser(client.id);
275
+ if (!user) {
276
+ throw new Error('User not found');
277
+ }
278
+
279
+ const textMsg = await wsService.sendMessage(roomId, user.id, 'Text', 'text');
280
+ expect(textMsg.type).toBe('text');
281
+
282
+ const systemMsg = await wsService.sendMessage(roomId, user.id, 'System', 'system');
283
+ expect(systemMsg.type).toBe('system');
284
+ });
285
+ });
286
+
287
+ // ==========================================
288
+ // BROADCAST TESTS
289
+ // ==========================================
290
+
291
+ describe('Broadcasting', () => {
292
+ let client1: ClientSocket;
293
+ let client2: ClientSocket;
294
+ let roomId: string;
295
+
296
+ beforeEach(async () => {
297
+ client1 = ioClient(serverAddress, {
298
+ query: { username: 'broadcaster1', email: 'b1@example.com' },
299
+ });
300
+ client2 = ioClient(serverAddress, {
301
+ query: { username: 'broadcaster2', email: 'b2@example.com' },
302
+ });
303
+
304
+ await Promise.all([waitForConnect(client1), waitForConnect(client2)]);
305
+
306
+ const room = await wsService.createRoom('broadcast-room', 'default');
307
+ roomId = room.id;
308
+ client1.emit('room:join', { roomId });
309
+ client2.emit('room:join', { roomId });
310
+ await wait(100);
311
+ });
312
+
313
+ afterEach(() => {
314
+ if (client1.connected) client1.disconnect();
315
+ if (client2.connected) client2.disconnect();
316
+ });
317
+
318
+ it('should broadcast to room', async () => {
319
+ const promise1 = waitForEvent<{ message: string }>(client1, 'test-event');
320
+ const promise2 = waitForEvent<{ message: string }>(client2, 'test-event');
321
+
322
+ await wait(100);
323
+ wsService.broadcastToRoom(roomId, 'test-event', { message: 'Broadcast test' });
324
+
325
+ const [data1, data2] = await Promise.all([promise1, promise2]);
326
+ expect(data1.message).toBe('Broadcast test');
327
+ expect(data2.message).toBe('Broadcast test');
328
+ });
329
+
330
+ it('should broadcast to room except sender', async () => {
331
+ const promise = waitForEvent<{ message: string }>(client2, 'test-except');
332
+
333
+ await wait(100);
334
+ wsService.broadcastToRoom(
335
+ roomId,
336
+ 'test-except',
337
+ { message: 'Not for sender' },
338
+ {
339
+ except: [client1.id],
340
+ }
341
+ );
342
+
343
+ const data = await promise;
344
+ expect(data.message).toBe('Not for sender');
345
+ });
346
+
347
+ it('should broadcast to all users', async () => {
348
+ const promise1 = waitForEvent<{ type: string }>(client1, 'global-event');
349
+ const promise2 = waitForEvent<{ type: string }>(client2, 'global-event');
350
+
351
+ await wait(100);
352
+ wsService.broadcastToAll('global-event', { type: 'announcement' });
353
+
354
+ const [data1, data2] = await Promise.all([promise1, promise2]);
355
+ expect(data1.type).toBe('announcement');
356
+ expect(data2.type).toBe('announcement');
357
+ });
358
+
359
+ it('should emit to specific socket', async () => {
360
+ const promise = waitForEvent<{ text: string }>(client1, 'private-message');
361
+
362
+ await wait(100);
363
+ wsService.emitToSocket(client1.id, 'private-message', { text: 'Just for you' });
364
+
365
+ const data = await promise;
366
+ expect(data.text).toBe('Just for you');
367
+ });
368
+ });
369
+
370
+ // ==========================================
371
+ // TYPING INDICATOR TESTS
372
+ // ==========================================
373
+
374
+ describe('Typing Indicators', () => {
375
+ let client1: ClientSocket;
376
+ let client2: ClientSocket;
377
+ let roomId: string;
378
+
379
+ beforeEach(async () => {
380
+ client1 = ioClient(serverAddress, {
381
+ query: { username: 'typist1', email: 't1@example.com' },
382
+ });
383
+ client2 = ioClient(serverAddress, {
384
+ query: { username: 'typist2', email: 't2@example.com' },
385
+ });
386
+
387
+ await Promise.all([waitForConnect(client1), waitForConnect(client2)]);
388
+
389
+ const room = await wsService.createRoom('typing-room', 'default');
390
+ roomId = room.id;
391
+ client1.emit('room:join', { roomId });
392
+ client2.emit('room:join', { roomId });
393
+ await wait(100);
394
+ });
395
+
396
+ afterEach(() => {
397
+ if (client1.connected) client1.disconnect();
398
+ if (client2.connected) client2.disconnect();
399
+ });
400
+
401
+ it('should broadcast typing indicator', async () => {
402
+ const promise = waitForEvent<{ userId: string; isTyping: boolean }>(client2, 'typing');
403
+
404
+ await wait(100);
405
+ client1.emit('typing', { roomId, isTyping: true });
406
+
407
+ const data = await promise;
408
+ expect(data.isTyping).toBe(true);
409
+ });
410
+
411
+ it('should broadcast stop typing', async () => {
412
+ const promise = waitForEvent<{ userId: string; isTyping: boolean }>(client2, 'typing');
413
+
414
+ await wait(100);
415
+ client1.emit('typing', { roomId, isTyping: false });
416
+
417
+ const data = await promise;
418
+ expect(data.isTyping).toBe(false);
419
+ });
420
+ });
421
+
422
+ // ==========================================
423
+ // STATISTICS TESTS
424
+ // ==========================================
425
+
426
+ describe('Statistics', () => {
427
+ it('should track connection stats', () => {
428
+ const stats = wsService.getStats();
429
+ expect(stats).toHaveProperty('totalConnections');
430
+ expect(stats).toHaveProperty('activeConnections');
431
+ expect(stats).toHaveProperty('totalRooms');
432
+ });
433
+
434
+ it('should track room stats', async () => {
435
+ const room = await wsService.createRoom('stats-room', 'default');
436
+ const stats = wsService.getRoomStats(room.id);
437
+
438
+ expect(stats).not.toBeNull();
439
+ expect(stats?.roomId).toBe(room.id);
440
+ expect(stats?.memberCount).toBe(0);
441
+ expect(stats?.messageCount).toBe(0);
442
+ });
443
+ });
444
+
445
+ // ==========================================
446
+ // USER MANAGEMENT TESTS
447
+ // ==========================================
448
+
449
+ describe('User Management', () => {
450
+ let client: ClientSocket;
451
+
452
+ beforeEach(async () => {
453
+ client = ioClient(serverAddress, {
454
+ query: { username: 'manageduser', email: 'managed@example.com' },
455
+ });
456
+ await waitForConnect(client);
457
+ });
458
+
459
+ afterEach(() => {
460
+ if (client.connected) {
461
+ client.disconnect();
462
+ }
463
+ });
464
+
465
+ it('should check if user is online', async () => {
466
+ await wait(100);
467
+ const user = wsService.getUser(client.id);
468
+ if (user) {
469
+ const isOnline = wsService.isUserOnline(user.id);
470
+ expect(isOnline).toBe(true);
471
+ }
472
+ });
473
+
474
+ it('should get user sockets', async () => {
475
+ await wait(100);
476
+ const user = wsService.getUser(client.id);
477
+ if (user) {
478
+ const sockets = wsService.getUserSockets(user.id);
479
+ expect(sockets).toContain(client.id);
480
+ }
481
+ });
482
+
483
+ it('should forcefully disconnect user', async () => {
484
+ await wait(200);
485
+ const user = wsService.getUser(client.id);
486
+ if (!user) {
487
+ return; // Skip test if user not connected yet
488
+ }
489
+
490
+ const disconnectPromise = new Promise<void>((resolve) => {
491
+ client.on('disconnect', () => resolve());
492
+ });
493
+
494
+ wsService.disconnectUser(user.id, 'admin_action');
495
+ await disconnectPromise;
496
+ await wait(100);
497
+
498
+ const isOnline = wsService.isUserOnline(user.id);
499
+ expect(isOnline).toBe(false);
500
+ });
501
+ });
502
+
503
+ // ==========================================
504
+ // ERROR HANDLING TESTS
505
+ // ==========================================
506
+
507
+ describe('Error Handling', () => {
508
+ let client: ClientSocket;
509
+
510
+ beforeEach(async () => {
511
+ client = ioClient(serverAddress, {
512
+ query: { username: 'erroruser', email: 'error@example.com' },
513
+ });
514
+ await waitForConnect(client);
515
+ });
516
+
517
+ afterEach(() => {
518
+ if (client.connected) {
519
+ client.disconnect();
520
+ }
521
+ });
522
+
523
+ it('should handle joining non-existent room', async () => {
524
+ const promise = waitForEvent<{ message: string }>(client, 'error');
525
+ client.emit('room:join', { roomId: 'non-existent-room' });
526
+
527
+ const error = await promise;
528
+ expect(error.message).toContain('Failed to join room');
529
+ });
530
+
531
+ it('should handle sending message to non-existent room', async () => {
532
+ const promise = waitForEvent<{ message: string }>(client, 'error');
533
+ client.emit('message', {
534
+ roomId: 'non-existent-room',
535
+ content: 'Test message',
536
+ });
537
+
538
+ const error = await promise;
539
+ expect(error.message).toContain('Failed to send message');
540
+ });
541
+
542
+ it('should handle concurrent operations', async () => {
543
+ const operations = [];
544
+ for (let i = 0; i < 10; i++) {
545
+ operations.push(wsService.createRoom(`concurrent-${i}`, 'default'));
546
+ }
547
+
548
+ const rooms = await Promise.all(operations);
549
+ expect(rooms.length).toBe(10);
550
+ });
551
+ });
552
+ });
package/tests/setup.ts CHANGED
@@ -1,17 +1,19 @@
1
- import { beforeAll, afterAll, afterEach } from 'vitest';
1
+ import { beforeAll, afterAll } from 'vitest';
2
2
 
3
- // Set test environment
3
+ // Set test environment variables BEFORE any imports
4
4
  process.env.NODE_ENV = 'test';
5
- process.env.LOG_LEVEL = 'error'; // Reduce log noise during tests
5
+ process.env.LOG_LEVEL = 'error';
6
+ process.env.DATABASE_URL =
7
+ 'postgresql://postgres:Lesourcier@localhost:5432/servcraft_test?schema=public';
8
+ process.env.REDIS_URL = 'redis://localhost:6379/1';
9
+ process.env.JWT_SECRET = 'test-secret-key-for-testing-purposes-only-32chars';
10
+ process.env.JWT_ACCESS_EXPIRES_IN = '15m';
11
+ process.env.JWT_REFRESH_EXPIRES_IN = '7d';
6
12
 
7
13
  beforeAll(async () => {
8
- // Setup code that runs before all tests
14
+ // Global setup
9
15
  });
10
16
 
11
17
  afterAll(async () => {
12
- // Cleanup code that runs after all tests
13
- });
14
-
15
- afterEach(async () => {
16
- // Cleanup code that runs after each test
18
+ // Global cleanup
17
19
  });
package/vitest.config.ts CHANGED
@@ -6,17 +6,12 @@ export default defineConfig({
6
6
  environment: 'node',
7
7
  include: ['tests/**/*.test.ts', 'tests/**/*.spec.ts'],
8
8
  exclude: ['node_modules', 'dist'],
9
+ // Run integration tests sequentially to avoid DB conflicts
10
+ fileParallelism: false,
9
11
  coverage: {
10
12
  provider: 'v8',
11
13
  reporter: ['text', 'json', 'html'],
12
- exclude: [
13
- 'node_modules',
14
- 'dist',
15
- 'tests',
16
- '**/*.d.ts',
17
- '**/*.config.*',
18
- '**/types.ts',
19
- ],
14
+ exclude: ['node_modules', 'dist', 'tests', '**/*.d.ts', '**/*.config.*', '**/types.ts'],
20
15
  },
21
16
  testTimeout: 10000,
22
17
  hookTimeout: 10000,
@@ -1,5 +0,0 @@
1
-
2
- d6bee00ec15bdca9908a56fdc20675409c9ed366 {"key":"pacote:tarball:file:/mnt/d4169edb-8c9f-4e15-9e9d-358608611b0c/Projetcts/Node.js/servcraft","integrity":"sha512-HNADRA1QCgSHYhqtHWQCl4NAaYl2YCBG244k+gPAHubAIsabBYL5aQQtlELuh2rDXAOOlg3UJ9HmIvokuOt9ug==","time":1766018965337,"size":220811}
3
- 9b065a6b5dd7776f840aa247a52a96b098d3280b {"key":"pacote:tarball:file:/mnt/d4169edb-8c9f-4e15-9e9d-358608611b0c/Projetcts/Node.js/servcraft","integrity":"sha512-4BLzYNyTFe5fF4RKDIwjPua/fDCDfEoC6g1Wxhx/erIcDpWOUO0sV8WfmDx2K5MFZ3jJAJsjmP/Cbe8Bg5mbEw==","time":1766019082154,"size":443837}
4
- 793f695e6f45c908cbf359d9bb41e39e78c1827f {"key":"pacote:tarball:file:/mnt/d4169edb-8c9f-4e15-9e9d-358608611b0c/Projetcts/Node.js/servcraft","integrity":"sha512-7bD64RYZAomPTJE8Z9f2zfa+BmWuw7OJucT08KEByh2lm63xtZxOADD1IjAjuNY8/lAcRqMsIMiV1Ps/EcoiMg==","time":1766019259320,"size":888564}
5
- d817e3cb064934d074e652dfa4e82c5a29df0bda {"key":"pacote:tarball:file:/mnt/d4169edb-8c9f-4e15-9e9d-358608611b0c/Projetcts/Node.js/servcraft","integrity":"sha512-QlUotJPKSRgz5aqw6cMQjSmrPzbCSMqI9F1GMGdPzpEwlZ5WrjCHl6wrYyj6fwmmELlVDtCcuXHQOYdtKT/GnQ==","time":1766019329526,"size":1777318}