@oalacea/demon 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/CHANGELOG.md +38 -0
- package/LICENSE +23 -0
- package/README.md +103 -0
- package/agents/deps-analyzer.js +366 -0
- package/agents/detector.js +570 -0
- package/agents/fix-engine.js +305 -0
- package/agents/perf-analyzer.js +294 -0
- package/agents/test-generator.js +387 -0
- package/agents/test-runner.js +318 -0
- package/bin/Dockerfile +65 -0
- package/bin/cli.js +455 -0
- package/lib/config.js +237 -0
- package/lib/docker.js +207 -0
- package/lib/reporter.js +297 -0
- package/package.json +34 -0
- package/prompts/DEPS_EFFICIENCY.md +558 -0
- package/prompts/E2E.md +491 -0
- package/prompts/EXECUTE.md +782 -0
- package/prompts/INTEGRATION_API.md +484 -0
- package/prompts/INTEGRATION_DB.md +425 -0
- package/prompts/PERF_API.md +433 -0
- package/prompts/PERF_DB.md +430 -0
- package/prompts/REMEDIATION.md +482 -0
- package/prompts/UNIT.md +260 -0
- package/scripts/dev.js +106 -0
- package/templates/README.md +22 -0
- package/templates/k6/load-test.js +54 -0
- package/templates/playwright/e2e.spec.ts +61 -0
- package/templates/vitest/api.test.ts +51 -0
- package/templates/vitest/component.test.ts +27 -0
- package/templates/vitest/hook.test.ts +36 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# API Integration Test Guide
|
|
2
|
+
|
|
3
|
+
This prompt is included by EXECUTE.md. It provides detailed guidance for API route testing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Setup for API Testing
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// tests/api/setup.ts
|
|
11
|
+
import { Hono } from 'hono';
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import { db } from '../db/setup';
|
|
14
|
+
|
|
15
|
+
// Create test app
|
|
16
|
+
export function createTestApp() {
|
|
17
|
+
const app = new Hono();
|
|
18
|
+
|
|
19
|
+
// Import and register routes
|
|
20
|
+
app.route('/api/users', userRoutes);
|
|
21
|
+
app.route('/api/posts', postRoutes);
|
|
22
|
+
|
|
23
|
+
return app;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Helper for making requests
|
|
27
|
+
export async function request(app: Hono, path: string, init?: RequestInit) {
|
|
28
|
+
const url = `http://localhost${path}`;
|
|
29
|
+
return app.request(url, init);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## GET Request Tests
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
describe('GET /api/users', () => {
|
|
39
|
+
let app: Hono;
|
|
40
|
+
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
app = createTestApp();
|
|
43
|
+
await db.begin();
|
|
44
|
+
await seedUsers(10);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
await db.rollback();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return list of users', async () => {
|
|
52
|
+
const response = await request(app, '/api/users');
|
|
53
|
+
|
|
54
|
+
expect(response.status).toBe(200);
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
expect(data.users).toHaveLength(10);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should support pagination', async () => {
|
|
60
|
+
const response = await request(app, '/api/users?page=1&limit=5');
|
|
61
|
+
|
|
62
|
+
expect(response.status).toBe(200);
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
expect(data.users).toHaveLength(5);
|
|
65
|
+
expect(data.page).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return empty array when no users', async () => {
|
|
69
|
+
await db.user.deleteMany();
|
|
70
|
+
const response = await request(app, '/api/users');
|
|
71
|
+
|
|
72
|
+
expect(response.status).toBe(200);
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
expect(data.users).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('GET /api/users/:id', () => {
|
|
79
|
+
let app: Hono;
|
|
80
|
+
let testUser: any;
|
|
81
|
+
|
|
82
|
+
beforeEach(async () => {
|
|
83
|
+
app = createTestApp();
|
|
84
|
+
await db.begin();
|
|
85
|
+
testUser = await db.user.create({
|
|
86
|
+
data: { email: 'test@example.com', name: 'Test' }
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(async () => {
|
|
91
|
+
await db.rollback();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return user by id', async () => {
|
|
95
|
+
const response = await request(app, `/api/users/${testUser.id}`);
|
|
96
|
+
|
|
97
|
+
expect(response.status).toBe(200);
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
expect(data.id).toBe(testUser.id);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return 404 for non-existent user', async () => {
|
|
103
|
+
const response = await request(app, '/api/users/non-existent-id');
|
|
104
|
+
|
|
105
|
+
expect(response.status).toBe(404);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
expect(data.error).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should include related data', async () => {
|
|
111
|
+
await db.post.create({
|
|
112
|
+
data: {
|
|
113
|
+
title: 'Test Post',
|
|
114
|
+
authorId: testUser.id
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const response = await request(app, `/api/users/${testUser.id}?include=posts`);
|
|
119
|
+
|
|
120
|
+
expect(response.status).toBe(200);
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
expect(data.posts).toBeDefined();
|
|
123
|
+
expect(data.posts).toHaveLength(1);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## POST Request Tests
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
describe('POST /api/users', () => {
|
|
134
|
+
let app: Hono;
|
|
135
|
+
|
|
136
|
+
beforeEach(async () => {
|
|
137
|
+
app = createTestApp();
|
|
138
|
+
await db.begin();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(async () => {
|
|
142
|
+
await db.rollback();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should create user with valid data', async () => {
|
|
146
|
+
const response = await request(app, '/api/users', {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
email: 'new@example.com',
|
|
151
|
+
name: 'New User',
|
|
152
|
+
age: 25
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(response.status).toBe(201);
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
expect(data.id).toBeDefined();
|
|
159
|
+
expect(data.email).toBe('new@example.com');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should reject invalid email format', async () => {
|
|
163
|
+
const response = await request(app, '/api/users', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: { 'Content-Type': 'application/json' },
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
email: 'not-an-email',
|
|
168
|
+
name: 'User'
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(response.status).toBe(400);
|
|
173
|
+
const data = await response.json();
|
|
174
|
+
expect(data.error).toContain('email');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should reject missing required fields', async () => {
|
|
178
|
+
const response = await request(app, '/api/users', {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
181
|
+
body: JSON.stringify({ name: 'User' }) // Missing email
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(response.status).toBe(400);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should reject duplicate email', async () => {
|
|
188
|
+
await db.user.create({
|
|
189
|
+
data: { email: 'test@example.com', name: 'User 1' }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const response = await request(app, '/api/users', {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
email: 'test@example.com',
|
|
197
|
+
name: 'User 2'
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(response.status).toBe(409); // Conflict
|
|
202
|
+
const data = await response.json();
|
|
203
|
+
expect(data.error).toContain('already exists');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle extra fields gracefully', async () => {
|
|
207
|
+
const response = await request(app, '/api/users', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
email: 'new@example.com',
|
|
212
|
+
name: 'User',
|
|
213
|
+
extraField: 'should be ignored'
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Extra fields should be ignored, not cause error
|
|
218
|
+
expect(response.status).toBe(201);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## PUT/PATCH Request Tests
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
describe('PATCH /api/users/:id', () => {
|
|
229
|
+
let app: Hono;
|
|
230
|
+
let testUser: any;
|
|
231
|
+
|
|
232
|
+
beforeEach(async () => {
|
|
233
|
+
app = createTestApp();
|
|
234
|
+
await db.begin();
|
|
235
|
+
testUser = await db.user.create({
|
|
236
|
+
data: { email: 'test@example.com', name: 'Test' }
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
afterEach(async () => {
|
|
241
|
+
await db.rollback();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should update user fields', async () => {
|
|
245
|
+
const response = await request(app, `/api/users/${testUser.id}`, {
|
|
246
|
+
method: 'PATCH',
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({ name: 'Updated Name' })
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(response.status).toBe(200);
|
|
252
|
+
const data = await response.json();
|
|
253
|
+
expect(data.name).toBe('Updated Name');
|
|
254
|
+
expect(data.email).toBe('test@example.com'); // Unchanged
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should return 404 for non-existent user', async () => {
|
|
258
|
+
const response = await request(app, '/api/users/unknown', {
|
|
259
|
+
method: 'PATCH',
|
|
260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
261
|
+
body: JSON.stringify({ name: 'Updated' })
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(response.status).toBe(404);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should not allow updating email to existing', async () => {
|
|
268
|
+
await db.user.create({
|
|
269
|
+
data: { email: 'other@example.com', name: 'Other' }
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const response = await request(app, `/api/users/${testUser.id}`, {
|
|
273
|
+
method: 'PATCH',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify({ email: 'other@example.com' })
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(response.status).toBe(409);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## DELETE Request Tests
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
describe('DELETE /api/users/:id', () => {
|
|
289
|
+
let app: Hono;
|
|
290
|
+
let testUser: any;
|
|
291
|
+
|
|
292
|
+
beforeEach(async () => {
|
|
293
|
+
app = createTestApp();
|
|
294
|
+
await db.begin();
|
|
295
|
+
testUser = await db.user.create({
|
|
296
|
+
data: { email: 'test@example.com', name: 'Test' }
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
afterEach(async () => {
|
|
301
|
+
await db.rollback();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should delete user', async () => {
|
|
305
|
+
const response = await request(app, `/api/users/${testUser.id}`, {
|
|
306
|
+
method: 'DELETE'
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expect(response.status).toBe(204); // No content
|
|
310
|
+
|
|
311
|
+
// Verify deletion
|
|
312
|
+
const found = await db.user.findUnique({
|
|
313
|
+
where: { id: testUser.id }
|
|
314
|
+
});
|
|
315
|
+
expect(found).toBeNull();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should return 404 for non-existent user', async () => {
|
|
319
|
+
const response = await request(app, '/api/users/unknown', {
|
|
320
|
+
method: 'DELETE'
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(response.status).toBe(404);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Authentication Tests
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
describe('Auth Middleware', () => {
|
|
334
|
+
let app: Hono;
|
|
335
|
+
let token: string;
|
|
336
|
+
|
|
337
|
+
beforeEach(async () => {
|
|
338
|
+
app = createTestApp();
|
|
339
|
+
await db.begin();
|
|
340
|
+
const user = await db.user.create({
|
|
341
|
+
data: { email: 'test@example.com', name: 'Test' }
|
|
342
|
+
});
|
|
343
|
+
token = generateToken(user);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
afterEach(async () => {
|
|
347
|
+
await db.rollback();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should allow access with valid token', async () => {
|
|
351
|
+
const response = await request(app, '/api/protected', {
|
|
352
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(response.status).toBe(200);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should reject request without token', async () => {
|
|
359
|
+
const response = await request(app, '/api/protected');
|
|
360
|
+
|
|
361
|
+
expect(response.status).toBe(401);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should reject request with invalid token', async () => {
|
|
365
|
+
const response = await request(app, '/api/protected', {
|
|
366
|
+
headers: { Authorization: 'Bearer invalid-token' }
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(response.status).toBe(401);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should reject expired token', async () => {
|
|
373
|
+
const expiredToken = generateExpiredToken();
|
|
374
|
+
const response = await request(app, '/api/protected', {
|
|
375
|
+
headers: { Authorization: `Bearer ${expiredToken}` }
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
expect(response.status).toBe(401);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Rate Limiting Tests
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
describe('Rate Limiting', () => {
|
|
389
|
+
let app: Hono;
|
|
390
|
+
|
|
391
|
+
beforeEach(async () => {
|
|
392
|
+
app = createTestApp();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should allow requests under limit', async () => {
|
|
396
|
+
const promises = Array.from({ length: 10 }, () =>
|
|
397
|
+
request(app, '/api/users')
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const responses = await Promise.all(promises);
|
|
401
|
+
responses.forEach(res => {
|
|
402
|
+
expect(res.status).toBe(200);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should block requests over limit', async () => {
|
|
407
|
+
const promises = Array.from({ length: 100 }, () =>
|
|
408
|
+
request(app, '/api/users')
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const responses = await Promise.all(promises);
|
|
412
|
+
const blockedCount = responses.filter(
|
|
413
|
+
res => res.status === 429
|
|
414
|
+
).length;
|
|
415
|
+
|
|
416
|
+
expect(blockedCount).toBeGreaterThan(0);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should include rate limit headers', async () => {
|
|
420
|
+
const response = await request(app, '/api/users');
|
|
421
|
+
|
|
422
|
+
expect(response.headers.get('X-RateLimit-Limit')).toBeDefined();
|
|
423
|
+
expect(response.headers.get('X-RateLimit-Remaining')).toBeDefined();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## File Upload Tests
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
describe('File Upload', () => {
|
|
434
|
+
let app: Hono;
|
|
435
|
+
|
|
436
|
+
beforeEach(async () => {
|
|
437
|
+
app = createTestApp();
|
|
438
|
+
await db.begin();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
afterEach(async () => {
|
|
442
|
+
await db.rollback();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should upload valid file', async () => {
|
|
446
|
+
const formData = new FormData();
|
|
447
|
+
formData.append('file', new Blob(['test content']), 'test.txt');
|
|
448
|
+
|
|
449
|
+
const response = await request(app, '/api/upload', {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
body: formData
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(response.status).toBe(200);
|
|
455
|
+
const data = await response.json();
|
|
456
|
+
expect(data.url).toBeDefined();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should reject file larger than limit', async () => {
|
|
460
|
+
const largeFile = new Blob(['x'.repeat(10 * 1024 * 1024)]); // 10MB
|
|
461
|
+
const formData = new FormData();
|
|
462
|
+
formData.append('file', largeFile, 'large.txt');
|
|
463
|
+
|
|
464
|
+
const response = await request(app, '/api/upload', {
|
|
465
|
+
method: 'POST',
|
|
466
|
+
body: formData
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(response.status).toBe(413); // Payload Too Large
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should reject invalid file type', async () => {
|
|
473
|
+
const formData = new FormData();
|
|
474
|
+
formData.append('file', new Blob(['<script>alert(1)</script>']), 'test.html');
|
|
475
|
+
|
|
476
|
+
const response = await request(app, '/api/upload', {
|
|
477
|
+
method: 'POST',
|
|
478
|
+
body: formData
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(response.status).toBe(400);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
```
|