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.
- package/.claude/settings.local.json +29 -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/README.md +1070 -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,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
|
|
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';
|
|
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
|
-
//
|
|
14
|
+
// Global setup
|
|
9
15
|
});
|
|
10
16
|
|
|
11
17
|
afterAll(async () => {
|
|
12
|
-
//
|
|
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,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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}
|