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.
- package/package.json +15 -4
- 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,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
|
-
})
|