@onebun/core 0.1.0 → 0.1.2

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,2919 @@
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/api/websocket.md
12
+ * - docs/examples/basic-app.md
13
+ * - docs/examples/crud-api.md
14
+ * - docs/examples/websocket-chat.md
15
+ */
16
+
17
+ import { type } from 'arktype';
18
+ import {
19
+ describe,
20
+ it,
21
+ expect,
22
+ } from 'bun:test';
23
+
24
+ import type {
25
+ WsClientData,
26
+ WsExecutionContext,
27
+ WsServerType,
28
+ } from './';
29
+ import type { ServerWebSocket } from 'bun';
30
+
31
+ import {
32
+ Controller,
33
+ Get,
34
+ Post,
35
+ Put,
36
+ Delete,
37
+ Patch,
38
+ Param,
39
+ Query,
40
+ Body,
41
+ Header,
42
+ Req,
43
+ Module,
44
+ Service,
45
+ BaseService,
46
+ BaseController,
47
+ UseMiddleware,
48
+ getServiceTag,
49
+ HttpStatusCode,
50
+ NotFoundError,
51
+ InternalServerError,
52
+ OneBunBaseError,
53
+ Env,
54
+ validate,
55
+ validateOrThrow,
56
+ MultiServiceApplication,
57
+ OneBunApplication,
58
+ createServiceDefinition,
59
+ createServiceClient,
60
+ WebSocketGateway,
61
+ BaseWebSocketGateway,
62
+ OnConnect,
63
+ OnDisconnect,
64
+ OnJoinRoom,
65
+ OnLeaveRoom,
66
+ OnMessage,
67
+ Client,
68
+ Socket,
69
+ MessageData,
70
+ RoomName,
71
+ PatternParams,
72
+ WsServer,
73
+ UseWsGuards,
74
+ WsAuthGuard,
75
+ WsPermissionGuard,
76
+ WsAnyPermissionGuard,
77
+ createGuard,
78
+ createInMemoryWsStorage,
79
+ SharedRedisProvider,
80
+ createWsServiceDefinition,
81
+ createWsClient,
82
+ matchPattern,
83
+ } from './';
84
+
85
+ /**
86
+ * @source docs/index.md#minimal-working-example
87
+ */
88
+ describe('Minimal Working Example (docs/index.md)', () => {
89
+ it('should define complete counter application in single block', () => {
90
+ // From docs/README.md: Minimal Working Example
91
+ // This test validates all components work together
92
+
93
+ // ============================================================================
94
+ // 1. Environment Schema (src/config.ts)
95
+ // ============================================================================
96
+ const envSchema = {
97
+ server: {
98
+ port: Env.number({ default: 3000 }),
99
+ host: Env.string({ default: '0.0.0.0' }),
100
+ },
101
+ };
102
+
103
+ // ============================================================================
104
+ // 2. Service Layer (src/counter.service.ts)
105
+ // ============================================================================
106
+ @Service()
107
+ class CounterService extends BaseService {
108
+ private value = 0;
109
+
110
+ getValue(): number {
111
+ return this.value;
112
+ }
113
+
114
+ increment(amount = 1): number {
115
+ this.value += amount;
116
+
117
+ return this.value;
118
+ }
119
+ }
120
+
121
+ // ============================================================================
122
+ // 3. Controller Layer (src/counter.controller.ts)
123
+ // ============================================================================
124
+ @Controller('/api/counter')
125
+ class CounterController extends BaseController {
126
+ constructor(private counterService: CounterService) {
127
+ super();
128
+ }
129
+
130
+ @Get('/')
131
+ async getValue(): Promise<Response> {
132
+ const value = this.counterService.getValue();
133
+
134
+ return this.success({ value });
135
+ }
136
+
137
+ @Post('/increment')
138
+ async increment(@Body() body?: { amount?: number }): Promise<Response> {
139
+ const newValue = this.counterService.increment(body?.amount);
140
+
141
+ return this.success({ value: newValue });
142
+ }
143
+ }
144
+
145
+ // ============================================================================
146
+ // 4. Module Definition (src/app.module.ts)
147
+ // ============================================================================
148
+ @Module({
149
+ controllers: [CounterController],
150
+ providers: [CounterService],
151
+ })
152
+ class AppModule {}
153
+
154
+ // ============================================================================
155
+ // 5. Application Entry Point (src/index.ts)
156
+ // ============================================================================
157
+ const app = new OneBunApplication(AppModule, {
158
+ port: 3000,
159
+ envSchema,
160
+ metrics: { enabled: true },
161
+ tracing: { enabled: true },
162
+ });
163
+
164
+ // Verify all components
165
+ expect(envSchema.server.port.type).toBe('number');
166
+ expect(envSchema.server.host.type).toBe('string');
167
+ expect(CounterService).toBeDefined();
168
+ expect(CounterController).toBeDefined();
169
+ expect(AppModule).toBeDefined();
170
+ expect(app).toBeDefined();
171
+ expect(typeof app.start).toBe('function');
172
+ expect(typeof app.stop).toBe('function');
173
+ });
174
+ });
175
+
176
+ describe('Core README Examples', () => {
177
+ describe('Quick Start (README)', () => {
178
+ it('should define controller with @Controller decorator', () => {
179
+ // From README: Quick Start example
180
+ @Controller('/api')
181
+ class AppController extends BaseController {
182
+ @Get('/hello')
183
+ async hello() {
184
+ return { message: 'Hello, OneBun!' };
185
+ }
186
+ }
187
+
188
+ expect(AppController).toBeDefined();
189
+ });
190
+
191
+ it('should define module with @Module decorator', () => {
192
+ // From README: Module definition
193
+ @Controller('/api')
194
+ class AppController extends BaseController {
195
+ @Get('/hello')
196
+ async hello() {
197
+ return { message: 'Hello, OneBun!' };
198
+ }
199
+ }
200
+
201
+ @Module({
202
+ controllers: [AppController],
203
+ })
204
+ class AppModule {}
205
+
206
+ expect(AppModule).toBeDefined();
207
+ });
208
+ });
209
+
210
+ describe('Route Decorators (README)', () => {
211
+ it('should define routes with HTTP method decorators', () => {
212
+ // From README: Route Decorators example
213
+ @Controller('/users')
214
+ class UsersController extends BaseController {
215
+ @Get()
216
+ getAllUsers() {
217
+ // Handle GET /users
218
+ return [];
219
+ }
220
+
221
+ @Get('/:id')
222
+ getUserById(@Param('id') id: string) {
223
+ // Handle GET /users/:id
224
+ return { id };
225
+ }
226
+
227
+ @Post()
228
+ createUser(@Body() userData: unknown) {
229
+ // Handle POST /users
230
+ return userData;
231
+ }
232
+
233
+ @Put('/:id')
234
+ updateUser(@Param('id') id: string, @Body() userData: unknown) {
235
+ // Handle PUT /users/:id
236
+ return { id, ...userData as object };
237
+ }
238
+
239
+ @Delete('/:id')
240
+ deleteUser(@Param('id') id: string) {
241
+ // Handle DELETE /users/:id
242
+ return { deleted: id };
243
+ }
244
+ }
245
+
246
+ expect(UsersController).toBeDefined();
247
+ });
248
+ });
249
+
250
+ describe('Parameter Decorators (README)', () => {
251
+ it('should use parameter decorators', () => {
252
+ // From README: Parameter Decorators example
253
+ @Controller('/api')
254
+ class ApiController extends BaseController {
255
+ @Get('/search')
256
+ search(
257
+ @Query('q') query: string,
258
+ @Query('limit') limit: string,
259
+ ) {
260
+ // Handle GET /api/search?q=something&limit=10
261
+ return { results: [], query, limit };
262
+ }
263
+
264
+ @Post('/users/:id/profile')
265
+ updateProfile(
266
+ @Param('id') userId: string,
267
+ @Body() _profileData: unknown,
268
+ @Header('Authorization') _token: string,
269
+ ) {
270
+ // Handle POST /api/users/123/profile
271
+ return { success: true, userId };
272
+ }
273
+ }
274
+
275
+ expect(ApiController).toBeDefined();
276
+ });
277
+ });
278
+
279
+ describe('Middleware (README)', () => {
280
+ it('should use middleware decorator', () => {
281
+ // From README: Middleware example
282
+ function loggerMiddleware(
283
+ _req: Request,
284
+ next: () => Promise<Response>,
285
+ ): Promise<Response> {
286
+ // eslint-disable-next-line no-console
287
+ console.log('Request received');
288
+
289
+ return next();
290
+ }
291
+
292
+ function authMiddleware(
293
+ req: Request,
294
+ next: () => Promise<Response>,
295
+ ): Promise<Response> {
296
+ const token = req.headers.get('Authorization');
297
+ if (!token) {
298
+ return Promise.resolve(new Response('Unauthorized', { status: 401 }));
299
+ }
300
+
301
+ return next();
302
+ }
303
+
304
+ @Controller('/admin')
305
+ class AdminController extends BaseController {
306
+ @Get('/dashboard')
307
+ @UseMiddleware(loggerMiddleware, authMiddleware)
308
+ getDashboard() {
309
+ return { stats: {} };
310
+ }
311
+ }
312
+
313
+ expect(AdminController).toBeDefined();
314
+ expect(loggerMiddleware).toBeDefined();
315
+ expect(authMiddleware).toBeDefined();
316
+ });
317
+ });
318
+
319
+ describe('Services (README)', () => {
320
+ it('should define service with @Service decorator', () => {
321
+ // From README: Services example
322
+ @Service()
323
+ class UserService extends BaseService {
324
+ private users: Array<{ id: string; name?: string }> = [];
325
+
326
+ findAll() {
327
+ return this.users;
328
+ }
329
+
330
+ findById(id: string) {
331
+ return this.users.find((user) => user.id === id);
332
+ }
333
+
334
+ create(userData: { name: string }) {
335
+ const user = { id: Date.now().toString(), ...userData };
336
+ this.users.push(user);
337
+
338
+ return user;
339
+ }
340
+ }
341
+
342
+ expect(UserService).toBeDefined();
343
+ });
344
+ });
345
+
346
+ describe('Modules (README)', () => {
347
+ it('should define module with providers and exports', () => {
348
+ // From README: Modules example
349
+ @Service()
350
+ class UsersService extends BaseService {}
351
+
352
+ @Controller('/users')
353
+ class UsersController extends BaseController {}
354
+
355
+ @Module({
356
+ controllers: [UsersController],
357
+ providers: [UsersService],
358
+ })
359
+ class UsersModule {}
360
+
361
+ expect(UsersModule).toBeDefined();
362
+ });
363
+ });
364
+ });
365
+
366
+ describe('Decorators API Documentation Examples', () => {
367
+ describe('@Module() decorator (docs/api/decorators.md)', () => {
368
+ it('should define module with all options', () => {
369
+ @Service()
370
+ class UserService extends BaseService {}
371
+
372
+ @Controller('/api/users')
373
+ class UserController extends BaseController {}
374
+
375
+ // From docs: @Module() example
376
+ @Module({
377
+ imports: [], // Other modules to import
378
+ controllers: [UserController],
379
+ providers: [UserService],
380
+ exports: [UserService],
381
+ })
382
+ class UserModule {}
383
+
384
+ expect(UserModule).toBeDefined();
385
+ });
386
+ });
387
+
388
+ describe('@Controller() decorator (docs/api/decorators.md)', () => {
389
+ it('should define controller with base path', () => {
390
+ // From docs: @Controller() example
391
+ @Controller('/api/users')
392
+ class UserController extends BaseController {
393
+ // All routes will be prefixed with /api/users
394
+ }
395
+
396
+ expect(UserController).toBeDefined();
397
+ });
398
+ });
399
+
400
+ describe('HTTP Method Decorators (docs/api/decorators.md)', () => {
401
+ it('should support all HTTP methods', () => {
402
+ // From docs: HTTP Method Decorators
403
+ @Controller('/users')
404
+ class UserController extends BaseController {
405
+ @Get('/') // GET /users
406
+ findAll() {
407
+ return [];
408
+ }
409
+
410
+ @Get('/:id') // GET /users/123
411
+ findOne(@Param('id') _id: string) {
412
+ return {};
413
+ }
414
+
415
+ @Get('/:userId/posts') // GET /users/123/posts
416
+ getUserPosts(@Param('userId') _userId: string) {
417
+ return [];
418
+ }
419
+
420
+ @Post('/') // POST /users
421
+ create(@Body() _body: unknown) {
422
+ return {};
423
+ }
424
+
425
+ @Put('/:id') // PUT /users/123
426
+ update(@Param('id') _id: string, @Body() _body: unknown) {
427
+ return {};
428
+ }
429
+
430
+ @Delete('/:id') // DELETE /users/123
431
+ remove(@Param('id') _id: string) {
432
+ return {};
433
+ }
434
+
435
+ @Patch('/:id') // PATCH /users/123
436
+ partialUpdate(@Param('id') _id: string, @Body() _body: unknown) {
437
+ return {};
438
+ }
439
+ }
440
+
441
+ expect(UserController).toBeDefined();
442
+ });
443
+ });
444
+
445
+ describe('Parameter Decorators (docs/api/decorators.md)', () => {
446
+ it('should support @Param decorator', () => {
447
+ // From docs: @Param() example
448
+ @Controller('/api')
449
+ class ApiController extends BaseController {
450
+ @Get('/:id')
451
+ findOne(
452
+ @Param('id') id: string, // No validation
453
+ ) {
454
+ return { id };
455
+ }
456
+ }
457
+
458
+ expect(ApiController).toBeDefined();
459
+ });
460
+
461
+ it('should support @Query decorator', () => {
462
+ // From docs: @Query() example
463
+ @Controller('/api')
464
+ class ApiController extends BaseController {
465
+ // GET /users?page=1&limit=10
466
+ @Get('/users')
467
+ findAll(@Query('page') page?: string, @Query('limit') limit?: string) {
468
+ return { page, limit };
469
+ }
470
+ }
471
+
472
+ expect(ApiController).toBeDefined();
473
+ });
474
+
475
+ it('should support @Header decorator', () => {
476
+ // From docs: @Header() example
477
+ @Controller('/api')
478
+ class ApiController extends BaseController {
479
+ @Get('/protected')
480
+ protected(
481
+ @Header('Authorization') auth: string,
482
+ @Header('X-Request-ID') requestId?: string,
483
+ ) {
484
+ return { auth: !!auth, requestId };
485
+ }
486
+ }
487
+
488
+ expect(ApiController).toBeDefined();
489
+ });
490
+
491
+ it('should support @Req decorator', () => {
492
+ // From docs: @Req() example
493
+ @Controller('/api')
494
+ class ApiController extends BaseController {
495
+ @Get('/raw')
496
+ handleRaw(@Req() request: Request) {
497
+ const url = new URL(request.url);
498
+
499
+ return { path: url.pathname };
500
+ }
501
+ }
502
+
503
+ expect(ApiController).toBeDefined();
504
+ });
505
+ });
506
+
507
+ describe('@Service() decorator (docs/api/decorators.md)', () => {
508
+ it('should define service with auto-generated tag', () => {
509
+ // From docs: @Service() example
510
+ @Service()
511
+ class UserService extends BaseService {
512
+ async findAll(): Promise<unknown[]> {
513
+ this.logger.info('Finding all users');
514
+
515
+ return [];
516
+ }
517
+ }
518
+
519
+ expect(UserService).toBeDefined();
520
+ });
521
+ });
522
+
523
+ describe('@UseMiddleware() decorator (docs/api/decorators.md)', () => {
524
+ it('should apply middleware to route handler', () => {
525
+ // From docs: @UseMiddleware() example
526
+ const authMiddleware = async (
527
+ req: Request,
528
+ next: () => Promise<Response>,
529
+ ) => {
530
+ const token = req.headers.get('Authorization');
531
+ if (!token) {
532
+ return new Response('Unauthorized', { status: 401 });
533
+ }
534
+
535
+ return await next();
536
+ };
537
+
538
+ const logMiddleware = async (
539
+ _req: Request,
540
+ next: () => Promise<Response>,
541
+ ) => {
542
+ // eslint-disable-next-line no-console
543
+ console.log('Request logged');
544
+
545
+ return await next();
546
+ };
547
+
548
+ @Controller('/users')
549
+ class UserController extends BaseController {
550
+ @Get('/protected')
551
+ @UseMiddleware(authMiddleware)
552
+ protectedRoute() {
553
+ return { message: 'Secret data' };
554
+ }
555
+
556
+ @Post('/action')
557
+ @UseMiddleware(logMiddleware, authMiddleware) // Multiple middleware
558
+ action() {
559
+ return { message: 'Action performed' };
560
+ }
561
+ }
562
+
563
+ expect(UserController).toBeDefined();
564
+ });
565
+ });
566
+ });
567
+
568
+ describe('Controllers API Documentation Examples', () => {
569
+ describe('BaseController (docs/api/controllers.md)', () => {
570
+ it('should extend BaseController for built-in features', () => {
571
+ @Service()
572
+ class UserService extends BaseService {
573
+ findAll() {
574
+ return [];
575
+ }
576
+ }
577
+
578
+ // From docs: Usage example
579
+ @Controller('/users')
580
+ class UserController extends BaseController {
581
+ constructor(private userService: UserService) {
582
+ super(); // Always call super()
583
+ }
584
+
585
+ @Get('/')
586
+ async findAll(): Promise<Response> {
587
+ const users = this.userService.findAll();
588
+
589
+ return this.success(users);
590
+ }
591
+ }
592
+
593
+ expect(UserController).toBeDefined();
594
+ });
595
+ });
596
+
597
+ describe('Response Methods (docs/api/controllers.md)', () => {
598
+ it('should have success() method', async () => {
599
+ @Controller('/test')
600
+ class TestController extends BaseController {
601
+ @Get('/')
602
+ async test(): Promise<Response> {
603
+ // From docs: success() examples
604
+ return this.success({ name: 'John', age: 30 });
605
+ }
606
+ }
607
+
608
+ expect(TestController).toBeDefined();
609
+ });
610
+
611
+ it('should have error() method', () => {
612
+ @Controller('/test')
613
+ class TestController extends BaseController {
614
+ @Get('/:id')
615
+ async findOne(): Promise<Response> {
616
+ // From docs: error() examples
617
+ return this.error('User not found', 404, 404);
618
+ }
619
+ }
620
+
621
+ expect(TestController).toBeDefined();
622
+ });
623
+
624
+ it('should have json() method', () => {
625
+ @Controller('/test')
626
+ class TestController extends BaseController {
627
+ @Get('/')
628
+ async test(): Promise<Response> {
629
+ return this.json({ data: 'test' });
630
+ }
631
+ }
632
+
633
+ expect(TestController).toBeDefined();
634
+ });
635
+
636
+ /**
637
+ * @source docs/api/controllers.md#text
638
+ */
639
+ it('should have text() method', () => {
640
+ @Controller('/test')
641
+ class TestController extends BaseController {
642
+ @Get('/health')
643
+ async health(): Promise<Response> {
644
+ // From docs: text() example
645
+ return this.text('OK');
646
+ }
647
+ }
648
+
649
+ expect(TestController).toBeDefined();
650
+ });
651
+ });
652
+
653
+ describe('Request Helpers (docs/api/controllers.md)', () => {
654
+ /**
655
+ * @source docs/api/controllers.md#isjson
656
+ */
657
+ it('should have isJson() method', () => {
658
+ @Controller('/test')
659
+ class TestController extends BaseController {
660
+ @Post('/')
661
+ async create(@Req() req: Request): Promise<Response> {
662
+ // From docs: isJson() example
663
+ if (!this.isJson(req)) {
664
+ return this.error('Content-Type must be application/json', 400, 400);
665
+ }
666
+
667
+ return this.success({ received: true });
668
+ }
669
+ }
670
+
671
+ expect(TestController).toBeDefined();
672
+ // Verify isJson method exists on prototype
673
+ const controller = new TestController();
674
+ expect(typeof controller['isJson']).toBe('function');
675
+ });
676
+
677
+ /**
678
+ * @source docs/api/controllers.md#parsejson
679
+ */
680
+ it('should have parseJson() method', () => {
681
+ interface CreateUserDto {
682
+ name: string;
683
+ email: string;
684
+ }
685
+
686
+ @Controller('/test')
687
+ class TestController extends BaseController {
688
+ @Post('/')
689
+ async create(@Req() req: Request): Promise<Response> {
690
+ // From docs: parseJson() example
691
+ const body = await this.parseJson<CreateUserDto>(req);
692
+
693
+ return this.success(body);
694
+ }
695
+ }
696
+
697
+ expect(TestController).toBeDefined();
698
+ // Verify parseJson method exists on prototype
699
+ const controller = new TestController();
700
+ expect(typeof controller['parseJson']).toBe('function');
701
+ });
702
+ });
703
+
704
+ describe('Accessing Services (docs/api/controllers.md)', () => {
705
+ /**
706
+ * @source docs/api/controllers.md#via-getservice-legacy
707
+ */
708
+ it('should have getService() method', () => {
709
+ @Service()
710
+ class UserService extends BaseService {
711
+ findAll() {
712
+ return [];
713
+ }
714
+ }
715
+
716
+ @Controller('/users')
717
+ class UserController extends BaseController {
718
+ @Get('/')
719
+ async findAll(): Promise<Response> {
720
+ // From docs: getService() example
721
+ const userService = this.getService(UserService);
722
+ const users = userService.findAll();
723
+
724
+ return this.success(users);
725
+ }
726
+ }
727
+
728
+ expect(UserController).toBeDefined();
729
+ // Verify getService method exists on prototype
730
+ const controller = new UserController();
731
+ expect(typeof controller['getService']).toBe('function');
732
+ });
733
+ });
734
+
735
+ describe('Accessing Logger (docs/api/controllers.md)', () => {
736
+ /**
737
+ * @source docs/api/controllers.md#accessing-logger
738
+ */
739
+ it('should have access to logger', () => {
740
+ @Controller('/users')
741
+ class UserController extends BaseController {
742
+ @Get('/')
743
+ async findAll(): Promise<Response> {
744
+ // From docs: Accessing Logger example
745
+ // Log levels: trace, debug, info, warn, error, fatal
746
+ this.logger.info('Finding all users');
747
+ this.logger.debug('Request received', { timestamp: Date.now() });
748
+
749
+ return this.success([]);
750
+ }
751
+ }
752
+
753
+ expect(UserController).toBeDefined();
754
+ });
755
+ });
756
+
757
+ describe('Accessing Configuration (docs/api/controllers.md)', () => {
758
+ /**
759
+ * @source docs/api/controllers.md#accessing-configuration
760
+ */
761
+ it('should have access to config', () => {
762
+ @Controller('/users')
763
+ class UserController extends BaseController {
764
+ @Get('/info')
765
+ async info(): Promise<Response> {
766
+ // From docs: Accessing Configuration example
767
+ // Note: config is typed as unknown, needs casting
768
+ const configAvailable = this.config !== null;
769
+
770
+ return this.success({ configAvailable });
771
+ }
772
+ }
773
+
774
+ expect(UserController).toBeDefined();
775
+ });
776
+ });
777
+
778
+ describe('HTTP Status Codes (docs/api/controllers.md)', () => {
779
+ /**
780
+ * @source docs/api/controllers.md#http-status-codes
781
+ */
782
+ it('should use HttpStatusCode enum', () => {
783
+ @Controller('/users')
784
+ class UserController extends BaseController {
785
+ @Get('/:id')
786
+ async findOne(@Param('id') _id: string): Promise<Response> {
787
+ // From docs: HTTP Status Codes example
788
+ const user = null; // Simulated not found
789
+ if (!user) {
790
+ return this.error('Not found', HttpStatusCode.NOT_FOUND, HttpStatusCode.NOT_FOUND);
791
+ }
792
+
793
+ return this.success(user, HttpStatusCode.OK);
794
+ }
795
+
796
+ @Post('/')
797
+ async create(@Body() _body: unknown): Promise<Response> {
798
+ return this.success({ id: '123' }, HttpStatusCode.CREATED);
799
+ }
800
+ }
801
+
802
+ expect(UserController).toBeDefined();
803
+ expect(HttpStatusCode.OK).toBe(200);
804
+ expect(HttpStatusCode.CREATED).toBe(201);
805
+ expect(HttpStatusCode.NOT_FOUND).toBe(404);
806
+ });
807
+
808
+ /**
809
+ * @source docs/api/controllers.md#available-status-codes
810
+ */
811
+ it('should have all documented status codes', () => {
812
+ // From docs: Available Status Codes
813
+ expect(HttpStatusCode.OK).toBe(200);
814
+ expect(HttpStatusCode.CREATED).toBe(201);
815
+ expect(HttpStatusCode.NO_CONTENT).toBe(204);
816
+ expect(HttpStatusCode.BAD_REQUEST).toBe(400);
817
+ expect(HttpStatusCode.UNAUTHORIZED).toBe(401);
818
+ expect(HttpStatusCode.FORBIDDEN).toBe(403);
819
+ expect(HttpStatusCode.NOT_FOUND).toBe(404);
820
+ expect(HttpStatusCode.CONFLICT).toBe(409);
821
+ expect(HttpStatusCode.INTERNAL_SERVER_ERROR).toBe(500);
822
+ });
823
+ });
824
+ });
825
+
826
+ describe('Services API Documentation Examples', () => {
827
+ describe('BaseService (docs/api/services.md)', () => {
828
+ it('should create basic service', () => {
829
+ // From docs: Basic Service example
830
+ @Service()
831
+ class CounterService extends BaseService {
832
+ private count = 0;
833
+
834
+ increment(): number {
835
+ this.count++;
836
+ this.logger.debug('Counter incremented', { count: this.count });
837
+
838
+ return this.count;
839
+ }
840
+
841
+ decrement(): number {
842
+ this.count--;
843
+
844
+ return this.count;
845
+ }
846
+
847
+ getValue(): number {
848
+ return this.count;
849
+ }
850
+ }
851
+
852
+ expect(CounterService).toBeDefined();
853
+ });
854
+
855
+ it('should create service with dependencies', () => {
856
+ // From docs: Service with Dependencies example
857
+ @Service()
858
+ class UserRepository extends BaseService {}
859
+
860
+ @Service()
861
+ class UserService extends BaseService {
862
+ constructor(private repository: UserRepository) {
863
+ super(); // Must call super()
864
+ }
865
+ }
866
+
867
+ expect(UserService).toBeDefined();
868
+ });
869
+ });
870
+
871
+ describe('getServiceTag (docs/api/services.md)', () => {
872
+ /**
873
+ * @source docs/api/services.md#service-tags-advanced
874
+ */
875
+ it('should get service tag from class', () => {
876
+ @Service()
877
+ class MyService extends BaseService {}
878
+
879
+ const tag = getServiceTag(MyService);
880
+ expect(tag).toBeDefined();
881
+ });
882
+ });
883
+
884
+ describe('BaseService Methods (docs/api/services.md)', () => {
885
+ /**
886
+ * @source docs/api/services.md#class-definition
887
+ */
888
+ it('should have runEffect method', () => {
889
+ // From docs: BaseService has runEffect method for Effect.js integration
890
+ // Note: Cannot instantiate service without OneBunApplication context
891
+ // Check prototype instead
892
+ expect(typeof BaseService.prototype['runEffect']).toBe('function');
893
+ });
894
+
895
+ /**
896
+ * @source docs/api/services.md#class-definition
897
+ */
898
+ it('should have formatError method', () => {
899
+ // From docs: BaseService has formatError method
900
+ // Note: Cannot instantiate service without OneBunApplication context
901
+ // Check prototype instead
902
+ expect(typeof BaseService.prototype['formatError']).toBe('function');
903
+ });
904
+ });
905
+
906
+ describe('Service Logger (docs/api/services.md)', () => {
907
+ /**
908
+ * @source docs/api/services.md#log-levels
909
+ */
910
+ it('should support all log levels', () => {
911
+ @Service()
912
+ class EmailService extends BaseService {
913
+ async send() {
914
+ // From docs: Log Levels
915
+ this.logger.trace('Very detailed info'); // Level 0
916
+ this.logger.debug('Debug information'); // Level 1
917
+ this.logger.info('General information'); // Level 2
918
+ this.logger.warn('Warning message'); // Level 3
919
+ this.logger.error('Error occurred'); // Level 4
920
+ this.logger.fatal('Fatal error'); // Level 5
921
+ }
922
+ }
923
+
924
+ expect(EmailService).toBeDefined();
925
+ });
926
+ });
927
+ });
928
+
929
+ describe('Validation API Documentation Examples', () => {
930
+ describe('validate function (docs/api/validation.md)', () => {
931
+ /**
932
+ * @source docs/api/validation.md#basic-usage
933
+ */
934
+ it('should validate data against schema', () => {
935
+ // From docs: validate() requires arktype schema, not plain object
936
+ // arktype `type()` returns a callable schema
937
+ const userSchema = type({
938
+ name: 'string',
939
+ age: 'number',
940
+ });
941
+
942
+ const result = validate(userSchema, { name: 'John', age: 30 });
943
+
944
+ // Result should have success property
945
+ expect(result).toHaveProperty('success');
946
+ expect(result.success).toBe(true);
947
+ });
948
+
949
+ /**
950
+ * @source docs/api/validation.md#basic-usage
951
+ */
952
+ it('should return errors for invalid data', () => {
953
+ const userSchema = type({
954
+ name: 'string',
955
+ age: 'number',
956
+ });
957
+
958
+ const result = validate(userSchema, { name: 'John', age: 'not a number' });
959
+
960
+ expect(result.success).toBe(false);
961
+ expect(result.errors).toBeDefined();
962
+ });
963
+ });
964
+
965
+ describe('validateOrThrow function (docs/api/validation.md)', () => {
966
+ /**
967
+ * @source docs/api/validation.md#validateorthrow
968
+ */
969
+ it('should throw on invalid data', () => {
970
+ const schema = type({
971
+ name: 'string',
972
+ age: 'number > 0',
973
+ });
974
+
975
+ // Valid data should not throw
976
+ expect(() => {
977
+ validateOrThrow(schema, { name: 'John', age: 30 });
978
+ }).not.toThrow();
979
+
980
+ // Invalid data should throw
981
+ expect(() => {
982
+ validateOrThrow(schema, { name: 'John', age: -5 });
983
+ }).toThrow();
984
+ });
985
+ });
986
+
987
+ describe('Schema Types (docs/api/validation.md)', () => {
988
+ /**
989
+ * @source docs/api/validation.md#primitives
990
+ */
991
+ it('should define primitive schemas', () => {
992
+ // From docs: Primitives
993
+ const stringSchema = type('string');
994
+ const numberSchema = type('number');
995
+ const booleanSchema = type('boolean');
996
+
997
+ expect(stringSchema).toBeDefined();
998
+ expect(numberSchema).toBeDefined();
999
+ expect(booleanSchema).toBeDefined();
1000
+ });
1001
+
1002
+ /**
1003
+ * @source docs/api/validation.md#string-constraints
1004
+ */
1005
+ it('should define string constraints', () => {
1006
+ // From docs: String Constraints
1007
+ const emailSchema = type('string.email');
1008
+ const uuidSchema = type('string.uuid');
1009
+
1010
+ expect(emailSchema).toBeDefined();
1011
+ expect(uuidSchema).toBeDefined();
1012
+
1013
+ // Validate email
1014
+ const emailResult = validate(emailSchema, 'test@example.com');
1015
+ expect(emailResult.success).toBe(true);
1016
+ });
1017
+
1018
+ /**
1019
+ * @source docs/api/validation.md#number-constraints
1020
+ */
1021
+ it('should define number constraints', () => {
1022
+ // From docs: Number Constraints
1023
+ const positiveSchema = type('number > 0');
1024
+ const rangeSchema = type('0 <= number <= 100');
1025
+
1026
+ expect(positiveSchema).toBeDefined();
1027
+ expect(rangeSchema).toBeDefined();
1028
+
1029
+ // Validate positive number
1030
+ const positiveResult = validate(positiveSchema, 10);
1031
+ expect(positiveResult.success).toBe(true);
1032
+ });
1033
+
1034
+ /**
1035
+ * @source docs/api/validation.md#arrays
1036
+ */
1037
+ it('should define array schemas', () => {
1038
+ // From docs: Arrays
1039
+ const stringArraySchema = type('string[]');
1040
+
1041
+ expect(stringArraySchema).toBeDefined();
1042
+
1043
+ const result = validate(stringArraySchema, ['a', 'b', 'c']);
1044
+ expect(result.success).toBe(true);
1045
+ });
1046
+
1047
+ /**
1048
+ * @source docs/api/validation.md#objects
1049
+ */
1050
+ it('should define object schemas', () => {
1051
+ // From docs: Objects
1052
+ /* eslint-disable @typescript-eslint/naming-convention */
1053
+ const userSchema = type({
1054
+ name: 'string',
1055
+ email: 'string.email',
1056
+ 'age?': 'number > 0', // Optional field
1057
+ });
1058
+ /* eslint-enable @typescript-eslint/naming-convention */
1059
+
1060
+ expect(userSchema).toBeDefined();
1061
+
1062
+ const result = validate(userSchema, {
1063
+ name: 'John',
1064
+ email: 'john@example.com',
1065
+ });
1066
+ expect(result.success).toBe(true);
1067
+ });
1068
+
1069
+ /**
1070
+ * @source docs/api/validation.md#using-in-controllers
1071
+ */
1072
+ it('should infer TypeScript type from schema', () => {
1073
+ // From docs: Type inference
1074
+ const userSchema = type({
1075
+ name: 'string',
1076
+ email: 'string.email',
1077
+ age: 'number > 0',
1078
+ });
1079
+ // Use userSchema to verify type inference
1080
+ expect(userSchema).toBeDefined();
1081
+
1082
+ type User = typeof userSchema.infer;
1083
+
1084
+ // TypeScript should infer: { name: string; email: string; age: number }
1085
+ const user: User = { name: 'John', email: 'john@example.com', age: 30 };
1086
+
1087
+ expect(user.name).toBe('John');
1088
+ expect(user.email).toBe('john@example.com');
1089
+ expect(user.age).toBe(30);
1090
+ });
1091
+ });
1092
+
1093
+ describe('Common Patterns (docs/api/validation.md)', () => {
1094
+ /**
1095
+ * @source docs/api/validation.md#create-update-dtos-pattern
1096
+ */
1097
+ it('should define create/update DTOs', () => {
1098
+ // From docs: Create/Update DTOs pattern
1099
+ const createUserSchema = type({
1100
+ name: 'string',
1101
+ email: 'string.email',
1102
+ password: 'string',
1103
+ });
1104
+
1105
+ /* eslint-disable @typescript-eslint/naming-convention */
1106
+ const updateUserSchema = type({
1107
+ 'name?': 'string',
1108
+ 'email?': 'string.email',
1109
+ });
1110
+ /* eslint-enable @typescript-eslint/naming-convention */
1111
+
1112
+ expect(createUserSchema).toBeDefined();
1113
+ expect(updateUserSchema).toBeDefined();
1114
+ });
1115
+
1116
+ /**
1117
+ * @source docs/api/validation.md#pagination-schema
1118
+ */
1119
+ it('should define pagination schema', () => {
1120
+ // From docs: Pagination Schema
1121
+ /* eslint-disable @typescript-eslint/naming-convention */
1122
+ const paginationSchema = type({
1123
+ 'page?': 'number > 0',
1124
+ 'limit?': 'number > 0',
1125
+ });
1126
+ /* eslint-enable @typescript-eslint/naming-convention */
1127
+
1128
+ expect(paginationSchema).toBeDefined();
1129
+
1130
+ const result = validate(paginationSchema, { page: 1, limit: 10 });
1131
+ expect(result.success).toBe(true);
1132
+ });
1133
+ });
1134
+ });
1135
+
1136
+ describe('Error Classes Examples', () => {
1137
+ describe('NotFoundError (docs/api/requests.md)', () => {
1138
+ it('should create NotFoundError', () => {
1139
+ // From docs: Error Classes example
1140
+ // NotFoundError(error: string, details?: Record<string, unknown>)
1141
+ const error = new NotFoundError('User not found', { userId: '123' });
1142
+
1143
+ expect(error).toBeInstanceOf(OneBunBaseError);
1144
+ expect(error.message).toContain('User not found');
1145
+ });
1146
+ });
1147
+
1148
+ describe('InternalServerError', () => {
1149
+ it('should create InternalServerError', () => {
1150
+ const error = new InternalServerError('Something went wrong');
1151
+
1152
+ expect(error).toBeInstanceOf(OneBunBaseError);
1153
+ expect(error.message).toBe('Something went wrong');
1154
+ });
1155
+ });
1156
+ });
1157
+
1158
+ describe('HttpStatusCode (docs/api/requests.md)', () => {
1159
+ it('should have correct status codes', () => {
1160
+ // From docs: Available Status Codes
1161
+ expect(HttpStatusCode.OK).toBe(200);
1162
+ expect(HttpStatusCode.CREATED).toBe(201);
1163
+ expect(HttpStatusCode.BAD_REQUEST).toBe(400);
1164
+ expect(HttpStatusCode.UNAUTHORIZED).toBe(401);
1165
+ expect(HttpStatusCode.FORBIDDEN).toBe(403);
1166
+ expect(HttpStatusCode.NOT_FOUND).toBe(404);
1167
+ expect(HttpStatusCode.CONFLICT).toBe(409);
1168
+ expect(HttpStatusCode.UNPROCESSABLE_ENTITY).toBe(422);
1169
+ expect(HttpStatusCode.INTERNAL_SERVER_ERROR).toBe(500);
1170
+ });
1171
+ });
1172
+
1173
+ describe('Env Helper (docs/api/envs.md)', () => {
1174
+ describe('Environment Variable Types', () => {
1175
+ it('should create string configuration', () => {
1176
+ const config = Env.string({ default: 'localhost' });
1177
+ expect(config.type).toBe('string');
1178
+ });
1179
+
1180
+ it('should create number configuration', () => {
1181
+ const config = Env.number({ default: 3000 });
1182
+ expect(config.type).toBe('number');
1183
+ });
1184
+
1185
+ it('should create boolean configuration', () => {
1186
+ const config = Env.boolean({ default: false });
1187
+ expect(config.type).toBe('boolean');
1188
+ });
1189
+
1190
+ it('should create array configuration', () => {
1191
+ const config = Env.array({ default: ['a', 'b'] });
1192
+ expect(config.type).toBe('array');
1193
+ });
1194
+ });
1195
+
1196
+ describe('Built-in Validators', () => {
1197
+ it('should have port validator', () => {
1198
+ const validator = Env.port();
1199
+ expect(typeof validator).toBe('function');
1200
+ });
1201
+
1202
+ it('should have url validator', () => {
1203
+ const validator = Env.url();
1204
+ expect(typeof validator).toBe('function');
1205
+ });
1206
+
1207
+ it('should have email validator', () => {
1208
+ const validator = Env.email();
1209
+ expect(typeof validator).toBe('function');
1210
+ });
1211
+
1212
+ it('should have oneOf validator', () => {
1213
+ const validator = Env.oneOf(['a', 'b', 'c']);
1214
+ expect(typeof validator).toBe('function');
1215
+ });
1216
+
1217
+ it('should have regex validator', () => {
1218
+ const validator = Env.regex(/^[a-z]+$/);
1219
+ expect(typeof validator).toBe('function');
1220
+ });
1221
+ });
1222
+ });
1223
+
1224
+ describe('Service Definition and Client (docs/api/requests.md)', () => {
1225
+ it('should create service definition from module class', () => {
1226
+ // From docs: createServiceDefinition expects a module class decorated with @Module
1227
+ @Controller('/users')
1228
+ class UsersController extends BaseController {
1229
+ @Get('/')
1230
+ findAll() {
1231
+ return [];
1232
+ }
1233
+
1234
+ @Get('/:id')
1235
+ findById(@Param('id') id: string) {
1236
+ return { id };
1237
+ }
1238
+
1239
+ @Post('/')
1240
+ create(@Body() data: unknown) {
1241
+ return data;
1242
+ }
1243
+ }
1244
+
1245
+ @Module({
1246
+ controllers: [UsersController],
1247
+ })
1248
+ class UsersModule {}
1249
+
1250
+ const UsersServiceDefinition = createServiceDefinition(UsersModule);
1251
+
1252
+ expect(UsersServiceDefinition).toBeDefined();
1253
+ expect(UsersServiceDefinition._endpoints).toBeDefined();
1254
+ expect(UsersServiceDefinition._controllers).toBeDefined();
1255
+ });
1256
+
1257
+ it('should create service client from definition', () => {
1258
+ @Controller('/users')
1259
+ class UsersController extends BaseController {
1260
+ @Get('/')
1261
+ findAll() {
1262
+ return [];
1263
+ }
1264
+ }
1265
+
1266
+ @Module({
1267
+ controllers: [UsersController],
1268
+ })
1269
+ class UsersModule {}
1270
+
1271
+ const usersDefinition = createServiceDefinition(UsersModule);
1272
+
1273
+ // From docs: Create typed client
1274
+ // Note: option is 'url', not 'baseUrl'
1275
+ const usersClient = createServiceClient(usersDefinition, {
1276
+ url: 'http://users-service:3001',
1277
+ });
1278
+
1279
+ expect(usersClient).toBeDefined();
1280
+ });
1281
+ });
1282
+
1283
+ describe('OneBunApplication (docs/api/core.md)', () => {
1284
+ /**
1285
+ * @source docs/api/core.md#onebunapplication
1286
+ */
1287
+ it('should create application instance', () => {
1288
+ @Controller('/api')
1289
+ class AppController extends BaseController {
1290
+ @Get('/hello')
1291
+ hello() {
1292
+ return { message: 'Hello' };
1293
+ }
1294
+ }
1295
+
1296
+ @Module({
1297
+ controllers: [AppController],
1298
+ })
1299
+ class AppModule {}
1300
+
1301
+ // From docs: OneBunApplication constructor
1302
+ const app = new OneBunApplication(AppModule, {
1303
+ port: 3000,
1304
+ basePath: '/api/v1',
1305
+ });
1306
+
1307
+ expect(app).toBeDefined();
1308
+ expect(typeof app.start).toBe('function');
1309
+ expect(typeof app.stop).toBe('function');
1310
+ expect(typeof app.getConfig).toBe('function');
1311
+ expect(typeof app.getLogger).toBe('function');
1312
+ expect(typeof app.getHttpUrl).toBe('function');
1313
+ expect(typeof app.getLayer).toBe('function');
1314
+ });
1315
+
1316
+ /**
1317
+ * @source docs/api/core.md#applicationoptions
1318
+ */
1319
+ it('should accept full application options', () => {
1320
+ @Module({ controllers: [] })
1321
+ class AppModule {}
1322
+
1323
+ // From docs: ApplicationOptions interface
1324
+ const options = {
1325
+ name: 'my-app',
1326
+ port: 3000,
1327
+ host: '0.0.0.0',
1328
+ basePath: '/api/v1',
1329
+ routePrefix: 'myservice',
1330
+ development: true,
1331
+ metrics: {
1332
+ enabled: true,
1333
+ path: '/metrics',
1334
+ prefix: 'myapp_',
1335
+ collectHttpMetrics: true,
1336
+ collectSystemMetrics: true,
1337
+ collectGcMetrics: true,
1338
+ },
1339
+ tracing: {
1340
+ enabled: true,
1341
+ serviceName: 'my-service',
1342
+ samplingRate: 1.0,
1343
+ },
1344
+ };
1345
+
1346
+ const app = new OneBunApplication(AppModule, options);
1347
+ expect(app).toBeDefined();
1348
+ });
1349
+
1350
+ /**
1351
+ * @source docs/api/core.md#metrics-options
1352
+ */
1353
+ it('should accept metrics configuration', () => {
1354
+ @Module({ controllers: [] })
1355
+ class AppModule {}
1356
+
1357
+ // From docs: MetricsOptions interface
1358
+ const metricsOptions = {
1359
+ enabled: true,
1360
+ path: '/metrics',
1361
+ defaultLabels: { service: 'my-service', environment: 'development' },
1362
+ collectHttpMetrics: true,
1363
+ collectSystemMetrics: true,
1364
+ collectGcMetrics: true,
1365
+ systemMetricsInterval: 5000,
1366
+ prefix: 'onebun_',
1367
+ httpDurationBuckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
1368
+ };
1369
+
1370
+ const app = new OneBunApplication(AppModule, { metrics: metricsOptions });
1371
+ expect(app).toBeDefined();
1372
+ });
1373
+
1374
+ /**
1375
+ * @source docs/api/core.md#tracing-options
1376
+ */
1377
+ it('should accept tracing configuration', () => {
1378
+ @Module({ controllers: [] })
1379
+ class AppModule {}
1380
+
1381
+ // From docs: TracingOptions interface
1382
+ /* eslint-disable @typescript-eslint/naming-convention */
1383
+ const tracingOptions = {
1384
+ enabled: true,
1385
+ serviceName: 'my-service',
1386
+ serviceVersion: '1.0.0',
1387
+ samplingRate: 1.0,
1388
+ traceHttpRequests: true,
1389
+ traceDatabaseQueries: true,
1390
+ defaultAttributes: { 'deployment.environment': 'production' },
1391
+ exportOptions: {
1392
+ endpoint: 'http://localhost:4318/v1/traces',
1393
+ headers: { Authorization: 'Bearer token' },
1394
+ timeout: 30000,
1395
+ batchSize: 100,
1396
+ batchTimeout: 5000,
1397
+ },
1398
+ };
1399
+ /* eslint-enable @typescript-eslint/naming-convention */
1400
+
1401
+ const app = new OneBunApplication(AppModule, { tracing: tracingOptions });
1402
+ expect(app).toBeDefined();
1403
+ });
1404
+ });
1405
+
1406
+ describe('MultiServiceApplication (docs/api/core.md)', () => {
1407
+ /**
1408
+ * @source docs/api/core.md#multiserviceapplication
1409
+ */
1410
+ it('should define multi-service configuration type', () => {
1411
+ // From docs: MultiServiceApplicationOptions
1412
+ @Module({
1413
+ controllers: [],
1414
+ })
1415
+ class UsersModule {}
1416
+
1417
+ @Module({
1418
+ controllers: [],
1419
+ })
1420
+ class OrdersModule {}
1421
+
1422
+ // This is just type checking, actual startup requires environment
1423
+ const config = {
1424
+ services: {
1425
+ users: {
1426
+ module: UsersModule,
1427
+ port: 3001,
1428
+ routePrefix: true,
1429
+ },
1430
+ orders: {
1431
+ module: OrdersModule,
1432
+ port: 3002,
1433
+ routePrefix: true,
1434
+ },
1435
+ },
1436
+ enabledServices: ['users', 'orders'],
1437
+ };
1438
+
1439
+ expect(config.services.users.module).toBe(UsersModule);
1440
+ expect(config.services.orders.module).toBe(OrdersModule);
1441
+ });
1442
+
1443
+ /**
1444
+ * @source docs/api/core.md#usage-example-1
1445
+ */
1446
+ it('should create MultiServiceApplication with service config', () => {
1447
+ @Module({ controllers: [] })
1448
+ class UsersModule {}
1449
+
1450
+ @Module({ controllers: [] })
1451
+ class OrdersModule {}
1452
+
1453
+ // From docs: MultiServiceApplication usage example
1454
+ // Note: routePrefix is boolean (true = use service name as prefix)
1455
+ const multiApp = new MultiServiceApplication({
1456
+ services: {
1457
+ users: {
1458
+ module: UsersModule,
1459
+ port: 3001,
1460
+ routePrefix: true, // Uses 'users' as route prefix
1461
+ },
1462
+ orders: {
1463
+ module: OrdersModule,
1464
+ port: 3002,
1465
+ routePrefix: true, // Uses 'orders' as route prefix
1466
+ envOverrides: {
1467
+ DB_NAME: { value: 'orders_db' },
1468
+ },
1469
+ },
1470
+ },
1471
+ enabledServices: ['users', 'orders'],
1472
+ });
1473
+
1474
+ expect(multiApp).toBeDefined();
1475
+ expect(typeof multiApp.start).toBe('function');
1476
+ expect(typeof multiApp.stop).toBe('function');
1477
+ expect(typeof multiApp.getRunningServices).toBe('function');
1478
+ });
1479
+ });
1480
+
1481
+ // ============================================================================
1482
+ // docs/examples Tests
1483
+ // ============================================================================
1484
+
1485
+ describe('Basic App Example (docs/examples/basic-app.md)', () => {
1486
+ /**
1487
+ * @source docs/examples/basic-app.md#srcconfigts
1488
+ */
1489
+ it('should define environment schema', () => {
1490
+ // From docs: src/config.ts
1491
+ const envSchema = {
1492
+ server: {
1493
+ port: Env.number({ default: 3000, env: 'PORT' }),
1494
+ host: Env.string({ default: '0.0.0.0', env: 'HOST' }),
1495
+ },
1496
+ app: {
1497
+ name: Env.string({ default: 'basic-app', env: 'APP_NAME' }),
1498
+ debug: Env.boolean({ default: false, env: 'DEBUG' }),
1499
+ },
1500
+ };
1501
+
1502
+ expect(envSchema.server.port).toBeDefined();
1503
+ expect(envSchema.server.host).toBeDefined();
1504
+ expect(envSchema.app.name).toBeDefined();
1505
+ expect(envSchema.app.debug).toBeDefined();
1506
+ });
1507
+
1508
+ /**
1509
+ * @source docs/examples/basic-app.md#srchelloservicets
1510
+ */
1511
+ it('should define HelloService', () => {
1512
+ // From docs: src/hello.service.ts
1513
+ @Service()
1514
+ class HelloService extends BaseService {
1515
+ private greetCount = 0;
1516
+
1517
+ greet(name: string): string {
1518
+ this.greetCount++;
1519
+
1520
+ return `Hello, ${name}! You are visitor #${this.greetCount}`;
1521
+ }
1522
+
1523
+ sayHello(): string {
1524
+ return 'Hello from OneBun!';
1525
+ }
1526
+
1527
+ getStats(): { greetCount: number } {
1528
+ return { greetCount: this.greetCount };
1529
+ }
1530
+ }
1531
+
1532
+ expect(HelloService).toBeDefined();
1533
+ });
1534
+
1535
+ /**
1536
+ * @source docs/examples/basic-app.md#srchellocontrollerts
1537
+ */
1538
+ it('should define HelloController', () => {
1539
+ @Service()
1540
+ class HelloService extends BaseService {
1541
+ sayHello(): string {
1542
+ return 'Hello!';
1543
+ }
1544
+
1545
+ greet(name: string): string {
1546
+ return `Hello, ${name}!`;
1547
+ }
1548
+
1549
+ getStats() {
1550
+ return { greetCount: 0 };
1551
+ }
1552
+ }
1553
+
1554
+ // From docs: src/hello.controller.ts
1555
+ @Controller('/api')
1556
+ class HelloController extends BaseController {
1557
+ constructor(private helloService: HelloService) {
1558
+ super();
1559
+ }
1560
+
1561
+ @Get('/hello')
1562
+ async hello(): Promise<Response> {
1563
+ const message = this.helloService.sayHello();
1564
+
1565
+ return this.success({ message });
1566
+ }
1567
+
1568
+ @Get('/hello/:name')
1569
+ async greet(@Param('name') name: string): Promise<Response> {
1570
+ const greeting = this.helloService.greet(name);
1571
+
1572
+ return this.success({ greeting });
1573
+ }
1574
+
1575
+ @Get('/stats')
1576
+ async stats(): Promise<Response> {
1577
+ const stats = this.helloService.getStats();
1578
+
1579
+ return this.success(stats);
1580
+ }
1581
+
1582
+ @Get('/health')
1583
+ async health(): Promise<Response> {
1584
+ return this.success({
1585
+ status: 'healthy',
1586
+ timestamp: new Date().toISOString(),
1587
+ });
1588
+ }
1589
+ }
1590
+
1591
+ expect(HelloController).toBeDefined();
1592
+ });
1593
+
1594
+ /**
1595
+ * @source docs/examples/basic-app.md#srcappmodulets
1596
+ */
1597
+ it('should define AppModule', () => {
1598
+ @Service()
1599
+ class HelloService extends BaseService {}
1600
+
1601
+ @Controller('/api')
1602
+ class HelloController extends BaseController {
1603
+ constructor(private helloService: HelloService) {
1604
+ super();
1605
+ }
1606
+ }
1607
+
1608
+ // From docs: src/app.module.ts
1609
+ @Module({
1610
+ controllers: [HelloController],
1611
+ providers: [HelloService],
1612
+ })
1613
+ class AppModule {}
1614
+
1615
+ expect(AppModule).toBeDefined();
1616
+ });
1617
+ });
1618
+
1619
+ describe('CRUD API Example (docs/examples/crud-api.md)', () => {
1620
+ /**
1621
+ * @source docs/examples/crud-api.md#srcusersschemasuserschemats
1622
+ */
1623
+ it('should define user schemas with validation', () => {
1624
+ // From docs: src/users/schemas/user.schema.ts
1625
+ /* eslint-disable @typescript-eslint/naming-convention */
1626
+ const createUserSchema = type({
1627
+ name: 'string',
1628
+ email: 'string.email',
1629
+ 'age?': 'number >= 0',
1630
+ });
1631
+
1632
+ const updateUserSchema = type({
1633
+ 'name?': 'string',
1634
+ 'email?': 'string.email',
1635
+ 'age?': 'number >= 0',
1636
+ });
1637
+ /* eslint-enable @typescript-eslint/naming-convention */
1638
+
1639
+ expect(createUserSchema).toBeDefined();
1640
+ expect(updateUserSchema).toBeDefined();
1641
+
1642
+ // Validate
1643
+ const result = validate(createUserSchema, {
1644
+ name: 'John',
1645
+ email: 'john@example.com',
1646
+ });
1647
+ expect(result.success).toBe(true);
1648
+ });
1649
+
1650
+ /**
1651
+ * @source docs/examples/crud-api.md#srcusersusersservicets
1652
+ */
1653
+ it('should define UsersService', () => {
1654
+ // From docs: src/users/users.service.ts
1655
+ @Service()
1656
+ class UsersRepository extends BaseService {
1657
+ private users: Array<{ id: string; name: string; email: string }> = [];
1658
+
1659
+ findAll() {
1660
+ return this.users;
1661
+ }
1662
+
1663
+ findById(id: string) {
1664
+ return this.users.find((u) => u.id === id) || null;
1665
+ }
1666
+
1667
+ create(data: { name: string; email: string }) {
1668
+ const user = { id: Date.now().toString(), ...data };
1669
+ this.users.push(user);
1670
+
1671
+ return user;
1672
+ }
1673
+
1674
+ update(id: string, data: Partial<{ name: string; email: string }>) {
1675
+ const index = this.users.findIndex((u) => u.id === id);
1676
+ if (index === -1) {
1677
+ return null;
1678
+ }
1679
+ this.users[index] = { ...this.users[index], ...data };
1680
+
1681
+ return this.users[index];
1682
+ }
1683
+
1684
+ delete(id: string): boolean {
1685
+ const index = this.users.findIndex((u) => u.id === id);
1686
+ if (index === -1) {
1687
+ return false;
1688
+ }
1689
+ this.users.splice(index, 1);
1690
+
1691
+ return true;
1692
+ }
1693
+ }
1694
+
1695
+ @Service()
1696
+ class UsersService extends BaseService {
1697
+ constructor(private repository: UsersRepository) {
1698
+ super();
1699
+ }
1700
+
1701
+ async findAll() {
1702
+ return this.repository.findAll();
1703
+ }
1704
+
1705
+ async findById(id: string) {
1706
+ return this.repository.findById(id);
1707
+ }
1708
+
1709
+ async create(data: { name: string; email: string }) {
1710
+ return this.repository.create(data);
1711
+ }
1712
+ }
1713
+
1714
+ expect(UsersService).toBeDefined();
1715
+ });
1716
+
1717
+ /**
1718
+ * @source docs/examples/crud-api.md#srcusersuserscontrollerts
1719
+ */
1720
+ it('should define UsersController with CRUD endpoints', () => {
1721
+ @Service()
1722
+ class UsersService extends BaseService {
1723
+ findAll() {
1724
+ return [];
1725
+ }
1726
+
1727
+ findById(id: string) {
1728
+ return { id };
1729
+ }
1730
+
1731
+ create(data: unknown) {
1732
+ return { id: '1', ...data as object };
1733
+ }
1734
+
1735
+ update(id: string, data: unknown) {
1736
+ return { id, ...data as object };
1737
+ }
1738
+
1739
+ delete(_id: string) {
1740
+ return true;
1741
+ }
1742
+ }
1743
+
1744
+ // From docs: src/users/users.controller.ts
1745
+ @Controller('/api/users')
1746
+ class UsersController extends BaseController {
1747
+ constructor(private usersService: UsersService) {
1748
+ super();
1749
+ }
1750
+
1751
+ @Get('/')
1752
+ async findAll(): Promise<Response> {
1753
+ const users = await this.usersService.findAll();
1754
+
1755
+ return this.success(users);
1756
+ }
1757
+
1758
+ @Get('/:id')
1759
+ async findOne(@Param('id') id: string): Promise<Response> {
1760
+ const user = await this.usersService.findById(id);
1761
+ if (!user) {
1762
+ return this.error('User not found', 404, 404);
1763
+ }
1764
+
1765
+ return this.success(user);
1766
+ }
1767
+
1768
+ @Post('/')
1769
+ async create(@Body() body: unknown): Promise<Response> {
1770
+ const user = await this.usersService.create(body);
1771
+
1772
+ return this.success(user, 201);
1773
+ }
1774
+
1775
+ @Put('/:id')
1776
+ async update(
1777
+ @Param('id') id: string,
1778
+ @Body() body: unknown,
1779
+ ): Promise<Response> {
1780
+ const user = await this.usersService.update(id, body);
1781
+ if (!user) {
1782
+ return this.error('User not found', 404, 404);
1783
+ }
1784
+
1785
+ return this.success(user);
1786
+ }
1787
+
1788
+ @Delete('/:id')
1789
+ async remove(@Param('id') id: string): Promise<Response> {
1790
+ const deleted = await this.usersService.delete(id);
1791
+ if (!deleted) {
1792
+ return this.error('User not found', 404, 404);
1793
+ }
1794
+
1795
+ return this.success({ deleted: true });
1796
+ }
1797
+ }
1798
+
1799
+ expect(UsersController).toBeDefined();
1800
+ });
1801
+
1802
+ /**
1803
+ * @source docs/examples/crud-api.md#srcusersusersmodulets
1804
+ */
1805
+ it('should define UsersModule', () => {
1806
+ @Service()
1807
+ class UsersRepository extends BaseService {}
1808
+
1809
+ @Service()
1810
+ class UsersService extends BaseService {}
1811
+
1812
+ @Controller('/api/users')
1813
+ class UsersController extends BaseController {}
1814
+
1815
+ // From docs: src/users/users.module.ts
1816
+ @Module({
1817
+ controllers: [UsersController],
1818
+ providers: [UsersService, UsersRepository],
1819
+ exports: [UsersService],
1820
+ })
1821
+ class UsersModule {}
1822
+
1823
+ expect(UsersModule).toBeDefined();
1824
+ });
1825
+ });
1826
+
1827
+ describe('Multi-Service Example (docs/examples/multi-service.md)', () => {
1828
+ /**
1829
+ * @source docs/examples/multi-service.md#srcusersusersmodulets
1830
+ */
1831
+ it('should define Users service module', () => {
1832
+ @Service()
1833
+ class UsersService extends BaseService {
1834
+ findById(id: string) {
1835
+ return { id, name: 'John' };
1836
+ }
1837
+ }
1838
+
1839
+ @Controller('/users')
1840
+ class UsersController extends BaseController {
1841
+ constructor(private usersService: UsersService) {
1842
+ super();
1843
+ }
1844
+
1845
+ @Get('/:id')
1846
+ async findOne(@Param('id') id: string): Promise<Response> {
1847
+ const user = this.usersService.findById(id);
1848
+
1849
+ return this.success(user);
1850
+ }
1851
+ }
1852
+
1853
+ @Module({
1854
+ controllers: [UsersController],
1855
+ providers: [UsersService],
1856
+ exports: [UsersService],
1857
+ })
1858
+ class UsersModule {}
1859
+
1860
+ expect(UsersModule).toBeDefined();
1861
+ });
1862
+
1863
+ /**
1864
+ * @source docs/examples/multi-service.md#srcordersordersmodulets
1865
+ */
1866
+ it('should define Orders service module', () => {
1867
+ @Service()
1868
+ class OrdersService extends BaseService {
1869
+ create(data: unknown) {
1870
+ return { id: '1', ...data as object };
1871
+ }
1872
+ }
1873
+
1874
+ @Controller('/orders')
1875
+ class OrdersController extends BaseController {
1876
+ constructor(private ordersService: OrdersService) {
1877
+ super();
1878
+ }
1879
+
1880
+ @Post('/')
1881
+ async create(@Body() body: unknown): Promise<Response> {
1882
+ const order = this.ordersService.create(body);
1883
+
1884
+ return this.success(order, 201);
1885
+ }
1886
+ }
1887
+
1888
+ @Module({
1889
+ controllers: [OrdersController],
1890
+ providers: [OrdersService],
1891
+ })
1892
+ class OrdersModule {}
1893
+
1894
+ expect(OrdersModule).toBeDefined();
1895
+ });
1896
+
1897
+ /**
1898
+ * @source docs/examples/multi-service.md#srcindexts
1899
+ */
1900
+ it('should define MultiServiceApplication configuration', () => {
1901
+ @Module({ controllers: [] })
1902
+ class UsersModule {}
1903
+
1904
+ @Module({ controllers: [] })
1905
+ class OrdersModule {}
1906
+
1907
+ // From docs: src/index.ts
1908
+ // Note: routePrefix is boolean (true = use service name as prefix)
1909
+ const multiApp = new MultiServiceApplication({
1910
+ services: {
1911
+ users: {
1912
+ module: UsersModule,
1913
+ port: 3001,
1914
+ routePrefix: true, // Uses 'users' as route prefix
1915
+ },
1916
+ orders: {
1917
+ module: OrdersModule,
1918
+ port: 3002,
1919
+ routePrefix: true, // Uses 'orders' as route prefix
1920
+ },
1921
+ },
1922
+ enabledServices: ['users', 'orders'],
1923
+ });
1924
+
1925
+ expect(multiApp).toBeDefined();
1926
+ expect(typeof multiApp.start).toBe('function');
1927
+ expect(typeof multiApp.stop).toBe('function');
1928
+ });
1929
+ });
1930
+
1931
+ // ============================================================================
1932
+ // Architecture & Getting Started Tests
1933
+ // ============================================================================
1934
+
1935
+ describe('Architecture Documentation (docs/architecture.md)', () => {
1936
+ describe('DI Resolution Flow (docs/architecture.md)', () => {
1937
+ /**
1938
+ * @source docs/architecture.md#di-resolution-flow
1939
+ */
1940
+ it('should demonstrate DI resolution flow', () => {
1941
+ // From docs: DI Resolution Flow example
1942
+ // 1. Service is decorated
1943
+ @Service()
1944
+ class CacheService extends BaseService {
1945
+ get(_key: string) {
1946
+ return null;
1947
+ }
1948
+ }
1949
+
1950
+ @Service()
1951
+ class UserService extends BaseService {
1952
+ constructor(private cacheService: CacheService) {
1953
+ super();
1954
+ }
1955
+ }
1956
+
1957
+ // 2. Module declares dependencies
1958
+ @Controller('/users')
1959
+ class UserController extends BaseController {
1960
+ constructor(private userService: UserService) {
1961
+ super();
1962
+ }
1963
+ }
1964
+
1965
+ @Module({
1966
+ providers: [CacheService, UserService],
1967
+ controllers: [UserController],
1968
+ })
1969
+ class UserModule {}
1970
+
1971
+ expect(UserModule).toBeDefined();
1972
+ });
1973
+
1974
+ /**
1975
+ * @source docs/architecture.md#explicit-injection
1976
+ */
1977
+ it('should demonstrate explicit injection pattern', () => {
1978
+ // From docs: Explicit Injection example
1979
+ @Service()
1980
+ class UserService extends BaseService {}
1981
+
1982
+ @Service()
1983
+ class CacheService extends BaseService {}
1984
+
1985
+ // For complex cases, use @Inject() - here we just verify pattern works
1986
+ @Controller('/users')
1987
+ class UserController extends BaseController {
1988
+ constructor(
1989
+ private userService: UserService,
1990
+ private cache: CacheService,
1991
+ ) {
1992
+ super();
1993
+ }
1994
+ }
1995
+
1996
+ expect(UserController).toBeDefined();
1997
+ });
1998
+ });
1999
+
2000
+ describe('Module System (docs/architecture.md)', () => {
2001
+ /**
2002
+ * @source docs/architecture.md#module-assembly
2003
+ */
2004
+ it('should demonstrate module export/import pattern', () => {
2005
+ // From docs: Module Assembly
2006
+ @Service()
2007
+ class SharedService extends BaseService {}
2008
+
2009
+ // Module that exports services
2010
+ @Module({
2011
+ providers: [SharedService],
2012
+ exports: [SharedService],
2013
+ })
2014
+ class SharedModule {}
2015
+
2016
+ // Module that imports services
2017
+ @Controller('/api')
2018
+ class ApiController extends BaseController {}
2019
+
2020
+ @Module({
2021
+ imports: [SharedModule],
2022
+ controllers: [ApiController],
2023
+ })
2024
+ class ApiModule {}
2025
+
2026
+ expect(SharedModule).toBeDefined();
2027
+ expect(ApiModule).toBeDefined();
2028
+ });
2029
+ });
2030
+ });
2031
+
2032
+ describe('Getting Started Documentation (docs/getting-started.md)', () => {
2033
+ describe('Environment Schema (docs/getting-started.md)', () => {
2034
+ /**
2035
+ * @source docs/getting-started.md#step-3-create-environment-schema
2036
+ */
2037
+ it('should define type-safe environment schema', () => {
2038
+ // From docs: src/config.ts
2039
+ const envSchema = {
2040
+ server: {
2041
+ port: Env.number({ default: 3000, env: 'PORT' }),
2042
+ host: Env.string({ default: '0.0.0.0', env: 'HOST' }),
2043
+ },
2044
+ app: {
2045
+ name: Env.string({ default: 'my-onebun-app', env: 'APP_NAME' }),
2046
+ debug: Env.boolean({ default: true, env: 'DEBUG' }),
2047
+ },
2048
+ database: {
2049
+ url: Env.string({ env: 'DATABASE_URL', sensitive: true }),
2050
+ },
2051
+ };
2052
+
2053
+ expect(envSchema.server.port.type).toBe('number');
2054
+ expect(envSchema.server.host.type).toBe('string');
2055
+ expect(envSchema.app.debug.type).toBe('boolean');
2056
+ expect(envSchema.database.url.sensitive).toBe(true);
2057
+ });
2058
+ });
2059
+
2060
+ describe('Service Creation (docs/getting-started.md)', () => {
2061
+ /**
2062
+ * @source docs/getting-started.md#step-4-create-a-service
2063
+ */
2064
+ it('should create service with logger access', () => {
2065
+ // From docs: src/hello.service.ts
2066
+ @Service()
2067
+ class HelloService extends BaseService {
2068
+ private greetCount = 0;
2069
+
2070
+ greet(name: string): string {
2071
+ this.greetCount++;
2072
+
2073
+ return `Hello, ${name}! You are visitor #${this.greetCount}`;
2074
+ }
2075
+
2076
+ getCount(): number {
2077
+ return this.greetCount;
2078
+ }
2079
+ }
2080
+
2081
+ expect(HelloService).toBeDefined();
2082
+ });
2083
+ });
2084
+
2085
+ describe('Controller Creation (docs/getting-started.md)', () => {
2086
+ /**
2087
+ * @source docs/getting-started.md#step-5-create-a-controller
2088
+ */
2089
+ it('should create controller with validation schema', () => {
2090
+ // From docs: Validation schema
2091
+ /* eslint-disable @typescript-eslint/naming-convention */
2092
+ const greetBodySchema = type({
2093
+ name: 'string',
2094
+ 'message?': 'string',
2095
+ });
2096
+ /* eslint-enable @typescript-eslint/naming-convention */
2097
+
2098
+ @Service()
2099
+ class HelloService extends BaseService {
2100
+ greet(name: string) {
2101
+ return `Hello, ${name}!`;
2102
+ }
2103
+ }
2104
+
2105
+ // From docs: src/hello.controller.ts
2106
+ @Controller('/api/hello')
2107
+ class HelloController extends BaseController {
2108
+ constructor(private helloService: HelloService) {
2109
+ super();
2110
+ }
2111
+
2112
+ @Get('/')
2113
+ async hello(): Promise<Response> {
2114
+ return this.success({ message: 'Hello, World!' });
2115
+ }
2116
+
2117
+ @Get('/:name')
2118
+ async greet(@Param('name') name: string): Promise<Response> {
2119
+ const greeting = this.helloService.greet(name);
2120
+
2121
+ return this.success({ greeting });
2122
+ }
2123
+
2124
+ @Post('/greet')
2125
+ async greetPost(@Body() body: typeof greetBodySchema.infer): Promise<Response> {
2126
+ const greeting = this.helloService.greet(body.name);
2127
+
2128
+ return this.success({ greeting, customMessage: body.message });
2129
+ }
2130
+ }
2131
+
2132
+ expect(HelloController).toBeDefined();
2133
+ expect(greetBodySchema).toBeDefined();
2134
+ });
2135
+ });
2136
+
2137
+ describe('Module Definition (docs/getting-started.md)', () => {
2138
+ /**
2139
+ * @source docs/getting-started.md#step-6-create-the-module
2140
+ */
2141
+ it('should create module with controllers and providers', () => {
2142
+ @Service()
2143
+ class HelloService extends BaseService {}
2144
+
2145
+ @Controller('/api/hello')
2146
+ class HelloController extends BaseController {}
2147
+
2148
+ // From docs: src/app.module.ts
2149
+ @Module({
2150
+ controllers: [HelloController],
2151
+ providers: [HelloService],
2152
+ })
2153
+ class AppModule {}
2154
+
2155
+ expect(AppModule).toBeDefined();
2156
+ });
2157
+ });
2158
+
2159
+ describe('Application Entry Point (docs/getting-started.md)', () => {
2160
+ /**
2161
+ * @source docs/getting-started.md#step-7-create-entry-point
2162
+ */
2163
+ it('should create OneBunApplication with all options', () => {
2164
+ @Module({ controllers: [] })
2165
+ class AppModule {}
2166
+
2167
+ const envSchema = {
2168
+ server: {
2169
+ port: Env.number({ default: 3000, env: 'PORT' }),
2170
+ host: Env.string({ default: '0.0.0.0', env: 'HOST' }),
2171
+ },
2172
+ };
2173
+
2174
+ // From docs: src/index.ts
2175
+ const app = new OneBunApplication(AppModule, {
2176
+ envSchema,
2177
+ envOptions: {
2178
+ loadDotEnv: true,
2179
+ envFilePath: '.env',
2180
+ },
2181
+ metrics: {
2182
+ enabled: true,
2183
+ path: '/metrics',
2184
+ },
2185
+ tracing: {
2186
+ enabled: true,
2187
+ serviceName: 'my-onebun-app',
2188
+ },
2189
+ });
2190
+
2191
+ expect(app).toBeDefined();
2192
+ expect(typeof app.start).toBe('function');
2193
+ expect(typeof app.stop).toBe('function');
2194
+ expect(typeof app.getConfig).toBe('function');
2195
+ expect(typeof app.getLogger).toBe('function');
2196
+ });
2197
+ });
2198
+ });
2199
+
2200
+ // ============================================================================
2201
+ // WebSocket Gateway Documentation Tests
2202
+ // ============================================================================
2203
+
2204
+ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
2205
+ describe('@WebSocketGateway decorator', () => {
2206
+ /**
2207
+ * @source docs/api/websocket.md#websocketgateway-decorator
2208
+ */
2209
+ it('should define gateway with path and namespace', () => {
2210
+ // From docs: WebSocketGateway Decorator example
2211
+ @WebSocketGateway({ path: '/ws', namespace: 'chat' })
2212
+ class ChatGateway extends BaseWebSocketGateway {
2213
+ // handlers...
2214
+ }
2215
+
2216
+ expect(ChatGateway).toBeDefined();
2217
+ });
2218
+ });
2219
+
2220
+ describe('Event Decorators', () => {
2221
+ /**
2222
+ * @source docs/api/websocket.md#onconnect
2223
+ */
2224
+ it('should handle @OnConnect decorator', () => {
2225
+ @WebSocketGateway({ path: '/ws' })
2226
+ class TestGateway extends BaseWebSocketGateway {
2227
+ @OnConnect()
2228
+ handleConnect(@Client() client: WsClientData) {
2229
+ // eslint-disable-next-line no-console
2230
+ console.log(`Client ${client.id} connected`);
2231
+
2232
+ return { event: 'welcome', data: { message: 'Welcome!' } };
2233
+ }
2234
+ }
2235
+
2236
+ expect(TestGateway).toBeDefined();
2237
+ });
2238
+
2239
+ /**
2240
+ * @source docs/api/websocket.md#ondisconnect
2241
+ */
2242
+ it('should handle @OnDisconnect decorator', () => {
2243
+ @WebSocketGateway({ path: '/ws' })
2244
+ class TestGateway extends BaseWebSocketGateway {
2245
+ @OnDisconnect()
2246
+ handleDisconnect(@Client() client: WsClientData) {
2247
+ // eslint-disable-next-line no-console
2248
+ console.log(`Client ${client.id} disconnected`);
2249
+ }
2250
+ }
2251
+
2252
+ expect(TestGateway).toBeDefined();
2253
+ });
2254
+
2255
+ /**
2256
+ * @source docs/api/websocket.md#onjoinroom
2257
+ */
2258
+ it('should handle @OnJoinRoom decorator with pattern', () => {
2259
+ @WebSocketGateway({ path: '/ws' })
2260
+ class TestGateway extends BaseWebSocketGateway {
2261
+ @OnJoinRoom('room:{roomId}')
2262
+ handleJoinRoom(
2263
+ @Client() client: WsClientData,
2264
+ @RoomName() room: string,
2265
+ @PatternParams() params: { roomId: string },
2266
+ ) {
2267
+ this.emitToRoom(room, 'user:joined', { userId: client.id });
2268
+
2269
+ return { event: 'joined', data: { roomId: params.roomId } };
2270
+ }
2271
+ }
2272
+
2273
+ expect(TestGateway).toBeDefined();
2274
+ });
2275
+
2276
+ /**
2277
+ * @source docs/api/websocket.md#onleaveroom
2278
+ */
2279
+ it('should handle @OnLeaveRoom decorator with wildcard', () => {
2280
+ @WebSocketGateway({ path: '/ws' })
2281
+ class TestGateway extends BaseWebSocketGateway {
2282
+ @OnLeaveRoom('room:*')
2283
+ handleLeaveRoom(@Client() client: WsClientData, @RoomName() room: string) {
2284
+ this.emitToRoom(room, 'user:left', { userId: client.id });
2285
+ }
2286
+ }
2287
+
2288
+ expect(TestGateway).toBeDefined();
2289
+ });
2290
+
2291
+ /**
2292
+ * @source docs/api/websocket.md#onmessage
2293
+ */
2294
+ it('should handle @OnMessage decorator', () => {
2295
+ @WebSocketGateway({ path: '/ws' })
2296
+ class TestGateway extends BaseWebSocketGateway {
2297
+ @OnMessage('chat:message')
2298
+ handleMessage(@Client() client: WsClientData, @MessageData() data: { text: string }) {
2299
+ this.broadcast('chat:message', { userId: client.id, text: data.text });
2300
+ }
2301
+ }
2302
+
2303
+ expect(TestGateway).toBeDefined();
2304
+ });
2305
+ });
2306
+
2307
+ describe('Pattern Syntax', () => {
2308
+ /**
2309
+ * @source docs/api/websocket.md#pattern-syntax
2310
+ */
2311
+ it('should match exact patterns', () => {
2312
+ const match = matchPattern('chat:message', 'chat:message');
2313
+ expect(match.matched).toBe(true);
2314
+ });
2315
+
2316
+ it('should match wildcard patterns', () => {
2317
+ const match = matchPattern('chat:*', 'chat:general');
2318
+ expect(match.matched).toBe(true);
2319
+ });
2320
+
2321
+ it('should match named parameter patterns', () => {
2322
+ const match = matchPattern('chat:{roomId}', 'chat:general');
2323
+ expect(match.matched).toBe(true);
2324
+ expect(match.params?.roomId).toBe('general');
2325
+ });
2326
+
2327
+ it('should match combined patterns', () => {
2328
+ const match = matchPattern('user:{id}:*', 'user:123:action');
2329
+ expect(match.matched).toBe(true);
2330
+ expect(match.params?.id).toBe('123');
2331
+ });
2332
+ });
2333
+
2334
+ describe('Parameter Decorators', () => {
2335
+ /**
2336
+ * @source docs/api/websocket.md#client
2337
+ */
2338
+ it('should use @Client() decorator', () => {
2339
+ @WebSocketGateway({ path: '/ws' })
2340
+ class TestGateway extends BaseWebSocketGateway {
2341
+ @OnMessage('ping')
2342
+ handlePing(@Client() client: WsClientData) {
2343
+ // eslint-disable-next-line no-console
2344
+ console.log(`Ping from ${client.id}`);
2345
+ }
2346
+ }
2347
+
2348
+ expect(TestGateway).toBeDefined();
2349
+ });
2350
+
2351
+ /**
2352
+ * @source docs/api/websocket.md#socket
2353
+ */
2354
+ it('should use @Socket() decorator', () => {
2355
+ @WebSocketGateway({ path: '/ws' })
2356
+ class TestGateway extends BaseWebSocketGateway {
2357
+ @OnMessage('raw')
2358
+ handleRaw(@Socket() socket: ServerWebSocket<WsClientData>) {
2359
+ socket.send('raw message');
2360
+ }
2361
+ }
2362
+
2363
+ expect(TestGateway).toBeDefined();
2364
+ });
2365
+
2366
+ /**
2367
+ * @source docs/api/websocket.md#messagedata
2368
+ */
2369
+ it('should use @MessageData() decorator with property', () => {
2370
+ @WebSocketGateway({ path: '/ws' })
2371
+ class TestGateway extends BaseWebSocketGateway {
2372
+ // Full data
2373
+ @OnMessage('chat:full')
2374
+ handleFull(@MessageData() data: { text: string }) {
2375
+ return data;
2376
+ }
2377
+
2378
+ // Specific property
2379
+ @OnMessage('chat:text')
2380
+ handleText(@MessageData('text') text: string) {
2381
+ return text;
2382
+ }
2383
+ }
2384
+
2385
+ expect(TestGateway).toBeDefined();
2386
+ });
2387
+
2388
+ /**
2389
+ * @source docs/api/websocket.md#roomname
2390
+ */
2391
+ it('should use @RoomName() decorator', () => {
2392
+ @WebSocketGateway({ path: '/ws' })
2393
+ class TestGateway extends BaseWebSocketGateway {
2394
+ @OnJoinRoom()
2395
+ handleJoin(@RoomName() room: string) {
2396
+ return { room };
2397
+ }
2398
+ }
2399
+
2400
+ expect(TestGateway).toBeDefined();
2401
+ });
2402
+
2403
+ /**
2404
+ * @source docs/api/websocket.md#patternparams
2405
+ */
2406
+ it('should use @PatternParams() decorator', () => {
2407
+ @WebSocketGateway({ path: '/ws' })
2408
+ class TestGateway extends BaseWebSocketGateway {
2409
+ @OnMessage('chat:{roomId}:message')
2410
+ handleMessage(@PatternParams() params: { roomId: string }) {
2411
+ return { roomId: params.roomId };
2412
+ }
2413
+ }
2414
+
2415
+ expect(TestGateway).toBeDefined();
2416
+ });
2417
+
2418
+ /**
2419
+ * @source docs/api/websocket.md#wsserver
2420
+ */
2421
+ it('should use @WsServer() decorator', () => {
2422
+ @WebSocketGateway({ path: '/ws' })
2423
+ class TestGateway extends BaseWebSocketGateway {
2424
+ @OnMessage('broadcast')
2425
+ handleBroadcast(@WsServer() server: WsServerType) {
2426
+ server.publish('all', 'Hello everyone!');
2427
+ }
2428
+ }
2429
+
2430
+ expect(TestGateway).toBeDefined();
2431
+ });
2432
+ });
2433
+
2434
+ describe('Guards', () => {
2435
+ /**
2436
+ * @source docs/api/websocket.md#built-in-guards
2437
+ */
2438
+ it('should use WsAuthGuard', () => {
2439
+ @WebSocketGateway({ path: '/ws' })
2440
+ class TestGateway extends BaseWebSocketGateway {
2441
+ @UseWsGuards(WsAuthGuard)
2442
+ @OnMessage('protected:*')
2443
+ handleProtected(@Client() client: WsClientData) {
2444
+ return { userId: client.auth?.userId };
2445
+ }
2446
+ }
2447
+
2448
+ expect(TestGateway).toBeDefined();
2449
+ });
2450
+
2451
+ /**
2452
+ * @source docs/api/websocket.md#built-in-guards
2453
+ */
2454
+ it('should use WsPermissionGuard', () => {
2455
+ @WebSocketGateway({ path: '/ws' })
2456
+ class TestGateway extends BaseWebSocketGateway {
2457
+ @UseWsGuards(new WsPermissionGuard('admin'))
2458
+ @OnMessage('admin:*')
2459
+ handleAdmin(@Client() client: WsClientData) {
2460
+ return { admin: true, userId: client.id };
2461
+ }
2462
+ }
2463
+
2464
+ expect(TestGateway).toBeDefined();
2465
+ });
2466
+
2467
+ /**
2468
+ * @source docs/api/websocket.md#built-in-guards
2469
+ */
2470
+ it('should use WsAnyPermissionGuard', () => {
2471
+ @WebSocketGateway({ path: '/ws' })
2472
+ class TestGateway extends BaseWebSocketGateway {
2473
+ @UseWsGuards(new WsAnyPermissionGuard(['admin', 'moderator']))
2474
+ @OnMessage('manage:*')
2475
+ handleManage(@Client() client: WsClientData) {
2476
+ return { clientId: client.id };
2477
+ }
2478
+ }
2479
+
2480
+ expect(TestGateway).toBeDefined();
2481
+ });
2482
+
2483
+ /**
2484
+ * @source docs/api/websocket.md#custom-guards
2485
+ */
2486
+ it('should create custom guard', () => {
2487
+ // eslint-disable-next-line @typescript-eslint/naming-convention
2488
+ const CustomGuard = createGuard((ctx: WsExecutionContext) => {
2489
+ return ctx.getClient().metadata.customCheck === true;
2490
+ });
2491
+
2492
+ @WebSocketGateway({ path: '/ws' })
2493
+ class TestGateway extends BaseWebSocketGateway {
2494
+ @UseWsGuards(CustomGuard)
2495
+ @OnMessage('custom:*')
2496
+ handleCustom(@Client() client: WsClientData) {
2497
+ return { clientId: client.id };
2498
+ }
2499
+ }
2500
+
2501
+ expect(TestGateway).toBeDefined();
2502
+ expect(CustomGuard).toBeDefined();
2503
+ });
2504
+ });
2505
+
2506
+ describe('Storage Adapters', () => {
2507
+ /**
2508
+ * @source docs/api/websocket.md#in-memory-storage-default
2509
+ */
2510
+ it('should create in-memory storage', () => {
2511
+ const storage = createInMemoryWsStorage();
2512
+ expect(storage).toBeDefined();
2513
+ expect(typeof storage.addClient).toBe('function');
2514
+ expect(typeof storage.removeClient).toBe('function');
2515
+ expect(typeof storage.getClient).toBe('function');
2516
+ });
2517
+
2518
+ /**
2519
+ * @source docs/api/websocket.md#redis-storage
2520
+ */
2521
+ it('should configure SharedRedisProvider', () => {
2522
+ // From docs: Redis Storage example
2523
+ // Note: This just tests the API, not actual connection
2524
+ expect(typeof SharedRedisProvider.configure).toBe('function');
2525
+ expect(typeof SharedRedisProvider.getClient).toBe('function');
2526
+ });
2527
+ });
2528
+
2529
+ describe('WebSocket Client', () => {
2530
+ /**
2531
+ * @source docs/api/websocket.md#creating-a-client
2532
+ */
2533
+ it('should create typed client from definition', () => {
2534
+ @WebSocketGateway({ path: '/chat' })
2535
+ class ChatGateway extends BaseWebSocketGateway {
2536
+ @OnMessage('chat:message')
2537
+ handleMessage(@Client() _client: WsClientData, @MessageData() data: { text: string }) {
2538
+ return { event: 'received', data };
2539
+ }
2540
+ }
2541
+
2542
+ @Module({ controllers: [ChatGateway] })
2543
+ class ChatModule {}
2544
+
2545
+ const definition = createWsServiceDefinition(ChatModule);
2546
+ expect(definition).toBeDefined();
2547
+ expect(definition._gateways).toBeDefined();
2548
+
2549
+ // Client creation (without actual connection)
2550
+ const client = createWsClient(definition, {
2551
+ url: 'ws://localhost:3000',
2552
+ auth: { token: 'xxx' },
2553
+ reconnect: true,
2554
+ reconnectInterval: 1000,
2555
+ maxReconnectAttempts: 10,
2556
+ });
2557
+
2558
+ expect(client).toBeDefined();
2559
+ expect(typeof client.connect).toBe('function');
2560
+ expect(typeof client.disconnect).toBe('function');
2561
+ expect(typeof client.on).toBe('function');
2562
+ });
2563
+ });
2564
+
2565
+ describe('Application Configuration', () => {
2566
+ /**
2567
+ * @source docs/api/websocket.md#application-options
2568
+ */
2569
+ it('should accept WebSocket configuration', () => {
2570
+ @WebSocketGateway({ path: '/ws' })
2571
+ class TestGateway extends BaseWebSocketGateway {}
2572
+
2573
+ @Module({ controllers: [TestGateway] })
2574
+ class AppModule {}
2575
+
2576
+ // From docs: Application Options example
2577
+ const app = new OneBunApplication(AppModule, {
2578
+ port: 3000,
2579
+ websocket: {
2580
+ enabled: true,
2581
+ storage: {
2582
+ type: 'memory',
2583
+ },
2584
+ pingInterval: 25000,
2585
+ pingTimeout: 20000,
2586
+ maxPayload: 1048576,
2587
+ },
2588
+ });
2589
+
2590
+ expect(app).toBeDefined();
2591
+ });
2592
+ });
2593
+ });
2594
+
2595
+ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
2596
+ describe('Chat Gateway', () => {
2597
+ /**
2598
+ * @source docs/examples/websocket-chat.md#chat-gateway
2599
+ */
2600
+ it('should define ChatGateway with all handlers', () => {
2601
+ interface ChatMessage {
2602
+ text: string;
2603
+ }
2604
+
2605
+ // Simplified ChatService for testing
2606
+ @Service()
2607
+ class ChatService extends BaseService {
2608
+ async getMessageHistory(_roomId: string): Promise<unknown[]> {
2609
+ return [];
2610
+ }
2611
+
2612
+ async saveMessage(data: { roomId: string; userId: string; text: string; timestamp: number }) {
2613
+ return { id: 'msg_1', ...data };
2614
+ }
2615
+ }
2616
+
2617
+ // From docs: Chat Gateway example
2618
+ @WebSocketGateway({ path: '/chat' })
2619
+ class ChatGateway extends BaseWebSocketGateway {
2620
+ constructor(private chatService: ChatService) {
2621
+ super();
2622
+ }
2623
+
2624
+ @OnConnect()
2625
+ async handleConnect(@Client() client: WsClientData) {
2626
+ // eslint-disable-next-line no-console
2627
+ console.log(`Client ${client.id} connected`);
2628
+
2629
+ return {
2630
+ event: 'welcome',
2631
+ data: {
2632
+ message: 'Welcome to the chat!',
2633
+ clientId: client.id,
2634
+ timestamp: Date.now(),
2635
+ },
2636
+ };
2637
+ }
2638
+
2639
+ @OnDisconnect()
2640
+ async handleDisconnect(@Client() client: WsClientData) {
2641
+ // eslint-disable-next-line no-console
2642
+ console.log(`Client ${client.id} disconnected`);
2643
+
2644
+ for (const room of client.rooms) {
2645
+ this.emitToRoom(room, 'user:left', {
2646
+ userId: client.id,
2647
+ room,
2648
+ });
2649
+ }
2650
+ }
2651
+
2652
+ @OnJoinRoom('room:{roomId}')
2653
+ async handleJoinRoom(
2654
+ @Client() client: WsClientData,
2655
+ @RoomName() room: string,
2656
+ @PatternParams() params: { roomId: string },
2657
+ ) {
2658
+ // eslint-disable-next-line no-console
2659
+ console.log(`Client ${client.id} joining room ${params.roomId}`);
2660
+
2661
+ await this.joinRoom(client.id, room);
2662
+
2663
+ this.emitToRoom(room, 'user:joined', {
2664
+ userId: client.id,
2665
+ room,
2666
+ }, [client.id]);
2667
+
2668
+ const history = await this.chatService.getMessageHistory(params.roomId);
2669
+
2670
+ return {
2671
+ event: 'room:joined',
2672
+ data: {
2673
+ room: params.roomId,
2674
+ history,
2675
+ },
2676
+ };
2677
+ }
2678
+
2679
+ @OnLeaveRoom('room:{roomId}')
2680
+ async handleLeaveRoom(
2681
+ @Client() client: WsClientData,
2682
+ @RoomName() room: string,
2683
+ ) {
2684
+ await this.leaveRoom(client.id, room);
2685
+
2686
+ this.emitToRoom(room, 'user:left', {
2687
+ userId: client.id,
2688
+ room,
2689
+ });
2690
+ }
2691
+
2692
+ @OnMessage('chat:{roomId}:message')
2693
+ async handleMessage(
2694
+ @Client() client: WsClientData,
2695
+ @MessageData() data: ChatMessage,
2696
+ @PatternParams() params: { roomId: string },
2697
+ ) {
2698
+ if (!client.rooms.includes(`room:${params.roomId}`)) {
2699
+ return {
2700
+ event: 'error',
2701
+ data: { message: 'Not in room' },
2702
+ };
2703
+ }
2704
+
2705
+ const message = await this.chatService.saveMessage({
2706
+ roomId: params.roomId,
2707
+ userId: client.id,
2708
+ text: data.text,
2709
+ timestamp: Date.now(),
2710
+ });
2711
+
2712
+ this.emitToRoom(`room:${params.roomId}`, 'chat:message', message);
2713
+
2714
+ return {
2715
+ event: 'chat:message:ack',
2716
+ data: { messageId: message.id },
2717
+ };
2718
+ }
2719
+
2720
+ @OnMessage('typing:{roomId}')
2721
+ handleTyping(
2722
+ @Client() client: WsClientData,
2723
+ @PatternParams() params: { roomId: string },
2724
+ ) {
2725
+ this.emitToRoom(
2726
+ `room:${params.roomId}`,
2727
+ 'typing',
2728
+ { userId: client.id },
2729
+ [client.id],
2730
+ );
2731
+ }
2732
+ }
2733
+
2734
+ expect(ChatGateway).toBeDefined();
2735
+ expect(ChatService).toBeDefined();
2736
+ });
2737
+ });
2738
+
2739
+ describe('Chat Service', () => {
2740
+ /**
2741
+ * @source docs/examples/websocket-chat.md#chat-service
2742
+ */
2743
+ it('should define ChatService', () => {
2744
+ interface Message {
2745
+ id: string;
2746
+ roomId: string;
2747
+ userId: string;
2748
+ text: string;
2749
+ timestamp: number;
2750
+ }
2751
+
2752
+ // From docs: Chat Service example
2753
+ @Service()
2754
+ class ChatService extends BaseService {
2755
+ private messages: Map<string, Message[]> = new Map();
2756
+ private messageIdCounter = 0;
2757
+
2758
+ async saveMessage(data: Omit<Message, 'id'>): Promise<Message> {
2759
+ const message: Message = {
2760
+ id: `msg_${++this.messageIdCounter}`,
2761
+ ...data,
2762
+ };
2763
+
2764
+ const roomMessages = this.messages.get(data.roomId) || [];
2765
+ roomMessages.push(message);
2766
+ this.messages.set(data.roomId, roomMessages);
2767
+
2768
+ return message;
2769
+ }
2770
+
2771
+ async getMessageHistory(roomId: string, limit = 50): Promise<Message[]> {
2772
+ const roomMessages = this.messages.get(roomId) || [];
2773
+
2774
+ return roomMessages.slice(-limit);
2775
+ }
2776
+
2777
+ async clearRoom(roomId: string): Promise<void> {
2778
+ this.messages.delete(roomId);
2779
+ }
2780
+ }
2781
+
2782
+ expect(ChatService).toBeDefined();
2783
+ });
2784
+ });
2785
+
2786
+ describe('Auth Guard', () => {
2787
+ /**
2788
+ * @source docs/examples/websocket-chat.md#auth-guard
2789
+ */
2790
+ it('should define custom ChatAuthGuard', () => {
2791
+ // From docs: Auth Guard example
2792
+ // eslint-disable-next-line @typescript-eslint/naming-convention
2793
+ const ChatAuthGuard = createGuard((context: WsExecutionContext) => {
2794
+ const client = context.getClient();
2795
+
2796
+ if (!client.auth?.authenticated) {
2797
+ return false;
2798
+ }
2799
+
2800
+ return true;
2801
+ });
2802
+
2803
+ expect(ChatAuthGuard).toBeDefined();
2804
+ // createGuard returns a class, not an instance
2805
+ const guardInstance = new ChatAuthGuard();
2806
+ expect(typeof guardInstance.canActivate).toBe('function');
2807
+ });
2808
+ });
2809
+
2810
+ describe('Module Setup', () => {
2811
+ /**
2812
+ * @source docs/examples/websocket-chat.md#module-setup
2813
+ */
2814
+ it('should define ChatModule', () => {
2815
+ @Service()
2816
+ class ChatService extends BaseService {}
2817
+
2818
+ @WebSocketGateway({ path: '/chat' })
2819
+ class ChatGateway extends BaseWebSocketGateway {
2820
+ constructor(private chatService: ChatService) {
2821
+ super();
2822
+ }
2823
+ }
2824
+
2825
+ // From docs: Module Setup example - Gateways go in controllers
2826
+ @Module({
2827
+ controllers: [ChatGateway],
2828
+ providers: [ChatService],
2829
+ })
2830
+ class ChatModule {}
2831
+
2832
+ expect(ChatModule).toBeDefined();
2833
+ });
2834
+ });
2835
+
2836
+ describe('Application Entry', () => {
2837
+ /**
2838
+ * @source docs/examples/websocket-chat.md#application-entry
2839
+ */
2840
+ it('should create chat application', () => {
2841
+ @Service()
2842
+ class ChatService extends BaseService {}
2843
+
2844
+ @WebSocketGateway({ path: '/chat' })
2845
+ class ChatGateway extends BaseWebSocketGateway {
2846
+ constructor(private chatService: ChatService) {
2847
+ super();
2848
+ }
2849
+ }
2850
+
2851
+ @Module({
2852
+ controllers: [ChatGateway],
2853
+ providers: [ChatService],
2854
+ })
2855
+ class ChatModule {}
2856
+
2857
+ // From docs: Application Entry example
2858
+ const app = new OneBunApplication(ChatModule, {
2859
+ port: 3000,
2860
+ websocket: {
2861
+ pingInterval: 25000,
2862
+ pingTimeout: 20000,
2863
+ },
2864
+ });
2865
+
2866
+ expect(app).toBeDefined();
2867
+ });
2868
+ });
2869
+
2870
+ describe('Client Implementation', () => {
2871
+ /**
2872
+ * @source docs/examples/websocket-chat.md#typed-client
2873
+ */
2874
+ it('should create typed chat client', () => {
2875
+ @Service()
2876
+ class ChatService extends BaseService {}
2877
+
2878
+ @WebSocketGateway({ path: '/chat' })
2879
+ class ChatGateway extends BaseWebSocketGateway {
2880
+ constructor(private chatService: ChatService) {
2881
+ super();
2882
+ }
2883
+
2884
+ @OnMessage('chat:message')
2885
+ handleMessage(@MessageData() data: { text: string }) {
2886
+ return { event: 'received', data };
2887
+ }
2888
+ }
2889
+
2890
+ @Module({
2891
+ controllers: [ChatGateway],
2892
+ providers: [ChatService],
2893
+ })
2894
+ class ChatModule {}
2895
+
2896
+ // From docs: Typed Client example
2897
+ const definition = createWsServiceDefinition(ChatModule);
2898
+ const client = createWsClient(definition, {
2899
+ url: 'ws://localhost:3000/chat',
2900
+ auth: {
2901
+ token: 'user-jwt-token',
2902
+ },
2903
+ reconnect: true,
2904
+ reconnectInterval: 2000,
2905
+ maxReconnectAttempts: 5,
2906
+ });
2907
+
2908
+ // Lifecycle events
2909
+ expect(typeof client.on).toBe('function');
2910
+
2911
+ // Connect/disconnect
2912
+ expect(typeof client.connect).toBe('function');
2913
+ expect(typeof client.disconnect).toBe('function');
2914
+
2915
+ // Gateway access
2916
+ expect(client.ChatGateway).toBeDefined();
2917
+ });
2918
+ });
2919
+ });