odac 1.4.0 → 1.4.2
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/.agent/rules/memory.md +8 -0
- package/.github/workflows/release.yml +1 -1
- package/.releaserc.js +9 -2
- package/CHANGELOG.md +61 -0
- package/README.md +10 -0
- package/bin/odac.js +193 -2
- package/client/odac.js +32 -13
- package/docs/ai/skills/SKILL.md +4 -3
- package/docs/ai/skills/backend/authentication.md +7 -0
- package/docs/ai/skills/backend/config.md +7 -0
- package/docs/ai/skills/backend/controllers.md +7 -0
- package/docs/ai/skills/backend/cron.md +9 -2
- package/docs/ai/skills/backend/database.md +37 -2
- package/docs/ai/skills/backend/forms.md +112 -11
- package/docs/ai/skills/backend/ipc.md +7 -0
- package/docs/ai/skills/backend/mail.md +7 -0
- package/docs/ai/skills/backend/migrations.md +86 -0
- package/docs/ai/skills/backend/request_response.md +7 -0
- package/docs/ai/skills/backend/routing.md +7 -0
- package/docs/ai/skills/backend/storage.md +7 -0
- package/docs/ai/skills/backend/streaming.md +7 -0
- package/docs/ai/skills/backend/structure.md +8 -1
- package/docs/ai/skills/backend/translations.md +7 -0
- package/docs/ai/skills/backend/utilities.md +7 -0
- package/docs/ai/skills/backend/validation.md +138 -31
- package/docs/ai/skills/backend/views.md +7 -0
- package/docs/ai/skills/frontend/core.md +7 -0
- package/docs/ai/skills/frontend/forms.md +48 -13
- package/docs/ai/skills/frontend/navigation.md +7 -0
- package/docs/ai/skills/frontend/realtime.md +7 -0
- package/docs/backend/08-database/02-basics.md +49 -9
- package/docs/backend/08-database/04-migrations.md +259 -37
- package/package.json +1 -1
- package/src/Auth.js +82 -43
- package/src/Config.js +1 -1
- package/src/Database/ConnectionFactory.js +70 -0
- package/src/Database/Migration.js +1228 -0
- package/src/Database/nanoid.js +30 -0
- package/src/Database.js +157 -46
- package/src/Ipc.js +37 -0
- package/src/Odac.js +1 -1
- package/src/Route/Cron.js +11 -0
- package/src/Route.js +8 -0
- package/src/Server.js +77 -23
- package/src/Storage.js +15 -1
- package/src/Validator.js +22 -20
- package/template/schema/users.js +23 -0
- package/test/{Auth.test.js → Auth/check.test.js} +153 -6
- package/test/Client/data.test.js +91 -0
- package/test/Client/get.test.js +90 -0
- package/test/Client/storage.test.js +87 -0
- package/test/Client/token.test.js +82 -0
- package/test/Client/ws.test.js +86 -0
- package/test/Config/deepMerge.test.js +14 -0
- package/test/Config/init.test.js +66 -0
- package/test/Config/interpolate.test.js +35 -0
- package/test/Database/ConnectionFactory/buildConnectionConfig.test.js +13 -0
- package/test/Database/ConnectionFactory/buildConnections.test.js +31 -0
- package/test/Database/ConnectionFactory/resolveClient.test.js +12 -0
- package/test/Database/Migration/migrate_column.test.js +52 -0
- package/test/Database/Migration/migrate_files.test.js +70 -0
- package/test/Database/Migration/migrate_index.test.js +89 -0
- package/test/Database/Migration/migrate_nanoid.test.js +160 -0
- package/test/Database/Migration/migrate_seed.test.js +77 -0
- package/test/Database/Migration/migrate_table.test.js +88 -0
- package/test/Database/Migration/rollback.test.js +61 -0
- package/test/Database/Migration/snapshot.test.js +38 -0
- package/test/Database/Migration/status.test.js +41 -0
- package/test/Database/autoNanoid.test.js +215 -0
- package/test/Database/nanoid.test.js +19 -0
- package/test/Lang/constructor.test.js +25 -0
- package/test/Lang/get.test.js +65 -0
- package/test/Lang/set.test.js +49 -0
- package/test/Odac/init.test.js +42 -0
- package/test/Odac/instance.test.js +58 -0
- package/test/Route/{Middleware.test.js → Middleware/chaining.test.js} +5 -29
- package/test/Route/Middleware/use.test.js +35 -0
- package/test/{Route.test.js → Route/check.test.js} +4 -55
- package/test/Route/set.test.js +52 -0
- package/test/Route/ws.test.js +23 -0
- package/test/View/EarlyHints/cache.test.js +32 -0
- package/test/View/EarlyHints/extractFromHtml.test.js +143 -0
- package/test/View/EarlyHints/formatLinkHeader.test.js +33 -0
- package/test/View/EarlyHints/send.test.js +99 -0
- package/test/View/{Form.test.js → Form/generateFieldHtml.test.js} +2 -2
- package/test/View/constructor.test.js +22 -0
- package/test/View/print.test.js +19 -0
- package/test/WebSocket/Client/limits.test.js +55 -0
- package/test/WebSocket/Server/broadcast.test.js +33 -0
- package/test/WebSocket/Server/route.test.js +37 -0
- package/test/Client.test.js +0 -197
- package/test/Config.test.js +0 -112
- package/test/Lang.test.js +0 -92
- package/test/Odac.test.js +0 -88
- package/test/View/EarlyHints.test.js +0 -282
- package/test/WebSocket.test.js +0 -238
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
const EarlyHints = require('../../src/View/EarlyHints')
|
|
2
|
-
|
|
3
|
-
describe('EarlyHints', () => {
|
|
4
|
-
let earlyHints
|
|
5
|
-
let mockConfig
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
mockConfig = {
|
|
9
|
-
enabled: true,
|
|
10
|
-
auto: true,
|
|
11
|
-
maxResources: 5
|
|
12
|
-
}
|
|
13
|
-
earlyHints = new EarlyHints(mockConfig)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe('initialization', () => {
|
|
17
|
-
it('should create instance with default config', () => {
|
|
18
|
-
const hints = new EarlyHints()
|
|
19
|
-
expect(hints).toBeDefined()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('should create instance with custom config', () => {
|
|
23
|
-
const hints = new EarlyHints({enabled: false})
|
|
24
|
-
expect(hints).toBeDefined()
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe('extractFromHtml', () => {
|
|
29
|
-
it('should extract CSS resources from head', () => {
|
|
30
|
-
const html = `
|
|
31
|
-
<html>
|
|
32
|
-
<head>
|
|
33
|
-
<link rel="stylesheet" href="/css/main.css">
|
|
34
|
-
<link rel="stylesheet" href="/css/theme.css">
|
|
35
|
-
</head>
|
|
36
|
-
<body></body>
|
|
37
|
-
</html>
|
|
38
|
-
`
|
|
39
|
-
const resources = earlyHints.extractFromHtml(html)
|
|
40
|
-
expect(resources).toHaveLength(2)
|
|
41
|
-
expect(resources[0]).toEqual({href: '/css/main.css', as: 'style'})
|
|
42
|
-
expect(resources[1]).toEqual({href: '/css/theme.css', as: 'style'})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('should extract JS resources from head', () => {
|
|
46
|
-
const html = `
|
|
47
|
-
<html>
|
|
48
|
-
<head>
|
|
49
|
-
<script src="/js/app.js"></script>
|
|
50
|
-
</head>
|
|
51
|
-
<body></body>
|
|
52
|
-
</html>
|
|
53
|
-
`
|
|
54
|
-
const resources = earlyHints.extractFromHtml(html)
|
|
55
|
-
expect(resources).toHaveLength(1)
|
|
56
|
-
expect(resources[0]).toEqual({href: '/js/app.js', as: 'script'})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('should not extract deferred JS', () => {
|
|
60
|
-
const html = `
|
|
61
|
-
<html>
|
|
62
|
-
<head>
|
|
63
|
-
<script src="/js/app.js" defer></script>
|
|
64
|
-
<script src="/js/async.js" async></script>
|
|
65
|
-
</head>
|
|
66
|
-
<body></body>
|
|
67
|
-
</html>
|
|
68
|
-
`
|
|
69
|
-
const resources = earlyHints.extractFromHtml(html)
|
|
70
|
-
expect(resources).toHaveLength(0)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('should extract font resources', () => {
|
|
74
|
-
const html = `
|
|
75
|
-
<html>
|
|
76
|
-
<head>
|
|
77
|
-
<link rel="preload" href="/fonts/main.woff2" as="font">
|
|
78
|
-
</head>
|
|
79
|
-
<body></body>
|
|
80
|
-
</html>
|
|
81
|
-
`
|
|
82
|
-
const resources = earlyHints.extractFromHtml(html)
|
|
83
|
-
expect(resources).toHaveLength(1)
|
|
84
|
-
expect(resources[0]).toEqual({
|
|
85
|
-
href: '/fonts/main.woff2',
|
|
86
|
-
as: 'font',
|
|
87
|
-
crossorigin: 'anonymous'
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('should limit resources to maxResources', () => {
|
|
92
|
-
const html = `
|
|
93
|
-
<html>
|
|
94
|
-
<head>
|
|
95
|
-
<link rel="stylesheet" href="/css/1.css">
|
|
96
|
-
<link rel="stylesheet" href="/css/2.css">
|
|
97
|
-
<link rel="stylesheet" href="/css/3.css">
|
|
98
|
-
<link rel="stylesheet" href="/css/4.css">
|
|
99
|
-
<link rel="stylesheet" href="/css/5.css">
|
|
100
|
-
<link rel="stylesheet" href="/css/6.css">
|
|
101
|
-
</head>
|
|
102
|
-
<body></body>
|
|
103
|
-
</html>
|
|
104
|
-
`
|
|
105
|
-
const resources = earlyHints.extractFromHtml(html)
|
|
106
|
-
expect(resources).toHaveLength(5)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('should return empty array when no head tag', () => {
|
|
110
|
-
const html = '<html><body></body></html>'
|
|
111
|
-
const resources = earlyHints.extractFromHtml(html)
|
|
112
|
-
expect(resources).toEqual([])
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('should return empty array when disabled', () => {
|
|
116
|
-
const hints = new EarlyHints({enabled: false})
|
|
117
|
-
const html = '<html><head><link rel="stylesheet" href="/css/main.css"></head></html>'
|
|
118
|
-
const resources = hints.extractFromHtml(html)
|
|
119
|
-
expect(resources).toEqual([])
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('should skip resources with defer attribute', () => {
|
|
123
|
-
const html = `
|
|
124
|
-
<html>
|
|
125
|
-
<head>
|
|
126
|
-
<link rel="stylesheet" href="/css/critical.css">
|
|
127
|
-
<link rel="stylesheet" href="/css/non-critical.css" defer>
|
|
128
|
-
<script src="/js/app.js"></script>
|
|
129
|
-
<script src="/js/analytics.js" defer></script>
|
|
130
|
-
</head>
|
|
131
|
-
<body></body>
|
|
132
|
-
</html>
|
|
133
|
-
`
|
|
134
|
-
const resources = earlyHints.extractFromHtml(html)
|
|
135
|
-
expect(resources).toHaveLength(2)
|
|
136
|
-
expect(resources[0]).toEqual({href: '/css/critical.css', as: 'style'})
|
|
137
|
-
expect(resources[1]).toEqual({href: '/js/app.js', as: 'script'})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('should only detect stylesheets with rel="stylesheet"', () => {
|
|
141
|
-
const html = `
|
|
142
|
-
<html>
|
|
143
|
-
<head>
|
|
144
|
-
<link rel="stylesheet" href="/css/main.css">
|
|
145
|
-
<link rel="icon" href="/favicon.css">
|
|
146
|
-
<link rel="preload" href="/data.css" as="fetch">
|
|
147
|
-
<link href="/other.css">
|
|
148
|
-
</head>
|
|
149
|
-
<body></body>
|
|
150
|
-
</html>
|
|
151
|
-
`
|
|
152
|
-
const resources = earlyHints.extractFromHtml(html)
|
|
153
|
-
expect(resources).toHaveLength(1)
|
|
154
|
-
expect(resources[0]).toEqual({href: '/css/main.css', as: 'style'})
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
describe('formatLinkHeader', () => {
|
|
159
|
-
it('should format basic resource', () => {
|
|
160
|
-
const resource = {href: '/css/main.css', as: 'style'}
|
|
161
|
-
const header = earlyHints.formatLinkHeader(resource)
|
|
162
|
-
expect(header).toBe('</css/main.css>; rel=preload; as=style')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('should format resource with crossorigin', () => {
|
|
166
|
-
const resource = {href: '/font.woff2', as: 'font', crossorigin: 'anonymous'}
|
|
167
|
-
const header = earlyHints.formatLinkHeader(resource)
|
|
168
|
-
expect(header).toBe('</font.woff2>; rel=preload; as=font; crossorigin')
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('should format resource with type', () => {
|
|
172
|
-
const resource = {href: '/data.json', as: 'fetch', type: 'application/json'}
|
|
173
|
-
const header = earlyHints.formatLinkHeader(resource)
|
|
174
|
-
expect(header).toBe('</data.json>; rel=preload; as=fetch; type=application/json')
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
describe('caching', () => {
|
|
179
|
-
it('should cache hints for route', () => {
|
|
180
|
-
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
181
|
-
earlyHints.cacheHints('/home', resources)
|
|
182
|
-
|
|
183
|
-
const cached = earlyHints.getHints(null, '/home')
|
|
184
|
-
expect(cached).toEqual(resources)
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('should not cache when disabled', () => {
|
|
188
|
-
const hints = new EarlyHints({enabled: false})
|
|
189
|
-
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
190
|
-
hints.cacheHints('/home', resources)
|
|
191
|
-
|
|
192
|
-
const cached = hints.getHints(null, '/home')
|
|
193
|
-
expect(cached).toBeNull()
|
|
194
|
-
})
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
describe('send', () => {
|
|
198
|
-
it('should return false when disabled', () => {
|
|
199
|
-
const hints = new EarlyHints({enabled: false})
|
|
200
|
-
const mockRes = {
|
|
201
|
-
headersSent: false,
|
|
202
|
-
writableEnded: false,
|
|
203
|
-
writeEarlyHints: jest.fn()
|
|
204
|
-
}
|
|
205
|
-
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
206
|
-
|
|
207
|
-
const result = hints.send(mockRes, resources)
|
|
208
|
-
expect(result).toBe(false)
|
|
209
|
-
expect(mockRes.writeEarlyHints).not.toHaveBeenCalled()
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('should return false when headers already sent', () => {
|
|
213
|
-
const mockRes = {
|
|
214
|
-
headersSent: true,
|
|
215
|
-
writableEnded: false,
|
|
216
|
-
writeEarlyHints: jest.fn()
|
|
217
|
-
}
|
|
218
|
-
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
219
|
-
|
|
220
|
-
const result = earlyHints.send(mockRes, resources)
|
|
221
|
-
expect(result).toBe(false)
|
|
222
|
-
expect(mockRes.writeEarlyHints).not.toHaveBeenCalled()
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('should return false when response ended', () => {
|
|
226
|
-
const mockRes = {
|
|
227
|
-
headersSent: false,
|
|
228
|
-
writableEnded: true,
|
|
229
|
-
writeEarlyHints: jest.fn()
|
|
230
|
-
}
|
|
231
|
-
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
232
|
-
|
|
233
|
-
const result = earlyHints.send(mockRes, resources)
|
|
234
|
-
expect(result).toBe(false)
|
|
235
|
-
expect(mockRes.writeEarlyHints).not.toHaveBeenCalled()
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('should return true even when writeEarlyHints not available', () => {
|
|
239
|
-
const mockRes = {
|
|
240
|
-
headersSent: false,
|
|
241
|
-
writableEnded: false,
|
|
242
|
-
setHeader: jest.fn()
|
|
243
|
-
}
|
|
244
|
-
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
245
|
-
|
|
246
|
-
const result = earlyHints.send(mockRes, resources)
|
|
247
|
-
expect(result).toBe(true)
|
|
248
|
-
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Odac-Early-Hints', JSON.stringify(['</css/main.css>; rel=preload; as=style']))
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('should send early hints successfully', () => {
|
|
252
|
-
const mockRes = {
|
|
253
|
-
headersSent: false,
|
|
254
|
-
writableEnded: false,
|
|
255
|
-
writeEarlyHints: jest.fn(),
|
|
256
|
-
setHeader: jest.fn()
|
|
257
|
-
}
|
|
258
|
-
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
259
|
-
|
|
260
|
-
const result = earlyHints.send(mockRes, resources)
|
|
261
|
-
expect(result).toBe(true)
|
|
262
|
-
expect(mockRes.writeEarlyHints).toHaveBeenCalledWith({
|
|
263
|
-
link: ['</css/main.css>; rel=preload; as=style']
|
|
264
|
-
})
|
|
265
|
-
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Odac-Early-Hints', JSON.stringify(['</css/main.css>; rel=preload; as=style']))
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
it('should handle writeEarlyHints errors gracefully', () => {
|
|
269
|
-
const mockRes = {
|
|
270
|
-
headersSent: false,
|
|
271
|
-
writableEnded: false,
|
|
272
|
-
writeEarlyHints: jest.fn(() => {
|
|
273
|
-
throw new Error('Write error')
|
|
274
|
-
})
|
|
275
|
-
}
|
|
276
|
-
const resources = [{href: '/css/main.css', as: 'style'}]
|
|
277
|
-
|
|
278
|
-
const result = earlyHints.send(mockRes, resources)
|
|
279
|
-
expect(result).toBe(false)
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
})
|
package/test/WebSocket.test.js
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
const {WebSocketServer} = require('../src/WebSocket.js')
|
|
2
|
-
|
|
3
|
-
describe('WebSocketServer', () => {
|
|
4
|
-
let server
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
server = new WebSocketServer()
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
describe('route', () => {
|
|
11
|
-
it('should register a route', () => {
|
|
12
|
-
const handler = jest.fn()
|
|
13
|
-
server.route('/chat', handler)
|
|
14
|
-
expect(server.getRoute('/chat').handler).toBe(handler)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('should return null for unregistered route', () => {
|
|
18
|
-
expect(server.getRoute('/unknown')).toBeNull()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('should match parameterized routes', () => {
|
|
22
|
-
const handler = jest.fn()
|
|
23
|
-
server.route('/room/{id}', handler)
|
|
24
|
-
|
|
25
|
-
const result = server.getRoute('/room/123')
|
|
26
|
-
expect(result).toBeDefined()
|
|
27
|
-
expect(result.handler).toBe(handler)
|
|
28
|
-
expect(result.params).toEqual({id: '123'})
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('should match multiple parameters', () => {
|
|
32
|
-
const handler = jest.fn()
|
|
33
|
-
server.route('/chat/{room}/user/{userId}', handler)
|
|
34
|
-
|
|
35
|
-
const result = server.getRoute('/chat/general/user/42')
|
|
36
|
-
expect(result.params).toEqual({room: 'general', userId: '42'})
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
describe('rooms', () => {
|
|
41
|
-
it('should join and leave rooms', () => {
|
|
42
|
-
server.joinRoom('client1', 'room1')
|
|
43
|
-
server.joinRoom('client2', 'room1')
|
|
44
|
-
|
|
45
|
-
server.leaveRoom('client1', 'room1')
|
|
46
|
-
server.leaveRoom('client2', 'room1')
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
describe('broadcast', () => {
|
|
51
|
-
it('should send message to all connected clients', () => {
|
|
52
|
-
const client1 = {id: 'c1', send: jest.fn()}
|
|
53
|
-
const client2 = {id: 'c2', send: jest.fn()}
|
|
54
|
-
|
|
55
|
-
server.clients.set('c1', client1)
|
|
56
|
-
server.clients.set('c2', client2)
|
|
57
|
-
|
|
58
|
-
server.broadcast('hello')
|
|
59
|
-
|
|
60
|
-
expect(client1.send).toHaveBeenCalledWith('hello')
|
|
61
|
-
expect(client2.send).toHaveBeenCalledWith('hello')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('should exclude specified client from broadcast', () => {
|
|
65
|
-
const client1 = {id: 'c1', send: jest.fn()}
|
|
66
|
-
const client2 = {id: 'c2', send: jest.fn()}
|
|
67
|
-
|
|
68
|
-
server.clients.set('c1', client1)
|
|
69
|
-
server.clients.set('c2', client2)
|
|
70
|
-
|
|
71
|
-
server.broadcast('hello', 'c1')
|
|
72
|
-
|
|
73
|
-
expect(client1.send).not.toHaveBeenCalled()
|
|
74
|
-
expect(client2.send).toHaveBeenCalledWith('hello')
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
describe('clients', () => {
|
|
79
|
-
it('should track client count', () => {
|
|
80
|
-
expect(server.clientCount).toBe(0)
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe('cleanup on disconnect', () => {
|
|
85
|
-
it('should be handled by Route.setWs wrapper', () => {
|
|
86
|
-
expect(true).toBe(true)
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
describe('maxPayload', () => {
|
|
90
|
-
it('should close connection if payload exceeds limit', () => {
|
|
91
|
-
const socket = {
|
|
92
|
-
pause: jest.fn(),
|
|
93
|
-
resume: jest.fn(),
|
|
94
|
-
on: jest.fn(),
|
|
95
|
-
write: jest.fn(),
|
|
96
|
-
end: jest.fn(),
|
|
97
|
-
removeAllListeners: jest.fn()
|
|
98
|
-
}
|
|
99
|
-
const {WebSocketClient} = require('../src/WebSocket.js')
|
|
100
|
-
new WebSocketClient(socket, server, 'test-id', {maxPayload: 10})
|
|
101
|
-
|
|
102
|
-
// We must send a MASKED frame because server expects masked frames from client
|
|
103
|
-
const buffer = Buffer.alloc(100)
|
|
104
|
-
buffer[0] = 0x81 // fin + text
|
|
105
|
-
buffer[1] = 0x80 | 20 // masked + length 20
|
|
106
|
-
// Mask key (4 bytes) + Payload (20 bytes) needed but header check happens first
|
|
107
|
-
|
|
108
|
-
const dataHandler = socket.on.mock.calls.find(call => call[0] === 'data')[1]
|
|
109
|
-
dataHandler(buffer)
|
|
110
|
-
|
|
111
|
-
expect(socket.end).toHaveBeenCalled()
|
|
112
|
-
// Verify close frame sent with 1009
|
|
113
|
-
// socket.write is called to send the Close frame
|
|
114
|
-
const writeCall = socket.write.mock.calls[0][0]
|
|
115
|
-
expect(writeCall[2]).toBe(0x03) // 1009 >> 8
|
|
116
|
-
expect(writeCall[3]).toBe(0xf1) // 1009 & 0xff
|
|
117
|
-
})
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
describe('rateLimit', () => {
|
|
121
|
-
it('should close connection if rate limit exceeded', () => {
|
|
122
|
-
const socket = {
|
|
123
|
-
pause: jest.fn(),
|
|
124
|
-
resume: jest.fn(),
|
|
125
|
-
on: jest.fn(),
|
|
126
|
-
write: jest.fn(),
|
|
127
|
-
end: jest.fn(),
|
|
128
|
-
removeAllListeners: jest.fn()
|
|
129
|
-
}
|
|
130
|
-
const {WebSocketClient} = require('../src/WebSocket.js')
|
|
131
|
-
new WebSocketClient(socket, server, 'test-id', {
|
|
132
|
-
rateLimit: {max: 2, window: 1000}
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// Valid MASKED frame (exact size 7 bytes)
|
|
136
|
-
const buffer = Buffer.alloc(7)
|
|
137
|
-
buffer[0] = 0x81
|
|
138
|
-
buffer[1] = 0x80 | 1 // masked + length 1
|
|
139
|
-
buffer[2] = 0x00
|
|
140
|
-
buffer[3] = 0x00
|
|
141
|
-
buffer[4] = 0x00
|
|
142
|
-
buffer[5] = 0x00 // mask key (0)
|
|
143
|
-
buffer[6] = 0x61 // 'a' (masked with 0 remains 'a')
|
|
144
|
-
|
|
145
|
-
const dataHandler = socket.on.mock.calls.find(call => call[0] === 'data')[1]
|
|
146
|
-
|
|
147
|
-
// Send 3 messages (limit is 2)
|
|
148
|
-
dataHandler(buffer)
|
|
149
|
-
dataHandler(buffer)
|
|
150
|
-
dataHandler(buffer)
|
|
151
|
-
|
|
152
|
-
expect(socket.end).toHaveBeenCalled()
|
|
153
|
-
// Verify close frame sent with 1008
|
|
154
|
-
const writeCalls = socket.write.mock.calls
|
|
155
|
-
const writeCall = writeCalls[writeCalls.length - 1][0]
|
|
156
|
-
expect(writeCall[2]).toBe(0x03) // 1008 >> 8
|
|
157
|
-
expect(writeCall[3]).toBe(0xf0) // 1008 & 0xff
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('should reset count after window', done => {
|
|
161
|
-
const socket = {
|
|
162
|
-
pause: jest.fn(),
|
|
163
|
-
resume: jest.fn(),
|
|
164
|
-
on: jest.fn(),
|
|
165
|
-
write: jest.fn(),
|
|
166
|
-
end: jest.fn(),
|
|
167
|
-
removeAllListeners: jest.fn()
|
|
168
|
-
}
|
|
169
|
-
const {WebSocketClient} = require('../src/WebSocket.js')
|
|
170
|
-
const client = new WebSocketClient(socket, server, 'test-id', {
|
|
171
|
-
rateLimit: {max: 2, window: 200}
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const buffer = Buffer.alloc(7)
|
|
175
|
-
buffer[0] = 0x81
|
|
176
|
-
buffer[1] = 0x80 | 1
|
|
177
|
-
buffer[2] = 0x00
|
|
178
|
-
buffer[3] = 0x00
|
|
179
|
-
buffer[4] = 0x00
|
|
180
|
-
buffer[5] = 0x00
|
|
181
|
-
buffer[6] = 0x61
|
|
182
|
-
|
|
183
|
-
const dataHandler = socket.on.mock.calls.find(call => call[0] === 'data')[1]
|
|
184
|
-
|
|
185
|
-
// Send 2 messages
|
|
186
|
-
dataHandler(buffer)
|
|
187
|
-
dataHandler(buffer)
|
|
188
|
-
expect(socket.end).not.toHaveBeenCalled()
|
|
189
|
-
|
|
190
|
-
setTimeout(() => {
|
|
191
|
-
// Send 1 more after window reset
|
|
192
|
-
dataHandler(buffer)
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
expect(socket.end).not.toHaveBeenCalled()
|
|
196
|
-
client.close()
|
|
197
|
-
done()
|
|
198
|
-
} catch (error) {
|
|
199
|
-
client.close()
|
|
200
|
-
done(error)
|
|
201
|
-
}
|
|
202
|
-
}, 300)
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
describe('Route WebSocket Integration', () => {
|
|
208
|
-
const Route = require('../src/Route.js')
|
|
209
|
-
|
|
210
|
-
beforeEach(() => {
|
|
211
|
-
global.Odac = {
|
|
212
|
-
Route: new Route()
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it('should support ws() method', () => {
|
|
217
|
-
expect(typeof Odac.Route.ws).toBe('function')
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('should support auth.ws() method', () => {
|
|
221
|
-
expect(typeof Odac.Route.auth.ws).toBe('function')
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('should support middleware with ws()', () => {
|
|
225
|
-
const chain = Odac.Route.use('test-middleware')
|
|
226
|
-
expect(typeof chain.ws).toBe('function')
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('should support middleware with auth.ws()', () => {
|
|
230
|
-
const chain = Odac.Route.use('test-middleware')
|
|
231
|
-
expect(typeof chain.auth.ws).toBe('function')
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('should support auth.use() with ws()', () => {
|
|
235
|
-
const chain = Odac.Route.auth.use('test-middleware')
|
|
236
|
-
expect(typeof chain.ws).toBe('function')
|
|
237
|
-
})
|
|
238
|
-
})
|