arckode-framework 1.0.0 → 1.0.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.
@@ -1,174 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'bun:test'
2
- import { Router } from '../framework'
3
- import { cors, rateLimit, requestLogger, bodyLimit, timeout, compression } from '../middlewares'
4
-
5
- function makeRouter(): Router {
6
- const r = new Router()
7
- r.get('/test', () => ({ status: 200, body: { ok: true } }))
8
- r.post('/data', (req) => ({ status: 201, body: req.body }))
9
- return r
10
- }
11
-
12
- describe('cors middleware', () => {
13
- it('agrega headers CORS a OPTIONS', async () => {
14
- const r = makeRouter()
15
- r.use(cors({ origins: ['http://example.com'] }))
16
-
17
- const res = await r.resolve('OPTIONS', '/test')
18
- expect(res.status).toBe(204)
19
- expect(res.headers).toBeDefined()
20
- expect(res.headers!['Access-Control-Allow-Origin']).toBe('http://example.com')
21
- expect(res.headers!['Access-Control-Allow-Methods']).toContain('GET')
22
- expect(res.headers!['Access-Control-Max-Age']).toBe('86400')
23
- })
24
-
25
- it('agrega CORS a responses normales', async () => {
26
- const r = makeRouter()
27
- r.use(cors())
28
-
29
- const res = await r.resolve('GET', '/test')
30
- expect(res.headers).toBeDefined()
31
- expect(res.headers!['Access-Control-Allow-Origin']).toBe('*')
32
- })
33
-
34
- it('no rompe si no se pasa configuración', async () => {
35
- const r = makeRouter()
36
- r.use(cors())
37
-
38
- const res = await r.resolve('GET', '/test')
39
- expect(res.status).toBe(200)
40
- expect(res.body).toEqual({ ok: true })
41
- })
42
- })
43
-
44
- describe('rateLimit middleware', () => {
45
- it('deja pasar requests dentro del límite', async () => {
46
- const r = makeRouter()
47
- const rl = rateLimit({ windowMs: 60000, max: 5 })
48
- r.use(rl)
49
-
50
- for (let i = 0; i < 5; i++) {
51
- const res = await r.resolve('GET', '/test')
52
- expect(res.status).toBe(200)
53
- }
54
- })
55
-
56
- it('bloquea cuando se excede el límite', async () => {
57
- const r = makeRouter()
58
- const rl = rateLimit({ windowMs: 60000, max: 2 })
59
- r.use(rl)
60
-
61
- await r.resolve('GET', '/test')
62
- await r.resolve('GET', '/test')
63
- const res = await r.resolve('GET', '/test')
64
- expect(res.status).toBe(429)
65
- })
66
-
67
- it('reset funciona para una clave específica', async () => {
68
- const r = makeRouter()
69
- const rl = rateLimit({ windowMs: 60000, max: 1, keyBy: () => 'test-ip' })
70
- r.use(rl)
71
-
72
- await r.resolve('GET', '/test')
73
- rl.reset('test-ip')
74
- const res = await r.resolve('GET', '/test')
75
- expect(res.status).toBe(200)
76
- })
77
- })
78
-
79
- describe('requestLogger middleware', () => {
80
- it('no rompe el request', async () => {
81
- const r = makeRouter()
82
- const logs: string[] = []
83
- const fakeLogger = { info: (msg: string) => logs.push(msg) }
84
- r.use(requestLogger(fakeLogger as any))
85
-
86
- const res = await r.resolve('GET', '/test')
87
- expect(res.status).toBe(200)
88
- expect(logs.length).toBeGreaterThan(0)
89
- })
90
- })
91
-
92
- describe('bodyLimit middleware', () => {
93
- it('rechaza cuerpos grandes', async () => {
94
- const r = makeRouter()
95
- r.use(bodyLimit(10))
96
-
97
- const res = await r.resolve('POST', '/data', { body: { data: 'x'.repeat(100) } })
98
- expect(res.status).toBe(413)
99
- })
100
-
101
- it('acepta cuerpos chicos', async () => {
102
- const r = makeRouter()
103
- r.use(bodyLimit(1024))
104
-
105
- const res = await r.resolve('POST', '/data', { body: { nombre: 'test' } })
106
- expect(res.status).toBe(201)
107
- })
108
- })
109
-
110
- describe('timeout middleware', () => {
111
- it('timeout si el handler tarda mucho', async () => {
112
- const r = new Router()
113
- r.get('/slow', async () => {
114
- await new Promise(r => setTimeout(r, 500))
115
- return { status: 200, body: {} }
116
- })
117
- r.use(timeout(50))
118
-
119
- const res = await r.resolve('GET', '/slow')
120
- expect(res.status).toBe(500)
121
- })
122
-
123
- it('no afecta handlers rápidos', async () => {
124
- const r = makeRouter()
125
- r.use(timeout(5000))
126
-
127
- const res = await r.resolve('GET', '/test')
128
- expect(res.status).toBe(200)
129
- })
130
- })
131
-
132
- describe('compression middleware', () => {
133
- it('comprime respuestas grandes con gzip', async () => {
134
- const r = new Router()
135
- r.get('/big', () => ({
136
- status: 200,
137
- body: { data: 'x'.repeat(2000) },
138
- }))
139
- r.use(compression({ threshold: 100 }))
140
-
141
- const res = await r.resolve('GET', '/big', {
142
- headers: { 'accept-encoding': 'gzip' },
143
- })
144
- expect(res.status).toBe(200)
145
- expect(res.headers).toBeDefined()
146
- expect(res.headers!['Content-Encoding']).toBe('gzip')
147
- expect(Buffer.isBuffer(res.body)).toBe(true)
148
- })
149
-
150
- it('no comprime si no se acepta gzip', async () => {
151
- const r = new Router()
152
- r.get('/big', () => ({
153
- status: 200,
154
- body: { data: 'x'.repeat(2000) },
155
- }))
156
- r.use(compression({ threshold: 100 }))
157
-
158
- const res = await r.resolve('GET', '/big')
159
- expect(res.status).toBe(200)
160
- expect(res.headers?.['Content-Encoding']).toBeUndefined()
161
- expect(typeof res.body).toBe('object')
162
- })
163
-
164
- it('no comprime respuestas sin body', async () => {
165
- const r = new Router()
166
- r.get('/empty', () => ({ status: 204, body: null }))
167
- r.use(compression())
168
-
169
- const res = await r.resolve('GET', '/empty', {
170
- headers: { 'accept-encoding': 'gzip' },
171
- })
172
- expect(res.status).toBe(204)
173
- })
174
- })
@@ -1,94 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
2
- import { mkdir, writeFile, rm } from 'node:fs/promises'
3
- import { join } from 'node:path'
4
- import { Router } from '../framework'
5
- import { serveStatic } from '../static'
6
-
7
- const TMP_DIR = join(import.meta.dirname, '..', '__test_fixtures__')
8
-
9
- beforeAll(async () => {
10
- await mkdir(TMP_DIR, { recursive: true })
11
- await writeFile(join(TMP_DIR, 'index.html'), '<h1>Hello</h1>')
12
- await writeFile(join(TMP_DIR, 'styles.css'), 'body { color: red; }')
13
- await writeFile(join(TMP_DIR, 'app.js'), 'console.log("hi")')
14
- })
15
-
16
- afterAll(async () => {
17
- await rm(TMP_DIR, { recursive: true, force: true })
18
- })
19
-
20
- describe('serveStatic', () => {
21
- it('sirve un archivo existente', async () => {
22
- const router = new Router()
23
- serveStatic(router, TMP_DIR)
24
-
25
- const res = await router.resolve('GET', '/index.html')
26
- expect(res.status).toBe(200)
27
- expect(res.headers).toBeDefined()
28
- expect(res.headers!['Content-Type']).toBe('text/html')
29
- expect(Buffer.isBuffer(res.body)).toBe(true)
30
- })
31
-
32
- it('sirve CSS con Content-Type correcto', async () => {
33
- const router = new Router()
34
- serveStatic(router, TMP_DIR)
35
-
36
- const res = await router.resolve('GET', '/styles.css')
37
- expect(res.status).toBe(200)
38
- expect(res.headers!['Content-Type']).toBe('text/css')
39
- })
40
-
41
- it('sirve JS con Content-Type correcto', async () => {
42
- const router = new Router()
43
- serveStatic(router, TMP_DIR)
44
-
45
- const res = await router.resolve('GET', '/app.js')
46
- expect(res.status).toBe(200)
47
- expect(res.headers!['Content-Type']).toBe('application/javascript')
48
- })
49
-
50
- it('devuelve 404 si el archivo no existe', async () => {
51
- const router = new Router()
52
- serveStatic(router, TMP_DIR)
53
-
54
- const res = await router.resolve('GET', '/no-existe.txt')
55
- expect(res.status).toBe(404)
56
- })
57
-
58
- it('sirve index.html por defecto en la raíz', async () => {
59
- const router = new Router()
60
- serveStatic(router, TMP_DIR)
61
-
62
- const res = await router.resolve('GET', '/')
63
- expect(res.status).toBe(200)
64
- expect(res.headers!['Content-Type']).toBe('text/html')
65
- })
66
-
67
- it('sirve fallback (SPA) si el archivo no existe', async () => {
68
- const router = new Router()
69
- serveStatic(router, TMP_DIR, { fallback: 'index.html' })
70
-
71
- const res = await router.resolve('GET', '/alguna/ruta/spa')
72
- expect(res.status).toBe(200)
73
- expect(res.headers!['Content-Type']).toBe('text/html')
74
- })
75
-
76
- it('usa prefijo para rutas', async () => {
77
- const router = new Router()
78
- serveStatic(router, TMP_DIR, { prefix: '/static' })
79
-
80
- const res1 = await router.resolve('GET', '/static/index.html')
81
- expect(res1.status).toBe(200)
82
-
83
- const res2 = await router.resolve('GET', '/index.html')
84
- expect(res2.status).toBe(404)
85
- })
86
-
87
- it('agrega Cache-Control header', async () => {
88
- const router = new Router()
89
- serveStatic(router, TMP_DIR, { cacheControl: 'public, max-age=3600' })
90
-
91
- const res = await router.resolve('GET', '/index.html')
92
- expect(res.headers!['Cache-Control']).toBe('public, max-age=3600')
93
- })
94
- })
@@ -1,114 +0,0 @@
1
- // modules/ws/__tests__/ws.test.ts — Tests del WebSocket server
2
- // Prueba: handshake, parseFrame, sendFrame, broadcast, sendTo
3
-
4
- import { describe, it, expect } from 'bun:test'
5
- import { WSServer } from '../index'
6
- import { createHash } from 'node:crypto'
7
-
8
- // ─── Test de generateAccept ────────────────────────────
9
- describe('WSServer - handshake', () => {
10
- it('generateAccept produce output correcto', () => {
11
- // El WebSocket server tiene método generateAccept
12
- // Probamos que devuelve un string base64 válido
13
- const ws = new WSServer() as any
14
- const key = 'dGhlIHNhbXBsZSBub25jZQ=='
15
- const accept = ws.generateAccept(key)
16
- expect(accept).toBeTruthy()
17
- expect(typeof accept).toBe('string')
18
- expect(accept.length).toBeGreaterThan(0)
19
-
20
- // Verificar que el hash es correcto según RFC 6455
21
- const expected = createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-5AB5DC11B725').digest('base64')
22
- expect(accept).toBe(expected)
23
- })
24
- })
25
-
26
- // ─── Test de parseFrame ────────────────────────────────
27
- describe('WSServer - parseFrame', () => {
28
- it('parsea un frame de texto sin mask', () => {
29
- const ws = new WSServer() as any
30
- // Frame: FIN=1, opcode=1 (text), payload=Hola
31
- const frame = Buffer.alloc(6)
32
- frame[0] = 0x81 // FIN + texto
33
- frame[1] = 4 // payload length
34
- frame.write('Hola', 2) // payload
35
- const result = ws.parseFrame(frame)
36
- expect(result).toBe('Hola')
37
- })
38
-
39
- it('parsea un frame con mask', () => {
40
- const ws = new WSServer() as any
41
- const payload = Buffer.from('Hello')
42
- const mask = Buffer.from([0x01, 0x02, 0x03, 0x04])
43
- const masked = Buffer.alloc(payload.length)
44
- for (let i = 0; i < payload.length; i++) {
45
- masked[i] = (payload[i] ?? 0) ^ (mask[i % 4] ?? 0)
46
- }
47
-
48
- const frame = Buffer.alloc(2 + 4 + payload.length)
49
- frame[0] = 0x81
50
- frame[1] = 0x80 | payload.length // MASK bit + length
51
- mask.copy(frame, 2)
52
- masked.copy(frame, 6)
53
-
54
- const result = ws.parseFrame(frame)
55
- expect(result).toBe('Hello')
56
- })
57
-
58
- it('devuelve null para frames no-texto', () => {
59
- const ws = new WSServer() as any
60
- // Frame: ping (opcode 0x09)
61
- const frame = Buffer.alloc(2)
62
- frame[0] = 0x89
63
- frame[1] = 0
64
- const result = ws.parseFrame(frame)
65
- expect(result).toBeNull()
66
- })
67
- })
68
-
69
- // ─── Test de sendFrame ────────────────────────────────
70
- describe('WSServer - sendFrame', () => {
71
- it('construye frame correctamente', () => {
72
- const ws = new WSServer() as any
73
- const chunks: Buffer[] = []
74
- const mockSocket = { write: (b: Buffer) => chunks.push(b) }
75
-
76
- ws.sendFrame(mockSocket, 'Hola')
77
- expect(chunks).toHaveLength(1)
78
- const frame = chunks[0]!
79
- expect(frame[0]).toBe(0x81) // FIN + texto
80
- expect(frame[1]).toBe(4) // longitud
81
- expect(frame.slice(2).toString()).toBe('Hola')
82
- })
83
- })
84
-
85
- // ─── Test de broadcast ────────────────────────────────
86
- describe('WSServer - broadcast', () => {
87
- it('envía a todos los clientes conectados', () => {
88
- const ws = new WSServer() as any
89
- const received: string[] = []
90
-
91
- // Simular clientes conectados
92
- ws.clients.set('c1', { id: 'c1', send: (d: string) => received.push(d), close: () => {} })
93
- ws.clients.set('c2', { id: 'c2', send: (d: string) => received.push(d), close: () => {} })
94
-
95
- ws.broadcast('test.event', { msg: 'hello' })
96
- expect(received).toHaveLength(2)
97
- expect(received[0]).toContain('test.event')
98
- expect(received[1]).toContain('hello')
99
- })
100
- })
101
-
102
- // ─── Test de sendTo ────────────────────────────────────
103
- describe('WSServer - sendTo', () => {
104
- it('envía a un cliente específico', () => {
105
- const ws = new WSServer() as any
106
- const received: string[] = []
107
-
108
- ws.clients.set('target', { id: 'target', send: (d: string) => received.push(d), close: () => {} })
109
- ws.clients.set('other', { id: 'other', send: (d: string) => received.push(d), close: () => {} })
110
-
111
- ws.sendTo('target', 'private.event', { secret: true })
112
- expect(received).toHaveLength(1)
113
- })
114
- })