Skip to content

Commit b443381

Browse files
committed
additional router OPTIONS tests
1 parent 1a19de1 commit b443381

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

test/unit/server/router.cpp

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)