arckode-framework 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.
- package/README.md +546 -0
- package/adapters/__tests__/mysql.test.ts +283 -0
- package/adapters/jwt.ts +18 -0
- package/adapters/mysql.ts +98 -0
- package/adapters/postgres.ts +52 -0
- package/adapters/redis-cache.ts +64 -0
- package/adapters/sqlite.ts +73 -0
- package/adapters/vendor.d.ts +48 -0
- package/bin/arckode.js +7 -0
- package/cli/analyze.ts +506 -0
- package/cli/commands/db-migrate.ts +121 -0
- package/cli/commands/db-seed.ts +54 -0
- package/cli/commands/generate-api-client.ts +106 -0
- package/cli/commands/make-adapter.ts +132 -0
- package/cli/commands/make-auth.ts +297 -0
- package/cli/commands/make-frontend-module.ts +271 -0
- package/cli/commands/make-helper.ts +65 -0
- package/cli/commands/make-migration.ts +30 -0
- package/cli/commands/make-seed.ts +29 -0
- package/cli/generate.ts +132 -0
- package/cli/index.ts +604 -0
- package/cli/stubs/frontend-stub.ts +294 -0
- package/cli/stubs/fullstack-stub.ts +46 -0
- package/cli/stubs/module-stub.ts +469 -0
- package/kernel/__tests__/adapters.test.ts +101 -0
- package/kernel/__tests__/analyzer.test.ts +282 -0
- package/kernel/__tests__/framework.test.ts +617 -0
- package/kernel/__tests__/middlewares.test.ts +174 -0
- package/kernel/__tests__/static.test.ts +94 -0
- package/kernel/framework.ts +1851 -0
- package/kernel/middlewares.ts +179 -0
- package/kernel/static.ts +76 -0
- package/kernel/testing.ts +237 -0
- package/modules/events/index.ts +99 -0
- package/modules/mail/index.ts +51 -0
- package/modules/mail/smtp-adapter.ts +42 -0
- package/modules/queue/index.ts +78 -0
- package/modules/storage/index.ts +40 -0
- package/modules/storage/local-adapter.ts +41 -0
- package/modules/ws/__tests__/ws.test.ts +114 -0
- package/modules/ws/index.ts +136 -0
- package/package.json +99 -0
- package/skills/auth/SKILL.md +243 -0
- package/skills/cli/SKILL.md +258 -0
- package/skills/config/SKILL.md +253 -0
- package/skills/connectors/SKILL.md +259 -0
- package/skills/helpers/SKILL.md +206 -0
- package/skills/middlewares/SKILL.md +282 -0
- package/skills/orm/SKILL.md +260 -0
- package/skills/realtime/SKILL.md +307 -0
- package/skills/services/SKILL.md +206 -0
- package/skills/testing/SKILL.md +257 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# SKILL: Arckode Services — Mail y Storage
|
|
2
|
+
|
|
3
|
+
> Activar cuando: enviar emails, subir archivos, gestionar attachments, servir uploads.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. MAIL SERVICE
|
|
8
|
+
|
|
9
|
+
### Setup en composition-root.ts
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { MailService } from 'arckode-framework/modules/mail'
|
|
13
|
+
import { SmtpAdapter } from 'arckode-framework/modules/mail/smtp-adapter'
|
|
14
|
+
|
|
15
|
+
const mail = new MailService(
|
|
16
|
+
new SmtpAdapter({
|
|
17
|
+
host: config.get('SMTP_HOST'),
|
|
18
|
+
port: config.get<number>('SMTP_PORT'),
|
|
19
|
+
user: config.get('SMTP_USER'),
|
|
20
|
+
pass: config.get('SMTP_PASS'),
|
|
21
|
+
secure: config.get<number>('SMTP_PORT') === 465,
|
|
22
|
+
}),
|
|
23
|
+
{ address: 'noreply@mi-app.com', name: 'Mi App' }, // from por defecto
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Config requerida en .env:**
|
|
28
|
+
```env
|
|
29
|
+
SMTP_HOST=smtp.gmail.com
|
|
30
|
+
SMTP_PORT=587
|
|
31
|
+
SMTP_USER=mi@gmail.com
|
|
32
|
+
SMTP_PASS=app-password-aqui
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### API del MailService
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// Email simple
|
|
41
|
+
await mail.send({
|
|
42
|
+
to: { address: 'user@example.com', name: 'Juan' },
|
|
43
|
+
subject: 'Bienvenido',
|
|
44
|
+
html: '<h1>Hola Juan!</h1>',
|
|
45
|
+
text: 'Hola Juan!', // fallback plain text (recomendado)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Múltiples destinatarios
|
|
49
|
+
await mail.send({
|
|
50
|
+
to: [{ address: 'a@x.com' }, { address: 'b@x.com' }],
|
|
51
|
+
cc: { address: 'cc@x.com' },
|
|
52
|
+
bcc: { address: 'bcc@x.com' },
|
|
53
|
+
subject: 'Notificación',
|
|
54
|
+
html: '<p>Mensaje masivo</p>',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Con adjunto
|
|
58
|
+
await mail.send({
|
|
59
|
+
to: { address: 'user@example.com' },
|
|
60
|
+
subject: 'Tu factura',
|
|
61
|
+
html: '<p>Adjunto tu factura</p>',
|
|
62
|
+
attachments: [{
|
|
63
|
+
filename: 'factura.pdf',
|
|
64
|
+
content: pdfBuffer, // Buffer
|
|
65
|
+
contentType: 'application/pdf',
|
|
66
|
+
}],
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Helpers pre-construidos
|
|
70
|
+
await mail.sendWelcome('user@example.com', 'Juan')
|
|
71
|
+
await mail.sendPasswordReset('user@example.com', resetToken)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### Inyectar MailService en un módulo
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// En index.ts del módulo
|
|
80
|
+
create({ logger, orm, cache, router, auth, mail }: ModuleDependencies) {
|
|
81
|
+
const service = new AuthService(repo, auth, mail, logger.child('auth'))
|
|
82
|
+
...
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// En actions/service.ts
|
|
86
|
+
class AuthService {
|
|
87
|
+
constructor(
|
|
88
|
+
private repo: RepositoryAdapter<UserDTO>,
|
|
89
|
+
private auth: Auth,
|
|
90
|
+
private mail: MailService,
|
|
91
|
+
private logger: Logger,
|
|
92
|
+
) {}
|
|
93
|
+
|
|
94
|
+
async registrar(dto: RegisterDTO) {
|
|
95
|
+
const user = await this.repo.create(...)
|
|
96
|
+
await this.mail.sendWelcome(user.email, user.nombre) // no bloquear si falla
|
|
97
|
+
return user
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Importante:** Envolver `mail.send()` en try/catch si el email no es crítico — el registro no debería fallar si el correo de bienvenida no llega.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Testear con mock
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const mockMail = { send: mock(() => Promise.resolve()), sendWelcome: mock(() => Promise.resolve()) }
|
|
110
|
+
const service = new AuthService(repo, auth, mockMail as any, logger)
|
|
111
|
+
|
|
112
|
+
test('registrar envía email de bienvenida', async () => {
|
|
113
|
+
await service.registrar({ email: 'a@b.com', password: '12345678', nombre: 'Juan' })
|
|
114
|
+
expect(mockMail.sendWelcome).toHaveBeenCalledWith('a@b.com', 'Juan')
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 2. STORAGE SERVICE
|
|
121
|
+
|
|
122
|
+
### Setup en composition-root.ts
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { StorageService } from 'arckode-framework/modules/storage'
|
|
126
|
+
import { LocalStorageAdapter } from 'arckode-framework/modules/storage/local-adapter'
|
|
127
|
+
|
|
128
|
+
const storage = new StorageService(
|
|
129
|
+
new LocalStorageAdapter(
|
|
130
|
+
'./uploads', // directorio en disco
|
|
131
|
+
'/uploads', // URL base pública
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Para servir los archivos subidos**, agregar el static server:
|
|
137
|
+
```ts
|
|
138
|
+
import { serveStatic } from 'arckode-framework/static'
|
|
139
|
+
serveStatic(router, './uploads', { prefix: '/uploads' })
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### API del StorageService
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// Subir archivo
|
|
148
|
+
const stored = await storage.upload(fileUpload, 'avatars')
|
|
149
|
+
// → { url: '/uploads/avatars/1234-abc.png', path: 'avatars/1234-abc.png', ... }
|
|
150
|
+
|
|
151
|
+
// Eliminar archivo
|
|
152
|
+
await storage.delete('avatars/1234-abc.png')
|
|
153
|
+
|
|
154
|
+
// Obtener URL pública
|
|
155
|
+
const url = storage.getUrl('avatars/1234-abc.png')
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Recibir archivo desde HTTP request
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// En el controller — el framework parsea multipart/form-data automáticamente
|
|
164
|
+
async uploadAvatar(req: HttpRequest): Promise<HttpResponse> {
|
|
165
|
+
const file = req.files?.['avatar'] // FileUpload
|
|
166
|
+
if (!file) throw new ValidationError('Archivo requerido', { avatar: 'requerido' })
|
|
167
|
+
if (!file.mimeType.startsWith('image/')) {
|
|
168
|
+
throw new ValidationError('Formato inválido', { avatar: 'debe ser una imagen' })
|
|
169
|
+
}
|
|
170
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
171
|
+
throw new ValidationError('Archivo muy grande', { avatar: 'máximo 5MB' })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const stored = await this.service.subirAvatar(req.user!.id, file)
|
|
175
|
+
return { status: 200, body: { url: stored.url } }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// En el service
|
|
179
|
+
async subirAvatar(userId: string, file: FileUpload): Promise<StoredFile> {
|
|
180
|
+
const stored = await this.storage.upload(file, `avatars/${userId}`)
|
|
181
|
+
await this.repo.update(userId, { avatarUrl: stored.url })
|
|
182
|
+
return stored
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### Inyectar StorageService en un módulo
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// ModuleDependencies incluye storage si fue pasado al System
|
|
192
|
+
create({ logger, orm, router, storage }: ModuleDependencies) {
|
|
193
|
+
const service = new UsuariosService(repo, storage, logger.child('usuarios'))
|
|
194
|
+
...
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Checklist Mail + Storage
|
|
201
|
+
|
|
202
|
+
- [ ] `SMTP_*` variables en .env (nunca hardcodeadas)
|
|
203
|
+
- [ ] `mail.send()` con try/catch si el email no es crítico para la operación
|
|
204
|
+
- [ ] Validar `mimeType` y `size` antes de `storage.upload()`
|
|
205
|
+
- [ ] `serveStatic()` configurado si se necesita acceso público a los archivos
|
|
206
|
+
- [ ] Mockear ambos servicios en tests (no enviar emails reales en tests)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# SKILL: Arckode Testing — Patrones, Utilidades y Estructura
|
|
2
|
+
|
|
3
|
+
> Activar cuando: escribir tests, configurar test suite, debuggear tests fallidos, agregar cobertura.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. SETUP DEL TEST (sin BD real)
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { describe, test, expect, beforeEach, mock } from 'bun:test'
|
|
11
|
+
import { createRecordingOrm, createSilentLogger, createNullCache } from 'arckode-framework/testing'
|
|
12
|
+
import { OrmRepository } from 'arckode-framework'
|
|
13
|
+
import { ProductosService } from '../actions/service'
|
|
14
|
+
import type { ProductoDTO } from '../types'
|
|
15
|
+
|
|
16
|
+
describe('ProductosService', () => {
|
|
17
|
+
let service: ProductosService
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
const orm = createRecordingOrm() // captura SQL sin ejecutar BD
|
|
21
|
+
const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')
|
|
22
|
+
service = new ProductosService(repo, createSilentLogger(), createNullCache())
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Regla de oro:** Testear el service, no el ORM. El ORM ya tiene sus propios tests.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. UTILIDADES DE TESTING
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import {
|
|
35
|
+
createRecordingOrm, // ORM que captura queries sin ejecutar nada
|
|
36
|
+
createSilentLogger, // Logger que descarta todo (sin ruido en tests)
|
|
37
|
+
createNullCache, // Cache que nunca cachea (siempre miss)
|
|
38
|
+
createTestClient, // Cliente HTTP para integration tests
|
|
39
|
+
} from 'arckode-framework/testing'
|
|
40
|
+
|
|
41
|
+
// RecordingOrm — verificar queries generadas sin BD
|
|
42
|
+
const orm = createRecordingOrm()
|
|
43
|
+
await service.listar()
|
|
44
|
+
const queries = orm.getRecordedQueries()
|
|
45
|
+
expect(queries[0]).toContain('SELECT')
|
|
46
|
+
|
|
47
|
+
// TestClient — integration tests con router real
|
|
48
|
+
const client = createTestClient(router)
|
|
49
|
+
const res = await client.get('/productos')
|
|
50
|
+
expect(res.status).toBe(200)
|
|
51
|
+
expect(res.body).toBeArray()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. ESTRUCTURA MÍNIMA OBLIGATORIA
|
|
57
|
+
|
|
58
|
+
Cada módulo necesita al menos 2 casos:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
describe('ProductosService', () => {
|
|
62
|
+
|
|
63
|
+
// CASO 1: Happy path
|
|
64
|
+
test('listar retorna array (vacío si no hay datos)', async () => {
|
|
65
|
+
const result = await service.listar()
|
|
66
|
+
expect(Array.isArray(result)).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// CASO 2: Error esperado (validación, not found, conflict, etc.)
|
|
70
|
+
test('crear lanza error si el nombre está vacío', async () => {
|
|
71
|
+
await expect(service.crear({ nombre: '', precio: 100 }))
|
|
72
|
+
.rejects.toThrow()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// CASO 3 (recomendado): IDOR / autorización
|
|
76
|
+
test('getById lanza NotFoundError si no existe', async () => {
|
|
77
|
+
await expect(service.getById('id-inexistente', { id: 'u1', role: 'user' }))
|
|
78
|
+
.rejects.toThrow('no encontrado')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**`arckode analyze` detecta:** `TESTS_WITHOUT_CASES` — archivo test sin ningún `test()`.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 4. MOCKEAR DEPENDENCIAS EXTERNAS
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { mock } from 'bun:test'
|
|
91
|
+
|
|
92
|
+
// Mockear el mail service en tests que lo usan
|
|
93
|
+
const mockMail = {
|
|
94
|
+
send: mock(() => Promise.resolve()),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const service = new AuthService(repo, auth, mockMail as any)
|
|
98
|
+
|
|
99
|
+
test('registro envía email de bienvenida', async () => {
|
|
100
|
+
await service.registrar({ email: 'test@test.com', password: '12345678' })
|
|
101
|
+
expect(mockMail.send).toHaveBeenCalledTimes(1)
|
|
102
|
+
expect(mockMail.send).toHaveBeenCalledWith(
|
|
103
|
+
expect.objectContaining({ to: 'test@test.com' })
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 5. INTEGRATION TESTS (router + controller)
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { Router } from 'arckode-framework'
|
|
114
|
+
import { createTestClient } from 'arckode-framework/testing'
|
|
115
|
+
|
|
116
|
+
describe('Productos API', () => {
|
|
117
|
+
let client: ReturnType<typeof createTestClient>
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
const router = new Router()
|
|
121
|
+
// montar el módulo de prueba
|
|
122
|
+
const orm = createRecordingOrm()
|
|
123
|
+
const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')
|
|
124
|
+
const service = new ProductosService(repo, createSilentLogger(), createNullCache())
|
|
125
|
+
const controller = new ProductosController(service)
|
|
126
|
+
|
|
127
|
+
router.get('/productos', req => controller.index(req))
|
|
128
|
+
router.post('/productos', req => controller.store(req))
|
|
129
|
+
router.delete('/productos/:id', req => controller.destroy(req))
|
|
130
|
+
|
|
131
|
+
client = createTestClient(router)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('GET /productos → 200', async () => {
|
|
135
|
+
const res = await client.get('/productos')
|
|
136
|
+
expect(res.status).toBe(200)
|
|
137
|
+
expect(Array.isArray(res.body)).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('POST /productos sin nombre → 400', async () => {
|
|
141
|
+
const res = await client.post('/productos', { body: { precio: 100 } })
|
|
142
|
+
expect(res.status).toBe(400)
|
|
143
|
+
expect(res.body).toHaveProperty('errors')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('POST /productos con datos válidos → 201', async () => {
|
|
147
|
+
const res = await client.post('/productos', {
|
|
148
|
+
body: { nombre: 'Zapato', precio: 999 }
|
|
149
|
+
})
|
|
150
|
+
expect(res.status).toBe(201)
|
|
151
|
+
expect(res.body).toHaveProperty('id')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 6. TESTEAR AUTH (con middleware)
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { Auth } from 'arckode-framework'
|
|
162
|
+
import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
|
|
163
|
+
|
|
164
|
+
const auth = new Auth(jwtTokenAdapter, 'test-secret-12345678901234567890', createSilentLogger())
|
|
165
|
+
const token = auth.createToken({ id: 'user-1', role: 'user' })
|
|
166
|
+
const adminToken = auth.createToken({ id: 'admin-1', role: 'admin' })
|
|
167
|
+
|
|
168
|
+
test('GET /pedidos sin token → 401', async () => {
|
|
169
|
+
const res = await client.get('/pedidos')
|
|
170
|
+
expect(res.status).toBe(401)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('GET /pedidos con token → 200', async () => {
|
|
174
|
+
const res = await client.get('/pedidos', {
|
|
175
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
176
|
+
})
|
|
177
|
+
expect(res.status).toBe(200)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('DELETE /admin/algo sin admin → 403', async () => {
|
|
181
|
+
const res = await client.delete('/admin/algo', {
|
|
182
|
+
headers: { Authorization: `Bearer ${token}` } // user, no admin
|
|
183
|
+
})
|
|
184
|
+
expect(res.status).toBe(403)
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 7. CORRER TESTS
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Todos los tests
|
|
194
|
+
bun test
|
|
195
|
+
|
|
196
|
+
# Tests de un módulo específico
|
|
197
|
+
bun test modules/productos
|
|
198
|
+
|
|
199
|
+
# Un archivo específico
|
|
200
|
+
bun test modules/productos/tests/service.test.ts
|
|
201
|
+
|
|
202
|
+
# Con watch (re-corre al cambiar archivos)
|
|
203
|
+
bun test --watch
|
|
204
|
+
|
|
205
|
+
# Con cobertura
|
|
206
|
+
bun test --coverage
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 8. QUÉ NO TESTEAR
|
|
212
|
+
|
|
213
|
+
- **Los adapters del framework** (SqliteAdapter, jwtTokenAdapter): ya tienen tests propios
|
|
214
|
+
- **El ORM** directamente: testealo via RepositoryAdapter en tu service
|
|
215
|
+
- **El router**: testear el controller con `createTestClient`, no el router directamente
|
|
216
|
+
- **Casos imposibles**: no testear `if` que nunca puede ser `false` con TypeScript strict
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 9. ERRORES COMUNES EN TESTS
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
// ❌ Olvidar el await — el test pasa siempre (false positive)
|
|
224
|
+
test('lanza error', () => {
|
|
225
|
+
expect(service.crear({})).rejects.toThrow() // sin await → siempre pasa
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// ✅ Siempre await en expects async
|
|
229
|
+
test('lanza error', async () => {
|
|
230
|
+
await expect(service.crear({})).rejects.toThrow()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// ❌ Describir implementación en lugar de comportamiento
|
|
234
|
+
test('llama a repo.create con los datos correctos', ...)
|
|
235
|
+
|
|
236
|
+
// ✅ Describir el comportamiento observable
|
|
237
|
+
test('crear retorna el producto con id asignado', ...)
|
|
238
|
+
|
|
239
|
+
// ❌ Test que testea el ORM en lugar del service
|
|
240
|
+
test('ORM recibe query INSERT', async () => {
|
|
241
|
+
const orm = createRecordingOrm()
|
|
242
|
+
await service.crear(...)
|
|
243
|
+
expect(orm.getRecordedQueries()[0]).toContain('INSERT') // testear el framework, no tu código
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 10. CHECKLIST TESTING
|
|
250
|
+
|
|
251
|
+
- [ ] Mínimo 2 test cases: happy path + error esperado
|
|
252
|
+
- [ ] Usar `createRecordingOrm` (sin BD real en unit tests)
|
|
253
|
+
- [ ] Usar `createSilentLogger` (sin ruido en consola)
|
|
254
|
+
- [ ] Usar `createNullCache` (cache predecible)
|
|
255
|
+
- [ ] Todo `expect(...).rejects` tiene `await`
|
|
256
|
+
- [ ] Los nombres de test describen comportamiento, no implementación
|
|
257
|
+
- [ ] `bun test` pasa sin errores antes del commit
|