@@ -7,7 +7,32 @@ import Server from '../src/Server.js';
77import Request from '../src/Request.js' ;
88import Response from '../src/Response.js' ;
99import type { ActionRouteProps } from '../src/types.js' ;
10- import { Controller , Get , On , Post , mount } from '../src/decorators.js' ;
10+ import {
11+ All ,
12+ Connect ,
13+ Controller ,
14+ Delete ,
15+ Get ,
16+ Head ,
17+ On ,
18+ Options ,
19+ Patch ,
20+ Post ,
21+ Put ,
22+ Trace ,
23+ addEvent ,
24+ addRoute ,
25+ assertHandler ,
26+ controllerOf ,
27+ hasEvent ,
28+ hasRoute ,
29+ metadataOf ,
30+ mount ,
31+ normalizePath ,
32+ registerEvent ,
33+ registerRoute ,
34+ routeDecorator
35+ } from '../src/decorators.js' ;
1136
1237type TestRouterProps = ActionRouteProps <
1338 unknown ,
@@ -127,4 +152,220 @@ describe('Decorator Tests', () => {
127152 await server . emit ( 'GET /api/users' , req , res ) ;
128153 expect ( res . headers . get ( 'x-hook' ) ) . to . equal ( 'true' ) ;
129154 } ) ;
155+
156+ it ( 'Should expose decorator helper utilities for direct mounting flows' ,
157+ async ( ) => {
158+ class UtilityController {
159+ public prefix = 'utility' ;
160+
161+ public route ( { res } : TestRouterProps ) {
162+ res . set ( 'text/plain' , this . prefix ) ;
163+ }
164+
165+ public event ( { res } : TestRouterProps ) {
166+ res . headers . set ( 'x-utility' , 'true' ) ;
167+ }
168+ }
169+
170+ const metadata = metadataOf ( UtilityController ) ;
171+ addRoute ( UtilityController , {
172+ method : 'GET' ,
173+ path : '/utility//path' ,
174+ property : 'route' ,
175+ priority : 4
176+ } ) ;
177+ addRoute ( UtilityController , {
178+ method : 'GET' ,
179+ path : '/utility//path' ,
180+ property : 'route' ,
181+ priority : 4
182+ } ) ;
183+ addEvent ( UtilityController , {
184+ event : 'GET /api/utility/path' ,
185+ property : 'event' ,
186+ priority : 6
187+ } ) ;
188+ addEvent ( UtilityController , {
189+ event : 'GET /api/utility/path' ,
190+ property : 'event' ,
191+ priority : 6
192+ } ) ;
193+
194+ const controller = controllerOf ( UtilityController ) ;
195+ const router = new Router ( ) ;
196+
197+ registerRoute ( router , controller , '/api/' , metadata . routes [ 0 ] ) ;
198+ registerEvent ( router , controller , metadata . events [ 0 ] ) ;
199+
200+ const req = new Request ( {
201+ method : 'GET' ,
202+ url : new URL ( 'http://localhost/api/utility/path' )
203+ } ) ;
204+ const res = new Response ( ) ;
205+ await router . emit ( 'GET /api/utility/path' , req , res ) ;
206+
207+ expect ( hasRoute ( metadata , metadata . routes [ 0 ] ) ) . to . equal ( true ) ;
208+ expect ( hasEvent ( metadata , metadata . events [ 0 ] ) ) . to . equal ( true ) ;
209+ expect ( normalizePath ( '/api/' , '/utility//path' ) ) . to . equal (
210+ '/api/utility/path'
211+ ) ;
212+ expect ( res . body ) . to . equal ( 'utility' ) ;
213+ expect ( res . headers . get ( 'x-utility' ) ) . to . equal ( 'true' ) ;
214+ } ) ;
215+
216+ it ( 'Should reject non-function controller members when mounting' , ( ) => {
217+ const controller = { broken : 'nope' } ;
218+
219+ expect ( ( ) => assertHandler ( controller , 'broken' ) ) . to . throw (
220+ 'Controller member "broken" is not a function'
221+ ) ;
222+ } ) ;
223+
224+ it ( 'Should cover the remaining http method decorators' , async ( ) => {
225+ @Controller ( '/extra' )
226+ class ExtraController {
227+ @All ( '/all' )
228+ public all ( { res } : TestRouterProps ) {
229+ res . set ( 'text/plain' , 'all' ) ;
230+ }
231+
232+ @Connect ( '/connect' )
233+ public connect ( { res } : TestRouterProps ) {
234+ res . set ( 'text/plain' , 'connect' ) ;
235+ }
236+
237+ @Delete ( '/delete' )
238+ public remove ( { res } : TestRouterProps ) {
239+ res . set ( 'text/plain' , 'delete' ) ;
240+ }
241+
242+ @Head ( '/head' )
243+ public head ( { res } : TestRouterProps ) {
244+ res . set ( 'text/plain' , 'head' ) ;
245+ }
246+
247+ @Options ( '/options' )
248+ public options ( { res } : TestRouterProps ) {
249+ res . set ( 'text/plain' , 'options' ) ;
250+ }
251+
252+ @Patch ( '/patch' )
253+ public patch ( { res } : TestRouterProps ) {
254+ res . set ( 'text/plain' , 'patch' ) ;
255+ }
256+
257+ @Put ( '/put' )
258+ public put ( { res } : TestRouterProps ) {
259+ res . set ( 'text/plain' , 'put' ) ;
260+ }
261+
262+ @Trace ( '/trace' )
263+ public trace ( { res } : TestRouterProps ) {
264+ res . set ( 'text/plain' , 'trace' ) ;
265+ }
266+ }
267+
268+ const router = new Router ( ) ;
269+ mount ( router , ExtraController ) ;
270+
271+ const expectations : Array < [ string , string , string ] > = [
272+ [ 'POST' , '/extra/all' , 'all' ] ,
273+ [ 'CONNECT' , '/extra/connect' , 'connect' ] ,
274+ [ 'DELETE' , '/extra/delete' , 'delete' ] ,
275+ [ 'HEAD' , '/extra/head' , 'head' ] ,
276+ [ 'OPTIONS' , '/extra/options' , 'options' ] ,
277+ [ 'PATCH' , '/extra/patch' , 'patch' ] ,
278+ [ 'PUT' , '/extra/put' , 'put' ] ,
279+ [ 'TRACE' , '/extra/trace' , 'trace' ]
280+ ] ;
281+
282+ for ( const [ method , pathname , body ] of expectations ) {
283+ const req = new Request ( {
284+ method : method as 'POST' ,
285+ url : new URL ( `http://localhost${ pathname } ` )
286+ } ) ;
287+ const res = new Response ( ) ;
288+ await router . emit ( `${ method } ${ pathname } ` , req , res ) ;
289+ expect ( res . body ) . to . equal ( body ) ;
290+ }
291+ } ) ;
292+
293+ it ( 'Should support the standard decorator initializer path directly' , ( ) => {
294+ class StandardController {
295+ public handler ( ) {
296+ return 'ok' ;
297+ }
298+ }
299+
300+ let routeInitializer : ( ( this : StandardController ) => void ) | undefined ;
301+ let eventInitializer : ( ( this : StandardController ) => void ) | undefined ;
302+
303+ routeDecorator ( 'GET' ) ( '/standard' , 3 ) (
304+ StandardController . prototype . handler ,
305+ {
306+ addInitializer ( callback ) {
307+ routeInitializer = callback as ( this : StandardController ) => void ;
308+ } ,
309+ kind : 'method' ,
310+ name : 'handler' ,
311+ static : false
312+ } as ClassMethodDecoratorContext
313+ ) ;
314+
315+ On ( 'GET /standard' , 5 ) (
316+ StandardController . prototype . handler ,
317+ {
318+ addInitializer ( callback ) {
319+ eventInitializer = callback as ( this : StandardController ) => void ;
320+ } ,
321+ kind : 'method' ,
322+ name : 'handler' ,
323+ static : false
324+ } as ClassMethodDecoratorContext
325+ ) ;
326+
327+ const controller = new StandardController ( ) ;
328+ routeInitializer ?. call ( controller ) ;
329+ eventInitializer ?. call ( controller ) ;
330+
331+ const metadata = metadataOf ( StandardController ) ;
332+
333+ expect ( metadata . routes ) . to . deep . equal ( [ {
334+ method : 'GET' ,
335+ path : '/standard' ,
336+ property : 'handler' ,
337+ priority : 3
338+ } ] ) ;
339+ expect ( metadata . events ) . to . deep . equal ( [ {
340+ event : 'GET /standard' ,
341+ property : 'handler' ,
342+ priority : 5
343+ } ] ) ;
344+ } ) ;
345+
346+ it ( 'Should reject static standard decorator targets' , ( ) => {
347+ expect ( ( ) => routeDecorator ( 'GET' ) ( '/bad' ) (
348+ ( ) => void 0 ,
349+ {
350+ addInitializer ( ) {
351+ return void 0 ;
352+ } ,
353+ kind : 'method' ,
354+ name : 'bad' ,
355+ static : true
356+ } as ClassMethodDecoratorContext
357+ ) ) . to . throw ( 'Decorator "bad" must be an instance method' ) ;
358+
359+ expect ( ( ) => On ( 'bad' ) (
360+ ( ) => void 0 ,
361+ {
362+ addInitializer ( ) {
363+ return void 0 ;
364+ } ,
365+ kind : 'method' ,
366+ name : 'bad' ,
367+ static : true
368+ } as ClassMethodDecoratorContext
369+ ) ) . to . throw ( 'Decorator "bad" must be an instance method' ) ;
370+ } ) ;
130371} ) ;
0 commit comments