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,441 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2
|
+
import { prisma } from '../../src/database/prisma.js';
|
|
3
|
+
import { UserRepository } from '../../src/modules/user/user.repository.js';
|
|
4
|
+
import type { CreateUserData, UpdateUserData } from '../../src/modules/user/types.js';
|
|
5
|
+
|
|
6
|
+
describe('UserRepository - Prisma Integration', () => {
|
|
7
|
+
let repository: UserRepository;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
repository = new UserRepository();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await prisma.$disconnect();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
// Clean up before each test
|
|
19
|
+
await repository.clear();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('Create Operations', () => {
|
|
23
|
+
it('should create a new user', async () => {
|
|
24
|
+
const userData: CreateUserData = {
|
|
25
|
+
email: 'test@example.com',
|
|
26
|
+
password: 'hashedpassword123',
|
|
27
|
+
name: 'Test User',
|
|
28
|
+
role: 'user',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const user = await repository.create(userData);
|
|
32
|
+
|
|
33
|
+
expect(user).toBeDefined();
|
|
34
|
+
expect(user.id).toBeDefined();
|
|
35
|
+
expect(user.email).toBe('test@example.com');
|
|
36
|
+
expect(user.name).toBe('Test User');
|
|
37
|
+
expect(user.role).toBe('user');
|
|
38
|
+
expect(user.status).toBe('active');
|
|
39
|
+
expect(user.emailVerified).toBe(false);
|
|
40
|
+
expect(user.createdAt).toBeInstanceOf(Date);
|
|
41
|
+
expect(user.updatedAt).toBeInstanceOf(Date);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should create user with lowercase email', async () => {
|
|
45
|
+
const userData: CreateUserData = {
|
|
46
|
+
email: 'TEST@EXAMPLE.COM',
|
|
47
|
+
password: 'hashedpassword123',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const user = await repository.create(userData);
|
|
51
|
+
expect(user.email).toBe('test@example.com');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should create user with default role', async () => {
|
|
55
|
+
const userData: CreateUserData = {
|
|
56
|
+
email: 'nonadmin@example.com',
|
|
57
|
+
password: 'hashedpassword123',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const user = await repository.create(userData);
|
|
61
|
+
expect(user.role).toBe('user');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should create admin user', async () => {
|
|
65
|
+
const userData: CreateUserData = {
|
|
66
|
+
email: 'admin@example.com',
|
|
67
|
+
password: 'hashedpassword123',
|
|
68
|
+
role: 'admin',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const user = await repository.create(userData);
|
|
72
|
+
expect(user.role).toBe('admin');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should fail to create user with duplicate email', async () => {
|
|
76
|
+
const userData: CreateUserData = {
|
|
77
|
+
email: 'duplicate@example.com',
|
|
78
|
+
password: 'hashedpassword123',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await repository.create(userData);
|
|
82
|
+
await expect(repository.create(userData)).rejects.toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Read Operations', () => {
|
|
87
|
+
it('should find user by ID', async () => {
|
|
88
|
+
const created = await repository.create({
|
|
89
|
+
email: 'findme@example.com',
|
|
90
|
+
password: 'hashedpassword123',
|
|
91
|
+
name: 'Find Me',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const found = await repository.findById(created.id);
|
|
95
|
+
expect(found).toBeDefined();
|
|
96
|
+
expect(found?.id).toBe(created.id);
|
|
97
|
+
expect(found?.email).toBe('findme@example.com');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return null for non-existent ID', async () => {
|
|
101
|
+
const found = await repository.findById('non-existent-id');
|
|
102
|
+
expect(found).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should find user by email (case-insensitive)', async () => {
|
|
106
|
+
await repository.create({
|
|
107
|
+
email: 'findme@example.com',
|
|
108
|
+
password: 'hashedpassword123',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const found = await repository.findByEmail('FINDME@EXAMPLE.COM');
|
|
112
|
+
expect(found).toBeDefined();
|
|
113
|
+
expect(found?.email).toBe('findme@example.com');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return null for non-existent email', async () => {
|
|
117
|
+
const found = await repository.findByEmail('nonexistent@example.com');
|
|
118
|
+
expect(found).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Update Operations', () => {
|
|
123
|
+
it('should update user name', async () => {
|
|
124
|
+
const user = await repository.create({
|
|
125
|
+
email: 'update@example.com',
|
|
126
|
+
password: 'hashedpassword123',
|
|
127
|
+
name: 'Old Name',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const updateData: UpdateUserData = {
|
|
131
|
+
name: 'New Name',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const updated = await repository.update(user.id, updateData);
|
|
135
|
+
expect(updated).toBeDefined();
|
|
136
|
+
expect(updated?.name).toBe('New Name');
|
|
137
|
+
expect(updated?.updatedAt.getTime()).toBeGreaterThan(user.updatedAt.getTime());
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should update user role', async () => {
|
|
141
|
+
const user = await repository.create({
|
|
142
|
+
email: 'promote@example.com',
|
|
143
|
+
password: 'hashedpassword123',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const updated = await repository.update(user.id, { role: 'admin' });
|
|
147
|
+
expect(updated?.role).toBe('admin');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should update user status', async () => {
|
|
151
|
+
const user = await repository.create({
|
|
152
|
+
email: 'suspend@example.com',
|
|
153
|
+
password: 'hashedpassword123',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const updated = await repository.update(user.id, { status: 'suspended' });
|
|
157
|
+
expect(updated?.status).toBe('suspended');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should update email verified status', async () => {
|
|
161
|
+
const user = await repository.create({
|
|
162
|
+
email: 'verify@example.com',
|
|
163
|
+
password: 'hashedpassword123',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const updated = await repository.update(user.id, { emailVerified: true });
|
|
167
|
+
expect(updated?.emailVerified).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should update user metadata', async () => {
|
|
171
|
+
const user = await repository.create({
|
|
172
|
+
email: 'meta@example.com',
|
|
173
|
+
password: 'hashedpassword123',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const metadata = { preferences: { theme: 'dark', language: 'en' } };
|
|
177
|
+
const updated = await repository.update(user.id, { metadata });
|
|
178
|
+
|
|
179
|
+
expect(updated?.metadata).toEqual(metadata);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should return null when updating non-existent user', async () => {
|
|
183
|
+
const updated = await repository.update('non-existent-id', { name: 'Test' });
|
|
184
|
+
expect(updated).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should update password', async () => {
|
|
188
|
+
const user = await repository.create({
|
|
189
|
+
email: 'changepass@example.com',
|
|
190
|
+
password: 'oldpassword',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const updated = await repository.updatePassword(user.id, 'newpassword');
|
|
194
|
+
expect(updated).toBeDefined();
|
|
195
|
+
expect(updated?.password).toBe('newpassword');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should update last login timestamp', async () => {
|
|
199
|
+
const user = await repository.create({
|
|
200
|
+
email: 'login@example.com',
|
|
201
|
+
password: 'hashedpassword123',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(user.lastLoginAt).toBeUndefined();
|
|
205
|
+
|
|
206
|
+
const updated = await repository.updateLastLogin(user.id);
|
|
207
|
+
expect(updated?.lastLoginAt).toBeInstanceOf(Date);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('Delete Operations', () => {
|
|
212
|
+
it('should delete user', async () => {
|
|
213
|
+
const user = await repository.create({
|
|
214
|
+
email: 'delete@example.com',
|
|
215
|
+
password: 'hashedpassword123',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const deleted = await repository.delete(user.id);
|
|
219
|
+
expect(deleted).toBe(true);
|
|
220
|
+
|
|
221
|
+
const found = await repository.findById(user.id);
|
|
222
|
+
expect(found).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should return false when deleting non-existent user', async () => {
|
|
226
|
+
const deleted = await repository.delete('non-existent-id');
|
|
227
|
+
expect(deleted).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('Pagination and Filtering', () => {
|
|
232
|
+
beforeEach(async () => {
|
|
233
|
+
// Create test users
|
|
234
|
+
await Promise.all([
|
|
235
|
+
repository.create({
|
|
236
|
+
email: 'user1@example.com',
|
|
237
|
+
password: 'pass',
|
|
238
|
+
name: 'Alice Admin',
|
|
239
|
+
role: 'admin',
|
|
240
|
+
}),
|
|
241
|
+
repository.create({
|
|
242
|
+
email: 'user2@example.com',
|
|
243
|
+
password: 'pass',
|
|
244
|
+
name: 'Bob User',
|
|
245
|
+
role: 'user',
|
|
246
|
+
}),
|
|
247
|
+
repository.create({
|
|
248
|
+
email: 'user3@example.com',
|
|
249
|
+
password: 'pass',
|
|
250
|
+
name: 'Charlie Moderator',
|
|
251
|
+
role: 'moderator',
|
|
252
|
+
}),
|
|
253
|
+
repository.create({
|
|
254
|
+
email: 'suspended@example.com',
|
|
255
|
+
password: 'pass',
|
|
256
|
+
name: 'David Suspended',
|
|
257
|
+
role: 'user',
|
|
258
|
+
}),
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
// Suspend one user
|
|
262
|
+
const suspended = await repository.findByEmail('suspended@example.com');
|
|
263
|
+
if (suspended) {
|
|
264
|
+
await repository.update(suspended.id, { status: 'suspended' });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should find all users with pagination', async () => {
|
|
269
|
+
const result = await repository.findMany({ page: 1, limit: 10 });
|
|
270
|
+
|
|
271
|
+
expect(result.data).toHaveLength(4);
|
|
272
|
+
expect(result.meta.total).toBe(4);
|
|
273
|
+
expect(result.meta.page).toBe(1);
|
|
274
|
+
expect(result.meta.limit).toBe(10);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should paginate results', async () => {
|
|
278
|
+
const page1 = await repository.findMany({ page: 1, limit: 2 });
|
|
279
|
+
expect(page1.data).toHaveLength(2);
|
|
280
|
+
expect(page1.meta.hasNextPage).toBe(true);
|
|
281
|
+
|
|
282
|
+
const page2 = await repository.findMany({ page: 2, limit: 2 });
|
|
283
|
+
expect(page2.data).toHaveLength(2);
|
|
284
|
+
expect(page2.meta.hasNextPage).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should filter by role', async () => {
|
|
288
|
+
const result = await repository.findMany({ page: 1, limit: 10 }, { role: 'admin' });
|
|
289
|
+
|
|
290
|
+
expect(result.data).toHaveLength(1);
|
|
291
|
+
expect(result.data[0]?.role).toBe('admin');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should filter by status', async () => {
|
|
295
|
+
const result = await repository.findMany({ page: 1, limit: 10 }, { status: 'suspended' });
|
|
296
|
+
|
|
297
|
+
expect(result.data).toHaveLength(1);
|
|
298
|
+
expect(result.data[0]?.status).toBe('suspended');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should filter by email verified', async () => {
|
|
302
|
+
const user = await repository.findByEmail('user1@example.com');
|
|
303
|
+
if (user) {
|
|
304
|
+
await repository.update(user.id, { emailVerified: true });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result = await repository.findMany({ page: 1, limit: 10 }, { emailVerified: true });
|
|
308
|
+
|
|
309
|
+
expect(result.data).toHaveLength(1);
|
|
310
|
+
expect(result.data[0]?.emailVerified).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should search by email or name', async () => {
|
|
314
|
+
const result = await repository.findMany({ page: 1, limit: 10 }, { search: 'alice' });
|
|
315
|
+
|
|
316
|
+
expect(result.data.length).toBeGreaterThan(0);
|
|
317
|
+
expect(result.data[0]?.name).toContain('Alice');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should sort users', async () => {
|
|
321
|
+
const result = await repository.findMany({
|
|
322
|
+
page: 1,
|
|
323
|
+
limit: 10,
|
|
324
|
+
sortBy: 'email',
|
|
325
|
+
sortOrder: 'asc',
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Compare strings alphabetically
|
|
329
|
+
const email1 = result.data[0]?.email || '';
|
|
330
|
+
const email2 = result.data[1]?.email || '';
|
|
331
|
+
expect(email1.localeCompare(email2)).toBeLessThan(0);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should combine multiple filters', async () => {
|
|
335
|
+
const result = await repository.findMany(
|
|
336
|
+
{ page: 1, limit: 10 },
|
|
337
|
+
{ role: 'user', status: 'active' }
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(result.data).toHaveLength(1);
|
|
341
|
+
expect(result.data[0]?.role).toBe('user');
|
|
342
|
+
expect(result.data[0]?.status).toBe('active');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('Count Operations', () => {
|
|
347
|
+
beforeEach(async () => {
|
|
348
|
+
await Promise.all([
|
|
349
|
+
repository.create({ email: 'count1@example.com', password: 'pass', role: 'admin' }),
|
|
350
|
+
repository.create({ email: 'count2@example.com', password: 'pass', role: 'user' }),
|
|
351
|
+
repository.create({ email: 'count3@example.com', password: 'pass', role: 'user' }),
|
|
352
|
+
]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should count all users', async () => {
|
|
356
|
+
const count = await repository.count();
|
|
357
|
+
expect(count).toBe(3);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should count users by role', async () => {
|
|
361
|
+
const count = await repository.count({ role: 'user' });
|
|
362
|
+
expect(count).toBe(2);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should count users by status', async () => {
|
|
366
|
+
const user = await repository.findByEmail('count1@example.com');
|
|
367
|
+
if (user) {
|
|
368
|
+
await repository.update(user.id, { status: 'suspended' });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const activeCount = await repository.count({ status: 'active' });
|
|
372
|
+
const suspendedCount = await repository.count({ status: 'suspended' });
|
|
373
|
+
|
|
374
|
+
expect(activeCount).toBe(2);
|
|
375
|
+
expect(suspendedCount).toBe(1);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('Clear Operation', () => {
|
|
380
|
+
it('should clear all users', async () => {
|
|
381
|
+
await Promise.all([
|
|
382
|
+
repository.create({ email: 'clear1@example.com', password: 'pass' }),
|
|
383
|
+
repository.create({ email: 'clear2@example.com', password: 'pass' }),
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
let count = await repository.count();
|
|
387
|
+
expect(count).toBe(2);
|
|
388
|
+
|
|
389
|
+
await repository.clear();
|
|
390
|
+
|
|
391
|
+
count = await repository.count();
|
|
392
|
+
expect(count).toBe(0);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('Enum Mapping', () => {
|
|
397
|
+
it('should correctly map all user roles', async () => {
|
|
398
|
+
const roles: Array<'user' | 'admin' | 'moderator' | 'super_admin'> = [
|
|
399
|
+
'user',
|
|
400
|
+
'admin',
|
|
401
|
+
'moderator',
|
|
402
|
+
'super_admin',
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
for (const role of roles) {
|
|
406
|
+
const user = await repository.create({
|
|
407
|
+
email: `${role}@example.com`,
|
|
408
|
+
password: 'pass',
|
|
409
|
+
role,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(user.role).toBe(role);
|
|
413
|
+
|
|
414
|
+
const found = await repository.findById(user.id);
|
|
415
|
+
expect(found?.role).toBe(role);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should correctly map all user statuses', async () => {
|
|
420
|
+
const user = await repository.create({
|
|
421
|
+
email: 'status@example.com',
|
|
422
|
+
password: 'pass',
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const statuses: Array<'active' | 'inactive' | 'suspended' | 'banned'> = [
|
|
426
|
+
'active',
|
|
427
|
+
'inactive',
|
|
428
|
+
'suspended',
|
|
429
|
+
'banned',
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
for (const status of statuses) {
|
|
433
|
+
const updated = await repository.update(user.id, { status });
|
|
434
|
+
expect(updated?.status).toBe(status);
|
|
435
|
+
|
|
436
|
+
const found = await repository.findById(user.id);
|
|
437
|
+
expect(found?.status).toBe(status);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|