bxo 0.0.5-dev.6 โ†’ 0.0.5-dev.61

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,598 @@
1
+ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'bun:test';
2
+ import BXO, { z } from '../../src/index';
3
+
4
+ describe('BXO Framework Integration', () => {
5
+ let app: BXO;
6
+ let baseUrl: string;
7
+
8
+ beforeAll(() => {
9
+ app = new BXO();
10
+ baseUrl = 'http://localhost:3001';
11
+ });
12
+
13
+ afterEach(async () => {
14
+ if (app.isServerRunning()) {
15
+ await app.stop();
16
+ }
17
+ });
18
+
19
+ afterAll(async () => {
20
+ if (app.isServerRunning()) {
21
+ await app.stop();
22
+ }
23
+ });
24
+
25
+ describe('Basic Routing', () => {
26
+ it('should handle GET requests', async () => {
27
+ app.get('/test-get', () => {
28
+ return { message: 'GET successful' };
29
+ });
30
+
31
+ await app.start(3001);
32
+
33
+ const response = await fetch(`${baseUrl}/test-get`);
34
+ const data = await response.json() as { message: string };
35
+
36
+ expect(response.status).toBe(200);
37
+ expect(data.message).toBe('GET successful');
38
+ });
39
+
40
+ it('should handle POST requests', async () => {
41
+ app.post('/test-post', (ctx) => {
42
+ return { message: 'POST successful', body: ctx.body };
43
+ });
44
+
45
+ const response = await fetch(`${baseUrl}/test-post`, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ test: 'data' })
49
+ });
50
+
51
+ const data = await response.json() as { message: string, body: { test: string } };
52
+
53
+ expect(response.status).toBe(200);
54
+ expect(data.message).toBe('POST successful');
55
+ expect(data.body.test).toBe('data');
56
+ });
57
+
58
+ it('should handle PUT requests', async () => {
59
+ app.put('/test-put', (ctx) => {
60
+ return { message: 'PUT successful', body: ctx.body };
61
+ });
62
+
63
+ const response = await fetch(`${baseUrl}/test-put`, {
64
+ method: 'PUT',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({ test: 'data' })
67
+ });
68
+
69
+ const data = await response.json() as { message: string };
70
+
71
+ expect(response.status).toBe(200);
72
+ expect(data.message).toBe('PUT successful');
73
+ });
74
+
75
+ it('should handle DELETE requests', async () => {
76
+ app.delete('/test-delete', () => {
77
+ return { message: 'DELETE successful' };
78
+ });
79
+
80
+ const response = await fetch(`${baseUrl}/test-delete`, {
81
+ method: 'DELETE'
82
+ });
83
+
84
+ const data = await response.json() as { message: string };
85
+
86
+ expect(response.status).toBe(200);
87
+ expect(data.message).toBe('DELETE successful');
88
+ });
89
+
90
+ it('should handle PATCH requests', async () => {
91
+ app.patch('/test-patch', (ctx) => {
92
+ return { message: 'PATCH successful', body: ctx.body };
93
+ });
94
+
95
+ const response = await fetch(`${baseUrl}/test-patch`, {
96
+ method: 'PATCH',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ test: 'data' })
99
+ });
100
+
101
+ const data = await response.json() as { message: string };
102
+
103
+ expect(response.status).toBe(200);
104
+ expect(data.message).toBe('PATCH successful');
105
+ });
106
+ });
107
+
108
+ describe('Route Parameters', () => {
109
+ it('should handle route parameters', async () => {
110
+ app.get('/users/:id', (ctx) => {
111
+ return { userId: ctx.params.id, message: 'User found' };
112
+ });
113
+
114
+ const response = await fetch(`${baseUrl}/users/123`);
115
+ const data = await response.json() as { userId: string, message: string };
116
+
117
+ expect(response.status).toBe(200);
118
+ expect(data.userId).toBe('123');
119
+ expect(data.message).toBe('User found');
120
+ });
121
+
122
+ it('should handle nested route parameters', async () => {
123
+ app.get('/users/:id/posts/:postId', (ctx) => {
124
+ return {
125
+ userId: ctx.params.id,
126
+ postId: ctx.params.postId,
127
+ message: 'Post found'
128
+ };
129
+ });
130
+
131
+ const response = await fetch(`${baseUrl}/users/123/posts/456`);
132
+ const data = await response.json() as { userId: string, postId: string, message: string };
133
+
134
+ expect(response.status).toBe(200);
135
+ expect(data.userId).toBe('123');
136
+ expect(data.postId).toBe('456');
137
+ expect(data.message).toBe('Post found');
138
+ });
139
+ });
140
+
141
+ describe('Query Parameters', () => {
142
+ it('should handle query parameters', async () => {
143
+ app.get('/search', (ctx) => {
144
+ return { query: ctx.query, message: 'Search completed' };
145
+ });
146
+
147
+ const response = await fetch(`${baseUrl}/search?q=test&page=1`);
148
+ const data = await response.json() as { query: { q: string, page: string }, message: string };
149
+
150
+ expect(response.status).toBe(200);
151
+ expect(data.query.q).toBe('test');
152
+ expect(data.query.page).toBe('1');
153
+ expect(data.message).toBe('Search completed');
154
+ });
155
+ });
156
+
157
+ describe('Request Body', () => {
158
+ it('should handle JSON body', async () => {
159
+ app.post('/api/users', (ctx) => {
160
+ return { user: ctx.body, message: 'User created' };
161
+ });
162
+
163
+ const userData = { name: 'John', email: 'john@example.com' };
164
+ const response = await fetch(`${baseUrl}/api/users`, {
165
+ method: 'POST',
166
+ headers: { 'Content-Type': 'application/json' },
167
+ body: JSON.stringify(userData)
168
+ });
169
+
170
+ const data = await response.json() as { user: { name: string, email: string }, message: string };
171
+
172
+ expect(response.status).toBe(200);
173
+ expect(data.user.name).toBe('John');
174
+ expect(data.user.email).toBe('john@example.com');
175
+ expect(data.message).toBe('User created');
176
+ });
177
+
178
+ it('should handle form data', async () => {
179
+ app.post('/api/contact', (ctx) => {
180
+ return { formData: ctx.body, message: 'Form submitted' };
181
+ });
182
+
183
+ const formData = new FormData();
184
+ formData.append('name', 'John');
185
+ formData.append('email', 'john@example.com');
186
+
187
+ const response = await fetch(`${baseUrl}/api/contact`, {
188
+ method: 'POST',
189
+ body: formData
190
+ });
191
+
192
+ const data = await response.json() as { formData: { name: string, email: string }, message: string };
193
+
194
+ expect(response.status).toBe(200);
195
+ expect(data.formData.name).toBe('John');
196
+ expect(data.formData.email).toBe('john@example.com');
197
+ expect(data.message).toBe('Form submitted');
198
+ });
199
+
200
+ it('should handle form data with arrays', async () => {
201
+ app.post('/api/records', (ctx) => {
202
+ return { formData: ctx.body, message: 'Records submitted' };
203
+ });
204
+
205
+ const formData = new FormData();
206
+ formData.append('app', 'zodula');
207
+ formData.append('model', 'zodula_User');
208
+ formData.append('recordIds[]', 'asdfasdfdsa');
209
+ formData.append('recordIds[]', 'jarupak.sri@gmail.com');
210
+ formData.append('fields', '*');
211
+
212
+ const response = await fetch(`${baseUrl}/api/records`, {
213
+ method: 'POST',
214
+ body: formData
215
+ });
216
+
217
+ const data = await response.json() as {
218
+ formData: {
219
+ app: string,
220
+ model: string,
221
+ recordIds: string[],
222
+ fields: string
223
+ },
224
+ message: string
225
+ };
226
+
227
+ expect(response.status).toBe(200);
228
+ expect(data.formData.app).toBe('zodula');
229
+ expect(data.formData.model).toBe('zodula_User');
230
+ expect(Array.isArray(data.formData.recordIds)).toBe(true);
231
+ expect(data.formData.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
232
+ expect(data.formData.fields).toBe('*');
233
+ expect(data.message).toBe('Records submitted');
234
+ });
235
+
236
+ it('should handle form data with nested objects', async () => {
237
+ app.post('/api/nested', (ctx) => {
238
+ return { formData: ctx.body, message: 'Nested data submitted' };
239
+ });
240
+
241
+ const formData = new FormData();
242
+ formData.append('test[test]', 'test');
243
+ formData.append('test[new]', 'new');
244
+ formData.append('test[hi][hi]', 'hi');
245
+
246
+ const response = await fetch(`${baseUrl}/api/nested`, {
247
+ method: 'POST',
248
+ body: formData
249
+ });
250
+
251
+ const data = await response.json() as {
252
+ formData: {
253
+ test: {
254
+ test: string,
255
+ new: string,
256
+ hi: {
257
+ hi: string
258
+ }
259
+ }
260
+ },
261
+ message: string
262
+ };
263
+
264
+ expect(response.status).toBe(200);
265
+ expect(data.formData.test).toEqual({
266
+ test: 'test',
267
+ new: 'new',
268
+ hi: {
269
+ hi: 'hi'
270
+ }
271
+ });
272
+ expect(data.message).toBe('Nested data submitted');
273
+ });
274
+
275
+ it('should handle form data with nested arrays and files', async () => {
276
+ app.post('/api/records-with-files', (ctx) => {
277
+ return { formData: ctx.body, message: 'Records with files submitted' };
278
+ });
279
+
280
+ const formData = new FormData();
281
+ formData.append('records[0][qrPayment]', new File(['test file content'], 'test.png', { type: 'image/png' }));
282
+ formData.append('records[1][qrPayment]', new File(['test file content 2'], 'test2.png', { type: 'image/png' }));
283
+ formData.append('records[0][name]', 'Record 1');
284
+ formData.append('records[1][name]', 'Record 2');
285
+
286
+ const response = await fetch(`${baseUrl}/api/records-with-files`, {
287
+ method: 'POST',
288
+ body: formData
289
+ });
290
+
291
+ const data = await response.json() as {
292
+ formData: {
293
+ records: Array<{
294
+ qrPayment: {
295
+ type: string;
296
+ name: string;
297
+ size: number;
298
+ lastModified: number;
299
+ },
300
+ name: string
301
+ }>
302
+ },
303
+ message: string
304
+ };
305
+
306
+ expect(response.status).toBe(200);
307
+ expect(Array.isArray(data.formData.records)).toBe(true);
308
+ expect(data.formData.records).toHaveLength(2);
309
+ expect(data.formData.records[0]?.name).toBe('Record 1');
310
+ expect(data.formData.records[1]?.name).toBe('Record 2');
311
+ expect(data.formData.records[0]?.qrPayment).toEqual({
312
+ type: 'File',
313
+ name: 'test.png',
314
+ size: 17,
315
+ lastModified: 0
316
+ });
317
+ expect(data.formData.records[1]?.qrPayment).toEqual({
318
+ type: 'File',
319
+ name: 'test2.png',
320
+ size: 19,
321
+ lastModified: 0
322
+ });
323
+ expect(data.message).toBe('Records with files submitted');
324
+ });
325
+
326
+ it('should have File objects in ctx.body', async () => {
327
+ app.post('/api/files-test', (ctx) => {
328
+ // Check if the files are actual File instances
329
+ const body = ctx.body as {
330
+ records: Array<{
331
+ qrPayment: File;
332
+ name: string;
333
+ }>;
334
+ };
335
+ const records = body.records;
336
+ console.log('File instance check:');
337
+ console.log('records[0].qrPayment instanceof File:', records[0]?.qrPayment instanceof File);
338
+ console.log('records[1].qrPayment instanceof File:', records[1]?.qrPayment instanceof File);
339
+ console.log('File name:', records[0]?.qrPayment.name);
340
+ console.log('File size:', records[0]?.qrPayment.size);
341
+ console.log('File type:', records[0]?.qrPayment.type);
342
+
343
+ return {
344
+ message: 'Files received',
345
+ fileInfo: {
346
+ isFile0: records[0]?.qrPayment instanceof File,
347
+ isFile1: records[1]?.qrPayment instanceof File,
348
+ name0: records[0]?.qrPayment.name,
349
+ size0: records[0]?.qrPayment.size,
350
+ type0: records[0]?.qrPayment.type
351
+ }
352
+ };
353
+ });
354
+
355
+ const formData = new FormData();
356
+ formData.append('records[0][qrPayment]', new File(['test file content'], 'test.png', { type: 'image/png' }));
357
+ formData.append('records[1][qrPayment]', new File(['test file content 2'], 'test2.png', { type: 'image/png' }));
358
+ formData.append('records[0][name]', 'Record 1');
359
+ formData.append('records[1][name]', 'Record 2');
360
+
361
+ const response = await fetch(`${baseUrl}/api/files-test`, {
362
+ method: 'POST',
363
+ body: formData
364
+ });
365
+
366
+ const data = await response.json() as {
367
+ message: string;
368
+ fileInfo: {
369
+ isFile0: boolean;
370
+ isFile1: boolean;
371
+ name0: string;
372
+ size0: number;
373
+ type0: string;
374
+ };
375
+ };
376
+
377
+ expect(response.status).toBe(200);
378
+ expect(data.fileInfo.isFile0).toBe(true);
379
+ expect(data.fileInfo.isFile1).toBe(true);
380
+ expect(data.fileInfo.name0).toBe('test.png');
381
+ expect(data.fileInfo.size0).toBe(17);
382
+ expect(data.fileInfo.type0).toBe('image/png');
383
+ });
384
+ });
385
+
386
+ describe('Response Handling', () => {
387
+ it('should handle string responses', async () => {
388
+ app.get('/text', () => {
389
+ return 'Hello World';
390
+ });
391
+
392
+ const response = await fetch(`${baseUrl}/text`);
393
+ const text = await response.text();
394
+
395
+ expect(response.status).toBe(200);
396
+ expect(text).toBe('Hello World');
397
+ expect(response.headers.get('content-type')).toBe('text/plain');
398
+ });
399
+
400
+ it('should handle object responses', async () => {
401
+ app.get('/json', () => {
402
+ return { message: 'Hello World', status: 'success' };
403
+ });
404
+
405
+ const response = await fetch(`${baseUrl}/json`);
406
+ const data = await response.json() as { message: string, status: string };
407
+
408
+ expect(response.status).toBe(200);
409
+ expect(data.message).toBe('Hello World');
410
+ expect(data.status).toBe('success');
411
+ expect(response.headers.get('content-type')).toBe('application/json');
412
+ });
413
+
414
+ it('should handle Response objects', async () => {
415
+ app.get('/custom-response', () => {
416
+ return new Response('Custom response', {
417
+ status: 201,
418
+ headers: { 'x-custom': 'value' }
419
+ });
420
+ });
421
+
422
+ const response = await fetch(`${baseUrl}/custom-response`);
423
+ const text = await response.text();
424
+
425
+ expect(response.status).toBe(201);
426
+ expect(text).toBe('Custom response');
427
+ expect(response.headers.get('x-custom')).toBe('value');
428
+ });
429
+ });
430
+
431
+ describe('Status and Headers', () => {
432
+ it('should handle custom status codes', async () => {
433
+ app.get('/not-found', (ctx) => {
434
+ ctx.status(404, 'Not Found');
435
+ return { error: 'Resource not found' };
436
+ });
437
+
438
+ const response = await fetch(`${baseUrl}/not-found`);
439
+ const data = await response.json() as { error: string };
440
+
441
+ expect(response.status).toBe(404);
442
+ expect(data.error).toBe('Resource not found');
443
+ });
444
+
445
+ it('should handle custom headers', async () => {
446
+ app.get('/custom-headers', (ctx) => {
447
+ ctx.set.headers['x-custom'] = 'value';
448
+ ctx.set.headers['authorization'] = 'Bearer token';
449
+ return { message: 'Headers set' };
450
+ });
451
+
452
+ const response = await fetch(`${baseUrl}/custom-headers`);
453
+ const data = await response.json() as { message: string };
454
+
455
+ expect(response.status).toBe(200);
456
+ expect(response.headers.get('x-custom')).toBe('value');
457
+ expect(response.headers.get('authorization')).toBe('Bearer token');
458
+ expect(data.message).toBe('Headers set');
459
+ });
460
+ });
461
+
462
+ describe('Cookies', () => {
463
+ it('should handle setting cookies', async () => {
464
+ app.get('/set-cookie', (ctx) => {
465
+ ctx.set.cookies('session', 'abc123', {
466
+ httpOnly: true,
467
+ secure: false
468
+ });
469
+ return { message: 'Cookie set' };
470
+ });
471
+
472
+ const response = await fetch(`${baseUrl}/set-cookie`);
473
+ const data = await response.json() as { message: string };
474
+
475
+ expect(response.status).toBe(200);
476
+ expect(response.headers.get('set-cookie')).toContain('session=abc123');
477
+ expect(response.headers.get('set-cookie')).toContain('HttpOnly');
478
+ expect(data.message).toBe('Cookie set');
479
+ });
480
+ });
481
+
482
+ describe('Redirects', () => {
483
+ it('should handle redirects', async () => {
484
+ app.get('/redirect', (ctx) => {
485
+ return ctx.redirect('/target', 301);
486
+ });
487
+
488
+ const response = await fetch(`${baseUrl}/redirect`, {
489
+ redirect: 'manual'
490
+ });
491
+
492
+ expect(response.status).toBe(301);
493
+ expect(response.headers.get('location')).toBe('/target');
494
+ });
495
+
496
+ it('should handle implicit redirects', async () => {
497
+ app.get('/implicit-redirect', (ctx) => {
498
+ ctx.set.headers['Location'] = '/target';
499
+ ctx.set.status = 302;
500
+ // Don't return anything - should create redirect automatically
501
+ });
502
+
503
+ const response = await fetch(`${baseUrl}/implicit-redirect`, {
504
+ redirect: 'manual'
505
+ });
506
+
507
+ expect(response.status).toBe(302);
508
+ expect(response.headers.get('location')).toBe('/target');
509
+ });
510
+ });
511
+
512
+ describe('Error Handling', () => {
513
+ it('should handle 404 for non-existent routes', async () => {
514
+ const response = await fetch(`${baseUrl}/non-existent`);
515
+
516
+ expect(response.status).toBe(404);
517
+ expect(await response.text()).toBe('Not Found');
518
+ });
519
+
520
+ it('should handle validation errors', async () => {
521
+ app.post('/validate', (ctx) => {
522
+ return { data: ctx.body };
523
+ }, {
524
+ body: z.object({
525
+ name: z.string().min(1),
526
+ email: z.string().email()
527
+ })
528
+ });
529
+
530
+ const response = await fetch(`${baseUrl}/validate`, {
531
+ method: 'POST',
532
+ headers: { 'Content-Type': 'application/json' },
533
+ body: JSON.stringify({ name: '', email: 'invalid' })
534
+ });
535
+
536
+ expect(response.status).toBe(400);
537
+ const data = await response.json() as { error: string };
538
+ expect(data.error).toContain('Validation failed');
539
+ });
540
+ });
541
+
542
+ describe('WebSocket Support', () => {
543
+ it('should handle WebSocket routes', async () => {
544
+ app.ws('/ws', {
545
+ onOpen: (ws) => {
546
+ ws.send('Connected');
547
+ },
548
+ onMessage: (ws, message) => {
549
+ ws.send(`Echo: ${message}`);
550
+ }
551
+ });
552
+
553
+ // WebSocket testing would require a WebSocket client
554
+ // For now, just verify the route is registered
555
+ expect(app.wsRoutes).toHaveLength(1);
556
+ expect(app.wsRoutes[0]?.path).toBe('/ws');
557
+ });
558
+ });
559
+
560
+ describe('Lifecycle Hooks', () => {
561
+ it('should execute lifecycle hooks', async () => {
562
+ let beforeStartCalled = false;
563
+ let afterStartCalled = false;
564
+
565
+ app.onBeforeStart(() => {
566
+ beforeStartCalled = true;
567
+ });
568
+
569
+ app.onAfterStart(() => {
570
+ afterStartCalled = true;
571
+ });
572
+
573
+ await app.start(3002);
574
+
575
+ expect(beforeStartCalled).toBe(true);
576
+ expect(afterStartCalled).toBe(true);
577
+
578
+ await app.stop();
579
+ });
580
+ });
581
+
582
+ describe('Server Information', () => {
583
+ it('should provide server information', async () => {
584
+ await app.start(3003);
585
+
586
+ const info = app.info;
587
+
588
+ expect(info.running).toBe(true);
589
+ expect(info.server).toBe('Bun');
590
+ expect(info.port).toBe(3003);
591
+ expect(info.hostname).toBe('localhost');
592
+ expect(info.runtime).toBe('Bun');
593
+ expect(info.totalRoutes).toBeGreaterThan(0);
594
+
595
+ await app.stop();
596
+ });
597
+ });
598
+ });
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { spawn } from 'child_process';
4
+
5
+ console.log('๐Ÿงช Running BXO Framework Tests...\n');
6
+
7
+ // Run unit tests
8
+ console.log('๐Ÿ“‹ Running Unit Tests...');
9
+ const unitTests = spawn('bun', ['test', 'tests/unit/'], {
10
+ stdio: 'inherit',
11
+ shell: true
12
+ });
13
+
14
+ unitTests.on('close', (code) => {
15
+ if (code === 0) {
16
+ console.log('\nโœ… Unit tests passed!\n');
17
+
18
+ // Run integration tests
19
+ console.log('๐Ÿ”— Running Integration Tests...');
20
+ const integrationTests = spawn('bun', ['test', 'tests/integration/'], {
21
+ stdio: 'inherit',
22
+ shell: true
23
+ });
24
+
25
+ integrationTests.on('close', (integrationCode) => {
26
+ if (integrationCode === 0) {
27
+ console.log('\nโœ… Integration tests passed!');
28
+ console.log('\n๐ŸŽ‰ All tests completed successfully!');
29
+ process.exit(0);
30
+ } else {
31
+ console.log('\nโŒ Integration tests failed!');
32
+ process.exit(integrationCode);
33
+ }
34
+ });
35
+ } else {
36
+ console.log('\nโŒ Unit tests failed!');
37
+ process.exit(code);
38
+ }
39
+ });
40
+
41
+ unitTests.on('error', (error) => {
42
+ console.error('โŒ Error running unit tests:', error);
43
+ process.exit(1);
44
+ });