odac 1.1.0 → 1.3.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 (117) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +42 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/release.yml +68 -1
  6. package/.github/workflows/test-coverage.yml +6 -5
  7. package/.husky/pre-commit +10 -0
  8. package/.husky/pre-push +13 -0
  9. package/.releaserc.js +1 -1
  10. package/CHANGELOG.md +99 -0
  11. package/README.md +16 -0
  12. package/bin/odac.js +196 -38
  13. package/client/odac.js +238 -171
  14. package/docs/backend/01-overview/03-development-server.md +38 -45
  15. package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
  16. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  17. package/docs/backend/03-config/01-database-connection.md +2 -2
  18. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  19. package/docs/backend/03-config/03-request-timeout.md +1 -1
  20. package/docs/backend/03-config/04-environment-variables.md +4 -4
  21. package/docs/backend/03-config/05-early-hints.md +2 -2
  22. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  23. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  24. package/docs/backend/04-routing/09-websocket.md +14 -1
  25. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  26. package/docs/backend/05-controllers/03-controller-classes.md +40 -20
  27. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  28. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  29. package/docs/backend/08-database/01-getting-started.md +2 -2
  30. package/docs/backend/10-authentication/03-register.md +1 -1
  31. package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
  32. package/docs/backend/10-authentication/05-session-management.md +15 -1
  33. package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
  34. package/docs/backend/10-authentication/07-magic-links.md +1 -1
  35. package/docs/index.json +5 -1
  36. package/jest.config.js +1 -1
  37. package/package.json +18 -14
  38. package/src/Auth.js +58 -23
  39. package/src/Config.js +7 -7
  40. package/src/Database.js +1 -1
  41. package/src/Env.js +3 -1
  42. package/src/Ipc.js +7 -0
  43. package/src/Lang.js +9 -2
  44. package/src/Mail.js +19 -9
  45. package/src/Odac.js +56 -44
  46. package/src/Request.js +6 -2
  47. package/src/Route/Cron.js +58 -17
  48. package/src/Route/Internal.js +1 -1
  49. package/src/Route.js +375 -125
  50. package/src/Server.js +40 -3
  51. package/src/Storage.js +4 -0
  52. package/src/Token.js +6 -4
  53. package/src/Validator.js +24 -15
  54. package/src/Var.js +22 -6
  55. package/src/View/EarlyHints.js +43 -33
  56. package/src/View/Form.js +17 -11
  57. package/src/View.js +68 -12
  58. package/src/WebSocket.js +45 -12
  59. package/template/package.json +3 -1
  60. package/template/view/content/home.html +3 -3
  61. package/template/view/head/main.html +2 -2
  62. package/test/Client.test.js +168 -0
  63. package/test/Config.test.js +112 -0
  64. package/test/Lang.test.js +92 -0
  65. package/test/Odac.test.js +88 -0
  66. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  67. package/test/{framework/Route.test.js → Route.test.js} +105 -1
  68. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  69. package/test/WebSocket.test.js +238 -0
  70. package/test/scripts/check-coverage.js +4 -4
  71. package/test/cli/Cli.test.js +0 -36
  72. package/test/core/Commands.test.js +0 -538
  73. package/test/core/Config.test.js +0 -1432
  74. package/test/core/Lang.test.js +0 -250
  75. package/test/core/Odac.test.js +0 -234
  76. package/test/core/Process.test.js +0 -156
  77. package/test/framework/WebSocket.test.js +0 -100
  78. package/test/server/Api.test.js +0 -647
  79. package/test/server/DNS.test.js +0 -2050
  80. package/test/server/DNS.test.js.bak +0 -2084
  81. package/test/server/Hub.test.js +0 -497
  82. package/test/server/Log.test.js +0 -73
  83. package/test/server/Mail.account.test_.js +0 -460
  84. package/test/server/Mail.init.test_.js +0 -411
  85. package/test/server/Mail.test_.js +0 -1340
  86. package/test/server/SSL.test_.js +0 -1491
  87. package/test/server/Server.test.js +0 -765
  88. package/test/server/Service.test_.js +0 -1127
  89. package/test/server/Subdomain.test.js +0 -440
  90. package/test/server/Web/Firewall.test.js +0 -175
  91. package/test/server/Web/Proxy.test.js +0 -397
  92. package/test/server/Web.test.js +0 -1494
  93. package/test/server/__mocks__/acme-client.js +0 -17
  94. package/test/server/__mocks__/bcrypt.js +0 -50
  95. package/test/server/__mocks__/child_process.js +0 -389
  96. package/test/server/__mocks__/crypto.js +0 -432
  97. package/test/server/__mocks__/fs.js +0 -450
  98. package/test/server/__mocks__/globalOdac.js +0 -227
  99. package/test/server/__mocks__/http.js +0 -575
  100. package/test/server/__mocks__/https.js +0 -272
  101. package/test/server/__mocks__/index.js +0 -249
  102. package/test/server/__mocks__/mail/server.js +0 -100
  103. package/test/server/__mocks__/mail/smtp.js +0 -31
  104. package/test/server/__mocks__/mailparser.js +0 -81
  105. package/test/server/__mocks__/net.js +0 -369
  106. package/test/server/__mocks__/node-forge.js +0 -328
  107. package/test/server/__mocks__/os.js +0 -320
  108. package/test/server/__mocks__/path.js +0 -291
  109. package/test/server/__mocks__/selfsigned.js +0 -8
  110. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  111. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  112. package/test/server/__mocks__/smtp-server.js +0 -106
  113. package/test/server/__mocks__/sqlite3.js +0 -394
  114. package/test/server/__mocks__/testFactories.js +0 -299
  115. package/test/server/__mocks__/testHelpers.js +0 -363
  116. package/test/server/__mocks__/tls.js +0 -229
  117. /package/template/{config.json → odac.json} +0 -0
