@onebun/core 0.1.24 → 0.2.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.
@@ -33,9 +33,93 @@ import type {
33
33
  BeforeApplicationDestroy,
34
34
  OnApplicationDestroy,
35
35
  } from './';
36
- import type { SseEvent, SseGenerator } from './types';
36
+ import type {
37
+ SseEvent,
38
+ SseGenerator,
39
+ OneBunRequest,
40
+ OneBunResponse,
41
+ } from './types';
37
42
  import type { ServerWebSocket } from 'bun';
38
43
 
44
+ import {
45
+ Controller,
46
+ Get,
47
+ Post,
48
+ Put,
49
+ Delete,
50
+ Patch,
51
+ Param,
52
+ Query,
53
+ Body,
54
+ Header,
55
+ Req,
56
+ Cookie,
57
+ Module,
58
+ Service,
59
+ BaseService,
60
+ BaseController,
61
+ UseMiddleware,
62
+ getServiceTag,
63
+ getControllerMetadata,
64
+ HttpStatusCode,
65
+ ParamType,
66
+ NotFoundError,
67
+ InternalServerError,
68
+ OneBunBaseError,
69
+ Env,
70
+ validate,
71
+ validateOrThrow,
72
+ MultiServiceApplication,
73
+ OneBunApplication,
74
+ createServiceDefinition,
75
+ createServiceClient,
76
+ WebSocketGateway,
77
+ BaseWebSocketGateway,
78
+ OnConnect,
79
+ OnDisconnect,
80
+ OnJoinRoom,
81
+ OnLeaveRoom,
82
+ OnMessage,
83
+ Client,
84
+ Socket,
85
+ MessageData,
86
+ RoomName,
87
+ PatternParams,
88
+ WsServer,
89
+ UseWsGuards,
90
+ WsAuthGuard,
91
+ WsPermissionGuard,
92
+ WsAnyPermissionGuard,
93
+ createGuard,
94
+ createInMemoryWsStorage,
95
+ SharedRedisProvider,
96
+ Sse,
97
+ getSseMetadata,
98
+ formatSseEvent,
99
+ createSseStream,
100
+ createWsServiceDefinition,
101
+ createWsClient,
102
+ createNativeWsClient,
103
+ matchPattern,
104
+ makeMockLoggerLayer,
105
+ hasOnModuleInit,
106
+ hasOnApplicationInit,
107
+ hasOnModuleDestroy,
108
+ hasBeforeApplicationDestroy,
109
+ hasOnApplicationDestroy,
110
+ callOnModuleInit,
111
+ callOnApplicationInit,
112
+ callOnModuleDestroy,
113
+ callBeforeApplicationDestroy,
114
+ callOnApplicationDestroy,
115
+ UploadedFile,
116
+ UploadedFiles,
117
+ FormField,
118
+ OneBunFile,
119
+ MimeType,
120
+ matchMimeType,
121
+ } from './';
122
+
39
123
 
