@onebun/core 0.1.0 → 0.1.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.
@@ -0,0 +1,2166 @@
1
+ /**
2
+ * Documentation Examples Tests for @onebun/core
3
+ *
4
+ * This file tests code examples from:
5
+ * - packages/core/README.md
6
+ * - docs/api/core.md
7
+ * - docs/api/controllers.md
8
+ * - docs/api/decorators.md
9
+ * - docs/api/services.md
10
+ * - docs/api/validation.md
11
+ * - docs/examples/basic-app.md
12
+ * - docs/examples/crud-api.md
13
+ */
14
+
15
+ import { type } from 'arktype';
16
+ import {
17
+ describe,
18
+ it,
19
+ expect,
20
+ } from 'bun:test';
21
+
22
+ import {
23
+ Controller,
24
+ Get,
25
+ Post,
26
+ Put,
27
+ Delete,
28
+ Patch,
29
+ Param,
30
+ Query,
31
+ Body,
32
+ Header,
33
+ Req,
34
+ Module,
35
+ Service,
36
+ BaseService,
37
+ BaseController,
38
+ UseMiddleware,
39
+ getServiceTag,
40
+ HttpStatusCode,
41
+ NotFoundError,
42
+ InternalServerError,
43
+ OneBunBaseError,
44
+ Env,
45
+ validate,
46
+ validateOrThrow,
47
+ MultiServiceApplication,
48
+ OneBunApplication,
49
+ createServiceDefinition,
50
+ createServiceClient,
51
+ } from './';
52
+
53
+ /**
54
+ * @source docs/index.md#minimal-working-example
55
+ */
56
+ describe('Minimal Working Example (docs/index.md)', () => {
57
+ it('should define complete counter application in single block', () => {
58
+ // From docs/README.md: Minimal Working Example
59
+ // This test validates all components work together
60
+
61
+ // ============================================================================
62
+ // 1. Environment Schema (src/config.ts)
63
+ // ============================================================================
64
+ const envSchema = {
65
+ server: {
66
+ port: Env.number({ default: 3000 }),
67
+ host: Env.string({ default: '0.0.0.0' }),
68
+ },
69
+ };
70
+
71
+ // ============================================================================
72
+ // 2. Service Layer (src/counter.service.ts)
73
+ // ============================================================================
74
+ @Service()
75
+ class CounterService extends BaseService {
76
+ private value = 0;
77
+
78
+ getValue(): number {
79
+ return this.value;
80
+ }
81
+
82
+ increment(amount = 1): number {
83
+ this.value += amount;
84
+
85
+ return this.value;
86
+ }
87
+ }
88
+
89
+ // ============================================================================
90
+ // 3. Controller Layer (src/counter.controller.ts)
91
+ // ============================================================================
92
+ @Controller('/api/counter')
93
+ class CounterController extends BaseController {
94
+ constructor(private counterService: CounterService) {
95
+ super();
96
+ }
97
+
98
+ @Get('/')
99
+ async getValue(): Promise<Response> {
100
+ const value = this.counterService.getValue();
101
+
102
+ return this.success({ value });
103
+ }
104
+
105
+ @Post('/increment')
106
+ async increment(@Body() body?: { amount?: number }): Promise<Response> {
107
+ const newValue = this.counterService.increment(body?.amount);
108
+
109
+ return this.success({ value: newValue });
110
+ }
111
+ }
112
+
113
+ // ============================================================================
114
+ // 4. Module Definition (src/app.module.ts)
115
+ // ============================================================================
116
+ @Module({
117
+ controllers: [CounterController],
118
+ providers: [CounterService],
119
+ })
120
+ class AppModule {}
121
+
122
+ // ============================================================================
123
+ // 5. Application Entry Point (src/index.ts)
124
+ // ============================================================================
125
+ const app = new OneBunApplication(AppModule, {
126
+ port: 3000,
127
+ envSchema,
128
+ metrics: { enabled: true },
129
+ tracing: { enabled: true },
130
+ });
131
+
132
+ // Verify all components
133
+ expect(envSchema.server.port.type).toBe('number');
134
+ expect(envSchema.server.host.type).toBe('string');
135
+ expect(CounterService).toBeDefined();
136
+ expect(CounterController).toBeDefined();
137
+ expect(AppModule).toBeDefined();
138
+ expect(app).toBeDefined();
139
+ expect(typeof app.start).toBe('function');
140
+ expect(typeof app.stop).toBe('function');
141
+ });
142
+ });
143
+
144
+ describe('Core README Examples', () => {
145
+ describe('Quick Start (README)', () => {
146
+ it('should define controller with @Controller decorator', () => {
147
+ // From README: Quick Start example
148
+ @Controller('/api')
149
+ class AppController extends BaseController {
150
+ @Get('/hello')
151
+ async hello() {
152
+ return { message: 'Hello, OneBun!' };
153
+ }
154
+ }
155
+
156
+ expect(AppController).toBeDefined();
157
+ });
158
+
159
+ it('should define module with @Module decorator', () => {
160
+ // From README: Module definition
161
+ @Controller('/api')
162
+ class AppController extends BaseController {
163
+ @Get('/hello')
164
+ async hello() {
165
+ return { message: 'Hello, OneBun!' };
166
+ }
167
+ }
168
+
169
+ @Module({
170
+ controllers: [AppController],
171
+ })
172
+ class AppModule {}
173
+
174
+ expect(AppModule).toBeDefined();
175
+ });
176
+ });
177
+
178
+ describe('Route Decorators (README)', () => {
179
+ it('should define routes with HTTP method decorators', () => {
180
+ // From README: Route Decorators example
181
+ @Controller('/users')
182
+ class UsersController extends BaseController {
183
+ @Get()
184
+ getAllUsers() {
185
+ // Handle GET /users
186
+ return [];
187
+ }
188
+
189
+ @Get('/:id')
190
+ getUserById(@Param('id') id: string) {
191
+ // Handle GET /users/:id
192
+ return { id };
193
+ }
194
+
195
+ @Post()
196
+ createUser(@Body() userData: unknown) {
197
+ // Handle POST /users
198
+ return userData;
199
+ }
200
+
201
+ @Put('/:id')
202
+ updateUser(@Param('id') id: string, @Body() userData: unknown) {
203
+ // Handle PUT /users/:id
204
+ return { id, ...userData as object };
205
+ }
206
+
207
+ @Delete('/:id')
208
+ deleteUser(@Param('id') id: string) {
209
+ // Handle DELETE /users/:id
210
+ return { deleted: id };
211
+ }
212
+ }
213
+
214
+ expect(UsersController).toBeDefined();
215
+ });
216
+ });
217
+
218
+ describe('Parameter Decorators (README)', () => {
219
+ it('should use parameter decorators', () => {
220
+ // From README: Parameter Decorators example
221
+ @Controller('/api')
222
+ class ApiController extends BaseController {
223
+ @Get('/search')
224
+ search(
225
+ @Query('q') query: string,
226
+ @Query('limit') limit: string,
227
+ ) {
228
+ // Handle GET /api/search?q=something&limit=10
229
+ return { results: [], query, limit };
230
+ }
231
+
232
+ @Post('/users/:id/profile')
233
+ updateProfile(
234
+ @Param('id') userId: string,
235
+ @Body() _profileData: unknown,
236
+ @Header('Authorization') _token: string,
237
+ ) {
238
+ // Handle POST /api/users/123/profile
239
+ return { success: true, userId };
240
+ }
241
+ }
242
+
243
+ expect(ApiController).toBeDefined();
244
+ });
245
+ });
246
+
247
+ describe('Middleware (README)', () => {
248
+ it('should use middleware decorator', () => {
249
+ // From README: Middleware example
250
+ function loggerMiddleware(
251
+ _req: Request,
252
+ next: () => Promise<Response>,
253
+ ): Promise<Response> {
254
+ // eslint-disable-next-line no-console
255
+ console.log('Request received');
256
+
257
+ return next();
258
+ }
259
+
260
+ function authMiddleware(
261
+ req: Request,
262
+ next: () => Promise<Response>,
263
+ ): Promise<Response> {
264
+ const token = req.headers.get('Authorization');
265
+ if (!token) {
266
+ return Promise.resolve(new Response('Unauthorized', { status: 401 }));
267
+ }
268
+
269
+ return next();
270
+ }
271
+
272
+ @Controller('/admin')
273
+ class AdminController extends BaseController {
274
+ @Get('/dashboard')
275
+ @UseMiddleware(loggerMiddleware, authMiddleware)
276
+ getDashboard() {
277
+ return { stats: {} };
278
+ }
279
+ }
280
+
281
+ expect(AdminController).toBeDefined();
282
+ expect(loggerMiddleware).toBeDefined();
283
+ expect(authMiddleware).toBeDefined();
284
+ });
285
+ });
286
+
287
+ describe('Services (README)', () => {
288
+ it('should define service with @Service decorator', () => {
289
+ // From README: Services example
290
+ @Service()
291
+ class UserService extends BaseService {
292
+ private users: Array<{ id: string; name?: string }> = [];
293
+
294
+ findAll() {
295
+ return this.users;
296
+ }
297
+
298
+ findById(id: string) {
299
+ return this.users.find((user) => user.id === id);
300
+ }
301
+
302
+ create(userData: { name: string }) {
303
+ const user = { id: Date.now().toString(), ...userData };
304
+ this.users.push(user);
305
+
306
+ return user;
307
+ }
308
+ }
309
+
310
+ expect(UserService).toBeDefined();
311
+ });
312
+ });
313
+
314
+ describe('Modules (README)', () => {
315
+ it('should define module with providers and exports', () => {
316
+ // From README: Modules example
317
+ @Service()
318
+ class UsersService extends BaseService {}
319
+
320
+ @Controller('/users')
321
+ class UsersController extends BaseController {}
322
+
323
+ @Module({
324
+ controllers: [UsersController],
325
+ providers: [UsersService],
326
+ })
327
+ class UsersModule {}
328
+
329
+ expect(UsersModule).toBeDefined();
330
+ });
331
+ });
332
+ });
333
+
334
+ describe('Decorators API Documentation Examples', () => {
335
+ describe('@Module() decorator (docs/api/decorators.md)', () => {
336
+ it('should define module with all options', () => {
337
+ @Service()
338
+ class UserService extends BaseService {}
339
+
340
+ @Controller('/api/users')
341
+ class UserController extends BaseController {}
342
+
343
+ // From docs: @Module() example
344
+ @Module({
345
+ imports: [], // Other modules to import
346
+ controllers: [UserController],
347
+ providers: [UserService],
348
+ exports: [UserService],
349
+ })
350
+ class UserModule {}
351
+
352
+ expect(UserModule).toBeDefined();
353
+ });
354
+ });
355
+
356
+ describe('@Controller() decorator (docs/api/decorators.md)', () => {
357
+ it('should define controller with base path', () => {
358
+ // From docs: @Controller() example
359
+ @Controller('/api/users')
360
+ class UserController extends BaseController {
361
+ // All routes will be prefixed with /api/users
362
+ }
363
+
364
+ expect(UserController).toBeDefined();
365
+ });
366
+ });
367
+
368
+ describe('HTTP Method Decorators (docs/api/decorators.md)', () => {
369
+ it('should support all HTTP methods', () => {
370
+ // From docs: HTTP Method Decorators
371
+ @Controller('/users')
372
+ class UserController extends BaseController {
373
+ @Get('/') // GET /users
374
+ findAll() {
375
+ return [];
376
+ }
377
+
378
+ @Get('/:id') // GET /users/123
379
+ findOne(@Param('id') _id: string) {
380
+ return {};
381
+ }
382
+
383
+ @Get('/:userId/posts') // GET /users/123/posts
384
+ getUserPosts(@Param('userId') _userId: string) {
385
+ return [];
386
+ }
387
+
388
+ @Post('/') // POST /users
389
+ create(@Body() _body: unknown) {
390
+ return {};
391
+ }
392
+
393
+ @Put('/:id') // PUT /users/123
394
+ update(@Param('id') _id: string, @Body() _body: unknown) {
395
+ return {};
396
+ }
397
+
398
+ @Delete('/:id') // DELETE /users/123
399
+ remove(@Param('id') _id: string) {
400
+ return {};
401
+ }
402
+
403
+ @Patch('/:id') // PATCH /users/123
404
+ partialUpdate(@Param('id') _id: string, @Body() _body: unknown) {
405
+ return {};
406
+ }
407
+ }
408
+
409
+ expect(UserController).toBeDefined();
410
+ });
411
+ });
412
+
413
+ describe('Parameter Decorators (docs/api/decorators.md)', () => {
414
+ it('should support @Param decorator', () => {
415
+ // From docs: @Param() example
416
+ @Controller('/api')
417
+ class ApiController extends BaseController {
418
+ @Get('/:id')
419
+ findOne(
420
+ @Param('id') id: string, // No validation
421
+ ) {
422
+ return { id };
423
+ }
424
+ }
425
+
426
+ expect(ApiController).toBeDefined();
427
+ });
428
+
429
+ it('should support @Query decorator', () => {
430
+ // From docs: @Query() example
431
+ @Controller('/api')
432
+ class ApiController extends BaseController {
433
+ // GET /users?page=1&limit=10
434
+ @Get('/users')
435
+ findAll(@Query('page') page?: string, @Query('limit') limit?: string) {
436
+ return { page, limit };
437
+ }
438
+ }
439
+
440
+ expect(ApiController).toBeDefined();
441
+ });
442
+
443
+ it('should support @Header decorator', () => {
444
+ // From docs: @Header() example
445
+ @Controller('/api')
446
+ class ApiController extends BaseController {
447
+ @Get('/protected')
448
+ protected(
449
+ @Header('Authorization') auth: string,
450
+ @Header('X-Request-ID') requestId?: string,
451
+ ) {
452
+ return { auth: !!auth, requestId };
453
+ }
454
+ }
455
+
456
+ expect(ApiController).toBeDefined();
457
+ });
458
+
459
+ it('should support @Req decorator', () => {
460
+ // From docs: @Req() example
461
+ @Controller('/api')
462
+ class ApiController extends BaseController {
463
+ @Get('/raw')
464
+ handleRaw(@Req() request: Request) {
465
+ const url = new URL(request.url);
466
+
467
+ return { path: url.pathname };
468
+ }
469
+ }
470
+
471
+ expect(ApiController).toBeDefined();
472
+ });
473
+ });
474
+
475
+ describe('@Service() decorator (docs/api/decorators.md)', () => {
476
+ it('should define service with auto-generated tag', () => {
477
+ // From docs: @Service() example
478
+ @Service()
479
+ class UserService extends BaseService {
480
+ async findAll(): Promise<unknown[]> {
481
+ this.logger.info('Finding all users');
482
+
483
+ return [];
484
+ }
485
+ }
486
+
487
+ expect(UserService).toBeDefined();
488
+ });
489
+ });
490
+
491
+ describe('@UseMiddleware() decorator (docs/api/decorators.md)', () => {
492
+ it('should apply middleware to route handler', () => {
493
+ // From docs: @UseMiddleware() example
494
+ const authMiddleware = async (
495
+ req: Request,
496
+ next: () => Promise<Response>,
497
+ ) => {
498
+ const token = req.headers.get('Authorization');
499
+ if (!token) {
500
+ return new Response('Unauthorized', { status: 401 });
501
+ }
502
+
503
+ return await next();
504
+ };
505
+
506
+ const logMiddleware = async (
507
+ _req: Request,
508
+ next: () => Promise<Response>,
509
+ ) => {
510
+ // eslint-disable-next-line no-console
511
+ console.log('Request logged');
512
+
513
+ return await next();
514
+ };
515
+
516
+ @Controller('/users')
517
+ class UserController extends BaseController {
518
+ @Get('/protected')
519
+ @UseMiddleware(authMiddleware)
520
+ protectedRoute() {
521
+ return { message: 'Secret data' };
522
+ }
523
+
524
+ @Post('/action')
525
+ @UseMiddleware(logMiddleware, authMiddleware) // Multiple middleware
526
+ action() {
527
+ return { message: 'Action performed' };
528
+ }
529
+ }
530
+
531
+ expect(UserController).toBeDefined();
532
+ });
533
+ });
534
+ });
535
+
536
+ describe('Controllers API Documentation Examples', () => {
537
+ describe('BaseController (docs/api/controllers.md)', () => {
538
+ it('should extend BaseController for built-in features', () => {
539
+ @Service()
540
+ class UserService extends BaseService {
541
+ findAll() {
542
+ return [];
543
+ }
544
+ }
545
+
546
+ // From docs: Usage example
547
+ @Controller('/users')
548
+ class UserController extends BaseController {
549
+ constructor(private userService: UserService) {
550
+ super(); // Always call super()
551
+ }
552
+
553
+ @Get('/')
554
+ async findAll(): Promise<Response> {
555
+ const users = this.userService.findAll();
556
+
557
+ return this.success(users);
558
+ }
559
+ }
560
+
561
+ expect(UserController).toBeDefined();
562
+ });
563
+ });
564
+
565
+ describe('Response Methods (docs/api/controllers.md)', () => {
566
+ it('should have success() method', async () => {
567
+ @Controller('/test')
568
+ class TestController extends BaseController {
569
+ @Get('/')
570
+ async test(): Promise<Response> {
571
+ // From docs: success() examples
572
+ return this.success({ name: 'John', age: 30 });
573
+ }
574
+ }
575
+
576
+ expect(TestController).toBeDefined();
577
+ });
578
+
579
+ it('should have error() method', () => {
580
+ @Controller('/test')
581
+ class TestController extends BaseController {
582
+ @Get('/:id')
583
+ async findOne(): Promise<Response> {
584
+ // From docs: error() examples
585
+ return this.error('User not found', 404, 404);
586
+ }
587
+ }
588
+
589
+ expect(TestController).toBeDefined();
590
+ });
591
+
592
+ it('should have json() method', () => {
593
+ @Controller('/test')
594
+ class TestController extends BaseController {
595
+ @Get('/')
596
+ async test(): Promise<Response> {
597
+ return this.json({ data: 'test' });
598
+ }
599
+ }
600
+
601
+ expect(TestController).toBeDefined();
602
+ });
603
+
604
+ /**
605
+ * @source docs/api/controllers.md#text
606
+ */
607
+ it('should have text() method', () => {
608
+ @Controller('/test')
609
+ class TestController extends BaseController {
610
+ @Get('/health')
611
+ async health(): Promise<Response> {
612
+ // From docs: text() example
613
+ return this.text('OK');
614
+ }
615
+ }
616
+
617
+ expect(TestController).toBeDefined();
618
+ });
619
+ });
620
+
621
+ describe('Request Helpers (docs/api/controllers.md)', () => {
622
+ /**
623
+ * @source docs/api/controllers.md#isjson
624
+ */
625
+ it('should have isJson() method', () => {
626
+ @Controller('/test')
627
+ class TestController extends BaseController {
628
+ @Post('/')
629
+ async create(@Req() req: Request): Promise<Response> {
630
+ // From docs: isJson() example
631
+ if (!this.isJson(req)) {
632
+ return this.error('Content-Type must be application/json', 400, 400);
633
+ }
634
+
635
+ return this.success({ received: true });
636
+ }
637
+ }
638
+
639
+ expect(TestController).toBeDefined();
640
+ // Verify isJson method exists on prototype
641
+ const controller = new TestController();
642
+ expect(typeof controller['isJson']).toBe('function');
643
+ });
644
+
645
+ /**
646
+ * @source docs/api/controllers.md#parsejson
647
+ */
648
+ it('should have parseJson() method', () => {
649
+ interface CreateUserDto {
650
+ name: string;
651
+ email: string;
652
+ }
653
+
654
+ @Controller('/test')
655
+ class TestController extends BaseController {
656
+ @Post('/')
657
+ async create(@Req() req: Request): Promise<Response> {
658
+ // From docs: parseJson() example
659
+ const body = await this.parseJson<CreateUserDto>(req);
660
+
661
+ return this.success(body);
662
+ }
663
+ }
664
+
665
+ expect(TestController).toBeDefined();
666
+ // Verify parseJson method exists on prototype
667
+ const controller = new TestController();
668
+ expect(typeof controller['parseJson']).toBe('function');
669
+ });
670
+ });
671
+
672
+ describe('Accessing Services (docs/api/controllers.md)', () => {
673
+ /**
674
+ * @source docs/api/controllers.md#via-getservice-legacy
675
+ */
676
+ it('should have getService() method', () => {
677
+ @Service()
678
+ class UserService extends BaseService {
679
+ findAll() {
680
+ return [];
681
+ }
682
+ }
683
+
684
+ @Controller('/users')
685
+ class UserController extends BaseController {
686
+ @Get('/')
687
+ async findAll(): Promise<Response> {
688
+ // From docs: getService() example
689
+ const userService = this.getService(UserService);
690
+ const users = userService.findAll();
691
+
692
+ return this.success(users);
693
+ }
694
+ }
695
+
696
+ expect(UserController).toBeDefined();
697
+ // Verify getService method exists on prototype
698
+ const controller = new UserController();
699
+ expect(typeof controller['getService']).toBe('function');
700
+ });
701
+ });
702
+
703
+ describe('Accessing Logger (docs/api/controllers.md)', () => {
704
+ /**
705
+ * @source docs/api/controllers.md#accessing-logger
706
+ */
707
+ it('should have access to logger', () => {
708
+ @Controller('/users')
709
+ class UserController extends BaseController {
710
+ @Get('/')
711
+ async findAll(): Promise<Response> {
712
+ // From docs: Accessing Logger example
713
+ // Log levels: trace, debug, info, warn, error, fatal
714
+ this.logger.info('Finding all users');
715
+ this.logger.debug('Request received', { timestamp: Date.now() });
716
+
717
+ return this.success([]);
718
+ }
719
+ }
720
+
721
+ expect(UserController).toBeDefined();
722
+ });
723
+ });
724
+
725
+ describe('Accessing Configuration (docs/api/controllers.md)', () => {
726
+ /**
727
+ * @source docs/api/controllers.md#accessing-configuration
728
+ */
729
+ it('should have access to config', () => {
730
+ @Controller('/users')
731
+ class UserController extends BaseController {
732
+ @Get('/info')
733
+ async info(): Promise<Response> {
734
+ // From docs: Accessing Configuration example
735
+ // Note: config is typed as unknown, needs casting
736
+ const configAvailable = this.config !== null;
737
+
738
+ return this.success({ configAvailable });
739
+ }
740
+ }
741
+
742
+ expect(UserController).toBeDefined();
743
+ });
744
+ });
745
+
746
+ describe('HTTP Status Codes (docs/api/controllers.md)', () => {
747
+ /**
748
+ * @source docs/api/controllers.md#http-status-codes
749
+ */
750
+ it('should use HttpStatusCode enum', () => {
751
+ @Controller('/users')
752
+ class UserController extends BaseController {
753
+ @Get('/:id')
754
+ async findOne(@Param('id') _id: string): Promise<Response> {
755
+ // From docs: HTTP Status Codes example
756
+ const user = null; // Simulated not found
757
+ if (!user) {
758
+ return this.error('Not found', HttpStatusCode.NOT_FOUND, HttpStatusCode.NOT_FOUND);
759
+ }
760
+
761
+ return this.success(user, HttpStatusCode.OK);
762
+ }
763
+
764
+ @Post('/')
765
+ async create(@Body() _body: unknown): Promise<Response> {
766
+ return this.success({ id: '123' }, HttpStatusCode.CREATED);
767
+ }
768
+ }
769
+
770
+ expect(UserController).toBeDefined();
771
+ expect(HttpStatusCode.OK).toBe(200);
772
+ expect(HttpStatusCode.CREATED).toBe(201);
773
+ expect(HttpStatusCode.NOT_FOUND).toBe(404);
774
+ });
775
+
776
+ /**
777
+ * @source docs/api/controllers.md#available-status-codes
778
+ */
779
+ it('should have all documented status codes', () => {
780
+ // From docs: Available Status Codes
781
+ expect(HttpStatusCode.OK).toBe(200);
782
+ expect(HttpStatusCode.CREATED).toBe(201);
783
+ expect(HttpStatusCode.NO_CONTENT).toBe(204);
784
+ expect(HttpStatusCode.BAD_REQUEST).toBe(400);
785
+ expect(HttpStatusCode.UNAUTHORIZED).toBe(401);
786
+ expect(HttpStatusCode.FORBIDDEN).toBe(403);
787
+ expect(HttpStatusCode.NOT_FOUND).toBe(404);
788
+ expect(HttpStatusCode.CONFLICT).toBe(409);
789
+ expect(HttpStatusCode.INTERNAL_SERVER_ERROR).toBe(500);
790
+ });
791
+ });
792
+ });
793
+
794
+ describe('Services API Documentation Examples', () => {
795
+ describe('BaseService (docs/api/services.md)', () => {
796
+ it('should create basic service', () => {
797
+ // From docs: Basic Service example
798
+ @Service()
799
+ class CounterService extends BaseService {
800
+ private count = 0;
801
+
802
+ increment(): number {
803
+ this.count++;
804
+ this.logger.debug('Counter incremented', { count: this.count });
805
+
806
+ return this.count;
807
+ }
808
+
809
+ decrement(): number {
810
+ this.count--;
811
+
812
+ return this.count;
813
+ }
814
+
815
+ getValue(): number {
816
+ return this.count;
817
+ }
818
+ }
819
+
820
+ expect(CounterService).toBeDefined();
821
+ });
822
+
823
+ it('should create service with dependencies', () => {
824
+ // From docs: Service with Dependencies example
825
+ @Service()
826
+ class UserRepository extends BaseService {}
827
+
828
+ @Service()
829
+ class UserService extends BaseService {
830
+ constructor(private repository: UserRepository) {
831
+ super(); // Must call super()
832
+ }
833
+ }
834
+
835
+ expect(UserService).toBeDefined();
836
+ });
837
+ });
838
+
839
+ describe('getServiceTag (docs/api/services.md)', () => {
840
+ /**
841
+ * @source docs/api/services.md#service-tags-advanced
842
+ */
843
+ it('should get service tag from class', () => {
844
+ @Service()
845
+ class MyService extends BaseService {}
846
+
847
+ const tag = getServiceTag(MyService);
848
+ expect(tag).toBeDefined();
849
+ });
850
+ });
851
+
852
+ describe('BaseService Methods (docs/api/services.md)', () => {
853
+ /**
854
+ * @source docs/api/services.md#class-definition
855
+ */
856
+ it('should have runEffect method', () => {
857
+ // From docs: BaseService has runEffect method for Effect.js integration
858
+ // Note: Cannot instantiate service without OneBunApplication context
859
+ // Check prototype instead
860
+ expect(typeof BaseService.prototype['runEffect']).toBe('function');
861
+ });
862
+
863
+ /**
864
+ * @source docs/api/services.md#class-definition
865
+ */
866
+ it('should have formatError method', () => {
867
+ // From docs: BaseService has formatError method
868
+ // Note: Cannot instantiate service without OneBunApplication context
869
+ // Check prototype instead
870
+ expect(typeof BaseService.prototype['formatError']).toBe('function');
871
+ });
872
+ });
873
+
874
+ describe('Service Logger (docs/api/services.md)', () => {
875
+ /**
876
+ * @source docs/api/services.md#log-levels
877
+ */
878
+ it('should support all log levels', () => {
879
+ @Service()
880
+ class EmailService extends BaseService {
881
+ async send() {
882
+ // From docs: Log Levels
883
+ this.logger.trace('Very detailed info'); // Level 0
884
+ this.logger.debug('Debug information'); // Level 1
885
+ this.logger.info('General information'); // Level 2
886
+ this.logger.warn('Warning message'); // Level 3
887
+ this.logger.error('Error occurred'); // Level 4
888
+ this.logger.fatal('Fatal error'); // Level 5
889
+ }
890
+ }
891
+
892
+ expect(EmailService).toBeDefined();
893
+ });
894
+ });
895
+ });
896
+
897
+ describe('Validation API Documentation Examples', () => {
898
+ describe('validate function (docs/api/validation.md)', () => {
899
+ /**
900
+ * @source docs/api/validation.md#basic-usage
901
+ */
902
+ it('should validate data against schema', () => {
903
+ // From docs: validate() requires arktype schema, not plain object
904
+ // arktype `type()` returns a callable schema
905
+ const userSchema = type({
906
+ name: 'string',
907
+ age: 'number',
908
+ });
909
+
910
+ const result = validate(userSchema, { name: 'John', age: 30 });
911
+
912
+ // Result should have success property
913
+ expect(result).toHaveProperty('success');
914
+ expect(result.success).toBe(true);
915
+ });
916
+
917
+ /**
918
+ * @source docs/api/validation.md#basic-usage
919
+ */
920
+ it('should return errors for invalid data', () => {
921
+ const userSchema = type({
922
+ name: 'string',
923
+ age: 'number',
924
+ });
925
+
926
+ const result = validate(userSchema, { name: 'John', age: 'not a number' });
927
+
928
+ expect(result.success).toBe(false);
929
+ expect(result.errors).toBeDefined();
930
+ });
931
+ });
932
+
933
+ describe('validateOrThrow function (docs/api/validation.md)', () => {
934
+ /**
935
+ * @source docs/api/validation.md#validateorthrow
936
+ */
937
+ it('should throw on invalid data', () => {
938
+ const schema = type({
939
+ name: 'string',
940
+ age: 'number > 0',
941
+ });
942
+
943
+ // Valid data should not throw
944
+ expect(() => {
945
+ validateOrThrow(schema, { name: 'John', age: 30 });
946
+ }).not.toThrow();
947
+
948
+ // Invalid data should throw
949
+ expect(() => {
950
+ validateOrThrow(schema, { name: 'John', age: -5 });
951
+ }).toThrow();
952
+ });
953
+ });
954
+
955
+ describe('Schema Types (docs/api/validation.md)', () => {
956
+ /**
957
+ * @source docs/api/validation.md#primitives
958
+ */
959
+ it('should define primitive schemas', () => {
960
+ // From docs: Primitives
961
+ const stringSchema = type('string');
962
+ const numberSchema = type('number');
963
+ const booleanSchema = type('boolean');
964
+
965
+ expect(stringSchema).toBeDefined();
966
+ expect(numberSchema).toBeDefined();
967
+ expect(booleanSchema).toBeDefined();
968
+ });
969
+
970
+ /**
971
+ * @source docs/api/validation.md#string-constraints
972
+ */
973
+ it('should define string constraints', () => {
974
+ // From docs: String Constraints
975
+ const emailSchema = type('string.email');
976
+ const uuidSchema = type('string.uuid');
977
+
978
+ expect(emailSchema).toBeDefined();
979
+ expect(uuidSchema).toBeDefined();
980
+
981
+ // Validate email
982
+ const emailResult = validate(emailSchema, 'test@example.com');
983
+ expect(emailResult.success).toBe(true);
984
+ });
985
+
986
+ /**
987
+ * @source docs/api/validation.md#number-constraints
988
+ */
989
+ it('should define number constraints', () => {
990
+ // From docs: Number Constraints
991
+ const positiveSchema = type('number > 0');
992
+ const rangeSchema = type('0 <= number <= 100');
993
+
994
+ expect(positiveSchema).toBeDefined();
995
+ expect(rangeSchema).toBeDefined();
996
+
997
+ // Validate positive number
998
+ const positiveResult = validate(positiveSchema, 10);
999
+ expect(positiveResult.success).toBe(true);
1000
+ });
1001
+
1002
+ /**
1003
+ * @source docs/api/validation.md#arrays
1004
+ */
1005
+ it('should define array schemas', () => {
1006
+ // From docs: Arrays
1007
+ const stringArraySchema = type('string[]');
1008
+
1009
+ expect(stringArraySchema).toBeDefined();
1010
+
1011
+ const result = validate(stringArraySchema, ['a', 'b', 'c']);
1012
+ expect(result.success).toBe(true);
1013
+ });
1014
+
1015
+ /**
1016
+ * @source docs/api/validation.md#objects
1017
+ */
1018
+ it('should define object schemas', () => {
1019
+ // From docs: Objects
1020
+ /* eslint-disable @typescript-eslint/naming-convention */
1021
+ const userSchema = type({
1022
+ name: 'string',
1023
+ email: 'string.email',
1024
+ 'age?': 'number > 0', // Optional field
1025
+ });
1026
+ /* eslint-enable @typescript-eslint/naming-convention */
1027
+
1028
+ expect(userSchema).toBeDefined();
1029
+
1030
+ const result = validate(userSchema, {
1031
+ name: 'John',
1032
+ email: 'john@example.com',
1033
+ });
1034
+ expect(result.success).toBe(true);
1035
+ });
1036
+
1037
+ /**
1038
+ * @source docs/api/validation.md#using-in-controllers
1039
+ */
1040
+ it('should infer TypeScript type from schema', () => {
1041
+ // From docs: Type inference
1042
+ const userSchema = type({
1043
+ name: 'string',
1044
+ email: 'string.email',
1045
+ age: 'number > 0',
1046
+ });
1047
+ // Use userSchema to verify type inference
1048
+ expect(userSchema).toBeDefined();
1049
+
1050
+ type User = typeof userSchema.infer;
1051
+
1052
+ // TypeScript should infer: { name: string; email: string; age: number }
1053
+ const user: User = { name: 'John', email: 'john@example.com', age: 30 };
1054
+
1055
+ expect(user.name).toBe('John');
1056
+ expect(user.email).toBe('john@example.com');
1057
+ expect(user.age).toBe(30);
1058
+ });
1059
+ });
1060
+
1061
+ describe('Common Patterns (docs/api/validation.md)', () => {
1062
+ /**
1063
+ * @source docs/api/validation.md#create-update-dtos-pattern
1064
+ */
1065
+ it('should define create/update DTOs', () => {
1066
+ // From docs: Create/Update DTOs pattern
1067
+ const createUserSchema = type({
1068
+ name: 'string',
1069
+ email: 'string.email',
1070
+ password: 'string',
1071
+ });
1072
+
1073
+ /* eslint-disable @typescript-eslint/naming-convention */
1074
+ const updateUserSchema = type({
1075
+ 'name?': 'string',
1076
+ 'email?': 'string.email',
1077
+ });
1078
+ /* eslint-enable @typescript-eslint/naming-convention */
1079
+
1080
+ expect(createUserSchema).toBeDefined();
1081
+ expect(updateUserSchema).toBeDefined();
1082
+ });
1083
+
1084
+ /**
1085
+ * @source docs/api/validation.md#pagination-schema
1086
+ */
1087
+ it('should define pagination schema', () => {
1088
+ // From docs: Pagination Schema
1089
+ /* eslint-disable @typescript-eslint/naming-convention */
1090
+ const paginationSchema = type({
1091
+ 'page?': 'number > 0',
1092
+ 'limit?': 'number > 0',
1093
+ });
1094
+ /* eslint-enable @typescript-eslint/naming-convention */
1095
+
1096
+ expect(paginationSchema).toBeDefined();
1097
+
1098
+ const result = validate(paginationSchema, { page: 1, limit: 10 });
1099
+ expect(result.success).toBe(true);
1100
+ });
1101
+ });
1102
+ });
1103
+
1104
+ describe('Error Classes Examples', () => {
1105
+ describe('NotFoundError (docs/api/requests.md)', () => {
1106
+ it('should create NotFoundError', () => {
1107
+ // From docs: Error Classes example
1108
+ // NotFoundError(error: string, details?: Record<string, unknown>)
1109
+ const error = new NotFoundError('User not found', { userId: '123' });
1110
+
1111
+ expect(error).toBeInstanceOf(OneBunBaseError);
1112
+ expect(error.message).toContain('User not found');
1113
+ });
1114
+ });
1115
+
1116
+ describe('InternalServerError', () => {
1117
+ it('should create InternalServerError', () => {
1118
+ const error = new InternalServerError('Something went wrong');
1119
+
1120
+ expect(error).toBeInstanceOf(OneBunBaseError);
1121
+ expect(error.message).toBe('Something went wrong');
1122
+ });
1123
+ });
1124
+ });
1125
+
1126
+ describe('HttpStatusCode (docs/api/requests.md)', () => {
1127
+ it('should have correct status codes', () => {
1128
+ // From docs: Available Status Codes
1129
+ expect(HttpStatusCode.OK).toBe(200);
1130
+ expect(HttpStatusCode.CREATED).toBe(201);
1131
+ expect(HttpStatusCode.BAD_REQUEST).toBe(400);
1132
+ expect(HttpStatusCode.UNAUTHORIZED).toBe(401);
1133
+ expect(HttpStatusCode.FORBIDDEN).toBe(403);
1134
+ expect(HttpStatusCode.NOT_FOUND).toBe(404);
1135
+ expect(HttpStatusCode.CONFLICT).toBe(409);
1136
+ expect(HttpStatusCode.UNPROCESSABLE_ENTITY).toBe(422);
1137
+ expect(HttpStatusCode.INTERNAL_SERVER_ERROR).toBe(500);
1138
+ });
1139
+ });
1140
+
1141
+ describe('Env Helper (docs/api/envs.md)', () => {
1142
+ describe('Environment Variable Types', () => {
1143
+ it('should create string configuration', () => {
1144
+ const config = Env.string({ default: 'localhost' });
1145
+ expect(config.type).toBe('string');
1146
+ });
1147
+
1148
+ it('should create number configuration', () => {
1149
+ const config = Env.number({ default: 3000 });
1150
+ expect(config.type).toBe('number');
1151
+ });
1152
+
1153
+ it('should create boolean configuration', () => {
1154
+ const config = Env.boolean({ default: false });
1155
+ expect(config.type).toBe('boolean');
1156
+ });
1157
+
1158
+ it('should create array configuration', () => {
1159
+ const config = Env.array({ default: ['a', 'b'] });
1160
+ expect(config.type).toBe('array');
1161
+ });
1162
+ });
1163
+
1164
+ describe('Built-in Validators', () => {
1165
+ it('should have port validator', () => {
1166
+ const validator = Env.port();
1167
+ expect(typeof validator).toBe('function');
1168
+ });
1169
+
1170
+ it('should have url validator', () => {
1171
+ const validator = Env.url();
1172
+ expect(typeof validator).toBe('function');
1173
+ });
1174
+
1175
+ it('should have email validator', () => {
1176
+ const validator = Env.email();
1177
+ expect(typeof validator).toBe('function');
1178
+ });
1179
+
1180
+ it('should have oneOf validator', () => {
1181
+ const validator = Env.oneOf(['a', 'b', 'c']);
1182
+ expect(typeof validator).toBe('function');
1183
+ });
1184
+
1185
+ it('should have regex validator', () => {
1186
+ const validator = Env.regex(/^[a-z]+$/);
1187
+ expect(typeof validator).toBe('function');
1188
+ });
1189
+ });
1190
+ });
1191
+
1192
+ describe('Service Definition and Client (docs/api/requests.md)', () => {
1193
+ it('should create service definition from module class', () => {
1194
+ // From docs: createServiceDefinition expects a module class decorated with @Module
1195
+ @Controller('/users')
1196
+ class UsersController extends BaseController {
1197
+ @Get('/')
1198
+ findAll() {
1199
+ return [];
1200
+ }
1201
+
1202
+ @Get('/:id')
1203
+ findById(@Param('id') id: string) {
1204
+ return { id };
1205
+ }
1206
+
1207
+ @Post('/')
1208
+ create(@Body() data: unknown) {
1209
+ return data;
1210
+ }
1211
+ }
1212
+
1213
+ @Module({
1214
+ controllers: [UsersController],
1215
+ })
1216
+ class UsersModule {}
1217
+
1218
+ const UsersServiceDefinition = createServiceDefinition(UsersModule);
1219
+
1220
+ expect(UsersServiceDefinition).toBeDefined();
1221
+ expect(UsersServiceDefinition._endpoints).toBeDefined();
1222
+ expect(UsersServiceDefinition._controllers).toBeDefined();
1223
+ });
1224
+
1225
+ it('should create service client from definition', () => {
1226
+ @Controller('/users')
1227
+ class UsersController extends BaseController {
1228
+ @Get('/')
1229
+ findAll() {
1230
+ return [];
1231
+ }
1232
+ }
1233
+
1234
+ @Module({
1235
+ controllers: [UsersController],
1236
+ })
1237
+ class UsersModule {}
1238
+
1239
+ const usersDefinition = createServiceDefinition(UsersModule);
1240
+
1241
+ // From docs: Create typed client
1242
+ // Note: option is 'url', not 'baseUrl'
1243
+ const usersClient = createServiceClient(usersDefinition, {
1244
+ url: 'http://users-service:3001',
1245
+ });
1246
+
1247
+ expect(usersClient).toBeDefined();
1248
+ });
1249
+ });
1250
+
1251
+ describe('OneBunApplication (docs/api/core.md)', () => {
1252
+ /**
1253
+ * @source docs/api/core.md#onebunapplication
1254
+ */
1255
+ it('should create application instance', () => {
1256
+ @Controller('/api')
1257
+ class AppController extends BaseController {
1258
+ @Get('/hello')
1259
+ hello() {
1260
+ return { message: 'Hello' };
1261
+ }
1262
+ }
1263
+
1264
+ @Module({
1265
+ controllers: [AppController],
1266
+ })
1267
+ class AppModule {}
1268
+
1269
+ // From docs: OneBunApplication constructor
1270
+ const app = new OneBunApplication(AppModule, {
1271
+ port: 3000,
1272
+ basePath: '/api/v1',
1273
+ });
1274
+
1275
+ expect(app).toBeDefined();
1276
+ expect(typeof app.start).toBe('function');
1277
+ expect(typeof app.stop).toBe('function');
1278
+ expect(typeof app.getConfig).toBe('function');
1279
+ expect(typeof app.getLogger).toBe('function');
1280
+ expect(typeof app.getHttpUrl).toBe('function');
1281
+ expect(typeof app.getLayer).toBe('function');
1282
+ });
1283
+
1284
+ /**
1285
+ * @source docs/api/core.md#applicationoptions
1286
+ */
1287
+ it('should accept full application options', () => {
1288
+ @Module({ controllers: [] })
1289
+ class AppModule {}
1290
+
1291
+ // From docs: ApplicationOptions interface
1292
+ const options = {
1293
+ name: 'my-app',
1294
+ port: 3000,
1295
+ host: '0.0.0.0',
1296
+ basePath: '/api/v1',
1297
+ routePrefix: 'myservice',
1298
+ development: true,
1299
+ metrics: {
1300
+ enabled: true,
1301
+ path: '/metrics',
1302
+ prefix: 'myapp_',
1303
+ collectHttpMetrics: true,
1304
+ collectSystemMetrics: true,
1305
+ collectGcMetrics: true,
1306
+ },
1307
+ tracing: {
1308
+ enabled: true,
1309
+ serviceName: 'my-service',
1310
+ samplingRate: 1.0,
1311
+ },
1312
+ };
1313
+
1314
+ const app = new OneBunApplication(AppModule, options);
1315
+ expect(app).toBeDefined();
1316
+ });
1317
+
1318
+ /**
1319
+ * @source docs/api/core.md#metrics-options
1320
+ */
1321
+ it('should accept metrics configuration', () => {
1322
+ @Module({ controllers: [] })
1323
+ class AppModule {}
1324
+
1325
+ // From docs: MetricsOptions interface
1326
+ const metricsOptions = {
1327
+ enabled: true,
1328
+ path: '/metrics',
1329
+ defaultLabels: { service: 'my-service', environment: 'development' },
1330
+ collectHttpMetrics: true,
1331
+ collectSystemMetrics: true,
1332
+ collectGcMetrics: true,
1333
+ systemMetricsInterval: 5000,
1334
+ prefix: 'onebun_',
1335
+ httpDurationBuckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
1336
+ };
1337
+
1338
+ const app = new OneBunApplication(AppModule, { metrics: metricsOptions });
1339
+ expect(app).toBeDefined();
1340
+ });
1341
+
1342
+ /**
1343
+ * @source docs/api/core.md#tracing-options
1344
+ */
1345
+ it('should accept tracing configuration', () => {
1346
+ @Module({ controllers: [] })
1347
+ class AppModule {}
1348
+
1349
+ // From docs: TracingOptions interface
1350
+ /* eslint-disable @typescript-eslint/naming-convention */
1351
+ const tracingOptions = {
1352
+ enabled: true,
1353
+ serviceName: 'my-service',
1354
+ serviceVersion: '1.0.0',
1355
+ samplingRate: 1.0,
1356
+ traceHttpRequests: true,
1357
+ traceDatabaseQueries: true,
1358
+ defaultAttributes: { 'deployment.environment': 'production' },
1359
+ exportOptions: {
1360
+ endpoint: 'http://localhost:4318/v1/traces',
1361
+ headers: { Authorization: 'Bearer token' },
1362
+ timeout: 30000,
1363
+ batchSize: 100,
1364
+ batchTimeout: 5000,
1365
+ },
1366
+ };
1367
+ /* eslint-enable @typescript-eslint/naming-convention */
1368
+
1369
+ const app = new OneBunApplication(AppModule, { tracing: tracingOptions });
1370
+ expect(app).toBeDefined();
1371
+ });
1372
+ });
1373
+
1374
+ describe('MultiServiceApplication (docs/api/core.md)', () => {
1375
+ /**
1376
+ * @source docs/api/core.md#multiserviceapplication
1377
+ */
1378
+ it('should define multi-service configuration type', () => {
1379
+ // From docs: MultiServiceApplicationOptions
1380
+ @Module({
1381
+ controllers: [],
1382
+ })
1383
+ class UsersModule {}
1384
+
1385
+ @Module({
1386
+ controllers: [],
1387
+ })
1388
+ class OrdersModule {}
1389
+
1390
+ // This is just type checking, actual startup requires environment
1391
+ const config = {
1392
+ services: {
1393
+ users: {
1394
+ module: UsersModule,
1395
+ port: 3001,
1396
+ routePrefix: true,
1397
+ },
1398
+ orders: {
1399
+ module: OrdersModule,
1400
+ port: 3002,
1401
+ routePrefix: true,
1402
+ },
1403
+ },
1404
+ enabledServices: ['users', 'orders'],
1405
+ };
1406
+
1407
+ expect(config.services.users.module).toBe(UsersModule);
1408
+ expect(config.services.orders.module).toBe(OrdersModule);
1409
+ });
1410
+
1411
+ /**
1412
+ * @source docs/api/core.md#usage-example-1
1413
+ */
1414
+ it('should create MultiServiceApplication with service config', () => {
1415
+ @Module({ controllers: [] })
1416
+ class UsersModule {}
1417
+
1418
+ @Module({ controllers: [] })
1419
+ class OrdersModule {}
1420
+
1421
+ // From docs: MultiServiceApplication usage example
1422
+ // Note: routePrefix is boolean (true = use service name as prefix)
1423
+ const multiApp = new MultiServiceApplication({
1424
+ services: {
1425
+ users: {
1426
+ module: UsersModule,
1427
+ port: 3001,
1428
+ routePrefix: true, // Uses 'users' as route prefix
1429
+ },
1430
+ orders: {
1431
+ module: OrdersModule,
1432
+ port: 3002,
1433
+ routePrefix: true, // Uses 'orders' as route prefix
1434
+ envOverrides: {
1435
+ DB_NAME: { value: 'orders_db' },
1436
+ },
1437
+ },
1438
+ },
1439
+ enabledServices: ['users', 'orders'],
1440
+ });
1441
+
1442
+ expect(multiApp).toBeDefined();
1443
+ expect(typeof multiApp.start).toBe('function');
1444
+ expect(typeof multiApp.stop).toBe('function');
1445
+ expect(typeof multiApp.getRunningServices).toBe('function');
1446
+ });
1447
+ });
1448
+
1449
+ // ============================================================================
1450
+ // docs/examples Tests
1451
+ // ============================================================================
1452
+
1453
+ describe('Basic App Example (docs/examples/basic-app.md)', () => {
1454
+ /**
1455
+ * @source docs/examples/basic-app.md#srcconfigts
1456
+ */
1457
+ it('should define environment schema', () => {
1458
+ // From docs: src/config.ts
1459
+ const envSchema = {
1460
+ server: {
1461
+ port: Env.number({ default: 3000, env: 'PORT' }),
1462
+ host: Env.string({ default: '0.0.0.0', env: 'HOST' }),
1463
+ },
1464
+ app: {
1465
+ name: Env.string({ default: 'basic-app', env: 'APP_NAME' }),
1466
+ debug: Env.boolean({ default: false, env: 'DEBUG' }),
1467
+ },
1468
+ };
1469
+
1470
+ expect(envSchema.server.port).toBeDefined();
1471
+ expect(envSchema.server.host).toBeDefined();
1472
+ expect(envSchema.app.name).toBeDefined();
1473
+ expect(envSchema.app.debug).toBeDefined();
1474
+ });
1475
+
1476
+ /**
1477
+ * @source docs/examples/basic-app.md#srchelloservicets
1478
+ */
1479
+ it('should define HelloService', () => {
1480
+ // From docs: src/hello.service.ts
1481
+ @Service()
1482
+ class HelloService extends BaseService {
1483
+ private greetCount = 0;
1484
+
1485
+ greet(name: string): string {
1486
+ this.greetCount++;
1487
+
1488
+ return `Hello, ${name}! You are visitor #${this.greetCount}`;
1489
+ }
1490
+
1491
+ sayHello(): string {
1492
+ return 'Hello from OneBun!';
1493
+ }
1494
+
1495
+ getStats(): { greetCount: number } {
1496
+ return { greetCount: this.greetCount };
1497
+ }
1498
+ }
1499
+
1500
+ expect(HelloService).toBeDefined();
1501
+ });
1502
+
1503
+ /**
1504
+ * @source docs/examples/basic-app.md#srchellocontrollerts
1505
+ */
1506
+ it('should define HelloController', () => {
1507
+ @Service()
1508
+ class HelloService extends BaseService {
1509
+ sayHello(): string {
1510
+ return 'Hello!';
1511
+ }
1512
+
1513
+ greet(name: string): string {
1514
+ return `Hello, ${name}!`;
1515
+ }
1516
+
1517
+ getStats() {
1518
+ return { greetCount: 0 };
1519
+ }
1520
+ }
1521
+
1522
+ // From docs: src/hello.controller.ts
1523
+ @Controller('/api')
1524
+ class HelloController extends BaseController {
1525
+ constructor(private helloService: HelloService) {
1526
+ super();
1527
+ }
1528
+
1529
+ @Get('/hello')
1530
+ async hello(): Promise<Response> {
1531
+ const message = this.helloService.sayHello();
1532
+
1533
+ return this.success({ message });
1534
+ }
1535
+
1536
+ @Get('/hello/:name')
1537
+ async greet(@Param('name') name: string): Promise<Response> {
1538
+ const greeting = this.helloService.greet(name);
1539
+
1540
+ return this.success({ greeting });
1541
+ }
1542
+
1543
+ @Get('/stats')
1544
+ async stats(): Promise<Response> {
1545
+ const stats = this.helloService.getStats();
1546
+
1547
+ return this.success(stats);
1548
+ }
1549
+
1550
+ @Get('/health')
1551
+ async health(): Promise<Response> {
1552
+ return this.success({
1553
+ status: 'healthy',
1554
+ timestamp: new Date().toISOString(),
1555
+ });
1556
+ }
1557
+ }
1558
+
1559
+ expect(HelloController).toBeDefined();
1560
+ });
1561
+
1562
+ /**
1563
+ * @source docs/examples/basic-app.md#srcappmodulets
1564
+ */
1565
+ it('should define AppModule', () => {
1566
+ @Service()
1567
+ class HelloService extends BaseService {}
1568
+
1569
+ @Controller('/api')
1570
+ class HelloController extends BaseController {
1571
+ constructor(private helloService: HelloService) {
1572
+ super();
1573
+ }
1574
+ }
1575
+
1576
+ // From docs: src/app.module.ts
1577
+ @Module({
1578
+ controllers: [HelloController],
1579
+ providers: [HelloService],
1580
+ })
1581
+ class AppModule {}
1582
+
1583
+ expect(AppModule).toBeDefined();
1584
+ });
1585
+ });
1586
+
1587
+ describe('CRUD API Example (docs/examples/crud-api.md)', () => {
1588
+ /**
1589
+ * @source docs/examples/crud-api.md#srcusersschemasuserschemats
1590
+ */
1591
+ it('should define user schemas with validation', () => {
1592
+ // From docs: src/users/schemas/user.schema.ts
1593
+ /* eslint-disable @typescript-eslint/naming-convention */
1594
+ const createUserSchema = type({
1595
+ name: 'string',
1596
+ email: 'string.email',
1597
+ 'age?': 'number >= 0',
1598
+ });
1599
+
1600
+ const updateUserSchema = type({
1601
+ 'name?': 'string',
1602
+ 'email?': 'string.email',
1603
+ 'age?': 'number >= 0',
1604
+ });
1605
+ /* eslint-enable @typescript-eslint/naming-convention */
1606
+
1607
+ expect(createUserSchema).toBeDefined();
1608
+ expect(updateUserSchema).toBeDefined();
1609
+
1610
+ // Validate
1611
+ const result = validate(createUserSchema, {
1612
+ name: 'John',
1613
+ email: 'john@example.com',
1614
+ });
1615
+ expect(result.success).toBe(true);
1616
+ });
1617
+
1618
+ /**
1619
+ * @source docs/examples/crud-api.md#srcusersusersservicets
1620
+ */
1621
+ it('should define UsersService', () => {
1622
+ // From docs: src/users/users.service.ts
1623
+ @Service()
1624
+ class UsersRepository extends BaseService {
1625
+ private users: Array<{ id: string; name: string; email: string }> = [];
1626
+
1627
+ findAll() {
1628
+ return this.users;
1629
+ }
1630
+
1631
+ findById(id: string) {
1632
+ return this.users.find((u) => u.id === id) || null;
1633
+ }
1634
+
1635
+ create(data: { name: string; email: string }) {
1636
+ const user = { id: Date.now().toString(), ...data };
1637
+ this.users.push(user);
1638
+
1639
+ return user;
1640
+ }
1641
+
1642
+ update(id: string, data: Partial<{ name: string; email: string }>) {
1643
+ const index = this.users.findIndex((u) => u.id === id);
1644
+ if (index === -1) {
1645
+ return null;
1646
+ }
1647
+ this.users[index] = { ...this.users[index], ...data };
1648
+
1649
+ return this.users[index];
1650
+ }
1651
+
1652
+ delete(id: string): boolean {
1653
+ const index = this.users.findIndex((u) => u.id === id);
1654
+ if (index === -1) {
1655
+ return false;
1656
+ }
1657
+ this.users.splice(index, 1);
1658
+
1659
+ return true;
1660
+ }
1661
+ }
1662
+
1663
+ @Service()
1664
+ class UsersService extends BaseService {
1665
+ constructor(private repository: UsersRepository) {
1666
+ super();
1667
+ }
1668
+
1669
+ async findAll() {
1670
+ return this.repository.findAll();
1671
+ }
1672
+
1673
+ async findById(id: string) {
1674
+ return this.repository.findById(id);
1675
+ }
1676
+
1677
+ async create(data: { name: string; email: string }) {
1678
+ return this.repository.create(data);
1679
+ }
1680
+ }
1681
+
1682
+ expect(UsersService).toBeDefined();
1683
+ });
1684
+
1685
+ /**
1686
+ * @source docs/examples/crud-api.md#srcusersuserscontrollerts
1687
+ */
1688
+ it('should define UsersController with CRUD endpoints', () => {
1689
+ @Service()
1690
+ class UsersService extends BaseService {
1691
+ findAll() {
1692
+ return [];
1693
+ }
1694
+
1695
+ findById(id: string) {
1696
+ return { id };
1697
+ }
1698
+
1699
+ create(data: unknown) {
1700
+ return { id: '1', ...data as object };
1701
+ }
1702
+
1703
+ update(id: string, data: unknown) {
1704
+ return { id, ...data as object };
1705
+ }
1706
+
1707
+ delete(_id: string) {
1708
+ return true;
1709
+ }
1710
+ }
1711
+
1712
+ // From docs: src/users/users.controller.ts
1713
+ @Controller('/api/users')
1714
+ class UsersController extends BaseController {
1715
+ constructor(private usersService: UsersService) {
1716
+ super();
1717
+ }
1718
+
1719
+ @Get('/')
1720
+ async findAll(): Promise<Response> {
1721
+ const users = await this.usersService.findAll();
1722
+
1723
+ return this.success(users);
1724
+ }
1725
+
1726
+ @Get('/:id')
1727
+ async findOne(@Param('id') id: string): Promise<Response> {
1728
+ const user = await this.usersService.findById(id);
1729
+ if (!user) {
1730
+ return this.error('User not found', 404, 404);
1731
+ }
1732
+
1733
+ return this.success(user);
1734
+ }
1735
+
1736
+ @Post('/')
1737
+ async create(@Body() body: unknown): Promise<Response> {
1738
+ const user = await this.usersService.create(body);
1739
+
1740
+ return this.success(user, 201);
1741
+ }
1742
+
1743
+ @Put('/:id')
1744
+ async update(
1745
+ @Param('id') id: string,
1746
+ @Body() body: unknown,
1747
+ ): Promise<Response> {
1748
+ const user = await this.usersService.update(id, body);
1749
+ if (!user) {
1750
+ return this.error('User not found', 404, 404);
1751
+ }
1752
+
1753
+ return this.success(user);
1754
+ }
1755
+
1756
+ @Delete('/:id')
1757
+ async remove(@Param('id') id: string): Promise<Response> {
1758
+ const deleted = await this.usersService.delete(id);
1759
+ if (!deleted) {
1760
+ return this.error('User not found', 404, 404);
1761
+ }
1762
+
1763
+ return this.success({ deleted: true });
1764
+ }
1765
+ }
1766
+
1767
+ expect(UsersController).toBeDefined();
1768
+ });
1769
+
1770
+ /**
1771
+ * @source docs/examples/crud-api.md#srcusersusersmodulets
1772
+ */
1773
+ it('should define UsersModule', () => {
1774
+ @Service()
1775
+ class UsersRepository extends BaseService {}
1776
+
1777
+ @Service()
1778
+ class UsersService extends BaseService {}
1779
+
1780
+ @Controller('/api/users')
1781
+ class UsersController extends BaseController {}
1782
+
1783
+ // From docs: src/users/users.module.ts
1784
+ @Module({
1785
+ controllers: [UsersController],
1786
+ providers: [UsersService, UsersRepository],
1787
+ exports: [UsersService],
1788
+ })
1789
+ class UsersModule {}
1790
+
1791
+ expect(UsersModule).toBeDefined();
1792
+ });
1793
+ });
1794
+
1795
+ describe('Multi-Service Example (docs/examples/multi-service.md)', () => {
1796
+ /**
1797
+ * @source docs/examples/multi-service.md#srcusersusersmodulets
1798
+ */
1799
+ it('should define Users service module', () => {
1800
+ @Service()
1801
+ class UsersService extends BaseService {
1802
+ findById(id: string) {
1803
+ return { id, name: 'John' };
1804
+ }
1805
+ }
1806
+
1807
+ @Controller('/users')
1808
+ class UsersController extends BaseController {
1809
+ constructor(private usersService: UsersService) {
1810
+ super();
1811
+ }
1812
+
1813
+ @Get('/:id')
1814
+ async findOne(@Param('id') id: string): Promise<Response> {
1815
+ const user = this.usersService.findById(id);
1816
+
1817
+ return this.success(user);
1818
+ }
1819
+ }
1820
+
1821
+ @Module({
1822
+ controllers: [UsersController],
1823
+ providers: [UsersService],
1824
+ exports: [UsersService],
1825
+ })
1826
+ class UsersModule {}
1827
+
1828
+ expect(UsersModule).toBeDefined();
1829
+ });
1830
+
1831
+ /**
1832
+ * @source docs/examples/multi-service.md#srcordersordersmodulets
1833
+ */
1834
+ it('should define Orders service module', () => {
1835
+ @Service()
1836
+ class OrdersService extends BaseService {
1837
+ create(data: unknown) {
1838
+ return { id: '1', ...data as object };
1839
+ }
1840
+ }
1841
+
1842
+ @Controller('/orders')
1843
+ class OrdersController extends BaseController {
1844
+ constructor(private ordersService: OrdersService) {
1845
+ super();
1846
+ }
1847
+
1848
+ @Post('/')
1849
+ async create(@Body() body: unknown): Promise<Response> {
1850
+ const order = this.ordersService.create(body);
1851
+
1852
+ return this.success(order, 201);
1853
+ }
1854
+ }
1855
+
1856
+ @Module({
1857
+ controllers: [OrdersController],
1858
+ providers: [OrdersService],
1859
+ })
1860
+ class OrdersModule {}
1861
+
1862
+ expect(OrdersModule).toBeDefined();
1863
+ });
1864
+
1865
+ /**
1866
+ * @source docs/examples/multi-service.md#srcindexts
1867
+ */
1868
+ it('should define MultiServiceApplication configuration', () => {
1869
+ @Module({ controllers: [] })
1870
+ class UsersModule {}
1871
+
1872
+ @Module({ controllers: [] })
1873
+ class OrdersModule {}
1874
+
1875
+ // From docs: src/index.ts
1876
+ // Note: routePrefix is boolean (true = use service name as prefix)
1877
+ const multiApp = new MultiServiceApplication({
1878
+ services: {
1879
+ users: {
1880
+ module: UsersModule,
1881
+ port: 3001,
1882
+ routePrefix: true, // Uses 'users' as route prefix
1883
+ },
1884
+ orders: {
1885
+ module: OrdersModule,
1886
+ port: 3002,
1887
+ routePrefix: true, // Uses 'orders' as route prefix
1888
+ },
1889
+ },
1890
+ enabledServices: ['users', 'orders'],
1891
+ });
1892
+
1893
+ expect(multiApp).toBeDefined();
1894
+ expect(typeof multiApp.start).toBe('function');
1895
+ expect(typeof multiApp.stop).toBe('function');
1896
+ });
1897
+ });
1898
+
1899
+ // ============================================================================
1900
+ // Architecture & Getting Started Tests
1901
+ // ============================================================================
1902
+
1903
+ describe('Architecture Documentation (docs/architecture.md)', () => {
1904
+ describe('DI Resolution Flow (docs/architecture.md)', () => {
1905
+ /**
1906
+ * @source docs/architecture.md#di-resolution-flow
1907
+ */
1908
+ it('should demonstrate DI resolution flow', () => {
1909
+ // From docs: DI Resolution Flow example
1910
+ // 1. Service is decorated
1911
+ @Service()
1912
+ class CacheService extends BaseService {
1913
+ get(_key: string) {
1914
+ return null;
1915
+ }
1916
+ }
1917
+
1918
+ @Service()
1919
+ class UserService extends BaseService {
1920
+ constructor(private cacheService: CacheService) {
1921
+ super();
1922
+ }
1923
+ }
1924
+
1925
+ // 2. Module declares dependencies
1926
+ @Controller('/users')
1927
+ class UserController extends BaseController {
1928
+ constructor(private userService: UserService) {
1929
+ super();
1930
+ }
1931
+ }
1932
+
1933
+ @Module({
1934
+ providers: [CacheService, UserService],
1935
+ controllers: [UserController],
1936
+ })
1937
+ class UserModule {}
1938
+
1939
+ expect(UserModule).toBeDefined();
1940
+ });
1941
+
1942
+ /**
1943
+ * @source docs/architecture.md#explicit-injection
1944
+ */
1945
+ it('should demonstrate explicit injection pattern', () => {
1946
+ // From docs: Explicit Injection example
1947
+ @Service()
1948
+ class UserService extends BaseService {}
1949
+
1950
+ @Service()
1951
+ class CacheService extends BaseService {}
1952
+
1953
+ // For complex cases, use @Inject() - here we just verify pattern works
1954
+ @Controller('/users')
1955
+ class UserController extends BaseController {
1956
+ constructor(
1957
+ private userService: UserService,
1958
+ private cache: CacheService,
1959
+ ) {
1960
+ super();
1961
+ }
1962
+ }
1963
+
1964
+ expect(UserController).toBeDefined();
1965
+ });
1966
+ });
1967
+
1968
+ describe('Module System (docs/architecture.md)', () => {
1969
+ /**
1970
+ * @source docs/architecture.md#module-assembly
1971
+ */
1972
+ it('should demonstrate module export/import pattern', () => {
1973
+ // From docs: Module Assembly
1974
+ @Service()
1975
+ class SharedService extends BaseService {}
1976
+
1977
+ // Module that exports services
1978
+ @Module({
1979
+ providers: [SharedService],
1980
+ exports: [SharedService],
1981
+ })
1982
+ class SharedModule {}
1983
+
1984
+ // Module that imports services
1985
+ @Controller('/api')
1986
+ class ApiController extends BaseController {}
1987
+
1988
+ @Module({
1989
+ imports: [SharedModule],
1990
+ controllers: [ApiController],
1991
+ })
1992
+ class ApiModule {}
1993
+
1994
+ expect(SharedModule).toBeDefined();
1995
+ expect(ApiModule).toBeDefined();
1996
+ });
1997
+ });
1998
+ });
1999
+
2000
+ describe('Getting Started Documentation (docs/getting-started.md)', () => {
2001
+ describe('Environment Schema (docs/getting-started.md)', () => {
2002
+ /**
2003
+ * @source docs/getting-started.md#step-3-create-environment-schema
2004
+ */
2005
+ it('should define type-safe environment schema', () => {
2006
+ // From docs: src/config.ts
2007
+ const envSchema = {
2008
+ server: {
2009
+ port: Env.number({ default: 3000, env: 'PORT' }),
2010
+ host: Env.string({ default: '0.0.0.0', env: 'HOST' }),
2011
+ },
2012
+ app: {
2013
+ name: Env.string({ default: 'my-onebun-app', env: 'APP_NAME' }),
2014
+ debug: Env.boolean({ default: true, env: 'DEBUG' }),
2015
+ },
2016
+ database: {
2017
+ url: Env.string({ env: 'DATABASE_URL', sensitive: true }),
2018
+ },
2019
+ };
2020
+
2021
+ expect(envSchema.server.port.type).toBe('number');
2022
+ expect(envSchema.server.host.type).toBe('string');
2023
+ expect(envSchema.app.debug.type).toBe('boolean');
2024
+ expect(envSchema.database.url.sensitive).toBe(true);
2025
+ });
2026
+ });
2027
+
2028
+ describe('Service Creation (docs/getting-started.md)', () => {
2029
+ /**
2030
+ * @source docs/getting-started.md#step-4-create-a-service
2031
+ */
2032
+ it('should create service with logger access', () => {
2033
+ // From docs: src/hello.service.ts
2034
+ @Service()
2035
+ class HelloService extends BaseService {
2036
+ private greetCount = 0;
2037
+
2038
+ greet(name: string): string {
2039
+ this.greetCount++;
2040
+
2041
+ return `Hello, ${name}! You are visitor #${this.greetCount}`;
2042
+ }
2043
+
2044
+ getCount(): number {
2045
+ return this.greetCount;
2046
+ }
2047
+ }
2048
+
2049
+ expect(HelloService).toBeDefined();
2050
+ });
2051
+ });
2052
+
2053
+ describe('Controller Creation (docs/getting-started.md)', () => {
2054
+ /**
2055
+ * @source docs/getting-started.md#step-5-create-a-controller
2056
+ */
2057
+ it('should create controller with validation schema', () => {
2058
+ // From docs: Validation schema
2059
+ /* eslint-disable @typescript-eslint/naming-convention */
2060
+ const greetBodySchema = type({
2061
+ name: 'string',
2062
+ 'message?': 'string',
2063
+ });
2064
+ /* eslint-enable @typescript-eslint/naming-convention */
2065
+
2066
+ @Service()
2067
+ class HelloService extends BaseService {
2068
+ greet(name: string) {
2069
+ return `Hello, ${name}!`;
2070
+ }
2071
+ }
2072
+
2073
+ // From docs: src/hello.controller.ts
2074
+ @Controller('/api/hello')
2075
+ class HelloController extends BaseController {
2076
+ constructor(private helloService: HelloService) {
2077
+ super();
2078
+ }
2079
+
2080
+ @Get('/')
2081
+ async hello(): Promise<Response> {
2082
+ return this.success({ message: 'Hello, World!' });
2083
+ }
2084
+
2085
+ @Get('/:name')
2086
+ async greet(@Param('name') name: string): Promise<Response> {
2087
+ const greeting = this.helloService.greet(name);
2088
+
2089
+ return this.success({ greeting });
2090
+ }
2091
+
2092
+ @Post('/greet')
2093
+ async greetPost(@Body() body: typeof greetBodySchema.infer): Promise<Response> {
2094
+ const greeting = this.helloService.greet(body.name);
2095
+
2096
+ return this.success({ greeting, customMessage: body.message });
2097
+ }
2098
+ }
2099
+
2100
+ expect(HelloController).toBeDefined();
2101
+ expect(greetBodySchema).toBeDefined();
2102
+ });
2103
+ });
2104
+
2105
+ describe('Module Definition (docs/getting-started.md)', () => {
2106
+ /**
2107
+ * @source docs/getting-started.md#step-6-create-the-module
2108
+ */
2109
+ it('should create module with controllers and providers', () => {
2110
+ @Service()
2111
+ class HelloService extends BaseService {}
2112
+
2113
+ @Controller('/api/hello')
2114
+ class HelloController extends BaseController {}
2115
+
2116
+ // From docs: src/app.module.ts
2117
+ @Module({
2118
+ controllers: [HelloController],
2119
+ providers: [HelloService],
2120
+ })
2121
+ class AppModule {}
2122
+
2123
+ expect(AppModule).toBeDefined();
2124
+ });
2125
+ });
2126
+
2127
+ describe('Application Entry Point (docs/getting-started.md)', () => {
2128
+ /**
2129
+ * @source docs/getting-started.md#step-7-create-entry-point
2130
+ */
2131
+ it('should create OneBunApplication with all options', () => {
2132
+ @Module({ controllers: [] })
2133
+ class AppModule {}
2134
+
2135
+ const envSchema = {
2136
+ server: {
2137
+ port: Env.number({ default: 3000, env: 'PORT' }),
2138
+ host: Env.string({ default: '0.0.0.0', env: 'HOST' }),
2139
+ },
2140
+ };
2141
+
2142
+ // From docs: src/index.ts
2143
+ const app = new OneBunApplication(AppModule, {
2144
+ envSchema,
2145
+ envOptions: {
2146
+ loadDotEnv: true,
2147
+ envFilePath: '.env',
2148
+ },
2149
+ metrics: {
2150
+ enabled: true,
2151
+ path: '/metrics',
2152
+ },
2153
+ tracing: {
2154
+ enabled: true,
2155
+ serviceName: 'my-onebun-app',
2156
+ },
2157
+ });
2158
+
2159
+ expect(app).toBeDefined();
2160
+ expect(typeof app.start).toBe('function');
2161
+ expect(typeof app.stop).toBe('function');
2162
+ expect(typeof app.getConfig).toBe('function');
2163
+ expect(typeof app.getLogger).toBe('function');
2164
+ });
2165
+ });
2166
+ });