arckode-framework 1.0.0 → 1.0.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/adapters/redis-cache.ts +8 -2
- package/cli/stubs/module-stub.ts +21 -15
- package/package.json +15 -4
- package/skills/connectors/SKILL.md +5 -4
- package/skills/testing/SKILL.md +46 -23
- package/adapters/__tests__/mysql.test.ts +0 -283
- package/adapters/vendor.d.ts +0 -48
- package/kernel/__tests__/adapters.test.ts +0 -101
- package/kernel/__tests__/analyzer.test.ts +0 -282
- package/kernel/__tests__/framework.test.ts +0 -617
- package/kernel/__tests__/middlewares.test.ts +0 -174
- package/kernel/__tests__/static.test.ts +0 -94
- package/modules/ws/__tests__/ws.test.ts +0 -114
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// kernel/__tests__/adapters.test.ts — Tests de adapters reales
|
|
2
|
-
// Bun:test. Requiere: better-sqlite3 (SQLite), pg (Postgres), redis (Redis)
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
|
|
5
|
-
|
|
6
|
-
// ─── SQLite Adapter ────────────────────────────────────
|
|
7
|
-
describe('SQLiteAdapter', () => {
|
|
8
|
-
let adapter: any
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
const { SqliteAdapter } = await import('../../adapters/sqlite')
|
|
12
|
-
adapter = new SqliteAdapter({ path: ':memory:' })
|
|
13
|
-
await adapter.connect()
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
afterAll(async () => {
|
|
17
|
-
await adapter.close()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('crea tabla e inserta datos', async () => {
|
|
21
|
-
await adapter.run('CREATE TABLE IF NOT EXISTS test (id TEXT PRIMARY KEY, nombre TEXT)')
|
|
22
|
-
await adapter.run('INSERT INTO test (id, nombre) VALUES (?, ?)', ['1', 'Test'])
|
|
23
|
-
const rows = await adapter.query('SELECT * FROM test WHERE id = ?', ['1'])
|
|
24
|
-
expect(rows).toHaveLength(1)
|
|
25
|
-
expect((rows[0] as any).nombre).toBe('Test')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('query con múltiples resultados', async () => {
|
|
29
|
-
await adapter.run('INSERT INTO test (id, nombre) VALUES (?, ?)', ['2', 'Otro'])
|
|
30
|
-
const rows = await adapter.query('SELECT * FROM test ORDER BY id')
|
|
31
|
-
expect(rows).toHaveLength(2)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('run devuelve changes', async () => {
|
|
35
|
-
const result = await adapter.run('DELETE FROM test WHERE id = ?', ['1'])
|
|
36
|
-
expect(result.changes).toBeGreaterThan(0)
|
|
37
|
-
expect(result.lastId).toBeDefined()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('query sin resultados devuelve array vacío', async () => {
|
|
41
|
-
const rows = await adapter.query('SELECT * FROM test WHERE id = ?', ['999'])
|
|
42
|
-
expect(rows).toEqual([])
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
// ─── JWT Adapter ───────────────────────────────────────
|
|
47
|
-
describe('JwtAdapter', () => {
|
|
48
|
-
it('sign y verify funcionan', async () => {
|
|
49
|
-
const { jwtTokenAdapter } = await import('../../adapters/jwt')
|
|
50
|
-
const token = jwtTokenAdapter.sign({ id: '1', role: 'admin' }, 'secret', '1h')
|
|
51
|
-
expect(token).toBeTruthy()
|
|
52
|
-
expect(typeof token).toBe('string')
|
|
53
|
-
|
|
54
|
-
const payload = jwtTokenAdapter.verify(token, 'secret')
|
|
55
|
-
expect(payload.id).toBe('1')
|
|
56
|
-
expect(payload.role).toBe('admin')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('verify rechaza token con secret incorrecto', async () => {
|
|
60
|
-
const { jwtTokenAdapter } = await import('../../adapters/jwt')
|
|
61
|
-
const token = jwtTokenAdapter.sign({ id: '1' }, 'correct', '1h')
|
|
62
|
-
expect(() => jwtTokenAdapter.verify(token, 'wrong')).toThrow()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('verify rechaza token expirado', async () => {
|
|
66
|
-
const { jwtTokenAdapter } = await import('../../adapters/jwt')
|
|
67
|
-
const token = jwtTokenAdapter.sign({ id: '1' }, 'secret', '0s')
|
|
68
|
-
expect(() => jwtTokenAdapter.verify(token, 'secret')).toThrow()
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
// ═════════════════════════════════════════════════════════
|
|
73
|
-
// Nota: PostgresAdapter y RedisCacheAdapter requieren
|
|
74
|
-
// servidores externos. Se testean con mock o con docker.
|
|
75
|
-
//
|
|
76
|
-
// Test Postgres (requiere pg instalado y servidor postgres):
|
|
77
|
-
// describe('PostgresAdapter', () => {
|
|
78
|
-
// it('conecta y ejecuta query', async () => {
|
|
79
|
-
// const { PostgresAdapter } = await import('../../adapters/postgres')
|
|
80
|
-
// const adapter = new PostgresAdapter({
|
|
81
|
-
// connectionString: 'postgres://test:test@localhost:5432/test'
|
|
82
|
-
// })
|
|
83
|
-
// await adapter.connect()
|
|
84
|
-
// const rows = await adapter.query('SELECT 1 as num')
|
|
85
|
-
// expect((rows[0] as any).num).toBe(1)
|
|
86
|
-
// await adapter.close()
|
|
87
|
-
// })
|
|
88
|
-
// })
|
|
89
|
-
//
|
|
90
|
-
// Test Redis (requiere redis instalado y servidor redis):
|
|
91
|
-
// describe('RedisCacheAdapter', () => {
|
|
92
|
-
// it('set y get', async () => {
|
|
93
|
-
// const { RedisCacheAdapter } = await import('../../adapters/redis-cache')
|
|
94
|
-
// const cache = new RedisCacheAdapter({ url: 'redis://localhost:6379' })
|
|
95
|
-
// await cache.connect()
|
|
96
|
-
// await cache.set('test', { ok: true })
|
|
97
|
-
// const val = await cache.get('test')
|
|
98
|
-
// expect(val).toEqual({ ok: true })
|
|
99
|
-
// await cache.delete('test')
|
|
100
|
-
// })
|
|
101
|
-
// })
|
|
@@ -1,282 +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 { analyzeProject } from '../../cli/analyze'
|
|
5
|
-
|
|
6
|
-
const TMP = join(import.meta.dirname, '..', '__test_fixtures__')
|
|
7
|
-
|
|
8
|
-
async function createProject(name: string): Promise<string> {
|
|
9
|
-
const dir = join(TMP, name)
|
|
10
|
-
await mkdir(dir, { recursive: true })
|
|
11
|
-
return dir
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function createFile(base: string, path: string, content: string): Promise<void> {
|
|
15
|
-
const fullPath = join(base, path)
|
|
16
|
-
await mkdir(fullPath.replace(/\/[^/]+$/, ''), { recursive: true })
|
|
17
|
-
await writeFile(fullPath, content)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function cleanup(name: string): Promise<void> {
|
|
21
|
-
await rm(join(TMP, name), { recursive: true, force: true })
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const CR = `import { ConfigStore, Logger, ORM, Router, System } from 'arckode-framework'
|
|
25
|
-
const c = new ConfigStore(); c.define({X:{type:'string',default:'ok'}}).load({})
|
|
26
|
-
const l = new Logger('test','error'); new System({config:c,container:{} as any,logger:l,orm:{} as any,router:new Router(),http:{start:async()=>{},stop:async()=>{}},cache:{} as any})
|
|
27
|
-
`
|
|
28
|
-
|
|
29
|
-
const MOD_INDEX = `import { createModule } from 'arckode-framework'
|
|
30
|
-
export function M() {
|
|
31
|
-
return createModule({name:'m',version:'1',description:'ok',
|
|
32
|
-
contract:{name:'m',version:'1',description:'ok',actions:[],events:[],tables:[],dependencies:[],rules:[]},
|
|
33
|
-
create:({logger})=>({ok:true})})
|
|
34
|
-
}`
|
|
35
|
-
|
|
36
|
-
const TYPES = `export interface DTO { id: string; name: string }`
|
|
37
|
-
|
|
38
|
-
const SERVICE = `export class S {
|
|
39
|
-
constructor(private repo: any) {}
|
|
40
|
-
async list() { return this.repo.findMany() }
|
|
41
|
-
}`
|
|
42
|
-
|
|
43
|
-
const CTRL = `import type { HttpRequest } from 'arckode-framework'
|
|
44
|
-
export class C {
|
|
45
|
-
constructor(private s: any) {}
|
|
46
|
-
async idx(req: HttpRequest) { return { status: 200, body: await this.s.list() } }
|
|
47
|
-
}`
|
|
48
|
-
|
|
49
|
-
const SCHEMA = `import type { ValidationSchema } from 'arckode-framework'
|
|
50
|
-
export const S: ValidationSchema = { name: { type: 'string', required: true } }`
|
|
51
|
-
|
|
52
|
-
const TEST_OK = `import { it } from 'bun:test'; it('works', () => {})`
|
|
53
|
-
|
|
54
|
-
async function fullModule(base: string, name: string): Promise<void> {
|
|
55
|
-
await createFile(base, `src/modules/${name}/index.ts`, MOD_INDEX)
|
|
56
|
-
await createFile(base, `src/modules/${name}/types.ts`, TYPES)
|
|
57
|
-
await createFile(base, `src/modules/${name}/actions/service.ts`, SERVICE)
|
|
58
|
-
await createFile(base, `src/modules/${name}/actions/controller.ts`, CTRL)
|
|
59
|
-
await createFile(base, `src/modules/${name}/validators/schema.ts`, SCHEMA)
|
|
60
|
-
await createFile(base, `src/modules/${name}/tests/service.test.ts`, TEST_OK)
|
|
61
|
-
await createFile(base, `src/modules/${name}/sockets.ts`, `export const sockets = {}`)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe('analyzeProject', () => {
|
|
65
|
-
describe('estructura básica', () => {
|
|
66
|
-
it('detecta falta de composition-root', async () => {
|
|
67
|
-
const dir = await createProject('no-cr')
|
|
68
|
-
await mkdir(join(dir, 'src', 'modules'), { recursive: true })
|
|
69
|
-
const result = await analyzeProject(dir)
|
|
70
|
-
expect(result.valid).toBe(false)
|
|
71
|
-
expect(result.warnings.some(w => w.type === 'NO_COMPOSITION_ROOT')).toBe(true)
|
|
72
|
-
await cleanup('no-cr')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('proyecto válido sin violaciones', async () => {
|
|
76
|
-
const dir = await createProject('valid')
|
|
77
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
78
|
-
await fullModule(dir, 'test')
|
|
79
|
-
const result = await analyzeProject(dir)
|
|
80
|
-
if (!result.valid) {
|
|
81
|
-
console.log('DEBUG violations:', JSON.stringify(result.violations.map(v => ({type:v.type, msg:v.message}))))
|
|
82
|
-
console.log('DEBUG warnings:', JSON.stringify(result.warnings))
|
|
83
|
-
}
|
|
84
|
-
expect(result.valid).toBe(true)
|
|
85
|
-
expect(result.violations).toHaveLength(0)
|
|
86
|
-
await cleanup('valid')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('detecta módulo sin index.ts', async () => {
|
|
90
|
-
const dir = await createProject('no-index')
|
|
91
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
92
|
-
await mkdir(join(dir, 'src/modules/noindex'), { recursive: true })
|
|
93
|
-
const result = await analyzeProject(dir)
|
|
94
|
-
expect(result.violations.some(v => v.type === 'MISSING_INDEX')).toBe(true)
|
|
95
|
-
await cleanup('no-index')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('detecta módulo sin types.ts', async () => {
|
|
99
|
-
const dir = await createProject('no-types')
|
|
100
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
101
|
-
await createFile(dir, 'src/modules/x/index.ts', MOD_INDEX)
|
|
102
|
-
const result = await analyzeProject(dir)
|
|
103
|
-
expect(result.violations.some(v => v.type === 'MISSING_TYPES')).toBe(true)
|
|
104
|
-
await cleanup('no-types')
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('detecta módulo sin tests', async () => {
|
|
108
|
-
const dir = await createProject('no-tests')
|
|
109
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
110
|
-
await createFile(dir, 'src/modules/x/index.ts', MOD_INDEX)
|
|
111
|
-
const result = await analyzeProject(dir)
|
|
112
|
-
expect(result.violations.some(v => v.type === 'MISSING_TESTS')).toBe(true)
|
|
113
|
-
await cleanup('no-tests')
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
describe('acoplamiento', () => {
|
|
118
|
-
it('detecta imports directos entre módulos', async () => {
|
|
119
|
-
const dir = await createProject('direct-import')
|
|
120
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
121
|
-
// Module "a" with service that imports from modules/b
|
|
122
|
-
await fullModule(dir, 'a')
|
|
123
|
-
await createFile(dir, 'src/modules/a/actions/service.ts',
|
|
124
|
-
`import { algo } from '../../../modules/b/types'\nexport class S { async list() { return [] } }`)
|
|
125
|
-
await fullModule(dir, 'b')
|
|
126
|
-
const result = await analyzeProject(dir)
|
|
127
|
-
expect(result.violations.some(v => v.type === 'DIRECT_MODULE_IMPORT')).toBe(true)
|
|
128
|
-
await cleanup('direct-import')
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
describe('conectores', () => {
|
|
133
|
-
it('detecta falta de conectores entre múltiples módulos', async () => {
|
|
134
|
-
const dir = await createProject('no-conn')
|
|
135
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
136
|
-
await fullModule(dir, 'a')
|
|
137
|
-
await fullModule(dir, 'b')
|
|
138
|
-
// No connectors directory
|
|
139
|
-
const result = await analyzeProject(dir)
|
|
140
|
-
expect(result.violations.some(v => v.type === 'MISSING_CONNECTORS')).toBe(true)
|
|
141
|
-
await cleanup('no-conn')
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('detecta lógica de negocio en conectores', async () => {
|
|
145
|
-
const dir = await createProject('conn-logic')
|
|
146
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
147
|
-
await fullModule(dir, 'a')
|
|
148
|
-
await fullModule(dir, 'b')
|
|
149
|
-
await mkdir(join(dir, 'src/connectors'), { recursive: true })
|
|
150
|
-
await createFile(dir, 'src/connectors/bad.ts', `export function c(ctx: any) {
|
|
151
|
-
const a = ctx.resolveModule('a'); const b = ctx.resolveModule('b')
|
|
152
|
-
if (a.x > 1) { for (const i of b.y) { if (i.active) { a.update(i.id, i) } } }
|
|
153
|
-
try { a.create(b.get()) } catch(e) { console.error(e) }
|
|
154
|
-
}`)
|
|
155
|
-
const result = await analyzeProject(dir)
|
|
156
|
-
expect(result.violations.some(v => v.type === 'CONNECTOR_BUSINESS_LOGIC')).toBe(true)
|
|
157
|
-
await cleanup('conn-logic')
|
|
158
|
-
})
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
describe('reglas de diseño', () => {
|
|
162
|
-
it('detecta controller sin validateSchema en rutas mutantes', async () => {
|
|
163
|
-
const dir = await createProject('no-validation')
|
|
164
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
165
|
-
await createFile(dir, 'src/modules/x/index.ts', MOD_INDEX)
|
|
166
|
-
await createFile(dir, 'src/modules/x/types.ts', TYPES)
|
|
167
|
-
await createFile(dir, 'src/modules/x/actions/service.ts', SERVICE)
|
|
168
|
-
// Controller with router.post but no validateSchema
|
|
169
|
-
// Controller que define ruta POST pero no valida — el analyzer
|
|
170
|
-
// detecta router.post() en el archivo del controller
|
|
171
|
-
await createFile(dir, 'src/modules/x/actions/controller.ts',
|
|
172
|
-
`import type { HttpRequest } from 'arckode-framework'
|
|
173
|
-
// router.post('/x', handler) — sin validateSchema
|
|
174
|
-
export class C {
|
|
175
|
-
async store(req: HttpRequest) {
|
|
176
|
-
return { status: 201, body: req.body }
|
|
177
|
-
}
|
|
178
|
-
}`)
|
|
179
|
-
await createFile(dir, 'src/modules/x/validators/schema.ts', SCHEMA)
|
|
180
|
-
await createFile(dir, 'src/modules/x/tests/service.test.ts', TEST_OK)
|
|
181
|
-
const result = await analyzeProject(dir)
|
|
182
|
-
expect(result.violations.some(v => v.type === 'CONTROLLER_MISSING_VALIDATION')).toBe(true)
|
|
183
|
-
await cleanup('no-validation')
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('detecta descripción vacía en módulo', async () => {
|
|
187
|
-
const dir = await createProject('empty-desc')
|
|
188
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
189
|
-
await createFile(dir, 'src/modules/x/index.ts',
|
|
190
|
-
`import { createModule } from 'arckode-framework'
|
|
191
|
-
export function M() {
|
|
192
|
-
return createModule({name:'x',version:'1',description:'',
|
|
193
|
-
contract:{name:'x',version:'1',description:'',actions:[],events:[],tables:[],dependencies:[],rules:[]},
|
|
194
|
-
create:()=>({})})
|
|
195
|
-
}`)
|
|
196
|
-
await createFile(dir, 'src/modules/x/types.ts', TYPES)
|
|
197
|
-
await createFile(dir, 'src/modules/x/actions/service.ts', SERVICE)
|
|
198
|
-
await createFile(dir, 'src/modules/x/actions/controller.ts', CTRL)
|
|
199
|
-
await createFile(dir, 'src/modules/x/validators/schema.ts', SCHEMA)
|
|
200
|
-
await createFile(dir, 'src/modules/x/tests/service.test.ts', TEST_OK)
|
|
201
|
-
const result = await analyzeProject(dir)
|
|
202
|
-
expect(result.violations.some(v => v.type === 'EMPTY_MODULE_DESCRIPTION')).toBe(true)
|
|
203
|
-
await cleanup('empty-desc')
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('detecta service que inyecta ORM en vez de RepositoryAdapter', async () => {
|
|
207
|
-
const dir = await createProject('orm-dep')
|
|
208
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
209
|
-
await createFile(dir, 'src/modules/x/index.ts', MOD_INDEX)
|
|
210
|
-
await createFile(dir, 'src/modules/x/types.ts', TYPES)
|
|
211
|
-
await createFile(dir, 'src/modules/x/actions/service.ts',
|
|
212
|
-
`import { ORM } from 'arckode-framework'\nexport class S {\n constructor(private orm: ORM) {}\n async list() { return this.orm.findMany('X') }\n}`)
|
|
213
|
-
await createFile(dir, 'src/modules/x/actions/controller.ts', CTRL)
|
|
214
|
-
await createFile(dir, 'src/modules/x/validators/schema.ts', SCHEMA)
|
|
215
|
-
await createFile(dir, 'src/modules/x/tests/service.test.ts', TEST_OK)
|
|
216
|
-
const result = await analyzeProject(dir)
|
|
217
|
-
expect(result.violations.some(v => v.type === 'SERVICE_DEPENDS_ON_ORM')).toBe(true)
|
|
218
|
-
await cleanup('orm-dep')
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it('detecta God Service (>200 líneas)', async () => {
|
|
222
|
-
const dir = await createProject('god')
|
|
223
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
224
|
-
await createFile(dir, 'src/modules/x/index.ts', MOD_INDEX)
|
|
225
|
-
await createFile(dir, 'src/modules/x/types.ts', TYPES)
|
|
226
|
-
await createFile(dir, 'src/modules/x/actions/service.ts', `export class S {\n${' x = 1\n'.repeat(210)}}`)
|
|
227
|
-
await createFile(dir, 'src/modules/x/actions/controller.ts', CTRL)
|
|
228
|
-
await createFile(dir, 'src/modules/x/validators/schema.ts', SCHEMA)
|
|
229
|
-
await createFile(dir, 'src/modules/x/tests/service.test.ts', TEST_OK)
|
|
230
|
-
const result = await analyzeProject(dir)
|
|
231
|
-
expect(result.violations.some(v => v.type === 'GOD_SERVICE')).toBe(true)
|
|
232
|
-
await cleanup('god')
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
it('detecta tests sin casos reales', async () => {
|
|
236
|
-
const dir = await createProject('empty-tests')
|
|
237
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
238
|
-
await createFile(dir, 'src/modules/x/index.ts', MOD_INDEX)
|
|
239
|
-
await createFile(dir, 'src/modules/x/types.ts', TYPES)
|
|
240
|
-
await createFile(dir, 'src/modules/x/actions/service.ts', SERVICE)
|
|
241
|
-
await createFile(dir, 'src/modules/x/actions/controller.ts', CTRL)
|
|
242
|
-
await createFile(dir, 'src/modules/x/validators/schema.ts', SCHEMA)
|
|
243
|
-
// File exists but no test/it/describe calls
|
|
244
|
-
await createFile(dir, 'src/modules/x/tests/service.test.ts', `// just a comment\nconst x = 1\n`)
|
|
245
|
-
const result = await analyzeProject(dir)
|
|
246
|
-
expect(result.violations.some(v => v.type === 'TESTS_WITHOUT_CASES')).toBe(true)
|
|
247
|
-
await cleanup('empty-tests')
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
describe('seguridad y performance', () => {
|
|
252
|
-
it('detecta ORM en loop (N+1)', async () => {
|
|
253
|
-
const dir = await createProject('nplus1')
|
|
254
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
255
|
-
await createFile(dir, 'src/modules/x/index.ts', MOD_INDEX)
|
|
256
|
-
await createFile(dir, 'src/modules/x/types.ts', TYPES)
|
|
257
|
-
await createFile(dir, 'src/modules/x/actions/service.ts',
|
|
258
|
-
`export class S {\n async bad(orm: any) {\n const items = await orm.findMany('X')\n for (const item of items) {\n const d = await orm.findById('Y', item.id)\n }\n return items\n }\n}`)
|
|
259
|
-
await createFile(dir, 'src/modules/x/actions/controller.ts', CTRL)
|
|
260
|
-
await createFile(dir, 'src/modules/x/validators/schema.ts', SCHEMA)
|
|
261
|
-
await createFile(dir, 'src/modules/x/tests/service.test.ts', TEST_OK)
|
|
262
|
-
const result = await analyzeProject(dir)
|
|
263
|
-
expect(result.violations.some(v => v.type === 'N_PLUS_ONE_RISK')).toBe(true)
|
|
264
|
-
await cleanup('nplus1')
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('detecta findById sin assertOwnership (IDOR)', async () => {
|
|
268
|
-
const dir = await createProject('idor')
|
|
269
|
-
await createFile(dir, 'src/composition-root.ts', CR)
|
|
270
|
-
await createFile(dir, 'src/modules/x/index.ts', MOD_INDEX)
|
|
271
|
-
await createFile(dir, 'src/modules/x/types.ts', TYPES)
|
|
272
|
-
await createFile(dir, 'src/modules/x/actions/service.ts',
|
|
273
|
-
`export class S {\n private repo: any\n async getById(id: string) {\n const item = await this.repo.findById(id)\n return item\n }\n}`)
|
|
274
|
-
await createFile(dir, 'src/modules/x/actions/controller.ts', CTRL)
|
|
275
|
-
await createFile(dir, 'src/modules/x/validators/schema.ts', SCHEMA)
|
|
276
|
-
await createFile(dir, 'src/modules/x/tests/service.test.ts', TEST_OK)
|
|
277
|
-
const result = await analyzeProject(dir)
|
|
278
|
-
expect(result.violations.some(v => v.type === 'IDOR_RISK')).toBe(true)
|
|
279
|
-
await cleanup('idor')
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
})
|