package/src/WebSocket.js CHANGED
@@ -1,7 +1,10 @@
1
1
  const nodeCrypto = require('crypto')
2
2
 
3
3
  const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
4
- const MAX_PAYLOAD_LENGTH = 10 * 1024 * 1024
4
+ const DEFAULT_MAX_PAYLOAD = 10 * 1024 * 1024
5
+ const DEFAULT_RATE_LIMIT_MAX = 50
6
+ const DEFAULT_RATE_LIMIT_WINDOW = 1000
7
+
5
8
  const OPCODE = {
6
9
  CONTINUATION: 0x0,
7
10
  TEXT: 0x1,
@@ -18,13 +21,29 @@ class WebSocketClient {
18
21
  #server
19
22
  #id
20
23
  #rooms = new Set()
24
+ #maxPayload
25
+ #rateLimitMax
26
+ #rateLimitWindow
27
+ #messageCount = 0
28
+ #rateLimitTimer
21
29
  data = {}
22
30
 
23
- constructor(socket, server, id) {
31
+ constructor(socket, server, id, options = {}) {
24
32
  this.#socket = socket
25
33
  this.#socket.pause()
26
34
  this.#server = server
27
35
  this.#id = id
36
+ this.#maxPayload = options.maxPayload || DEFAULT_MAX_PAYLOAD
37
+
38
+ this.#rateLimitMax = options.rateLimit?.max ?? DEFAULT_RATE_LIMIT_MAX
39
+ this.#rateLimitWindow = options.rateLimit?.window ?? DEFAULT_RATE_LIMIT_WINDOW
40
+
41
+ if (this.#rateLimitMax > 0) {
42
+ this.#rateLimitTimer = setInterval(() => {
43
+ this.#messageCount = 0
44
+ }, this.#rateLimitWindow)
45
+ }
46
+
28
47
  this.#setupListeners()
29
48
  }
30
49
 
@@ -93,7 +112,7 @@ class WebSocketClient {
93
112
  offset = 10
94
113
  }
95
114
 
96
- if (payloadLength > MAX_PAYLOAD_LENGTH) {
115
+ if (payloadLength > this.#maxPayload) {
97
116
  this.close(1009, 'Payload too large')
98
117
  return null
99
118
  }
@@ -125,6 +144,14 @@ class WebSocketClient {
125
144
  }
126
145
 
127
146
  #handleFrame(frame) {
147
+ if (this.#rateLimitMax > 0) {
148
+ this.#messageCount++
149
+ if (this.#messageCount > this.#rateLimitMax) {
150
+ this.close(1008, 'Rate limit exceeded')
151
+ return
152
+ }
153
+ }
154
+
128
155
  switch (frame.opcode) {
129
156
  case OPCODE.TEXT:
130
157
  this.#handleMessage(frame.payload.toString('utf8'))
@@ -158,6 +185,8 @@ class WebSocketClient {
158
185
  if (this.#closed) return
159
186
  this.#closed = true
160
187
 
188
+ if (this.#rateLimitTimer) clearInterval(this.#rateLimitTimer)
189
+
161
190
  this.#socket.removeAllListeners()
162
191
 
163
192
  for (const room of this.#rooms) {
@@ -169,8 +198,7 @@ class WebSocketClient {
169
198
  }
170
199
 
171
200
  #sendFrame(opcode, data) {
172
- if (this.#closed) return
173
-
201
+ if (this.#closed && opcode !== OPCODE.CLOSE) return
174
202
  const payload = Buffer.isBuffer(data) ? data : Buffer.from(data)
175
203
  const length = payload.length
176
204
 
@@ -240,6 +268,8 @@ class WebSocketClient {
240
268
  if (this.#closed) return
241
269
  this.#closed = true
242
270
 
271
+ if (this.#rateLimitTimer) clearInterval(this.#rateLimitTimer)
272
+
243
273
  const reasonBuffer = Buffer.from(reason)
244
274
  const payload = Buffer.alloc(2 + reasonBuffer.length)
245
275
  payload.writeUInt16BE(code, 0)
@@ -287,14 +317,14 @@ class WebSocketServer {
287
317
  #rooms = new Map()
288
318
  #routes = new Map()
289
319
 
290
- route(path, handler) {
291
- this.#routes.set(path, handler)
320
+ route(path, handler, options = {}) {
321
+ this.#routes.set(path, {handler, options})
292
322
  }
293
323
 
294
324
  getRoute(path) {
295
325
  if (this.#routes.has(path)) return this.#routes.get(path)
296
326
 
297
- for (const [pattern, handler] of this.#routes) {
327
+ for (const [pattern, config] of this.#routes) {
298
328
  if (!pattern.includes('{')) continue
299
329
  const regex = new RegExp('^' + pattern.replace(/\{[^}]+\}/g, '([^/]+)') + '$')
300
330
  const match = path.match(regex)
@@ -304,7 +334,11 @@ class WebSocketServer {
304
334
  paramNames.forEach((name, i) => {
305
335
  params[name.slice(1, -1)] = match[i + 1]
306
336
  })
307
- return {handler, params}
337
+ return {
338
+ handler: config.handler,
339
+ options: config.options,
340
+ params
341
+ }
308
342
  }
309
343
  }
310
344
  return null
@@ -320,8 +354,7 @@ class WebSocketServer {
320
354
  return
321
355
  }
322
356
 
323
- const handler = typeof routeInfo === 'function' ? routeInfo : routeInfo.handler
324
- const params = routeInfo.params || {}
357
+ const {handler, params = {}, options = {}} = routeInfo
325
358
 
326
359
  const key = req.headers['sec-websocket-key']
327
360
  if (!key) {
@@ -353,7 +386,7 @@ class WebSocketServer {
353
386
  if (head && head.length > 0) socket.unshift(head)
354
387
 
355
388
  const clientId = nodeCrypto.randomUUID()
356
- const client = new WebSocketClient(socket, this, clientId)
389
+ const client = new WebSocketClient(socket, this, clientId, options)
357
390
  this.#clients.set(clientId, client)
358
391
 
359
392
  if (params) {
@@ -3,7 +3,9 @@
3
3
  "version": "1.0.0",
4
4
  "description": "Website for {{domain_original}}",
5
5
  "scripts": {
6
- "dev": "odac dev"
6
+ "dev": "odac dev",
7
+ "build": "odac build",
8
+ "start": "odac start"
7
9
  },
8
10
  "dependencies": {
9
11
  "odac": "*"
@@ -60,9 +60,9 @@
60
60
 
61
61
  <div class="structure-item">
62
62
  <div class="structure-icon">⚙️</div>
63
- <h3 class="structure-title">config.json</h3>
63
+ <h3 class="structure-title">odac.json</h3>
64
64
  <p class="structure-description">Configuration file for database, routes, and other settings.</p>
65
- <code class="structure-path">config.json</code>
65
+ <code class="structure-path">odac.json</code>
66
66
  </div>
67
67
  </div>
68
68
  </div>
@@ -156,7 +156,7 @@
156
156
  <div class="feature-icon">🗄️</div>
157
157
  <div class="feature-content">
158
158
  <h3>Database Ready</h3>
159
- <p>MySQL and SQLite support built-in. Configure in <code>config.json</code></p>
159
+ <p>MySQL and SQLite support built-in. Configure in <code>odac.json</code></p>
160
160
  </div>
161
161
  </div>
162
162
 
@@ -1,5 +1,5 @@
1
1
  <meta charset="UTF-8" />
2
2
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3
- <title>Odac</title>
3
+ <title>ODAC</title>
4
4
  <meta name="description" content="A next-generation server and framework toolkit for modern web development" />
5
- <link rel="stylesheet" href="/assets/css/style.css" />
5
+ <link rel="stylesheet" href="/assets/css/app.css" />
@@ -0,0 +1,168 @@
1
+ describe('Client (odac.js)', () => {
2
+ let mockXhr
3
+
4
+ beforeEach(() => {
5
+ jest.resetModules()
6
+
7
+ mockXhr = {
8
+ open: jest.fn(),
9
+ setRequestHeader: jest.fn(),
10
+ send: jest.fn(),
11
+ getResponseHeader: jest.fn(),
12
+ status: 200,
13
+ responseText: '{}',
14
+ response: '{}',
15
+ onload: null,
16
+ onerror: null
17
+ }
18
+
19
+ const mockDocument = {
20
+ getElementById: jest.fn(),
21
+ querySelectorAll: jest.fn(() => []),
22
+ querySelector: jest.fn(),
23
+ addEventListener: jest.fn(),
24
+ removeEventListener: jest.fn(),
25
+ dispatchEvent: jest.fn(),
26
+ documentElement: {dataset: {}},
27
+ cookie: '',
28
+ readyState: 'complete',
29
+ createElement: jest.fn(() => ({
30
+ setAttribute: jest.fn(),
31
+ style: {},
32
+ appendChild: jest.fn(),
33
+ parentNode: {insertBefore: jest.fn()}
34
+ }))
35
+ }
36
+
37
+ const mockWindow = {
38
+ location: {
39
+ protocol: 'http:',
40
+ host: 'localhost',
41
+ href: 'http://localhost/'
42
+ },
43
+ history: {
44
+ pushState: jest.fn()
45
+ },
46
+ scrollTo: jest.fn(),
47
+ addEventListener: jest.fn(),
48
+ XMLHttpRequest: jest.fn(() => mockXhr),
49
+ localStorage: {
50
+ getItem: jest.fn(),
51
+ setItem: jest.fn(),
52
+ removeItem: jest.fn()
53
+ },
54
+ CustomEvent: jest.fn((name, detail) => ({name, detail})),
55
+ setTimeout: jest.fn(),
56
+ clearTimeout: jest.fn(),
57
+ requestAnimationFrame: jest.fn(cb => cb(Date.now())),
58
+ WebSocket: jest.fn(() => ({
59
+ send: jest.fn(),
60
+ close: jest.fn(),
61
+ readyState: 1 // OPEN
62
+ })),
63
+ FormData: jest.fn()
64
+ }
65
+
66
+ mockWindow.window = mockWindow
67
+ mockWindow.document = mockDocument
68
+ mockWindow.WebSocket.OPEN = 1
69
+ mockWindow.WebSocket.CLOSED = 3
70
+
71
+ global.window = mockWindow
72
+ global.document = mockDocument
73
+ global.location = mockWindow.location
74
+ global.XMLHttpRequest = mockWindow.XMLHttpRequest
75
+ global.localStorage = mockWindow.localStorage
76
+ global.CustomEvent = mockWindow.CustomEvent
77
+ global.WebSocket = mockWindow.WebSocket
78
+ global.setTimeout = mockWindow.setTimeout
79
+ global.clearTimeout = mockWindow.clearTimeout
80
+ global.requestAnimationFrame = mockWindow.requestAnimationFrame
81
+ global.FormData = mockWindow.FormData
82
+
83
+ delete require.cache[require.resolve('../client/odac.js')]
84
+ require('../client/odac.js')
85
+ })
86
+
87
+ afterEach(() => {
88
+ delete global.window
89
+ delete global.document
90
+ delete global.location
91
+ delete global.XMLHttpRequest
92
+ delete global.localStorage
93
+ delete global.CustomEvent
94
+ delete global.WebSocket
95
+ delete global.setTimeout
96
+ delete global.clearTimeout
97
+ delete global.requestAnimationFrame
98
+ delete global.FormData
99
+ delete global.Odac
100
+ })
101
+
102
+ test('Odac should be initialized on window', () => {
103
+ expect(window.Odac).toBeDefined()
104
+ })
105
+
106
+ describe('data()', () => {
107
+ test('should retrieve data from odac-data script tag', () => {
108
+ const mockData = {user: 'emre'}
109
+ document.getElementById.mockReturnValue({
110
+ textContent: JSON.stringify(mockData)
111
+ })
112
+
113
+ const result = window.Odac.data()
114
+ expect(result).toEqual(mockData)
115
+ expect(document.getElementById).toHaveBeenCalledWith('odac-data')
116
+ })
117
+
118
+ test('should return specific key from data', () => {
119
+ const mockData = {user: 'emre', role: 'admin'}
120
+ document.getElementById.mockReturnValue({
121
+ textContent: JSON.stringify(mockData)
122
+ })
123
+
124
+ expect(window.Odac.data('user')).toBe('emre')
125
+ expect(window.Odac.data('role')).toBe('admin')
126
+ })
127
+ })
128
+
129
+ describe('storage()', () => {
130
+ test('should get item from localStorage', () => {
131
+ localStorage.getItem.mockReturnValue('val')
132
+ expect(window.Odac.storage('key')).toBe('val')
133
+ expect(localStorage.getItem).toHaveBeenCalledWith('key')
134
+ })
135
+
136
+ test('should set item in localStorage', () => {
137
+ window.Odac.storage('key', 'val')
138
+ expect(localStorage.setItem).toHaveBeenCalledWith('key', 'val')
139
+ })
140
+ })
141
+
142
+ describe('token()', () => {
143
+ test('should fetch token via sync XHR if hash is empty', () => {
144
+ mockXhr.response = JSON.stringify({token: 'new-token'})
145
+ document.cookie = 'odac_client=abc'
146
+
147
+ const token = window.Odac.token()
148
+
149
+ expect(window.XMLHttpRequest).toHaveBeenCalled()
150
+ expect(token).toBe('new-token')
151
+ })
152
+ })
153
+
154
+ describe('OdacWebSocket', () => {
155
+ test('should connect to WebSocket and handle events', () => {
156
+ const ws = window.Odac.ws('/test-ws', {token: false})
157
+ expect(window.WebSocket).toHaveBeenCalled()
158
+
159
+ const openHandler = jest.fn()
160
+ ws.on('open', openHandler)
161
+
162
+ const socketInstance = WebSocket.mock.results[0].value
163
+ socketInstance.onopen()
164
+
165
+ expect(openHandler).toHaveBeenCalled()
166
+ })
167
+ })
168
+ })
@@ -0,0 +1,112 @@
1
+ const fs = require('fs')
2
+ const os = require('os')
3
+
4
+ const Config = require('../src/Config')
5
+
6
+ jest.mock('fs')
7
+ jest.mock('os')
8
+
9
+ describe('Config', () => {
10
+ beforeEach(() => {
11
+ jest.clearAllMocks()
12
+ // Reset global.__dir which is used in Config.js
13
+ global.__dir = '/mock/project'
14
+
15
+ // Reset Config properties to defaults before each test
16
+ Config.system = undefined
17
+ Config.encrypt.key = 'odac'
18
+ })
19
+
20
+ describe('init', () => {
21
+ it('should load system config from home directory', () => {
22
+ os.homedir.mockReturnValue('/home/user')
23
+ fs.readFileSync.mockImplementation(path => {
24
+ if (path === '/home/user/.odac/config.json') {
25
+ return JSON.stringify({deviceId: '123'})
26
+ }
27
+ return '{}'
28
+ })
29
+ fs.existsSync.mockReturnValue(false)
30
+
31
+ Config.init()
32
+
33
+ expect(Config.system).toEqual({deviceId: '123'})
34
+ expect(fs.readFileSync).toHaveBeenCalledWith('/home/user/.odac/config.json')
35
+ })
36
+
37
+ it('should load project config and merge it', () => {
38
+ os.homedir.mockReturnValue('/home/user')
39
+ fs.existsSync.mockImplementation(path => {
40
+ if (path === '/mock/project/odac.json') return true
41
+ return false
42
+ })
43
+ fs.readFileSync.mockImplementation(path => {
44
+ if (path === '/mock/project/odac.json') {
45
+ return JSON.stringify({encrypt: {key: 'secret'}})
46
+ }
47
+ return '{}'
48
+ })
49
+
50
+ Config.init()
51
+
52
+ // The key gets hashed in init(), so it won't be 'secret' anymore
53
+ expect(Config.encrypt.key).not.toBe('secret')
54
+ expect(Config.encrypt.key).toBeInstanceOf(Buffer)
55
+ })
56
+
57
+ it('should interpolate variables in config', () => {
58
+ process.env.TEST_VAR = 'env_value'
59
+ os.homedir.mockReturnValue('/home/user')
60
+ fs.existsSync.mockReturnValue(true)
61
+ fs.readFileSync.mockReturnValue(
62
+ JSON.stringify({
63
+ custom: 'value-${TEST_VAR}'
64
+ })
65
+ )
66
+
67
+ Config.init()
68
+
69
+ expect(Config.custom).toBe('value-env_value')
70
+ })
71
+ })
72
+
73
+ describe('_interpolate', () => {
74
+ it('should replace ${VAR} with environment variables', () => {
75
+ process.env.FOO = 'bar'
76
+ const result = Config._interpolate('hello-${FOO}')
77
+ expect(result).toBe('hello-bar')
78
+ })
79
+
80
+ it('should replace ${odac} with client path', () => {
81
+ // __dirname in Config.js is /.../src, so it replaces /src with /client
82
+ const result = Config._interpolate('path-${odac}')
83
+ expect(result).toMatch(/\/client$/)
84
+ })
85
+
86
+ it('should handle nested objects and arrays', () => {
87
+ process.env.VAR = 'x'
88
+ const obj = {
89
+ a: ['${VAR}'],
90
+ b: {c: '${VAR}'}
91
+ }
92
+ const result = Config._interpolate(obj)
93
+ expect(result).toEqual({
94
+ a: ['x'],
95
+ b: {c: 'x'}
96
+ })
97
+ })
98
+ })
99
+
100
+ describe('_deepMerge', () => {
101
+ it('should merge objects deeply', () => {
102
+ const target = {a: {b: 1}, c: 2}
103
+ const source = {a: {d: 3}, e: 4}
104
+ Config._deepMerge(target, source)
105
+ expect(target).toEqual({
106
+ a: {b: 1, d: 3},
107
+ c: 2,
108
+ e: 4
109
+ })
110
+ })
111
+ })
112
+ })
@@ -0,0 +1,92 @@
1
+ const fs = require('fs')
2
+ const Lang = require('../src/Lang')
3
+
4
+ jest.mock('fs')
5
+
6
+ describe('Lang', () => {
7
+ let mockOdac
8
+ let lang
9
+
10
+ beforeEach(() => {
11
+ jest.clearAllMocks()
12
+ global.__dir = '/mock/project'
13
+
14
+ mockOdac = {
15
+ Config: {lang: {default: 'en'}},
16
+ Var: jest.fn(val => ({
17
+ is: jest.fn(type => type === 'alpha' && /^[a-zA-Z]+$/.test(val))
18
+ })),
19
+ Request: {
20
+ header: jest.fn()
21
+ }
22
+ }
23
+
24
+ // Default fs mock behaviors
25
+ fs.existsSync.mockReturnValue(false)
26
+ fs.mkdirSync.mockImplementation(() => {})
27
+ fs.writeFileSync.mockImplementation(() => {})
28
+ fs.readFileSync.mockImplementation(() => {
29
+ throw new Error('ENOENT')
30
+ })
31
+
32
+ lang = new Lang(mockOdac)
33
+ })
34
+
35
+ describe('constructor and set', () => {
36
+ it('should default to en when no config or header', () => {
37
+ lang = new Lang(mockOdac)
38
+ // Private #lang is not accessible, but we can check where it tries to save
39
+ lang.get('test')
40
+ expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/en.json'), expect.any(String))
41
+ })
42
+
43
+ it('should use lang from header if available', () => {
44
+ mockOdac.Request.header.mockReturnValue('tr-TR')
45
+ lang = new Lang(mockOdac)
46
+ lang.get('test')
47
+ expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/tr.json'), expect.any(String))
48
+ })
49
+
50
+ it('should use explicit lang in set()', () => {
51
+ lang = new Lang(mockOdac)
52
+ lang.set('fr')
53
+ lang.get('test')
54
+ expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/fr.json'), expect.any(String))
55
+ })
56
+ })
57
+
58
+ describe('get', () => {
59
+ it('should return matching string and support placeholders', () => {
60
+ // Mock loading tr.json
61
+ fs.existsSync.mockImplementation(path => path.includes('/tr.json'))
62
+ fs.readFileSync.mockReturnValue(
63
+ JSON.stringify({
64
+ welcome: 'Merhaba %s!'
65
+ })
66
+ )
67
+
68
+ lang.set('tr')
69
+ expect(lang.get('welcome', 'Emre')).toBe('Merhaba Emre!')
70
+ })
71
+
72
+ it('should support numbered placeholders', () => {
73
+ fs.existsSync.mockImplementation(path => path.includes('/en.json'))
74
+ fs.readFileSync.mockReturnValue(
75
+ JSON.stringify({
76
+ order: 'First: %s1, Second: %s2'
77
+ })
78
+ )
79
+
80
+ lang.set('en')
81
+ expect(lang.get('order', 'A', 'B')).toBe('First: A, Second: B')
82
+ })
83
+
84
+ it('should auto-save new keys', () => {
85
+ lang.set('en')
86
+ const result = lang.get('new_key')
87
+
88
+ expect(result).toBe('new_key')
89
+ expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining('/en.json'), expect.stringContaining('"new_key": "new_key"'))
90
+ })
91
+ })
92
+ })
@@ -0,0 +1,88 @@
1
+ const Odac = require('../src/Odac')
2
+
3
+ // Mock all dependencies
4
+ jest.mock('../src/Storage', () => ({init: jest.fn()}))
5
+ jest.mock('../src/Env', () => ({init: jest.fn(), get: jest.fn()}))
6
+ jest.mock('../src/Config', () => ({
7
+ init: jest.fn(),
8
+ request: {timeout: 10000},
9
+ lang: {default: 'en'}
10
+ }))
11
+ jest.mock('../src/Database', () => ({init: jest.fn()}))
12
+ jest.mock('../src/Ipc', () => ({init: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn()}))
13
+ jest.mock('../src/Route', () => {
14
+ return jest.fn().mockImplementation(() => ({
15
+ init: jest.fn(),
16
+ routes: {
17
+ www: {}
18
+ }
19
+ }))
20
+ })
21
+ jest.mock('../src/Server', () => ({init: jest.fn()}))
22
+
23
+ describe('Odac', () => {
24
+ beforeEach(() => {
25
+ jest.clearAllMocks()
26
+ global.__dir = '/mock/project'
27
+ })
28
+
29
+ describe('init', () => {
30
+ it('should initialize all components and set global.Odac', async () => {
31
+ await Odac.init()
32
+ expect(global.Odac).toBeDefined()
33
+ expect(global.Odac.Storage).toBeDefined()
34
+ expect(global.Odac.Config).toBeDefined()
35
+ expect(global.Odac.Env).toBeDefined()
36
+ expect(global.Odac.Database).toBeDefined()
37
+ expect(global.Odac.Ipc).toBeDefined()
38
+ expect(global.Odac.Route).toBeDefined()
39
+ expect(global.Odac.Server).toBeDefined()
40
+ expect(typeof global.__).toBe('function')
41
+ })
42
+ })
43
+
44
+ describe('instance', () => {
45
+ it('should create a context object without req/res', () => {
46
+ const ctx = Odac.instance('id-123')
47
+ expect(ctx.Config).toBeDefined()
48
+ expect(ctx.Database).toBeDefined()
49
+ expect(ctx.Ipc).toBeDefined()
50
+ expect(ctx.Request).toBeUndefined()
51
+ })
52
+
53
+ it('should create a context object with req/res', () => {
54
+ const mockReq = {url: '/', method: 'GET', headers: {host: 'example.com'}, connection: {remoteAddress: '127.0.0.1'}, on: jest.fn()}
55
+ const mockRes = {writeHead: jest.fn(), end: jest.fn(), finished: false}
56
+ const ctx = Odac.instance('id-123', mockReq, mockRes)
57
+ expect(ctx.Request).toBeDefined()
58
+ expect(ctx.Auth).toBeDefined()
59
+ expect(ctx.Token).toBeDefined()
60
+ expect(ctx.Lang).toBeDefined()
61
+ expect(ctx.View).toBeDefined()
62
+ ctx.cleanup()
63
+ })
64
+
65
+ it('should provide helper methods on the context', () => {
66
+ const mockReq = {url: '/', method: 'GET', headers: {host: 'example.com'}, connection: {remoteAddress: '127.0.0.1'}, on: jest.fn()}
67
+ const mockRes = {writeHead: jest.fn(), end: jest.fn(), write: jest.fn(), finished: false}
68
+ const ctx = Odac.instance('id-123', mockReq, mockRes)
69
+
70
+ expect(typeof ctx.env).toBe('function')
71
+ expect(typeof ctx.return).toBe('function')
72
+ expect(typeof ctx.write).toBe('function')
73
+ ctx.cleanup()
74
+ })
75
+
76
+ it('should handle Ipc subscription through Proxy', async () => {
77
+ const ctx = Odac.instance('id-123')
78
+ const callback = jest.fn()
79
+ const IpcSingleton = require('../src/Ipc')
80
+ IpcSingleton.subscribe.mockResolvedValue('sub-id')
81
+
82
+ await ctx.Ipc.subscribe('test-channel', callback)
83
+
84
+ expect(IpcSingleton.subscribe).toHaveBeenCalledWith('test-channel', callback)
85
+ expect(ctx._ipcSubs).toHaveLength(1)
86
+ })
87
+ })
88
+ })
@@ -1,4 +1,4 @@
1
- const Route = require('../../framework/src/Route.js')
1
+ const Route = require('../../src/Route.js')
2
2
 
3
3
  describe('Middleware System', () => {
4
4
  let route
@@ -52,7 +52,7 @@ describe('Middleware System', () => {
52
52
 
53
53
  test('chaining should work: auth.use().page()', () => {
54
54
  route.auth.use('admin').page('/admin', () => {})
55
- expect(route.routes.test.page['/admin'].middlewares).toEqual(['admin'])
55
+ expect(route.routes.test['#page']['/admin'].middlewares).toEqual(['admin'])
56
56
  })
57
57
 
58
58
  test('middlewares should be attached to routes', () => {