create-rebe 1.0.0

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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/bin/create-rebe.js +310 -0
  4. package/package.json +40 -0
  5. package/template/.env.example +141 -0
  6. package/template/.gitattributes +16 -0
  7. package/template/.nvmrc +1 -0
  8. package/template/.prettierignore +10 -0
  9. package/template/.prettierrc +12 -0
  10. package/template/LICENSE +21 -0
  11. package/template/README.md +150 -0
  12. package/template/SECURITY.md +133 -0
  13. package/template/app/hooks/register.hook.js +22 -0
  14. package/template/app/http/controllers/auth.controller.js +34 -0
  15. package/template/app/http/middlewares/auth.middleware.js +25 -0
  16. package/template/app/http/middlewares/register.middleware.js +18 -0
  17. package/template/app/http/validators/auth.validator.js +8 -0
  18. package/template/app/jobs/register.job.js +17 -0
  19. package/template/app/queue/register.queue.js +11 -0
  20. package/template/app/routes/api.route.js +18 -0
  21. package/template/app/routes/register.route.js +8 -0
  22. package/template/app/routes/web.route.js +7 -0
  23. package/template/app/socket/register.socket.js +15 -0
  24. package/template/config/app.config.js +12 -0
  25. package/template/config/bcrypt.config.js +7 -0
  26. package/template/config/cache.config.js +7 -0
  27. package/template/config/cors.config.js +37 -0
  28. package/template/config/cron.config.js +9 -0
  29. package/template/config/database.config.js +51 -0
  30. package/template/config/express.config.js +37 -0
  31. package/template/config/index.js +19 -0
  32. package/template/config/jwt.config.js +10 -0
  33. package/template/config/logger.config.js +10 -0
  34. package/template/config/mail.config.js +14 -0
  35. package/template/config/queue.config.js +11 -0
  36. package/template/config/redis.config.js +32 -0
  37. package/template/config/runtime.config.js +11 -0
  38. package/template/config/socket.config.js +15 -0
  39. package/template/config/storage.config.js +10 -0
  40. package/template/core/bootstrap.core.js +92 -0
  41. package/template/core/common/array.js +144 -0
  42. package/template/core/common/cache.js +100 -0
  43. package/template/core/common/collection.js +173 -0
  44. package/template/core/common/crypt.js +69 -0
  45. package/template/core/common/date.js +254 -0
  46. package/template/core/common/hash.js +61 -0
  47. package/template/core/common/object.js +155 -0
  48. package/template/core/common/path.js +80 -0
  49. package/template/core/common/storage.js +97 -0
  50. package/template/core/common/string.js +137 -0
  51. package/template/core/common/url.js +81 -0
  52. package/template/core/common.core.js +93 -0
  53. package/template/core/cron.core.js +141 -0
  54. package/template/core/database.core.js +113 -0
  55. package/template/core/error.core.js +83 -0
  56. package/template/core/express.core.js +161 -0
  57. package/template/core/hooks.core.js +47 -0
  58. package/template/core/jwt.core.js +81 -0
  59. package/template/core/logger.core.js +100 -0
  60. package/template/core/mailer.core.js +65 -0
  61. package/template/core/queue.core.js +226 -0
  62. package/template/core/redis.core.js +75 -0
  63. package/template/core/register.core.js +91 -0
  64. package/template/core/runtime.core.js +27 -0
  65. package/template/core/socket.core.js +93 -0
  66. package/template/core/validator.core.js +34 -0
  67. package/template/database/.gitkeep +0 -0
  68. package/template/database/models/post.model.js +26 -0
  69. package/template/database/models/user.model.js +30 -0
  70. package/template/docs/Bootstrap.md +50 -0
  71. package/template/docs/Common.md +48 -0
  72. package/template/docs/Cron.md +47 -0
  73. package/template/docs/Database.md +61 -0
  74. package/template/docs/Express.md +63 -0
  75. package/template/docs/Hooks.md +48 -0
  76. package/template/docs/Jwt.md +63 -0
  77. package/template/docs/Logger.md +60 -0
  78. package/template/docs/Mailer.md +50 -0
  79. package/template/docs/Queue.md +57 -0
  80. package/template/docs/README.md +32 -0
  81. package/template/docs/Redis.md +54 -0
  82. package/template/docs/Register.md +52 -0
  83. package/template/docs/Socket.md +55 -0
  84. package/template/docs/Validator.md +45 -0
  85. package/template/ecosystem.config.js +54 -0
  86. package/template/index.js +6 -0
  87. package/template/jest.config.js +27 -0
  88. package/template/jsconfig.json +37 -0
  89. package/template/logs/.gitkeep +0 -0
  90. package/template/nodemon.json +23 -0
  91. package/template/package-lock.json +7033 -0
  92. package/template/package.json +82 -0
  93. package/template/scripts/cli.js +258 -0
  94. package/template/storage/.gitkeep +0 -0
  95. package/template/tests/common.test.js +45 -0
  96. package/template/tests/crypt-hash-storage.test.js +44 -0
  97. package/template/tests/jwt.test.js +45 -0
  98. package/template/tests/register.test.js +55 -0
  99. package/template/tests/setup.js +11 -0
  100. package/template/tests/validator.test.js +65 -0
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "rebe",
3
+ "version": "1.0.0",
4
+ "description": "Ready-to-use Node.js backend framework. One process serves a JSON API, Socket.IO, cron jobs, and an in-process queue on a layered config/core/app architecture, with JWT auth, Sequelize, Winston logging, and centralized configuration.",
5
+ "license": "MIT",
6
+ "author": "Refkinscallv <refkinscallv@gmail.com>",
7
+ "type": "commonjs",
8
+ "main": "index.js",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/refkinscallv/rebe.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/refkinscallv/rebe/issues"
15
+ },
16
+ "homepage": "https://github.com/refkinscallv/rebe#readme",
17
+ "engines": {
18
+ "node": ">=20.9.0"
19
+ },
20
+ "keywords": [
21
+ "rebe",
22
+ "backend",
23
+ "framework",
24
+ "boilerplate",
25
+ "express",
26
+ "socket.io",
27
+ "cron",
28
+ "queue",
29
+ "jwt",
30
+ "sequelize"
31
+ ],
32
+ "scripts": {
33
+ "test": "jest",
34
+ "test:watch": "jest --watch",
35
+ "test:coverage": "jest --coverage",
36
+ "cli": "node ./scripts/cli.js",
37
+ "start": "node .",
38
+ "dev": "nodemon",
39
+ "format": "prettier --write ."
40
+ },
41
+ "_moduleAliases": {
42
+ "@config": "config",
43
+ "@core": "core",
44
+ "@app": "app",
45
+ "@http": "app/http",
46
+ "@routes": "app/routes",
47
+ "@jobs": "app/jobs",
48
+ "@queue": "app/queue",
49
+ "@socket": "app/socket",
50
+ "@hooks": "app/hooks",
51
+ "@database": "database",
52
+ "@storage": "storage"
53
+ },
54
+ "devDependencies": {
55
+ "jest": "^30.4.2",
56
+ "nodemon": "^3.1.14",
57
+ "prettier": "^3.8.4"
58
+ },
59
+ "dependencies": {
60
+ "@refkinscallv/express-routing": "^3.2.0",
61
+ "axios": "^1.17.0",
62
+ "bcrypt": "^6.0.0",
63
+ "compression": "^1.8.1",
64
+ "cors": "^2.8.6",
65
+ "dotenv": "^17.4.2",
66
+ "express": "^5.2.1",
67
+ "express-rate-limit": "^8.5.2",
68
+ "helmet": "^8.2.0",
69
+ "ioredis": "^5.11.1",
70
+ "jsonwebtoken": "^9.0.3",
71
+ "module-alias": "^2.3.4",
72
+ "multer": "^2.1.1",
73
+ "mysql2": "^3.22.5",
74
+ "node-cron": "^4.2.1",
75
+ "nodemailer": "^8.0.10",
76
+ "sequelize": "^6.37.8",
77
+ "socket.io": "^4.8.3",
78
+ "winston": "^3.19.0",
79
+ "winston-daily-rotate-file": "^5.0.0",
80
+ "zod": "^4.4.3"
81
+ }
82
+ }
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict'
4
+
5
+ require('dotenv').config()
6
+
7
+ const fs = require('fs')
8
+ const path = require('path')
9
+ const crypto = require('crypto')
10
+ const { execSync } = require('child_process')
11
+
12
+ // Display name is driven by APP_NAME (no hardcoded brand). Falls back to 'rebe'
13
+ // when .env is not present yet (e.g. before `setup`).
14
+ const APP_NAME = (process.env.APP_NAME || 'rebe').toUpperCase()
15
+
16
+ class Log {
17
+ static colors = {
18
+ reset: '\x1b[0m',
19
+ red: '\x1b[31m',
20
+ green: '\x1b[32m',
21
+ yellow: '\x1b[33m',
22
+ blue: '\x1b[34m',
23
+ cyan: '\x1b[36m',
24
+ white: '\x1b[37m',
25
+ gray: '\x1b[90m',
26
+ }
27
+
28
+ static color(color, message) {
29
+ return `${this.colors[color]}${message}${this.colors.reset}`
30
+ }
31
+
32
+ static info(message) {
33
+ console.log(this.color('blue', `[INFO] ${message}`))
34
+ }
35
+
36
+ static success(message) {
37
+ console.log(this.color('green', `[ OK ] ${message}`))
38
+ }
39
+
40
+ static warn(message) {
41
+ console.log(this.color('yellow', `[WARN] ${message}`))
42
+ }
43
+
44
+ static error(message) {
45
+ console.log(this.color('red', `[FAIL] ${message}`))
46
+ }
47
+
48
+ static line() {
49
+ console.log(this.color('gray', '────────────────────────────────────────'))
50
+ }
51
+
52
+ static banner() {
53
+ const width = 38
54
+ const title = `${APP_NAME} CLI`
55
+ const padTotal = Math.max(0, width - title.length)
56
+ const left = Math.floor(padTotal / 2)
57
+ const centered = ' '.repeat(left) + title + ' '.repeat(padTotal - left)
58
+
59
+ console.log()
60
+ console.log(this.color('cyan', `╔${'═'.repeat(width)}╗`))
61
+ console.log(this.color('cyan', `║${centered}║`))
62
+ console.log(this.color('cyan', `╚${'═'.repeat(width)}╝`))
63
+ console.log()
64
+ }
65
+ }
66
+
67
+ const command = process.argv[2]
68
+ const args = process.argv.slice(3)
69
+
70
+ function generateKey(bytes = 32) {
71
+ return crypto.randomBytes(bytes).toString('hex')
72
+ }
73
+
74
+ function getEnvPath() {
75
+ return path.join(process.cwd(), '.env')
76
+ }
77
+
78
+ function updateEnv(key, value) {
79
+ const envPath = getEnvPath()
80
+
81
+ if (!fs.existsSync(envPath)) {
82
+ Log.error('.env file not found')
83
+ process.exit(1)
84
+ }
85
+
86
+ let content = fs.readFileSync(envPath, 'utf8')
87
+
88
+ const regex = new RegExp(`^${key}=.*$`, 'm')
89
+
90
+ if (regex.test(content)) {
91
+ content = content.replace(regex, `${key}=${value}`)
92
+ } else {
93
+ content += `\n${key}=${value}`
94
+ }
95
+
96
+ fs.writeFileSync(envPath, content)
97
+
98
+ return value
99
+ }
100
+
101
+ function help() {
102
+ Log.banner()
103
+
104
+ console.log(Log.color('yellow', 'Usage'))
105
+ Log.line()
106
+
107
+ console.log(' npm run cli -- setup')
108
+ console.log(' npm run cli -- help')
109
+ console.log(' npm run cli -- key:generate')
110
+ console.log(' npm run cli -- key:jwt')
111
+ console.log(' npm run cli -- key:jwt --refresh')
112
+
113
+ console.log()
114
+
115
+ console.log(Log.color('yellow', 'Commands'))
116
+ Log.line()
117
+
118
+ console.log(' setup')
119
+ console.log(Log.color('gray', ' Initialize project'))
120
+
121
+ console.log()
122
+
123
+ console.log(' help')
124
+ console.log(Log.color('gray', ' Show help information'))
125
+
126
+ console.log()
127
+
128
+ console.log(' key:generate')
129
+ console.log(Log.color('gray', ' Generate APP_KEY'))
130
+
131
+ console.log()
132
+
133
+ console.log(' key:jwt')
134
+ console.log(Log.color('gray', ' Generate JWT_SECRET'))
135
+
136
+ console.log()
137
+
138
+ console.log(' key:jwt --refresh')
139
+ console.log(Log.color('gray', ' Generate JWT_REFRESH_SECRET'))
140
+
141
+ console.log()
142
+ }
143
+
144
+ function setup() {
145
+ Log.banner()
146
+
147
+ const examplePath = path.join(process.cwd(), '.env.example')
148
+ const envPath = path.join(process.cwd(), '.env')
149
+
150
+ if (!fs.existsSync(examplePath)) {
151
+ Log.error('.env.example not found')
152
+ process.exit(1)
153
+ }
154
+
155
+ if (!fs.existsSync(envPath)) {
156
+ fs.copyFileSync(examplePath, envPath)
157
+ Log.success('.env created')
158
+ } else {
159
+ Log.warn('.env already exists')
160
+ }
161
+
162
+ try {
163
+ Log.info('Updating package.json dependencies...')
164
+ execSync('npx npm-check-updates -u', {
165
+ stdio: 'inherit',
166
+ })
167
+
168
+ Log.info('Installing dependencies...')
169
+ execSync('npm install', {
170
+ stdio: 'inherit',
171
+ })
172
+
173
+ generateAppKey()
174
+ generateJwtSecret()
175
+ generateJwtRefreshSecret()
176
+
177
+ Log.success('Setup completed')
178
+ } catch (error) {
179
+ Log.error(error.message)
180
+ process.exit(1)
181
+ }
182
+ }
183
+
184
+ function generateAppKey() {
185
+ Log.banner()
186
+
187
+ const key = generateKey(32)
188
+
189
+ updateEnv('APP_KEY', key)
190
+
191
+ Log.success('APP_KEY generated')
192
+ console.log()
193
+ console.log(Log.color('cyan', key))
194
+ console.log()
195
+ }
196
+
197
+ function generateJwtSecret() {
198
+ Log.banner()
199
+
200
+ const key = generateKey(64)
201
+
202
+ updateEnv('JWT_SECRET', key)
203
+
204
+ Log.success('JWT_SECRET generated')
205
+ console.log()
206
+ console.log(Log.color('cyan', key))
207
+ console.log()
208
+ }
209
+
210
+ function generateJwtRefreshSecret() {
211
+ Log.banner()
212
+
213
+ const key = generateKey(64)
214
+
215
+ updateEnv('JWT_REFRESH_SECRET', key)
216
+
217
+ Log.success('JWT_REFRESH_SECRET generated')
218
+ console.log()
219
+ console.log(Log.color('cyan', key))
220
+ console.log()
221
+ }
222
+
223
+ switch (command) {
224
+ case undefined:
225
+ case 'help': {
226
+ help()
227
+ break
228
+ }
229
+
230
+ case 'setup': {
231
+ setup()
232
+ break
233
+ }
234
+
235
+ case 'key:generate': {
236
+ generateAppKey()
237
+ break
238
+ }
239
+
240
+ case 'key:jwt': {
241
+ if (args.includes('--refresh')) {
242
+ generateJwtRefreshSecret()
243
+ } else {
244
+ generateJwtSecret()
245
+ }
246
+
247
+ break
248
+ }
249
+
250
+ default: {
251
+ Log.banner()
252
+ Log.error(`Unknown command '${command}'`)
253
+ console.log()
254
+ Log.info(`Run: npm run cli -- help`)
255
+ console.log()
256
+ process.exit(1)
257
+ }
258
+ }
File without changes
@@ -0,0 +1,45 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ describe('Common env readers', () => {
6
+ test('getEnvBool parses truthy and falls back', () => {
7
+ process.env.X_BOOL = 'yes'
8
+ expect(Common.getEnvBool('X_BOOL')).toBe(true)
9
+ expect(Common.getEnvBool('X_MISSING', false)).toBe(false)
10
+ })
11
+
12
+ test('getEnvInt parses and falls back on non-numeric', () => {
13
+ process.env.X_INT = '42'
14
+ expect(Common.getEnvInt('X_INT')).toBe(42)
15
+ process.env.X_INT = 'nope'
16
+ expect(Common.getEnvInt('X_INT', 7)).toBe(7)
17
+ })
18
+
19
+ test('getEnvArray trims and drops empties', () => {
20
+ process.env.X_ARR = 'a, b ,c,'
21
+ expect(Common.getEnvArray('X_ARR')).toEqual(['a', 'b', 'c'])
22
+ })
23
+ })
24
+
25
+ describe('Common helpers', () => {
26
+ test('isEmpty', () => {
27
+ expect(Common.isEmpty('')).toBe(true)
28
+ expect(Common.isEmpty([])).toBe(true)
29
+ expect(Common.isEmpty({})).toBe(true)
30
+ expect(Common.isEmpty([1])).toBe(false)
31
+ expect(Common.isEmpty('x')).toBe(false)
32
+ })
33
+
34
+ test('attempt returns [err, result]', async () => {
35
+ const [e1, r1] = await Common.attempt(() => 5)
36
+ expect(e1).toBeNull()
37
+ expect(r1).toBe(5)
38
+
39
+ const [e2, r2] = await Common.attempt(() => {
40
+ throw new Error('boom')
41
+ })
42
+ expect(e2).toBeInstanceOf(Error)
43
+ expect(r2).toBeNull()
44
+ })
45
+ })
@@ -0,0 +1,44 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ describe('Crypt', () => {
6
+ test('encrypt/decrypt roundtrip for strings and objects', () => {
7
+ const enc = Common.Crypt.encrypt('hello')
8
+ expect(Common.Crypt.decrypt(enc)).toBe('hello')
9
+
10
+ const encObj = Common.Crypt.encrypt({ a: 1, b: 'two' })
11
+ expect(Common.Crypt.decrypt(encObj, true)).toEqual({ a: 1, b: 'two' })
12
+ })
13
+
14
+ test('tampered ciphertext fails authentication', () => {
15
+ const enc = Common.Crypt.encrypt('hello')
16
+ const tampered = Buffer.from(enc, 'base64')
17
+ tampered[tampered.length - 1] ^= 0xff
18
+ expect(() => Common.Crypt.decrypt(tampered.toString('base64'))).toThrow()
19
+ })
20
+ })
21
+
22
+ describe('Hash', () => {
23
+ test('make/check bcrypt', async () => {
24
+ const hashed = await Common.Hash.make('password123')
25
+ expect(await Common.Hash.check('password123', hashed)).toBe(true)
26
+ expect(await Common.Hash.check('wrong', hashed)).toBe(false)
27
+ })
28
+
29
+ test('equals is constant-time and length-aware', () => {
30
+ expect(Common.Hash.equals('abc', 'abc')).toBe(true)
31
+ expect(Common.Hash.equals('abc', 'abd')).toBe(false)
32
+ expect(Common.Hash.equals('abc', 'abcd')).toBe(false)
33
+ })
34
+ })
35
+
36
+ describe('Storage', () => {
37
+ test('path stays inside the storage root', () => {
38
+ expect(Common.Storage.path('uploads/file.txt')).toContain('storage')
39
+ })
40
+
41
+ test('rejects path traversal', () => {
42
+ expect(() => Common.Storage.path('../../etc/passwd')).toThrow()
43
+ })
44
+ })
@@ -0,0 +1,45 @@
1
+ 'use strict'
2
+
3
+ const Jwt = require('@core/jwt.core')
4
+
5
+ describe('Jwt', () => {
6
+ test('sign/verify roundtrip', () => {
7
+ const token = Jwt.sign({ sub: 1, role: 'admin' })
8
+ const payload = Jwt.verify(token)
9
+ expect(payload.sub).toBe(1)
10
+ expect(payload.role).toBe('admin')
11
+ })
12
+
13
+ test('tryVerify reports invalid tokens without throwing', () => {
14
+ const result = Jwt.tryVerify('not.a.real.token')
15
+ expect(result.valid).toBe(false)
16
+ expect(result.payload).toBeNull()
17
+ expect(result.error).toBeInstanceOf(Error)
18
+ })
19
+
20
+ test('fromHeader extracts a bearer token', () => {
21
+ expect(Jwt.fromHeader('Bearer abc.def')).toBe('abc.def')
22
+ expect(Jwt.fromHeader('bearer xyz')).toBe('xyz')
23
+ expect(Jwt.fromHeader('Basic abc')).toBeNull()
24
+ expect(Jwt.fromHeader(undefined)).toBeNull()
25
+ })
26
+
27
+ test('issue returns an access + refresh pair', () => {
28
+ const issued = Jwt.issue({ sub: 7 })
29
+ expect(issued.tokenType).toBe('Bearer')
30
+ expect(typeof issued.accessToken).toBe('string')
31
+ expect(Jwt.verify(issued.accessToken).sub).toBe(7)
32
+ expect(Jwt.verifyRefresh(issued.refreshToken).sub).toBe(7)
33
+ })
34
+
35
+ test('expiresAt / isExpired', () => {
36
+ const token = Jwt.sign({ sub: 1 })
37
+ expect(Jwt.expiresAt(token)).toBeInstanceOf(Date)
38
+ expect(Jwt.isExpired(token)).toBe(false)
39
+ })
40
+
41
+ test('the access secret does not verify a refresh token', () => {
42
+ const issued = Jwt.issue({ sub: 1 })
43
+ expect(() => Jwt.verify(issued.refreshToken)).toThrow()
44
+ })
45
+ })
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const os = require('os')
5
+ const path = require('path')
6
+
7
+ const Register = require('@core/register.core')
8
+
9
+ function tempModule(content) {
10
+ const file = path.join(os.tmpdir(), `rebe-reg-${Date.now()}-${Math.random().toString(16).slice(2)}.js`)
11
+ fs.writeFileSync(file, content)
12
+ return file
13
+ }
14
+
15
+ describe('Register', () => {
16
+ test('path resolves under the project root', () => {
17
+ expect(Register.path('app', 'jobs', 'register.job.js')).toBe(path.resolve(process.cwd(), 'app', 'jobs', 'register.job.js'))
18
+ })
19
+
20
+ test('require returns null for a missing optional file', () => {
21
+ expect(Register.require(path.join(os.tmpdir(), 'definitely-missing-xyz.js'))).toBeNull()
22
+ })
23
+
24
+ test('require throws for a missing required file', () => {
25
+ expect(() => Register.require(path.join(os.tmpdir(), 'missing-required.js'), { optional: false })).toThrow()
26
+ })
27
+
28
+ test('invoke calls a function export with args', async () => {
29
+ const file = tempModule('module.exports = (x) => x * 2')
30
+ await expect(Register.invoke(file, [21])).resolves.toBe(42)
31
+ fs.unlinkSync(file)
32
+ })
33
+
34
+ test('invoke supports the { register } shape', async () => {
35
+ const file = tempModule('module.exports = { register: (x) => x + 1 }')
36
+ await expect(Register.invoke(file, [41])).resolves.toBe(42)
37
+ fs.unlinkSync(file)
38
+ })
39
+
40
+ test('invoke rejects an invalid export shape', async () => {
41
+ const file = tempModule('module.exports = 123')
42
+ await expect(Register.invoke(file)).rejects.toThrow()
43
+ fs.unlinkSync(file)
44
+ })
45
+
46
+ test('object validates handler keys are functions', () => {
47
+ const ok = tempModule('module.exports = { before: () => {}, after: () => {} }')
48
+ expect(Register.object(ok, ['before', 'after', 'shutdown'])).toBeTruthy()
49
+ fs.unlinkSync(ok)
50
+
51
+ const bad = tempModule('module.exports = { before: 5 }')
52
+ expect(() => Register.object(bad, ['before'])).toThrow()
53
+ fs.unlinkSync(bad)
54
+ })
55
+ })
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ // Deterministic environment for the test run. Set before any config/core module is
4
+ // required (setupFiles runs before the test files load). Independent of .env.
5
+ process.env.APP_ENV = process.env.APP_ENV || 'test'
6
+ process.env.APP_NAME = process.env.APP_NAME || 'TestApp'
7
+ process.env.APP_KEY = process.env.APP_KEY || 'test_app_key_0123456789abcdef0123456789abcdef'
8
+ process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_jwt_secret_value'
9
+ process.env.JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'test_jwt_refresh_secret_value'
10
+ process.env.BCRYPT_SALT_ROUNDS = process.env.BCRYPT_SALT_ROUNDS || '4'
11
+ process.env.LOG_LEVEL = process.env.LOG_LEVEL || 'error'
@@ -0,0 +1,65 @@
1
+ 'use strict'
2
+
3
+ const { z } = require('zod')
4
+ const Validator = require('@core/validator.core')
5
+
6
+ function mockRes() {
7
+ return {
8
+ statusCode: 200,
9
+ body: null,
10
+ status(code) {
11
+ this.statusCode = code
12
+ return this
13
+ },
14
+ json(body) {
15
+ this.body = body
16
+ return this
17
+ },
18
+ }
19
+ }
20
+
21
+ describe('Validator.make', () => {
22
+ const schema = z.object({ email: z.string().email(), password: z.string().min(6) })
23
+
24
+ test('passes valid input, strips unknown keys, replaces req.body', () => {
25
+ const req = { body: { email: 'a@b.com', password: 'secret1', extra: 'drop-me' } }
26
+ const res = mockRes()
27
+ let nextCalled = false
28
+
29
+ Validator.make(schema).handle({ req, res, next: () => (nextCalled = true) })
30
+
31
+ expect(nextCalled).toBe(true)
32
+ expect(req.body).toEqual({ email: 'a@b.com', password: 'secret1' })
33
+ expect(req.validated.body).toEqual({ email: 'a@b.com', password: 'secret1' })
34
+ })
35
+
36
+ test('rejects invalid input with the 422 envelope', () => {
37
+ const req = { body: { email: 'nope', password: '1' } }
38
+ const res = mockRes()
39
+
40
+ Validator.make(schema).handle({
41
+ req,
42
+ res,
43
+ next: () => {
44
+ throw new Error('next must not be called on validation failure')
45
+ },
46
+ })
47
+
48
+ expect(res.statusCode).toBe(422)
49
+ expect(res.body.status).toBe(false)
50
+ expect(res.body.code).toBe(422)
51
+ expect(Array.isArray(res.body.errors)).toBe(true)
52
+ expect(res.body.errors.map((e) => e.field)).toEqual(expect.arrayContaining(['email', 'password']))
53
+ })
54
+
55
+ test('validates a non-body source without mutating req.body', () => {
56
+ const querySchema = z.object({ page: z.coerce.number() })
57
+ const req = { query: { page: '3' }, body: { keep: true } }
58
+ const res = mockRes()
59
+
60
+ Validator.make(querySchema, 'query').handle({ req, res, next: () => {} })
61
+
62
+ expect(req.validated.query).toEqual({ page: 3 })
63
+ expect(req.body).toEqual({ keep: true })
64
+ })
65
+ })