40
124
  /**
41
125
  * @source docs/index.md#minimal-working-example
@@ -3416,76 +3500,6 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
3416
3500
  // SSE (Server-Sent Events) Documentation Tests
3417
3501
  // ============================================================================
3418
3502
 
3419
- import { Sse, getSseMetadata } from './decorators/decorators';
3420
- import { formatSseEvent, createSseStream } from './module/controller';
3421
-
3422
- import {
3423
- Controller,
3424
- Get,
3425
- Post,
3426
- Put,
3427
- Delete,
3428
- Patch,
3429
- Param,
3430
- Query,
3431
- Body,
3432
- Header,
3433
- Req,
3434
- Module,
3435
- Service,
3436
- BaseService,
3437
- BaseController,
3438
- UseMiddleware,
3439
- getServiceTag,
3440
- HttpStatusCode,
3441
- NotFoundError,
3442
- InternalServerError,
3443
- OneBunBaseError,
3444
- Env,
3445
- validate,
3446
- validateOrThrow,
3447
- MultiServiceApplication,
3448
- OneBunApplication,
3449
- createServiceDefinition,
3450
- createServiceClient,
3451
- WebSocketGateway,
3452
- BaseWebSocketGateway,
3453
- OnConnect,
3454
- OnDisconnect,
3455
- OnJoinRoom,
3456
- OnLeaveRoom,
3457
- OnMessage,
3458
- Client,
3459
- Socket,
3460
- MessageData,
3461
- RoomName,
3462
- PatternParams,
3463
- WsServer,
3464
- UseWsGuards,
3465
- WsAuthGuard,
3466
- WsPermissionGuard,
3467
- WsAnyPermissionGuard,
3468
- createGuard,
3469
- createInMemoryWsStorage,
3470
- SharedRedisProvider,
3471
- createWsServiceDefinition,
3472
- createWsClient,
3473
- createNativeWsClient,
3474
- matchPattern,
3475
- makeMockLoggerLayer,
3476
- hasOnModuleInit,
3477
- hasOnApplicationInit,
3478
- hasOnModuleDestroy,
3479
- hasBeforeApplicationDestroy,
3480
- hasOnApplicationDestroy,
3481
- callOnModuleInit,
3482
- callOnApplicationInit,
3483
- callOnModuleDestroy,
3484
- callBeforeApplicationDestroy,
3485
- callOnApplicationDestroy,
3486
- } from './';
3487
-
3488
-
3489
3503
  describe('SSE (Server-Sent Events) API Documentation (docs/api/controllers.md)', () => {
3490
3504
  describe('SseEvent Type (docs/api/controllers.md)', () => {
3491
3505
  /**
@@ -3829,3 +3843,588 @@ describe('SSE (Server-Sent Events) API Documentation (docs/api/controllers.md)',
3829
3843
  });
3830
3844
  });
3831
3845
  });
3846
+
3847
+ // ============================================================================
3848
+ // Cookie, Headers, @Req with OneBunRequest
3849
+ // ============================================================================
3850
+
3851
+ describe('@Cookie Decorator (docs/api/decorators.md)', () => {
3852
+ /**
3853
+ * @source docs/api/decorators.md#cookie
3854
+ */
3855
+ it('should define @Cookie decorator with optional parameter', () => {
3856
+ // From docs: @Cookie('session_id') - optional
3857
+ @Controller('/api')
3858
+ class ApiController extends BaseController {
3859
+ @Get('/me')
3860
+ async getMe(@Cookie('session_id') sessionId?: string) {
3861
+ return { sessionId };
3862
+ }
3863
+ }
3864
+
3865
+ expect(ApiController).toBeDefined();
3866
+ const metadata = getControllerMetadata(ApiController);
3867
+ expect(metadata).toBeDefined();
3868
+ expect(metadata!.routes.length).toBe(1);
3869
+ expect(metadata!.routes[0].params!.length).toBe(1);
3870
+ expect(metadata!.routes[0].params![0].type).toBe(ParamType.COOKIE);
3871
+ expect(metadata!.routes[0].params![0].name).toBe('session_id');
3872
+ expect(metadata!.routes[0].params![0].isRequired).toBe(false);
3873
+ });
3874
+
3875
+ /**
3876
+ * @source docs/api/decorators.md#cookie-required
3877
+ */
3878
+ it('should define @Cookie decorator with required option', () => {
3879
+ // From docs: @Cookie('session_id', { required: true }) - required
3880
+ @Controller('/api')
3881
+ class AuthController extends BaseController {
3882
+ @Get('/protected')
3883
+ async protectedRoute(@Cookie('session_id', { required: true }) sessionId: string) {
3884
+ return { sessionId };
3885
+ }
3886
+ }
3887
+
3888
+ expect(AuthController).toBeDefined();
3889
+ const metadata = getControllerMetadata(AuthController);
3890
+ expect(metadata!.routes[0].params![0].isRequired).toBe(true);
3891
+ });
3892
+
3893
+ /**
3894
+ * @source docs/api/decorators.md#cookie-with-validation
3895
+ */
3896
+ it('should define @Cookie decorator with validation schema', () => {
3897
+ // From docs: @Cookie('session_id', schema) - optional with validation
3898
+ const uuidSchema = type('string');
3899
+
3900
+ @Controller('/api')
3901
+ class ApiController extends BaseController {
3902
+ @Get('/session')
3903
+ async getSession(@Cookie('session_id', uuidSchema) sessionId?: string) {
3904
+ return { sessionId };
3905
+ }
3906
+ }
3907
+
3908
+ expect(ApiController).toBeDefined();
3909
+ const metadata = getControllerMetadata(ApiController);
3910
+ expect(metadata!.routes[0].params![0].schema).toBeDefined();
3911
+ });
3912
+
3913
+ /**
3914
+ * @source docs/api/decorators.md#cookie-combined-example
3915
+ */
3916
+ it('should combine @Cookie with other parameter decorators', () => {
3917
+ // From docs: combining @Cookie, @Param, @Header, @Query
3918
+ @Controller('/api')
3919
+ class CombinedController extends BaseController {
3920
+ @Get('/users/:id')
3921
+ async getUser(
3922
+ @Param('id') id: string,
3923
+ @Query('fields') fields?: string,
3924
+ @Header('Authorization') auth?: string,
3925
+ @Cookie('session') session?: string,
3926
+ ) {
3927
+ return {
3928
+ id, fields, auth, session,
3929
+ };
3930
+ }
3931
+ }
3932
+
3933
+ expect(CombinedController).toBeDefined();
3934
+ const metadata = getControllerMetadata(CombinedController);
3935
+ expect(metadata!.routes[0].params!.length).toBe(4);
3936
+ });
3937
+ });
3938
+
3939
+ describe('@Req() with OneBunRequest (docs/api/decorators.md)', () => {
3940
+ /**
3941
+ * @source docs/api/decorators.md#req-onebunrequest
3942
+ */
3943
+ it('should define @Req() handler with OneBunRequest type', () => {
3944
+ // From docs: @Req() with OneBunRequest type
3945
+ // OneBunRequest extends Request with .cookies (CookieMap) and .params
3946
+ @Controller('/api')
3947
+ class ApiController extends BaseController {
3948
+ @Get('/raw')
3949
+ async handleRaw(@Req() req: OneBunRequest) {
3950
+ const url = new URL(req.url);
3951
+
3952
+ // req.cookies is CookieMap, req.params is available from routes API
3953
+ return { url: url.pathname };
3954
+ }
3955
+ }
3956
+
3957
+ expect(ApiController).toBeDefined();
3958
+ const metadata = getControllerMetadata(ApiController);
3959
+ expect(metadata!.routes[0].params![0].type).toBe(ParamType.REQUEST);
3960
+ });
3961
+
3962
+ /**
3963
+ * @source docs/api/decorators.md#req-cookies-access
3964
+ */
3965
+ it('should define handler accessing cookies via req.cookies', () => {
3966
+ // From docs: reading cookies through @Req()
3967
+ @Controller('/api')
3968
+ class ApiController extends BaseController {
3969
+ @Get('/session')
3970
+ async session(@Req() req: OneBunRequest) {
3971
+ // Access cookies via CookieMap
3972
+ const session = req.cookies.get('session');
3973
+
3974
+ return { session };
3975
+ }
3976
+ }
3977
+
3978
+ expect(ApiController).toBeDefined();
3979
+ });
3980
+ });
3981
+
3982
+ describe('OneBunRequest and OneBunResponse Types (docs/api/decorators.md)', () => {
3983
+ /**
3984
+ * @source docs/api/decorators.md#onebunrequest-type
3985
+ */
3986
+ it('should use OneBunRequest as type alias for BunRequest', () => {
3987
+ // OneBunRequest is an alias for BunRequest
3988
+ // It extends standard Request with .cookies and .params
3989
+ const _check: OneBunRequest extends Request ? true : false = true;
3990
+ expect(_check).toBe(true);
3991
+ });
3992
+
3993
+ /**
3994
+ * @source docs/api/decorators.md#onebunresponse-type
3995
+ */
3996
+ it('should use OneBunResponse as type alias for Response', () => {
3997
+ // OneBunResponse is an alias for Response
3998
+ const response: OneBunResponse = new Response('ok');
3999
+ expect(response).toBeInstanceOf(Response);
4000
+ });
4001
+ });
4002
+
4003
+ describe('Custom Response Headers (docs/api/controllers.md)', () => {
4004
+ /**
4005
+ * @source docs/api/controllers.md#custom-response-headers
4006
+ */
4007
+ it('should define handler returning Response with custom headers', () => {
4008
+ // From docs: returning Response with custom headers
4009
+ @Controller('/api')
4010
+ class ApiController extends BaseController {
4011
+ @Get('/download')
4012
+ async download() {
4013
+ return new Response(JSON.stringify({ data: 'file content' }), {
4014
+ status: 200,
4015
+ headers: {
4016
+ // eslint-disable-next-line @typescript-eslint/naming-convention
4017
+ 'Content-Type': 'application/json',
4018
+ // eslint-disable-next-line @typescript-eslint/naming-convention
4019
+ 'X-Custom-Header': 'custom-value',
4020
+ },
4021
+ });
4022
+ }
4023
+ }
4024
+
4025
+ expect(ApiController).toBeDefined();
4026
+ });
4027
+
4028
+ /**
4029
+ * @source docs/api/controllers.md#set-cookie-header
4030
+ */
4031
+ it('should define handler returning Response with Set-Cookie header', () => {
4032
+ // From docs: setting cookies via Set-Cookie header
4033
+ @Controller('/api')
4034
+ class AuthController extends BaseController {
4035
+ @Post('/login')
4036
+
4037
+ async login(@Body() _body: unknown) {
4038
+ const headers = new Headers();
4039
+ headers.set('Content-Type', 'application/json');
4040
+ headers.append('Set-Cookie', 'session=abc123; Path=/; HttpOnly');
4041
+ headers.append('Set-Cookie', 'theme=dark; Path=/');
4042
+
4043
+ return new Response(JSON.stringify({ loggedIn: true }), {
4044
+ status: 200,
4045
+ headers,
4046
+ });
4047
+ }
4048
+ }
4049
+
4050
+ expect(AuthController).toBeDefined();
4051
+ });
4052
+ });
4053
+
4054
+ describe('Working with Cookies (docs/api/controllers.md)', () => {
4055
+ /**
4056
+ * @source docs/api/controllers.md#reading-cookies-via-decorator
4057
+ */
4058
+ it('should define handler reading cookies via @Cookie decorator', () => {
4059
+ // From docs: reading cookies via @Cookie('name')
4060
+ @Controller('/api')
4061
+ class PrefsController extends BaseController {
4062
+ @Get('/preferences')
4063
+ async getPrefs(
4064
+ @Cookie('theme') theme?: string,
4065
+ @Cookie('lang') lang?: string,
4066
+ ) {
4067
+ return {
4068
+ theme: theme ?? 'light',
4069
+ lang: lang ?? 'en',
4070
+ };
4071
+ }
4072
+ }
4073
+
4074
+ expect(PrefsController).toBeDefined();
4075
+ });
4076
+
4077
+ /**
4078
+ * @source docs/api/controllers.md#reading-cookies-via-req
4079
+ */
4080
+ it('should define handler reading cookies via req.cookies', () => {
4081
+ // From docs: reading cookies through @Req() with req.cookies.get()
4082
+ @Controller('/api')
4083
+ class ApiController extends BaseController {
4084
+ @Get('/session')
4085
+ async session(@Req() req: OneBunRequest) {
4086
+ const session = req.cookies.get('session');
4087
+
4088
+ return { session };
4089
+ }
4090
+ }
4091
+
4092
+ expect(ApiController).toBeDefined();
4093
+ });
4094
+
4095
+ /**
4096
+ * @source docs/api/controllers.md#setting-cookies-via-req
4097
+ */
4098
+ it('should define handler setting cookies via req.cookies', () => {
4099
+ // From docs: setting cookies using req.cookies.set()
4100
+ @Controller('/api')
4101
+ class AuthController extends BaseController {
4102
+ @Post('/login')
4103
+
4104
+ async login(@Req() req: OneBunRequest, @Body() _body: unknown) {
4105
+ // Set cookie via CookieMap
4106
+ req.cookies.set('session', 'new-session-id', {
4107
+ httpOnly: true,
4108
+ path: '/',
4109
+ maxAge: 3600,
4110
+ });
4111
+
4112
+ return { loggedIn: true };
4113
+ }
4114
+ }
4115
+
4116
+ expect(AuthController).toBeDefined();
4117
+ });
4118
+
4119
+ /**
4120
+ * @source docs/api/controllers.md#deleting-cookies-via-req
4121
+ */
4122
+ it('should define handler deleting cookies via req.cookies', () => {
4123
+ // From docs: deleting cookies using req.cookies.delete()
4124
+ @Controller('/api')
4125
+ class AuthController extends BaseController {
4126
+ @Post('/logout')
4127
+ async logout(@Req() req: OneBunRequest) {
4128
+ req.cookies.delete('session');
4129
+
4130
+ return { loggedOut: true };
4131
+ }
4132
+ }
4133
+
4134
+ expect(AuthController).toBeDefined();
4135
+ });
4136
+ });
4137
+
4138
+ // ============================================================================
4139
+ // File Upload Documentation Tests
4140
+ // ============================================================================
4141
+
4142
+ describe('File Upload API Documentation (docs/api/decorators.md)', () => {
4143
+ /**
4144
+ * @source docs/api/decorators.md#uploadedfile
4145
+ */
4146
+ describe('Single File Upload (docs/api/decorators.md#uploadedfile)', () => {
4147
+ it('should define controller with @UploadedFile decorator', () => {
4148
+ @Controller('/api/files')
4149
+ class FileController extends BaseController {
4150
+ @Post('/avatar')
4151
+ async uploadAvatar(
4152
+ @UploadedFile('avatar', {
4153
+ maxSize: 5 * 1024 * 1024,
4154
+ mimeTypes: [MimeType.ANY_IMAGE],
4155
+ }) file: OneBunFile,
4156
+ ): Promise<Response> {
4157
+ await file.writeTo(`./uploads/${file.name}`);
4158
+
4159
+ return this.success({ filename: file.name, size: file.size });
4160
+ }
4161
+ }
4162
+
4163
+ expect(FileController).toBeDefined();
4164
+ const metadata = getControllerMetadata(FileController);
4165
+ expect(metadata).toBeDefined();
4166
+ expect(metadata!.routes).toHaveLength(1);
4167
+
4168
+ const route = metadata!.routes[0];
4169
+ expect(route.params).toBeDefined();
4170
+ expect(route.params!.length).toBe(1);
4171
+ expect(route.params![0].type).toBe(ParamType.FILE);
4172
+ expect(route.params![0].name).toBe('avatar');
4173
+ expect(route.params![0].isRequired).toBe(true);
4174
+ expect(route.params![0].fileOptions).toBeDefined();
4175
+ expect(route.params![0].fileOptions!.maxSize).toBe(5 * 1024 * 1024);
4176
+ expect(route.params![0].fileOptions!.mimeTypes).toEqual([MimeType.ANY_IMAGE]);
4177
+ });
4178
+ });
4179
+
4180
+ /**
4181
+ * @source docs/api/decorators.md#uploadedfiles
4182
+ */
4183
+ describe('Multiple File Upload (docs/api/decorators.md#uploadedfiles)', () => {
4184
+ it('should define controller with @UploadedFiles decorator', () => {
4185
+ @Controller('/api/files')
4186
+ class FileController extends BaseController {
4187
+ @Post('/documents')
4188
+ async uploadDocs(
4189
+ @UploadedFiles('docs', { maxCount: 10 }) files: OneBunFile[],
4190
+ ): Promise<Response> {
4191
+ for (const file of files) {
4192
+ await file.writeTo(`./uploads/${file.name}`);
4193
+ }
4194
+
4195
+ return this.success({ count: files.length });
4196
+ }
4197
+ }
4198
+
4199
+ expect(FileController).toBeDefined();
4200
+ const metadata = getControllerMetadata(FileController);
4201
+ expect(metadata).toBeDefined();
4202
+
4203
+ const route = metadata!.routes[0];
4204
+ expect(route.params).toBeDefined();
4205
+ expect(route.params![0].type).toBe(ParamType.FILES);
4206
+ expect(route.params![0].name).toBe('docs');
4207
+ expect(route.params![0].fileOptions!.maxCount).toBe(10);
4208
+ });
4209
+
4210
+ it('should support @UploadedFiles without field name (all files)', () => {
4211
+ @Controller('/api/files')
4212
+ class FileController extends BaseController {
4213
+ @Post('/batch')
4214
+ async uploadBatch(
4215
+ @UploadedFiles(undefined, { maxCount: 20 }) files: OneBunFile[],
4216
+ ): Promise<Response> {
4217
+ return this.success({ count: files.length });
4218
+ }
4219
+ }
4220
+
4221
+ expect(FileController).toBeDefined();
4222
+ const metadata = getControllerMetadata(FileController);
4223
+ const route = metadata!.routes[0];
4224
+ expect(route.params![0].type).toBe(ParamType.FILES);
4225
+ expect(route.params![0].name).toBe('');
4226
+ });
4227
+ });
4228
+
4229
+ /**
4230
+ * @source docs/api/decorators.md#formfield
4231
+ */
4232
+ describe('Form Field (docs/api/decorators.md#formfield)', () => {
4233
+ it('should define controller with @FormField decorator', () => {
4234
+ @Controller('/api/files')
4235
+ class FileController extends BaseController {
4236
+ @Post('/profile')
4237
+ async createProfile(
4238
+ @UploadedFile('avatar', { mimeTypes: [MimeType.ANY_IMAGE] }) avatar: OneBunFile,
4239
+ @FormField('name', { required: true }) name: string,
4240
+ @FormField('email') email: string,
4241
+ ): Promise<Response> {
4242
+ await avatar.writeTo(`./uploads/${avatar.name}`);
4243
+
4244
+ return this.success({ name, email, avatar: avatar.name });
4245
+ }
4246
+ }
4247
+
4248
+ expect(FileController).toBeDefined();
4249
+ const metadata = getControllerMetadata(FileController);
4250
+ const route = metadata!.routes[0];
4251
+ expect(route.params).toBeDefined();
4252
+ expect(route.params!.length).toBe(3);
4253
+
4254
+ // @UploadedFile
4255
+ const fileParam = route.params!.find((p) => p.type === ParamType.FILE);
4256
+ expect(fileParam).toBeDefined();
4257
+ expect(fileParam!.name).toBe('avatar');
4258
+
4259
+ // @FormField (required)
4260
+ const nameParam = route.params!.find((p) => p.name === 'name');
4261
+ expect(nameParam).toBeDefined();
4262
+ expect(nameParam!.type).toBe(ParamType.FORM_FIELD);
4263
+ expect(nameParam!.isRequired).toBe(true);
4264
+
4265
+ // @FormField (optional)
4266
+ const emailParam = route.params!.find((p) => p.name === 'email');
4267
+ expect(emailParam).toBeDefined();
4268
+ expect(emailParam!.type).toBe(ParamType.FORM_FIELD);
4269
+ expect(emailParam!.isRequired).toBe(false);
4270
+ });
4271
+ });
4272
+
4273
+ /**
4274
+ * @source docs/api/decorators.md#onebunfile
4275
+ */
4276
+ describe('OneBunFile Class (docs/api/decorators.md#onebunfile)', () => {
4277
+ it('should create OneBunFile from File and support all methods', async () => {
4278
+ const content = 'test file content';
4279
+ const file = new File([content], 'test.txt', { type: 'text/plain' });
4280
+ const oneBunFile = new OneBunFile(file);
4281
+
4282
+ expect(oneBunFile.name).toBe('test.txt');
4283
+ expect(oneBunFile.size).toBe(content.length);
4284
+ expect(oneBunFile.type).toStartWith('text/plain');
4285
+
4286
+ const base64 = await oneBunFile.toBase64();
4287
+ expect(base64).toBe(btoa(content));
4288
+
4289
+ const buffer = await oneBunFile.toBuffer();
4290
+ expect(buffer.toString()).toBe(content);
4291
+
4292
+ const blob = oneBunFile.toBlob();
4293
+ expect(blob.size).toBe(content.length);
4294
+ });
4295
+
4296
+ it('should create OneBunFile from base64', async () => {
4297
+ const content = 'base64 content';
4298
+ const base64 = btoa(content);
4299
+ const file = OneBunFile.fromBase64(base64, 'decoded.txt', 'text/plain');
4300
+
4301
+ expect(file.name).toBe('decoded.txt');
4302
+ expect(file.type).toStartWith('text/plain');
4303
+
4304
+ const roundTripped = await file.toBase64();
4305
+ expect(roundTripped).toBe(base64);
4306
+ });
4307
+ });
4308
+
4309
+ /**
4310
+ * @source docs/api/decorators.md#mimetype-enum
4311
+ */
4312
+ describe('MimeType Enum (docs/api/decorators.md#mimetype-enum)', () => {
4313
+ it('should provide common MIME type constants', () => {
4314
+ // Wildcards
4315
+ expect(String(MimeType.ANY)).toBe('*/*');
4316
+ expect(String(MimeType.ANY_IMAGE)).toBe('image/*');
4317
+ expect(String(MimeType.ANY_VIDEO)).toBe('video/*');
4318
+ expect(String(MimeType.ANY_AUDIO)).toBe('audio/*');
4319
+
4320
+ // Specific types
4321
+ expect(String(MimeType.PNG)).toBe('image/png');
4322
+ expect(String(MimeType.PDF)).toBe('application/pdf');
4323
+ expect(String(MimeType.MP4)).toBe('video/mp4');
4324
+
4325
+ // Wildcard matching
4326
+ expect(matchMimeType('image/png', MimeType.ANY_IMAGE)).toBe(true);
4327
+ expect(matchMimeType('video/mp4', MimeType.ANY_IMAGE)).toBe(false);
4328
+ });
4329
+ });
4330
+
4331
+ /**
4332
+ * @source docs/api/decorators.md#json-base64-upload-format
4333
+ */
4334
+ describe('JSON Base64 Upload (docs/api/decorators.md#json-base64-upload-format)', () => {
4335
+ it('should parse full JSON base64 format', () => {
4336
+ const base64 = btoa('png image data');
4337
+ const file = OneBunFile.fromBase64(base64, 'photo.png', 'image/png');
4338
+
4339
+ expect(file.name).toBe('photo.png');
4340
+ expect(file.type).toBe('image/png');
4341
+ });
4342
+
4343
+ it('should parse data URI format', () => {
4344
+ const base64 = btoa('svg data');
4345
+ const dataUri = `data:image/svg+xml;base64,${base64}`;
4346
+ const file = OneBunFile.fromBase64(dataUri, 'icon.svg');
4347
+
4348
+ expect(file.type).toBe('image/svg+xml');
4349
+ expect(file.name).toBe('icon.svg');
4350
+ });
4351
+ });
4352
+ });
4353
+
4354
+ describe('File Upload API Documentation (docs/api/controllers.md)', () => {
4355
+ /**
4356
+ * @source docs/api/controllers.md#single-file-upload
4357
+ */
4358
+ it('should define single file upload controller', () => {
4359
+ @Controller('/api/files')
4360
+ class FileController extends BaseController {
4361
+ @Post('/avatar')
4362
+ async uploadAvatar(
4363
+ @UploadedFile('avatar', {
4364
+ maxSize: 5 * 1024 * 1024,
4365
+ mimeTypes: [MimeType.ANY_IMAGE],
4366
+ }) file: OneBunFile,
4367
+ ): Promise<Response> {
4368
+ await file.writeTo(`./uploads/${file.name}`);
4369
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4370
+ const _base64 = await file.toBase64();
4371
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4372
+ const _buffer = await file.toBuffer();
4373
+
4374
+ return this.success({
4375
+ filename: file.name,
4376
+ size: file.size,
4377
+ type: file.type,
4378
+ });
4379
+ }
4380
+ }
4381
+
4382
+ expect(FileController).toBeDefined();
4383
+ });
4384
+
4385
+ /**
4386
+ * @source docs/api/controllers.md#multiple-file-upload
4387
+ */
4388
+ it('should define multiple file upload controller', () => {
4389
+ @Controller('/api/files')
4390
+ class FileController extends BaseController {
4391
+ @Post('/documents')
4392
+ async uploadDocuments(
4393
+ @UploadedFiles('docs', {
4394
+ maxCount: 10,
4395
+ maxSize: 10 * 1024 * 1024,
4396
+ mimeTypes: [MimeType.PDF, MimeType.DOCX],
4397
+ }) files: OneBunFile[],
4398
+ ): Promise<Response> {
4399
+ for (const file of files) {
4400
+ await file.writeTo(`./uploads/${file.name}`);
4401
+ }
4402
+
4403
+ return this.success({ uploaded: files.length });
4404
+ }
4405
+ }
4406
+
4407
+ expect(FileController).toBeDefined();
4408
+ });
4409
+
4410
+ /**
4411
+ * @source docs/api/controllers.md#file-with-form-fields
4412
+ */
4413
+ it('should define file with form fields controller', () => {
4414
+ @Controller('/api/files')
4415
+ class FileController extends BaseController {
4416
+ @Post('/profile')
4417
+ async createProfile(
4418
+ @UploadedFile('avatar', { mimeTypes: [MimeType.ANY_IMAGE] }) avatar: OneBunFile,
4419
+ @FormField('name', { required: true }) name: string,
4420
+ @FormField('email') email: string,
4421
+ ): Promise<Response> {
4422
+ await avatar.writeTo(`./uploads/${avatar.name}`);
4423
+
4424
+ return this.success({ name, email, avatar: avatar.name });
4425
+ }
4426
+ }
4427
+
4428
+ expect(FileController).toBeDefined();
4429
+ });
4430
+ });