@onebun/core 0.1.12 → 0.1.13
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/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type as arktype } from 'arktype';
|
|
1
2
|
import {
|
|
2
3
|
describe,
|
|
3
4
|
test,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
Param,
|
|
20
21
|
Query,
|
|
21
22
|
Body,
|
|
23
|
+
Header,
|
|
22
24
|
} from '../decorators/decorators';
|
|
23
25
|
import { Controller as BaseController } from '../module/controller';
|
|
24
26
|
import { makeMockLoggerLayer } from '../testing/test-utils';
|
|
@@ -947,10 +949,722 @@ describe('OneBunApplication', () => {
|
|
|
947
949
|
|
|
948
950
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
949
951
|
const response = await (mockServer as any).fetchHandler(request);
|
|
952
|
+
const body = await response.json();
|
|
950
953
|
|
|
951
|
-
expect(response).
|
|
952
|
-
|
|
953
|
-
expect(
|
|
954
|
+
expect(response.status).toBe(200);
|
|
955
|
+
expect(body.result.query).toBe('test');
|
|
956
|
+
expect(body.result.limit).toBe(5);
|
|
957
|
+
expect(body.result.results).toEqual(['item1', 'item2']);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test('should handle URL-encoded query parameters', async () => {
|
|
961
|
+
@Controller('/api')
|
|
962
|
+
class ApiController extends BaseController {
|
|
963
|
+
@Get('/search')
|
|
964
|
+
async search(
|
|
965
|
+
@Query('name') name: string,
|
|
966
|
+
@Query('filter') filter?: string,
|
|
967
|
+
) {
|
|
968
|
+
return { name, filter };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
@Module({
|
|
973
|
+
controllers: [ApiController],
|
|
974
|
+
})
|
|
975
|
+
class TestModule {}
|
|
976
|
+
|
|
977
|
+
const app = createTestApp(TestModule);
|
|
978
|
+
await app.start();
|
|
979
|
+
|
|
980
|
+
// Test URL-encoded values: "John Doe" and "test&value"
|
|
981
|
+
const request = new Request('http://localhost:3000/api/search?name=John%20Doe&filter=test%26value', {
|
|
982
|
+
method: 'GET',
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
986
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
987
|
+
const body = await response.json();
|
|
988
|
+
|
|
989
|
+
expect(response.status).toBe(200);
|
|
990
|
+
expect(body.result.name).toBe('John Doe');
|
|
991
|
+
expect(body.result.filter).toBe('test&value');
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
test('should handle OAuth callback query string with special characters', async () => {
|
|
995
|
+
@Controller('/api/auth/google')
|
|
996
|
+
class AuthController extends BaseController {
|
|
997
|
+
@Get('/callback')
|
|
998
|
+
async callback(
|
|
999
|
+
@Query('state') state: string,
|
|
1000
|
+
@Query('code') code: string,
|
|
1001
|
+
@Query('scope') scope: string,
|
|
1002
|
+
@Query('authuser') authuser?: string,
|
|
1003
|
+
@Query('prompt') prompt?: string,
|
|
1004
|
+
) {
|
|
1005
|
+
return {
|
|
1006
|
+
state, code, scope, authuser, prompt,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
@Module({
|
|
1012
|
+
controllers: [AuthController],
|
|
1013
|
+
})
|
|
1014
|
+
class TestModule {}
|
|
1015
|
+
|
|
1016
|
+
const app = createTestApp(TestModule);
|
|
1017
|
+
await app.start();
|
|
1018
|
+
|
|
1019
|
+
// Real OAuth callback URL from the user's example
|
|
1020
|
+
const queryString = 'state=b6d290537858f64d894a47480c5e3edd&code=4/0ASc3gC0o5UhWEjUTslteiiSpR6_NsLYXXdfCjDq0rPFYymqB7LMofianDqC1l4NHJXvA3A&scope=email%20profile%20https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/userinfo.email%20openid&authuser=0&prompt=consent';
|
|
1021
|
+
const request = new Request(`http://localhost:3000/api/auth/google/callback?${queryString}`, {
|
|
1022
|
+
method: 'GET',
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1026
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1027
|
+
const body = await response.json();
|
|
1028
|
+
|
|
1029
|
+
expect(response.status).toBe(200);
|
|
1030
|
+
expect(body.result.state).toBe('b6d290537858f64d894a47480c5e3edd');
|
|
1031
|
+
expect(body.result.code).toBe('4/0ASc3gC0o5UhWEjUTslteiiSpR6_NsLYXXdfCjDq0rPFYymqB7LMofianDqC1l4NHJXvA3A');
|
|
1032
|
+
expect(body.result.scope).toBe('email profile https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email openid');
|
|
1033
|
+
expect(body.result.authuser).toBe('0');
|
|
1034
|
+
expect(body.result.prompt).toBe('consent');
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test('should handle multiple query parameters with same key as array', async () => {
|
|
1038
|
+
@Controller('/api')
|
|
1039
|
+
class ApiController extends BaseController {
|
|
1040
|
+
@Get('/filter')
|
|
1041
|
+
async filter(@Query('tag') tag: string | string[]) {
|
|
1042
|
+
return { tags: Array.isArray(tag) ? tag : [tag] };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
@Module({
|
|
1047
|
+
controllers: [ApiController],
|
|
1048
|
+
})
|
|
1049
|
+
class TestModule {}
|
|
1050
|
+
|
|
1051
|
+
const app = createTestApp(TestModule);
|
|
1052
|
+
await app.start();
|
|
1053
|
+
|
|
1054
|
+
// Multiple values with same key: ?tag=a&tag=b&tag=c
|
|
1055
|
+
const request = new Request('http://localhost:3000/api/filter?tag=a&tag=b&tag=c', {
|
|
1056
|
+
method: 'GET',
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1060
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1061
|
+
const body = await response.json();
|
|
1062
|
+
|
|
1063
|
+
expect(response.status).toBe(200);
|
|
1064
|
+
expect(body.result.tags).toEqual(['a', 'b', 'c']);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
test('should handle array notation query parameters (tag[]=a&tag[]=b)', async () => {
|
|
1068
|
+
@Controller('/api')
|
|
1069
|
+
class ApiController extends BaseController {
|
|
1070
|
+
@Get('/filter')
|
|
1071
|
+
async filter(@Query('tag') tag: string[]) {
|
|
1072
|
+
return { tags: tag };
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
@Module({
|
|
1077
|
+
controllers: [ApiController],
|
|
1078
|
+
})
|
|
1079
|
+
class TestModule {}
|
|
1080
|
+
|
|
1081
|
+
const app = createTestApp(TestModule);
|
|
1082
|
+
await app.start();
|
|
1083
|
+
|
|
1084
|
+
// Array notation: ?tag[]=a&tag[]=b&tag[]=c
|
|
1085
|
+
const request = new Request('http://localhost:3000/api/filter?tag[]=a&tag[]=b&tag[]=c', {
|
|
1086
|
+
method: 'GET',
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1090
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1091
|
+
const body = await response.json();
|
|
1092
|
+
|
|
1093
|
+
expect(response.status).toBe(200);
|
|
1094
|
+
expect(body.result.tags).toEqual(['a', 'b', 'c']);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
test('should handle single value with array notation (tag[]=a)', async () => {
|
|
1098
|
+
@Controller('/api')
|
|
1099
|
+
class ApiController extends BaseController {
|
|
1100
|
+
@Get('/filter')
|
|
1101
|
+
async filter(@Query('tag') tag: string[]) {
|
|
1102
|
+
return { tags: tag, isArray: Array.isArray(tag) };
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
@Module({
|
|
1107
|
+
controllers: [ApiController],
|
|
1108
|
+
})
|
|
1109
|
+
class TestModule {}
|
|
1110
|
+
|
|
1111
|
+
const app = createTestApp(TestModule);
|
|
1112
|
+
await app.start();
|
|
1113
|
+
|
|
1114
|
+
// Single value with array notation should still be an array
|
|
1115
|
+
const request = new Request('http://localhost:3000/api/filter?tag[]=single', {
|
|
1116
|
+
method: 'GET',
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1120
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1121
|
+
const body = await response.json();
|
|
1122
|
+
|
|
1123
|
+
expect(response.status).toBe(200);
|
|
1124
|
+
expect(body.result.tags).toEqual(['single']);
|
|
1125
|
+
expect(body.result.isArray).toBe(true);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test('should handle empty query parameter values', async () => {
|
|
1129
|
+
@Controller('/api')
|
|
1130
|
+
class ApiController extends BaseController {
|
|
1131
|
+
@Get('/params')
|
|
1132
|
+
async params(
|
|
1133
|
+
@Query('empty') empty: string,
|
|
1134
|
+
@Query('other') other: string,
|
|
1135
|
+
) {
|
|
1136
|
+
return { empty, other, emptyIsString: typeof empty === 'string' };
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
@Module({
|
|
1141
|
+
controllers: [ApiController],
|
|
1142
|
+
})
|
|
1143
|
+
class TestModule {}
|
|
1144
|
+
|
|
1145
|
+
const app = createTestApp(TestModule);
|
|
1146
|
+
await app.start();
|
|
1147
|
+
|
|
1148
|
+
// Empty value: ?empty=&other=value
|
|
1149
|
+
const request = new Request('http://localhost:3000/api/params?empty=&other=value', {
|
|
1150
|
+
method: 'GET',
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1154
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1155
|
+
const body = await response.json();
|
|
1156
|
+
|
|
1157
|
+
expect(response.status).toBe(200);
|
|
1158
|
+
expect(body.result.empty).toBe('');
|
|
1159
|
+
expect(body.result.other).toBe('value');
|
|
1160
|
+
expect(body.result.emptyIsString).toBe(true);
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
test('should handle missing optional query parameters', async () => {
|
|
1164
|
+
@Controller('/api')
|
|
1165
|
+
class ApiController extends BaseController {
|
|
1166
|
+
@Get('/optional')
|
|
1167
|
+
async optional(
|
|
1168
|
+
@Query('required') required: string,
|
|
1169
|
+
@Query('optional') optional?: string,
|
|
1170
|
+
) {
|
|
1171
|
+
return { required, optional, hasOptional: optional !== undefined };
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
@Module({
|
|
1176
|
+
controllers: [ApiController],
|
|
1177
|
+
})
|
|
1178
|
+
class TestModule {}
|
|
1179
|
+
|
|
1180
|
+
const app = createTestApp(TestModule);
|
|
1181
|
+
await app.start();
|
|
1182
|
+
|
|
1183
|
+
// Only required parameter, optional is missing
|
|
1184
|
+
const request = new Request('http://localhost:3000/api/optional?required=value', {
|
|
1185
|
+
method: 'GET',
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1189
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1190
|
+
const body = await response.json();
|
|
1191
|
+
|
|
1192
|
+
expect(response.status).toBe(200);
|
|
1193
|
+
expect(body.result.required).toBe('value');
|
|
1194
|
+
expect(body.result.optional).toBeUndefined();
|
|
1195
|
+
expect(body.result.hasOptional).toBe(false);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
test('should handle multiple path parameters', async () => {
|
|
1199
|
+
@Controller('/api')
|
|
1200
|
+
class ApiController extends BaseController {
|
|
1201
|
+
@Get('/users/:userId/posts/:postId')
|
|
1202
|
+
async getPost(
|
|
1203
|
+
@Param('userId') userId: string,
|
|
1204
|
+
@Param('postId') postId: string,
|
|
1205
|
+
) {
|
|
1206
|
+
return { userId: parseInt(userId), postId: parseInt(postId) };
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
@Module({
|
|
1211
|
+
controllers: [ApiController],
|
|
1212
|
+
})
|
|
1213
|
+
class TestModule {}
|
|
1214
|
+
|
|
1215
|
+
const app = createTestApp(TestModule);
|
|
1216
|
+
await app.start();
|
|
1217
|
+
|
|
1218
|
+
const request = new Request('http://localhost:3000/api/users/42/posts/123', {
|
|
1219
|
+
method: 'GET',
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1223
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1224
|
+
const body = await response.json();
|
|
1225
|
+
|
|
1226
|
+
expect(response.status).toBe(200);
|
|
1227
|
+
expect(body.result.userId).toBe(42);
|
|
1228
|
+
expect(body.result.postId).toBe(123);
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
test('should handle URL-encoded path parameters', async () => {
|
|
1232
|
+
@Controller('/api')
|
|
1233
|
+
class ApiController extends BaseController {
|
|
1234
|
+
@Get('/files/:filename')
|
|
1235
|
+
async getFile(@Param('filename') filename: string) {
|
|
1236
|
+
return { filename };
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
@Module({
|
|
1241
|
+
controllers: [ApiController],
|
|
1242
|
+
})
|
|
1243
|
+
class TestModule {}
|
|
1244
|
+
|
|
1245
|
+
const app = createTestApp(TestModule);
|
|
1246
|
+
await app.start();
|
|
1247
|
+
|
|
1248
|
+
// URL-encoded filename: "my file.txt"
|
|
1249
|
+
const request = new Request('http://localhost:3000/api/files/my%20file.txt', {
|
|
1250
|
+
method: 'GET',
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1254
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1255
|
+
const body = await response.json();
|
|
1256
|
+
|
|
1257
|
+
expect(response.status).toBe(200);
|
|
1258
|
+
expect(body.result.filename).toBe('my%20file.txt');
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
test('should handle path parameters with query parameters together', async () => {
|
|
1262
|
+
@Controller('/api')
|
|
1263
|
+
class ApiController extends BaseController {
|
|
1264
|
+
@Get('/users/:id/posts')
|
|
1265
|
+
async getUserPosts(
|
|
1266
|
+
@Param('id') userId: string,
|
|
1267
|
+
@Query('page') page?: string,
|
|
1268
|
+
@Query('limit') limit?: string,
|
|
1269
|
+
) {
|
|
1270
|
+
return {
|
|
1271
|
+
userId: parseInt(userId),
|
|
1272
|
+
page: page ? parseInt(page) : 1,
|
|
1273
|
+
limit: limit ? parseInt(limit) : 10,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
@Module({
|
|
1279
|
+
controllers: [ApiController],
|
|
1280
|
+
})
|
|
1281
|
+
class TestModule {}
|
|
1282
|
+
|
|
1283
|
+
const app = createTestApp(TestModule);
|
|
1284
|
+
await app.start();
|
|
1285
|
+
|
|
1286
|
+
const request = new Request('http://localhost:3000/api/users/5/posts?page=2&limit=20', {
|
|
1287
|
+
method: 'GET',
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1291
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1292
|
+
const body = await response.json();
|
|
1293
|
+
|
|
1294
|
+
expect(response.status).toBe(200);
|
|
1295
|
+
expect(body.result.userId).toBe(5);
|
|
1296
|
+
expect(body.result.page).toBe(2);
|
|
1297
|
+
expect(body.result.limit).toBe(20);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
test('should handle nested JSON body', async () => {
|
|
1301
|
+
@Controller('/api')
|
|
1302
|
+
class ApiController extends BaseController {
|
|
1303
|
+
@Post('/complex')
|
|
1304
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1305
|
+
async createComplex(@Body() data: any) {
|
|
1306
|
+
return { received: data };
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
@Module({
|
|
1311
|
+
controllers: [ApiController],
|
|
1312
|
+
})
|
|
1313
|
+
class TestModule {}
|
|
1314
|
+
|
|
1315
|
+
const app = createTestApp(TestModule);
|
|
1316
|
+
await app.start();
|
|
1317
|
+
|
|
1318
|
+
const complexData = {
|
|
1319
|
+
user: {
|
|
1320
|
+
name: 'John',
|
|
1321
|
+
address: {
|
|
1322
|
+
city: 'NYC',
|
|
1323
|
+
zip: '10001',
|
|
1324
|
+
},
|
|
1325
|
+
},
|
|
1326
|
+
items: [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }],
|
|
1327
|
+
metadata: {
|
|
1328
|
+
created: '2024-01-01',
|
|
1329
|
+
tags: ['tag1', 'tag2'],
|
|
1330
|
+
},
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
const request = new Request('http://localhost:3000/api/complex', {
|
|
1334
|
+
method: 'POST',
|
|
1335
|
+
headers: {
|
|
1336
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1337
|
+
'Content-Type': 'application/json',
|
|
1338
|
+
},
|
|
1339
|
+
body: JSON.stringify(complexData),
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1343
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1344
|
+
const body = await response.json();
|
|
1345
|
+
|
|
1346
|
+
expect(response.status).toBe(200);
|
|
1347
|
+
expect(body.result.received).toEqual(complexData);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test('should handle empty body gracefully', async () => {
|
|
1351
|
+
@Controller('/api')
|
|
1352
|
+
class ApiController extends BaseController {
|
|
1353
|
+
@Post('/empty-body')
|
|
1354
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1355
|
+
async handleEmpty(@Body() data?: any) {
|
|
1356
|
+
return { hasBody: data !== undefined, data };
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
@Module({
|
|
1361
|
+
controllers: [ApiController],
|
|
1362
|
+
})
|
|
1363
|
+
class TestModule {}
|
|
1364
|
+
|
|
1365
|
+
const app = createTestApp(TestModule);
|
|
1366
|
+
await app.start();
|
|
1367
|
+
|
|
1368
|
+
const request = new Request('http://localhost:3000/api/empty-body', {
|
|
1369
|
+
method: 'POST',
|
|
1370
|
+
headers: {
|
|
1371
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1372
|
+
'Content-Type': 'application/json',
|
|
1373
|
+
},
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1377
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1378
|
+
const body = await response.json();
|
|
1379
|
+
|
|
1380
|
+
expect(response.status).toBe(200);
|
|
1381
|
+
expect(body.result.hasBody).toBe(false);
|
|
1382
|
+
expect(body.result.data).toBeUndefined();
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
test('should handle header parameters', async () => {
|
|
1386
|
+
@Controller('/api')
|
|
1387
|
+
class ApiController extends BaseController {
|
|
1388
|
+
@Get('/headers')
|
|
1389
|
+
async getHeaders(
|
|
1390
|
+
@Header('Authorization') auth: string,
|
|
1391
|
+
@Header('X-Custom-Header') custom?: string,
|
|
1392
|
+
) {
|
|
1393
|
+
return { auth, custom };
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
@Module({
|
|
1398
|
+
controllers: [ApiController],
|
|
1399
|
+
})
|
|
1400
|
+
class TestModule {}
|
|
1401
|
+
|
|
1402
|
+
const app = createTestApp(TestModule);
|
|
1403
|
+
await app.start();
|
|
1404
|
+
|
|
1405
|
+
const request = new Request('http://localhost:3000/api/headers', {
|
|
1406
|
+
method: 'GET',
|
|
1407
|
+
headers: {
|
|
1408
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1409
|
+
'Authorization': 'Bearer token123',
|
|
1410
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1411
|
+
'X-Custom-Header': 'custom-value',
|
|
1412
|
+
},
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1416
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1417
|
+
const body = await response.json();
|
|
1418
|
+
|
|
1419
|
+
expect(response.status).toBe(200);
|
|
1420
|
+
expect(body.result.auth).toBe('Bearer token123');
|
|
1421
|
+
expect(body.result.custom).toBe('custom-value');
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
test('should handle missing optional header', async () => {
|
|
1425
|
+
@Controller('/api')
|
|
1426
|
+
class ApiController extends BaseController {
|
|
1427
|
+
@Get('/optional-header')
|
|
1428
|
+
async getOptionalHeader(
|
|
1429
|
+
@Header('X-Required') required: string,
|
|
1430
|
+
@Header('X-Optional') optional?: string | null,
|
|
1431
|
+
) {
|
|
1432
|
+
// Note: headers.get() returns null for missing headers, not undefined
|
|
1433
|
+
return { required, optional, hasOptional: optional !== null };
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
@Module({
|
|
1438
|
+
controllers: [ApiController],
|
|
1439
|
+
})
|
|
1440
|
+
class TestModule {}
|
|
1441
|
+
|
|
1442
|
+
const app = createTestApp(TestModule);
|
|
1443
|
+
await app.start();
|
|
1444
|
+
|
|
1445
|
+
const request = new Request('http://localhost:3000/api/optional-header', {
|
|
1446
|
+
method: 'GET',
|
|
1447
|
+
headers: {
|
|
1448
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1449
|
+
'X-Required': 'required-value',
|
|
1450
|
+
},
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1454
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1455
|
+
const body = await response.json();
|
|
1456
|
+
|
|
1457
|
+
expect(response.status).toBe(200);
|
|
1458
|
+
expect(body.result.required).toBe('required-value');
|
|
1459
|
+
// headers.get() returns null for missing headers
|
|
1460
|
+
expect(body.result.optional).toBeNull();
|
|
1461
|
+
expect(body.result.hasOptional).toBe(false);
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
test('should return 500 when required query parameter is missing', async () => {
|
|
1465
|
+
@Controller('/api')
|
|
1466
|
+
class ApiController extends BaseController {
|
|
1467
|
+
@Get('/required-query')
|
|
1468
|
+
async requiredQuery(
|
|
1469
|
+
@Query('required', { required: true }) required: string,
|
|
1470
|
+
) {
|
|
1471
|
+
return { required };
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
@Module({
|
|
1476
|
+
controllers: [ApiController],
|
|
1477
|
+
})
|
|
1478
|
+
class TestModule {}
|
|
1479
|
+
|
|
1480
|
+
const app = createTestApp(TestModule);
|
|
1481
|
+
await app.start();
|
|
1482
|
+
|
|
1483
|
+
// Missing required query parameter
|
|
1484
|
+
const request = new Request('http://localhost:3000/api/required-query', {
|
|
1485
|
+
method: 'GET',
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1489
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1490
|
+
|
|
1491
|
+
expect(response.status).toBe(500);
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
test('should pass validation with required query parameter present', async () => {
|
|
1495
|
+
@Controller('/api')
|
|
1496
|
+
class ApiController extends BaseController {
|
|
1497
|
+
@Get('/required-query')
|
|
1498
|
+
async requiredQuery(
|
|
1499
|
+
@Query('required', { required: true }) required: string,
|
|
1500
|
+
) {
|
|
1501
|
+
return { required };
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
@Module({
|
|
1506
|
+
controllers: [ApiController],
|
|
1507
|
+
})
|
|
1508
|
+
class TestModule {}
|
|
1509
|
+
|
|
1510
|
+
const app = createTestApp(TestModule);
|
|
1511
|
+
await app.start();
|
|
1512
|
+
|
|
1513
|
+
const request = new Request('http://localhost:3000/api/required-query?required=value', {
|
|
1514
|
+
method: 'GET',
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1518
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1519
|
+
const body = await response.json();
|
|
1520
|
+
|
|
1521
|
+
expect(response.status).toBe(200);
|
|
1522
|
+
expect(body.result.required).toBe('value');
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
test('should validate query parameter with arktype schema', async () => {
|
|
1526
|
+
const numberSchema = arktype('string.numeric.parse');
|
|
1527
|
+
|
|
1528
|
+
@Controller('/api')
|
|
1529
|
+
class ApiController extends BaseController {
|
|
1530
|
+
@Get('/validated')
|
|
1531
|
+
async validated(
|
|
1532
|
+
@Query('count', numberSchema) count: number,
|
|
1533
|
+
) {
|
|
1534
|
+
return { count, typeOf: typeof count };
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
@Module({
|
|
1539
|
+
controllers: [ApiController],
|
|
1540
|
+
})
|
|
1541
|
+
class TestModule {}
|
|
1542
|
+
|
|
1543
|
+
const app = createTestApp(TestModule);
|
|
1544
|
+
await app.start();
|
|
1545
|
+
|
|
1546
|
+
const request = new Request('http://localhost:3000/api/validated?count=42', {
|
|
1547
|
+
method: 'GET',
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1551
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1552
|
+
const body = await response.json();
|
|
1553
|
+
|
|
1554
|
+
expect(response.status).toBe(200);
|
|
1555
|
+
expect(body.result.count).toBe(42);
|
|
1556
|
+
expect(body.result.typeOf).toBe('number');
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
test('should fail validation with invalid arktype schema value', async () => {
|
|
1560
|
+
const numberSchema = arktype('string.numeric.parse');
|
|
1561
|
+
|
|
1562
|
+
@Controller('/api')
|
|
1563
|
+
class ApiController extends BaseController {
|
|
1564
|
+
@Get('/validated')
|
|
1565
|
+
async validated(
|
|
1566
|
+
@Query('count', numberSchema) count: number,
|
|
1567
|
+
) {
|
|
1568
|
+
return { count };
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
@Module({
|
|
1573
|
+
controllers: [ApiController],
|
|
1574
|
+
})
|
|
1575
|
+
class TestModule {}
|
|
1576
|
+
|
|
1577
|
+
const app = createTestApp(TestModule);
|
|
1578
|
+
await app.start();
|
|
1579
|
+
|
|
1580
|
+
// Invalid value: "not-a-number" instead of numeric string
|
|
1581
|
+
const request = new Request('http://localhost:3000/api/validated?count=not-a-number', {
|
|
1582
|
+
method: 'GET',
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1586
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1587
|
+
|
|
1588
|
+
expect(response.status).toBe(500);
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
test('should validate body with arktype schema', async () => {
|
|
1592
|
+
const userSchema = arktype({
|
|
1593
|
+
name: 'string',
|
|
1594
|
+
age: 'number',
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
@Controller('/api')
|
|
1598
|
+
class ApiController extends BaseController {
|
|
1599
|
+
@Post('/user')
|
|
1600
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1601
|
+
async createUser(@Body(userSchema) user: any) {
|
|
1602
|
+
return { user };
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
@Module({
|
|
1607
|
+
controllers: [ApiController],
|
|
1608
|
+
})
|
|
1609
|
+
class TestModule {}
|
|
1610
|
+
|
|
1611
|
+
const app = createTestApp(TestModule);
|
|
1612
|
+
await app.start();
|
|
1613
|
+
|
|
1614
|
+
const request = new Request('http://localhost:3000/api/user', {
|
|
1615
|
+
method: 'POST',
|
|
1616
|
+
headers: {
|
|
1617
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1618
|
+
'Content-Type': 'application/json',
|
|
1619
|
+
},
|
|
1620
|
+
body: JSON.stringify({ name: 'John', age: 30 }),
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1624
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1625
|
+
const body = await response.json();
|
|
1626
|
+
|
|
1627
|
+
expect(response.status).toBe(200);
|
|
1628
|
+
expect(body.result.user).toEqual({ name: 'John', age: 30 });
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
test('should fail body validation with invalid data', async () => {
|
|
1632
|
+
const userSchema = arktype({
|
|
1633
|
+
name: 'string',
|
|
1634
|
+
age: 'number',
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
@Controller('/api')
|
|
1638
|
+
class ApiController extends BaseController {
|
|
1639
|
+
@Post('/user')
|
|
1640
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1641
|
+
async createUser(@Body(userSchema) user: any) {
|
|
1642
|
+
return { user };
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
@Module({
|
|
1647
|
+
controllers: [ApiController],
|
|
1648
|
+
})
|
|
1649
|
+
class TestModule {}
|
|
1650
|
+
|
|
1651
|
+
const app = createTestApp(TestModule);
|
|
1652
|
+
await app.start();
|
|
1653
|
+
|
|
1654
|
+
// Invalid: age is string instead of number
|
|
1655
|
+
const request = new Request('http://localhost:3000/api/user', {
|
|
1656
|
+
method: 'POST',
|
|
1657
|
+
headers: {
|
|
1658
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
1659
|
+
'Content-Type': 'application/json',
|
|
1660
|
+
},
|
|
1661
|
+
body: JSON.stringify({ name: 'John', age: 'thirty' }),
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1665
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1666
|
+
|
|
1667
|
+
expect(response.status).toBe(500);
|
|
954
1668
|
});
|
|
955
1669
|
|
|
956
1670
|
test('should handle metrics endpoint', async () => {
|
|
@@ -635,6 +635,26 @@ export class OneBunApplication {
|
|
|
635
635
|
let route = routes.get(exactRouteKey);
|
|
636
636
|
const paramValues: Record<string, string | string[]> = {};
|
|
637
637
|
|
|
638
|
+
// Extract query parameters from URL
|
|
639
|
+
for (const [rawKey, value] of url.searchParams.entries()) {
|
|
640
|
+
// Handle array notation: tag[] -> tag (as array)
|
|
641
|
+
const isArrayNotation = rawKey.endsWith('[]');
|
|
642
|
+
const key = isArrayNotation ? rawKey.replace('[]', '') : rawKey;
|
|
643
|
+
|
|
644
|
+
const existing = paramValues[key];
|
|
645
|
+
if (existing !== undefined) {
|
|
646
|
+
// Handle multiple values with same key (e.g., ?tag=a&tag=b or ?tag[]=a&tag[]=b)
|
|
647
|
+
paramValues[key] = Array.isArray(existing)
|
|
648
|
+
? [...existing, value]
|
|
649
|
+
: [existing, value];
|
|
650
|
+
} else if (isArrayNotation) {
|
|
651
|
+
// Array notation always creates an array, even with single value
|
|
652
|
+
paramValues[key] = [value];
|
|
653
|
+
} else {
|
|
654
|
+
paramValues[key] = value;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
638
658
|
// If no exact match, try pattern matching
|
|
639
659
|
if (!route) {
|
|
640
660
|
for (const [_routeKey, routeData] of routes) {
|