diff --git a/src/chttpd_handlers.erl b/src/chttpd_handlers.erl index e21986b..6ff42e4 100644 --- a/src/chttpd_handlers.erl +++ b/src/chttpd_handlers.erl @@ -16,7 +16,11 @@ provider/2, url_handler/2, db_handler/2, - design_handler/2 + design_handler/2, + endpoints/1, + endpoints/2, + handler/3, + handlers/2 ]). -include_lib("couch/include/couch_db.hrl"). @@ -38,6 +42,24 @@ db_handler(HandlerKey, DefaultFun) -> design_handler(HandlerKey, DefaultFun) -> select(collect(design_handler, [HandlerKey]), DefaultFun). +%% ------------------------------------------------------------------ +%% Introspection Function Definitions +%% ------------------------------------------------------------------ + +endpoints(Module, EndpointType) -> + Module:endpoints(EndpointType). + +endpoints(EndpointType) -> + Results = do_apply(endpoints, [EndpointType], [ignore_providers]), + lists:append(Results). + +handler(Module, EndpointType, HandlerKey) -> + Module:EndpointType(HandlerKey). + +handlers(Module, EndpointType) -> + Endpoints = endpoints(Module, EndpointType), + [{E, handler(Module, EndpointType, E)} || E <- Endpoints]. + %% ------------------------------------------------------------------ %% Internal Function Definitions %% ------------------------------------------------------------------ diff --git a/src/chttpd_httpd_handlers.erl b/src/chttpd_httpd_handlers.erl index b91aae9..2f25c99 100644 --- a/src/chttpd_httpd_handlers.erl +++ b/src/chttpd_httpd_handlers.erl @@ -12,7 +12,7 @@ -module(chttpd_httpd_handlers). --export([url_handler/1, db_handler/1, design_handler/1]). +-export([url_handler/1, db_handler/1, design_handler/1, endpoints/1]). url_handler(<<>>) -> fun chttpd_misc:handle_welcome_req/1; url_handler(<<"favicon.ico">>) -> fun chttpd_misc:handle_favicon_req/1; @@ -41,3 +41,44 @@ design_handler(<<"_update">>) -> fun chttpd_show:handle_doc_update_req/3; design_handler(<<"_info">>) -> fun chttpd_db:handle_design_info_req/3; design_handler(<<"_rewrite">>) -> fun chttpd_rewrite:handle_rewrite_req/3; design_handler(_) -> no_match. + +endpoints(url_handler) -> + [ + <<>>, + <<"favicon.ico">>, + <<"_utils">>, + <<"_all_dbs">>, + <<"_active_tasks">>, + <<"_node">>, + <<"_reload_query_servers">>, + <<"_replicate">>, + <<"_uuids">>, + <<"_session">>, + <<"_up">> + ]; +endpoints(db_handler) -> + [ + <<"_view_cleanup">>, + <<"_compact">>, + <<"_design">>, + <<"_temp_view">>, + <<"_changes">> + ]; +endpoints(design_handler) -> + [ + <<"_view">>, + <<"_show">>, + <<"_list">>, + <<"_update">>, + <<"_info">>, + <<"_rewrite">> + ]. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +chttpd_endpoints_test_() -> + Apps = [couch_epi, chttpd], + chttpd_httpd_handlers_test_util:endpoints_test(chttpd, ?MODULE, Apps). + +-endif. diff --git a/src/chttpd_httpd_handlers_test_util.erl b/src/chttpd_httpd_handlers_test_util.erl new file mode 100644 index 0000000..18bd241 --- /dev/null +++ b/src/chttpd_httpd_handlers_test_util.erl @@ -0,0 +1,204 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(chttpd_httpd_handlers_test_util). + +-export([endpoints_test/3]). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). + +%%%========================================================================= +%%% Test environment defintions +%%%========================================================================= + +start(App, Apps) -> + Ctx = test_util:start_couch(Apps), + wait_handlers(App), + Ctx. + +setup(mocked) -> + fun setup_mocked/1; +setup(not_mocked) -> + fun setup_not_mocked/1; +setup(skip) -> + fun(_) -> undefined end. + +setup_mocked({Endpoint, {_Path, Module, Function}}) -> + catch meck:unload(Module), + meck:new(Module, [passthrough, non_strict]), + Expected = mock_handler(Endpoint, Module, Function), + Expected. + +setup_not_mocked({_Endpoint, {_Path, Module, _Function}}) -> + catch meck:unload(Module), + meck:new(Module, [non_strict]), + undefined. + +teardown({_Endpoint, {Module, _F, _A}}, _) -> + catch meck:unload(Module), + ok. + +endpoints_test(App, Module, Apps) -> + { + "Checking dynamic endpoints", + { + setup, + fun() -> start(App, Apps) end, + fun test_util:stop/1, + %% we use instantiator to postpone test instantiation + %% so we can detect endpoint overrides + fun(_) -> make_tests(App, Module, [ + {mocked, url_handler}, + {mocked, db_handler}, + {mocked, design_handler}, + {not_mocked, url_handler}, + {not_mocked, db_handler}, + {not_mocked, design_handler} + ]) + end + } + }. + +check_dynamic_endpoints(Setup, EndpointType, App, Module) -> + {Specs, Skips} = get_handlers(EndpointType, Module), + TestMessage = "Checking '" + ++ atom_to_list(App) ++ " -- " + ++ atom_to_list(EndpointType) + ++ "' [" ++ atom_to_list(Setup) ++ "] dynamic endpoints", + { + TestMessage, [ + make_test_case(Setup, EndpointType, Spec) || Spec <- Specs + ] ++ [ + make_test_case(skip, EndpointType, Spec) || Spec <- Skips + ] + }. + +make_test_case(Setup, EndpointType, {Path, Module, Function}) -> + { + lists:flatten(io_lib:format("~s -- \"~s\"", [EndpointType, ?b2l(Path)])), + { + foreachx, setup(Setup), fun teardown/2, + [ + {{EndpointType, {Path, Module, Function}}, select_test(Setup)} + ] + } + }. + + +make_tests(App, Module, Casses) -> + [ + check_dynamic_endpoints(Setup, EndpointType, App, Module) + || {Setup, EndpointType} <- Casses + ]. + +select_test(mocked) -> fun ensure_called/2; +select_test(not_mocked) -> fun verify_we_fail_if_missing/2; +select_test(skip) -> fun ensure_skip_overridden/2. + +mock_handler(url_handler = Endpoint, M, F) -> + meck:expect(M, F, fun(X) -> {return, Endpoint, X} end), + fun M:F/1; +mock_handler(db_handler = Endpoint, M, F) -> + meck:expect(M, F, fun(X, Y) -> {return, Endpoint, X, Y} end), + fun M:F/2; +mock_handler(design_handler = Endpoint, M, F) -> + meck:expect(M, F, fun(X, Y, Z) -> {return, Endpoint, X, Y, Z} end), + fun M:F/3. + +%%%========================================================================= +%%% Test functions definitions +%%%========================================================================= + +ensure_skip_overridden(_, _) -> + ?_assert(true). + +ensure_called({url_handler = Endpoint, {Path, _M, _Fun}}, ExpectedFun) -> + HandlerFun = handler(Endpoint, Path), + ?_test(begin + ?assertEqual(ExpectedFun, HandlerFun), + ?assertMatch({return, Endpoint, x}, HandlerFun(x)) + end); +ensure_called({db_handler = Endpoint, {Path, _M, _Fun}}, ExpectedFun) -> + HandlerFun = handler(Endpoint, Path), + ?_test(begin + ?assertEqual(ExpectedFun, HandlerFun), + ?assertMatch({return, Endpoint, x, y}, HandlerFun(x, y)) + end); +ensure_called({design_handler = Endpoint, {Path, _M, _Fun}}, ExpectedFun) -> + HandlerFun = handler(Endpoint, Path), + ?_test(begin + ?assertEqual(ExpectedFun, HandlerFun), + ?assertMatch({return, Endpoint, x, y, z}, HandlerFun(x, y, z)) + end). + +%% Test the test: when the final target function is missing, +%% the Fun call must fail. +verify_we_fail_if_missing({url_handler = Endpoint, {Path, _M, _Fun}}, _) -> + HandlerFun = handler(Endpoint, Path), + ?_test(begin + ?assert(is_function(HandlerFun)), + ?assertError(undef, HandlerFun(x)) + end); +verify_we_fail_if_missing({db_handler = Endpoint, {Path, _M, _Fun}}, _) -> + HandlerFun = handler(Endpoint, Path), + ?_test(begin + ?assert(is_function(HandlerFun)), + ?assertError(undef, HandlerFun(x, y)) + end); +verify_we_fail_if_missing({design_handler = Endpoint, {Path, _M, _Fun}}, _) -> + HandlerFun = handler(Endpoint, Path), + ?_test(begin + ?assert(is_function(HandlerFun)), + ?assertError(undef, HandlerFun(x, y, z)) + end). + +%%%========================================================================= +%%% Internal functions definitions +%%%========================================================================= + +handler(EndpointType, HandlerKey) -> + chttpd_handlers:EndpointType(HandlerKey, default_handler(EndpointType)). + +get_active_handler(EndpointType, HandlerKey) -> + fun2spec(HandlerKey, handler(EndpointType, HandlerKey)). + +fun2spec(HandlerKey, Fun) -> + Info = erlang:fun_info(Fun), + { + HandlerKey, + proplists:get_value(module, Info), + proplists:get_value(name, Info) + }. + +default_handler(url_handler) -> fun chttpd_db:handle_request/1; +default_handler(db_handler) -> fun chttpd_db:db_req/2; +default_handler(design_handler) -> fun chttpd_db:bad_action_req/3. + +get_handlers(EndpointType, Module) -> + Handlers = chttpd_handlers:handlers(Module, EndpointType), + HandlerSpecs = [fun2spec(HandlerKey, Fun) || {HandlerKey, Fun} <- Handlers], + lists:partition(fun(Spec) -> + is_active(EndpointType, Spec) + end, HandlerSpecs). + +is_active(EndpointType, {Path, _Module, _Function} = Spec) -> + get_active_handler(EndpointType, Path) == Spec. + +wait_handlers(App) -> + Handle = couch_epi:get_handle(chttpd_handlers), + test_util:wait(fun() -> + case couch_epi:is_defined_by_app(Handle, App, url_handler, 1) of + false -> wait; + _ -> true + end + end).