@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.
- package/README.md +44 -3
- package/package.json +13 -2
- package/src/docs-examples.test.ts +2166 -0
|
@@ -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
|
+
});
|