@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.
@@ -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
+ ```