arckode-framework 1.3.2 → 1.4.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/adapters/jwt.ts +6 -4
- package/adapters/mysql.ts +7 -2
- package/adapters/postgres.ts +37 -0
- package/adapters/sqlite.ts +7 -1
- package/adapters/vendor.d.ts +48 -0
- package/cli/analyze/checks.ts +333 -0
- package/cli/analyze/index.ts +44 -0
- package/cli/analyze/report.ts +107 -0
- package/cli/analyze/types.ts +46 -0
- package/cli/analyze/utils.ts +36 -0
- package/cli/analyze.ts +2 -647
- package/cli/commands/db-migrate.ts +213 -89
- package/cli/commands/db-seed.ts +97 -32
- package/cli/commands/db-utils.ts +192 -0
- package/cli/commands/new.ts +175 -0
- package/cli/commands/routes.ts +94 -0
- package/cli/index.ts +57 -404
- package/cli/stubs/module/core.ts +162 -0
- package/cli/stubs/module/data.ts +171 -0
- package/cli/stubs/module/index.ts +5 -0
- package/cli/stubs/module/service.ts +198 -0
- package/cli/stubs/module/types.ts +12 -0
- package/cli/stubs/module-stub.ts +2 -552
- package/kernel/auth.ts +114 -0
- package/kernel/cache.ts +37 -0
- package/kernel/config.ts +129 -0
- package/kernel/container.ts +64 -0
- package/kernel/db/orm-migrate.ts +136 -0
- package/kernel/db/orm-repository.ts +45 -0
- package/kernel/db/orm-utils.ts +93 -0
- package/kernel/db/orm.ts +254 -0
- package/kernel/db/transactor.ts +17 -0
- package/kernel/db/types.ts +72 -0
- package/kernel/errors.ts +102 -0
- package/kernel/framework.default.ts +41 -0
- package/kernel/framework.ts +8 -2144
- package/kernel/http/router.ts +131 -0
- package/kernel/http/server.ts +303 -0
- package/kernel/http/types.ts +56 -0
- package/kernel/index.ts +25 -0
- package/kernel/logger.ts +50 -0
- package/kernel/middlewares.ts +19 -7
- package/kernel/modules/create-module.ts +5 -0
- package/kernel/modules/system.ts +149 -0
- package/kernel/modules/types.ts +46 -0
- package/kernel/seeds.ts +48 -0
- package/kernel/static.ts +11 -2
- package/kernel/testing.ts +8 -3
- package/kernel/validator.ts +116 -0
- package/modules/events/index.ts +19 -3
- package/modules/mail/index.ts +14 -2
- package/modules/storage/local-adapter.ts +19 -5
- package/modules/ws/index.ts +123 -18
- package/package.json +8 -11
- package/skills/auth/SKILL.md +36 -220
- package/skills/cli/SKILL.md +32 -251
- package/skills/config/SKILL.md +30 -239
- package/skills/connectors/SKILL.md +32 -295
- package/skills/helpers/SKILL.md +26 -195
- package/skills/middlewares/SKILL.md +30 -280
- package/skills/orm/SKILL.md +42 -349
- package/skills/realtime/SKILL.md +22 -297
- package/skills/services/SKILL.md +40 -183
- package/skills/testing/SKILL.md +34 -266
package/skills/testing/SKILL.md
CHANGED
|
@@ -1,280 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Testing — Bun Test + Patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Stack
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`bun test` con `describe`/`it`/`expect` global.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Ejecución
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import { ProductosService } from '../service'
|
|
16
|
-
import type { ProductoDTO } from '../types'
|
|
17
|
-
|
|
18
|
-
// silentLogger es factory function — SIEMPRE llamar con ()
|
|
19
|
-
const log = silentLogger()
|
|
20
|
-
const silentCache: CacheAdapter = { get: async () => null, set: async () => {}, delete: async () => {}, flush: async () => {} }
|
|
21
|
-
|
|
22
|
-
function makeRepo(overrides: Partial<RepositoryAdapter<ProductoDTO>> = {}): RepositoryAdapter<ProductoDTO> {
|
|
23
|
-
return {
|
|
24
|
-
findMany: async () => [],
|
|
25
|
-
findById: async () => null,
|
|
26
|
-
findOne: async () => null,
|
|
27
|
-
create: async (data) => ({ id: 'test-id', ...data } as ProductoDTO),
|
|
28
|
-
update: async (id, data) => ({ id, ...data } as ProductoDTO),
|
|
29
|
-
delete: async () => true,
|
|
30
|
-
count: async () => 0,
|
|
31
|
-
paginate: async () => ({ data: [], total: 0, limit: 20, offset: 0, pages: 0 }),
|
|
32
|
-
...overrides,
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('ProductosService', () => {
|
|
37
|
-
let service: ProductosService
|
|
38
|
-
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
service = new ProductosService(makeRepo(), log, silentCache)
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**Regla de oro:** Testear el service, no el ORM. El ORM ya tiene sus propios tests.
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## 2. UTILIDADES DE TESTING
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
// Exports REALES del módulo arckode-framework/testing:
|
|
53
|
-
import {
|
|
54
|
-
silentLogger, // factory: silentLogger() → Logger (NO usarla sin ())
|
|
55
|
-
createRecordingDb, // captura SQL sin ejecutar BD real → db.calls, db.lastSql()
|
|
56
|
-
createTestClient, // cliente HTTP sin levantar servidor
|
|
57
|
-
createIntegrationClient, // cliente HTTP con servidor real (puerto aleatorio)
|
|
58
|
-
mockAuth, // middleware mock para simular autenticación
|
|
59
|
-
} from 'arckode-framework/testing'
|
|
60
|
-
|
|
61
|
-
// ❌ ESTOS NO EXISTEN — nunca usar:
|
|
62
|
-
// createRecordingOrm, createSilentLogger, createNullCache
|
|
63
|
-
|
|
64
|
-
// silentLogger — factory, siempre con ()
|
|
65
|
-
const log = silentLogger()
|
|
66
|
-
|
|
67
|
-
// createRecordingDb — captura SQL sin ejecutar nada
|
|
68
|
-
const db = createRecordingDb({ queryData: [{ id: '1', nombre: 'Laptop' }] })
|
|
69
|
-
// db.calls → array de { sql, params, type }
|
|
70
|
-
// db.lastSql() → último SQL ejecutado
|
|
71
|
-
// db.reset() → limpia el historial
|
|
72
|
-
|
|
73
|
-
// TestClient — router real, sin HTTP
|
|
74
|
-
const client = createTestClient(router)
|
|
75
|
-
const res = await client.get('/productos')
|
|
76
|
-
expect(res.status).toBe(200)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## 3. ESTRUCTURA MÍNIMA OBLIGATORIA
|
|
82
|
-
|
|
83
|
-
Cada módulo necesita al menos 2 casos:
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
describe('ProductosService', () => {
|
|
87
|
-
|
|
88
|
-
// CASO 1: Happy path
|
|
89
|
-
test('listar retorna array (vacío si no hay datos)', async () => {
|
|
90
|
-
const result = await service.listar()
|
|
91
|
-
expect(Array.isArray(result)).toBe(true)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
// CASO 2: Error esperado (validación, not found, conflict, etc.)
|
|
95
|
-
test('crear lanza error si el nombre está vacío', async () => {
|
|
96
|
-
await expect(service.crear({ nombre: '', precio: 100 }))
|
|
97
|
-
.rejects.toThrow()
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
// CASO 3 (recomendado): IDOR / autorización
|
|
101
|
-
test('getById lanza NotFoundError si no existe', async () => {
|
|
102
|
-
await expect(service.getById('id-inexistente', { id: 'u1', role: 'user' }))
|
|
103
|
-
.rejects.toThrow('no encontrado')
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
**`arckode analyze` detecta:** `TESTS_WITHOUT_CASES` — archivo test sin ningún `test()`.
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
## 4. MOCKEAR DEPENDENCIAS EXTERNAS
|
|
113
|
-
|
|
114
|
-
```ts
|
|
115
|
-
import { mock } from 'bun:test'
|
|
116
|
-
|
|
117
|
-
// Mockear el mail service en tests que lo usan
|
|
118
|
-
const mockMail = {
|
|
119
|
-
send: mock(() => Promise.resolve()),
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const service = new AuthService(repo, auth, mockMail as any)
|
|
123
|
-
|
|
124
|
-
test('registro envía email de bienvenida', async () => {
|
|
125
|
-
await service.registrar({ email: 'test@test.com', password: '12345678' })
|
|
126
|
-
expect(mockMail.send).toHaveBeenCalledTimes(1)
|
|
127
|
-
expect(mockMail.send).toHaveBeenCalledWith(
|
|
128
|
-
expect.objectContaining({ to: 'test@test.com' })
|
|
129
|
-
)
|
|
130
|
-
})
|
|
131
|
-
```
|
|
9
|
+
| Comando | Scope |
|
|
10
|
+
|---------|-------|
|
|
11
|
+
| `bun test` | All |
|
|
12
|
+
| `bun test tests/modules/users.test.ts` | Archivo |
|
|
13
|
+
| `bun test --filter="users"` | Match |
|
|
14
|
+
| `bun run typecheck` | tsc --noEmit |
|
|
132
15
|
|
|
133
|
-
|
|
16
|
+
## Reglas
|
|
134
17
|
|
|
135
|
-
|
|
18
|
+
1. Test por módulo: `tests/modules/{modulo}.test.ts`
|
|
19
|
+
2. Service test: mock RepositoryAdapter, NUNCA ORM real
|
|
20
|
+
3. Controller test: mock Service, solo valida HTTP contract
|
|
21
|
+
4. Integration test: DB real con `beforeAll` setup + `afterAll` cleanup
|
|
22
|
+
5. NO testear helpers ni config (salvo lógica crítica)
|
|
23
|
+
6. Nombre: `UserService.create → crea usuario con datos válidos`
|
|
136
24
|
|
|
137
|
-
|
|
138
|
-
import { Router } from 'arckode-framework'
|
|
139
|
-
import { createTestClient } from 'arckode-framework/testing'
|
|
140
|
-
|
|
141
|
-
describe('Productos API', () => {
|
|
142
|
-
let client: ReturnType<typeof createTestClient>
|
|
143
|
-
|
|
144
|
-
beforeEach(() => {
|
|
145
|
-
const router = new Router()
|
|
146
|
-
// montar el módulo de prueba
|
|
147
|
-
const service = new ProductosService(makeRepo(), log, silentCache)
|
|
148
|
-
const controller = new ProductosController(service)
|
|
149
|
-
|
|
150
|
-
router.get('/productos', req => controller.index(req))
|
|
151
|
-
router.post('/productos', req => controller.store(req))
|
|
152
|
-
router.delete('/productos/:id', req => controller.destroy(req))
|
|
153
|
-
|
|
154
|
-
client = createTestClient(router)
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('GET /productos → 200', async () => {
|
|
158
|
-
const res = await client.get('/productos')
|
|
159
|
-
expect(res.status).toBe(200)
|
|
160
|
-
expect(Array.isArray(res.body)).toBe(true)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
test('POST /productos sin nombre → 400', async () => {
|
|
164
|
-
const res = await client.post('/productos', { body: { precio: 100 } })
|
|
165
|
-
expect(res.status).toBe(400)
|
|
166
|
-
expect(res.body).toHaveProperty('errors')
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test('POST /productos con datos válidos → 201', async () => {
|
|
170
|
-
const res = await client.post('/productos', {
|
|
171
|
-
body: { nombre: 'Zapato', precio: 999 }
|
|
172
|
-
})
|
|
173
|
-
expect(res.status).toBe(201)
|
|
174
|
-
expect(res.body).toHaveProperty('id')
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
## 6. TESTEAR AUTH (con middleware)
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
import { Auth } from 'arckode-framework'
|
|
185
|
-
import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
|
|
186
|
-
|
|
187
|
-
const auth = new Auth(jwtTokenAdapter, 'test-secret-12345678901234567890', silentLogger())
|
|
188
|
-
const token = auth.createToken({ id: 'user-1', role: 'user' })
|
|
189
|
-
const adminToken = auth.createToken({ id: 'admin-1', role: 'admin' })
|
|
190
|
-
|
|
191
|
-
test('GET /pedidos sin token → 401', async () => {
|
|
192
|
-
const res = await client.get('/pedidos')
|
|
193
|
-
expect(res.status).toBe(401)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
test('GET /pedidos con token → 200', async () => {
|
|
197
|
-
const res = await client.get('/pedidos', {
|
|
198
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
199
|
-
})
|
|
200
|
-
expect(res.status).toBe(200)
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
test('DELETE /admin/algo sin admin → 403', async () => {
|
|
204
|
-
const res = await client.delete('/admin/algo', {
|
|
205
|
-
headers: { Authorization: `Bearer ${token}` } // user, no admin
|
|
206
|
-
})
|
|
207
|
-
expect(res.status).toBe(403)
|
|
208
|
-
})
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## 7. CORRER TESTS
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
# Todos los tests
|
|
217
|
-
bun test
|
|
218
|
-
|
|
219
|
-
# Tests de un módulo específico
|
|
220
|
-
bun test modules/productos
|
|
221
|
-
|
|
222
|
-
# Un archivo específico
|
|
223
|
-
bun test modules/productos/tests/service.test.ts
|
|
224
|
-
|
|
225
|
-
# Con watch (re-corre al cambiar archivos)
|
|
226
|
-
bun test --watch
|
|
227
|
-
|
|
228
|
-
# Con cobertura
|
|
229
|
-
bun test --coverage
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
---
|
|
233
|
-
|
|
234
|
-
## 8. QUÉ NO TESTEAR
|
|
235
|
-
|
|
236
|
-
- **Los adapters del framework** (SqliteAdapter, jwtTokenAdapter): ya tienen tests propios
|
|
237
|
-
- **El ORM** directamente: testealo via RepositoryAdapter en tu service
|
|
238
|
-
- **El router**: testear el controller con `createTestClient`, no el router directamente
|
|
239
|
-
- **Casos imposibles**: no testear `if` que nunca puede ser `false` con TypeScript strict
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## 9. ERRORES COMUNES EN TESTS
|
|
25
|
+
## Patrón
|
|
244
26
|
|
|
245
27
|
```ts
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// ✅ Siempre await en expects async
|
|
252
|
-
test('lanza error', async () => {
|
|
253
|
-
await expect(service.crear({})).rejects.toThrow()
|
|
254
|
-
})
|
|
28
|
+
// Arrange
|
|
29
|
+
const mockRepo = new MockRepositoryAdapter<UserDTO>() // implementa RepositoryAdapter<T>
|
|
30
|
+
mockRepo.setData(mockUsers)
|
|
31
|
+
const service = new UsersService(mockRepo)
|
|
255
32
|
|
|
256
|
-
//
|
|
257
|
-
|
|
33
|
+
// Act
|
|
34
|
+
const result = await service.findById('1')
|
|
258
35
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// ❌ Test que testea el ORM en lugar del service
|
|
263
|
-
test('ORM recibe query INSERT', async () => {
|
|
264
|
-
const orm = createRecordingOrm()
|
|
265
|
-
await service.crear(...)
|
|
266
|
-
expect(orm.getRecordedQueries()[0]).toContain('INSERT') // testear el framework, no tu código
|
|
267
|
-
})
|
|
36
|
+
// Assert
|
|
37
|
+
expect(result).toEqual(mockUsers[0])
|
|
268
38
|
```
|
|
269
39
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
## 10. CHECKLIST TESTING
|
|
40
|
+
## Errores silenciosos
|
|
273
41
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
42
|
+
| Error | Señal | Fix |
|
|
43
|
+
|-------|-------|-----|
|
|
44
|
+
| Service test con ORM real | DB connection en test | Mock RepositoryAdapter |
|
|
45
|
+
| Test sin cleanup | Datos entre tests | `afterEach` reset |
|
|
46
|
+
| Mock sin tipo | `as any` en todo | Usar `MockRepositoryAdapter<T>` |
|
|
47
|
+
| Cobertura baja | <70% branch | `bun run analyze` |
|
|
48
|
+
| `bun test` no encuentra tests | Archivos sin `.test.ts` | Renombrar |
|