@@ -570,6 +570,226 @@ struct router_test
570570 }
571571 }
572572
573+ void testOptionsMethod ()
574+ {
575+ static auto const GET = http::method::get;
576+ static auto const POST = http::method::post ;
577+ static auto const PUT = http::method::put;
578+ static auto const DELETE_ = http::method::delete_;
579+ static auto const OPTIONS = http::method::options;
580+
581+ // OPTIONS on single-verb route
582+ { test_router r; r.add (GET, " /x" , h_send); check (r, OPTIONS, " /x" ); }
583+
584+ // OPTIONS on multi-verb route
585+ { test_router r; r.route (" /x" ).add (GET, h_send).add (POST, h_send); check (r, OPTIONS, " /x" ); }
586+
587+ // OPTIONS on unmatched path -> route_next
588+ { test_router r; r.add (GET, " /x" , h_send); check (r, OPTIONS, " /y" , route_next); }
589+
590+ // 405 sets Allow header (non-OPTIONS wrong method)
591+ {
592+ test_router r;
593+ r.add (GET, " /x" , h_send);
594+ params req;
595+ init_sink (req);
596+ route_result rv;
597+ capy::test::run_blocking ([&](route_result res) { rv = res; })(
598+ r.dispatch (POST, urls::url_view (" /x" ), req));
599+ BOOST_TEST (rv.what () == route_what::done);
600+ BOOST_TEST (req.res .exists (field::allow));
601+ BOOST_TEST (req.res .status () == status::method_not_allowed);
602+ }
603+
604+ // Allow header lists all registered verbs
605+ {
606+ test_router r;
607+ r.route (" /api" )
608+ .add (GET, h_send)
609+ .add (POST, h_send)
610+ .add (DELETE_, h_send);
611+ params req;
612+ init_sink (req);
613+ route_result rv;
614+ capy::test::run_blocking ([&](route_result res) { rv = res; })(
615+ r.dispatch (PUT, urls::url_view (" /api" ), req));
616+ auto allow = req.res .value_or (field::allow, " " );
617+ BOOST_TEST (allow.find (" GET" ) != std::string_view::npos);
618+ BOOST_TEST (allow.find (" POST" ) != std::string_view::npos);
619+ BOOST_TEST (allow.find (" DELETE" ) != std::string_view::npos);
620+ }
621+
622+ // OPTIONS with custom verb
623+ {
624+ test_router r;
625+ r.add (GET, " /x" , h_send);
626+ r.add (" CUSTOM" , " /x" , h_send);
627+ params req;
628+ init_sink (req);
629+ route_result rv;
630+ capy::test::run_blocking ([&](route_result res) { rv = res; })(
631+ r.dispatch (PUT, urls::url_view (" /x" ), req));
632+ auto allow = req.res .value_or (field::allow, " " );
633+ BOOST_TEST (allow.find (" CUSTOM" ) != std::string_view::npos);
634+ BOOST_TEST (allow.find (" GET" ) != std::string_view::npos);
635+ }
636+
637+ // all() produces full method list on 405
638+ {
639+ test_router r;
640+ r.all (" /x" , h_next);
641+ params req;
642+ init_sink (req);
643+ route_result rv;
644+ capy::test::run_blocking ([&](route_result res) { rv = res; })(
645+ r.dispatch (GET, urls::url_view (" /x" ), req));
646+ // all() handler returned route_next, so
647+ // the router sees path matched and falls
648+ // through to 405 with the full set
649+ auto allow = req.res .value_or (field::allow, " " );
650+ BOOST_TEST (allow.find (" GET" ) != std::string_view::npos);
651+ BOOST_TEST (allow.find (" POST" ) != std::string_view::npos);
652+ BOOST_TEST (allow.find (" PUT" ) != std::string_view::npos);
653+ BOOST_TEST (allow.find (" DELETE" ) != std::string_view::npos);
654+ }
655+
656+ // set_options_handler customizes OPTIONS response
657+ {
658+ test_router r;
659+ r.add (GET, " /x" , h_send);
660+ r.add (POST, " /x" , h_send);
661+ r.set_options_handler (
662+ [](params& rp, std::string_view allow) -> route_task
663+ {
664+ rp.res .set (field::allow, allow);
665+ rp.res .set (field::access_control_allow_methods, allow);
666+ co_return route_done;
667+ });
668+ params req;
669+ init_sink (req);
670+ route_result rv;
671+ capy::test::run_blocking ([&](route_result res) { rv = res; })(
672+ r.dispatch (OPTIONS, urls::url_view (" /x" ), req));
673+ BOOST_TEST (rv.what () == route_what::done);
674+ BOOST_TEST (req.res .exists (field::allow));
675+ BOOST_TEST (req.res .exists (
676+ field::access_control_allow_methods));
677+ }
678+ }
679+
680+ void testMultiLevelVerbs ()
681+ {
682+ static auto const GET = http::method::get;
683+ static auto const POST = http::method::post ;
684+ static auto const PUT = http::method::put;
685+ static auto const DELETE_ = http::method::delete_;
686+ static auto const OPTIONS = http::method::options;
687+
688+ // Multi-level route tree: /api/users and /api/posts
689+ // with different verbs at each level
690+ {
691+ test_router r;
692+ r.use (" /api" , []{
693+ test_router r2;
694+ r2.add (GET, " /users" , h_send);
695+ r2.add (POST, " /users" , h_send);
696+ r2.add (GET, " /posts" , h_send);
697+ r2.add (PUT, " /posts" , h_send);
698+ r2.add (DELETE_, " /posts" , h_send);
699+ return r2;
700+ }());
701+
702+ // Direct verb matches
703+ check (r, GET, " /api/users" );
704+ check (r, POST, " /api/users" );
705+ check (r, GET, " /api/posts" );
706+ check (r, PUT, " /api/posts" );
707+ check (r, DELETE_, " /api/posts" );
708+
709+ // OPTIONS on each sub-path
710+ check (r, OPTIONS, " /api/users" );
711+ check (r, OPTIONS, " /api/posts" );
712+
713+ // Wrong verb -> 405
714+ {
715+ params req;
716+ init_sink (req);
717+ route_result rv;
718+ capy::test::run_blocking ([&](route_result res) { rv = res; })(
719+ r.dispatch (DELETE_, urls::url_view (" /api/users" ), req));
720+ BOOST_TEST (req.res .status () == status::method_not_allowed);
721+ auto allow = req.res .value_or (field::allow, " " );
722+ BOOST_TEST (allow.find (" GET" ) != std::string_view::npos);
723+ BOOST_TEST (allow.find (" POST" ) != std::string_view::npos);
724+ // DELETE not allowed on /users
725+ BOOST_TEST (allow.find (" DELETE" ) == std::string_view::npos);
726+ }
727+
728+ // Wrong verb on /posts
729+ {
730+ params req;
731+ init_sink (req);
732+ route_result rv;
733+ capy::test::run_blocking ([&](route_result res) { rv = res; })(
734+ r.dispatch (POST, urls::url_view (" /api/posts" ), req));
735+ BOOST_TEST (req.res .status () == status::method_not_allowed);
736+ auto allow = req.res .value_or (field::allow, " " );
737+ BOOST_TEST (allow.find (" GET" ) != std::string_view::npos);
738+ BOOST_TEST (allow.find (" PUT" ) != std::string_view::npos);
739+ BOOST_TEST (allow.find (" DELETE" ) != std::string_view::npos);
740+ }
741+ }
742+
743+ // Deeply nested: /v1/admin/settings
744+ {
745+ test_router r;
746+ r.use (" /v1" , []{
747+ test_router r2;
748+ r2.use (" /admin" , []{
749+ test_router r3;
750+ r3.add (GET, " /settings" , h_send);
751+ r3.add (PUT, " /settings" , h_send);
752+ return r3;
753+ }());
754+ return r2;
755+ }());
756+
757+ check (r, GET, " /v1/admin/settings" );
758+ check (r, PUT, " /v1/admin/settings" );
759+ check (r, OPTIONS, " /v1/admin/settings" );
760+
761+ // Wrong verb
762+ {
763+ params req;
764+ init_sink (req);
765+ route_result rv;
766+ capy::test::run_blocking ([&](route_result res) { rv = res; })(
767+ r.dispatch (POST, urls::url_view (" /v1/admin/settings" ), req));
768+ BOOST_TEST (req.res .status () == status::method_not_allowed);
769+ }
770+ }
771+
772+ // Sibling sub-routers with non-overlapping verbs
773+ {
774+ test_router r;
775+ r.use (" /svc" , []{
776+ test_router r2;
777+ r2.add (GET, " /health" , h_send);
778+ return r2;
779+ }());
780+ r.use (" /svc" , []{
781+ test_router r2;
782+ r2.add (POST, " /rpc" , h_send);
783+ return r2;
784+ }());
785+
786+ check (r, GET, " /svc/health" );
787+ check (r, POST, " /svc/rpc" );
788+ check (r, OPTIONS, " /svc/health" );
789+ check (r, OPTIONS, " /svc/rpc" );
790+ }
791+ }
792+
573793 void run ()
574794 {
575795 testUse ();
@@ -582,6 +802,8 @@ struct router_test
582802 testPathDecoding ();
583803 testCrossTypeConstruction ();
584804 testDynamicTransform ();
805+ testOptionsMethod ();
806+ testMultiLevelVerbs ();
585807 }
586808};
587809
0 commit comments