From 2ecfdab3e89535a519a82f72795b9979c3269a7f Mon Sep 17 00:00:00 2001 From: ILYA Khlopotov Date: Mon, 24 Aug 2015 17:16:56 -0700 Subject: [PATCH 1/5] Refactor test suite for httpd endpoints Split out common parts into util module so we can reuse test suite. The util module could be used for testing: - endpoints defined in other applications - overridden endpoints COUCHDB-2789 --- src/chttpd_httpd_handlers_test_util.erl | 186 ++++++++++++++++++++++++ test/chttpd_httpd_handlers_tests.erl | 54 +++++++ 2 files changed, 240 insertions(+) create mode 100644 src/chttpd_httpd_handlers_test_util.erl create mode 100644 test/chttpd_httpd_handlers_tests.erl diff --git a/src/chttpd_httpd_handlers_test_util.erl b/src/chttpd_httpd_handlers_test_util.erl new file mode 100644 index 0000000..f3ce52e --- /dev/null +++ b/src/chttpd_httpd_handlers_test_util.erl @@ -0,0 +1,186 @@ +% 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 +%%%========================================================================= + +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() -> test_util:start_couch(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) + ++ "' [" ++ 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) -> + Info = erlang:fun_info(handler(EndpointType, HandlerKey)), + { + 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 = Module:handlers(EndpointType), + lists:partition(fun(Spec) -> + is_active(EndpointType, Spec) + end, Handlers). + +is_active(EndpointType, {Path, _Module, _Function} = Spec) -> + get_active_handler(EndpointType, Path) == Spec. diff --git a/test/chttpd_httpd_handlers_tests.erl b/test/chttpd_httpd_handlers_tests.erl new file mode 100644 index 0000000..8a7a78d --- /dev/null +++ b/test/chttpd_httpd_handlers_tests.erl @@ -0,0 +1,54 @@ +% 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_tests). + +-export([handlers/1]). + +-include_lib("couch/include/couch_eunit.hrl"). + +handlers(url_handler) -> + [ + {<<"">>, chttpd_misc, handle_welcome_req}, + {<<"favicon.ico">>, chttpd_misc, handle_favicon_req}, + {<<"_utils">>, chttpd_misc, handle_utils_dir_req}, + {<<"_all_dbs">>, chttpd_misc, handle_all_dbs_req}, + {<<"_active_tasks">>, chttpd_misc, handle_task_status_req}, + {<<"_node">>, chttpd_misc, handle_node_req}, + {<<"_reload_query_servers">>, chttpd_misc, handle_reload_query_servers_req}, + {<<"_replicate">>, chttpd_misc, handle_replicate_req}, + {<<"_uuids">>, chttpd_misc, handle_uuids_req}, + {<<"_session">>, chttpd_auth, handle_session_req}, + {<<"_up">>, chttpd_misc, handle_up_req}, + {<<"anything">>, chttpd_db, handle_request} + ]; +handlers(db_handler) -> + [ + {<<"_view_cleanup">>, chttpd_db, handle_view_cleanup_req}, + {<<"_compact">>, chttpd_db, handle_compact_req}, + {<<"_design">>, chttpd_db, handle_design_req}, + {<<"_temp_view">>, chttpd_view, handle_temp_view_req}, + {<<"_changes">>, chttpd_db, handle_changes_req} + ]; +handlers(design_handler) -> + [ + {<<"_view">>, chttpd_view, handle_view_req}, + {<<"_show">>, chttpd_show, handle_doc_show_req}, + {<<"_list">>, chttpd_show, handle_view_list_req}, + {<<"_update">>, chttpd_show, handle_doc_update_req}, + {<<"_info">>, chttpd_db, handle_design_info_req}, + {<<"_rewrite">>, chttpd_rewrite, handle_rewrite_req} + ]. + +chttpd_endpoints_test_() -> + Apps = [couch_epi, chttpd], + chttpd_httpd_handlers_test_util:endpoints_test(chttpd, ?MODULE, Apps). From 1df52fe1a9e3df4247c2cecb93180891b6d54056 Mon Sep 17 00:00:00 2001 From: ILYA Khlopotov Date: Mon, 24 Aug 2015 20:14:51 -0700 Subject: [PATCH 2/5] Wait for handlers to be propoerly configured COUCHDB-2789 --- src/chttpd_httpd_handlers_test_util.erl | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/chttpd_httpd_handlers_test_util.erl b/src/chttpd_httpd_handlers_test_util.erl index f3ce52e..f93d8d6 100644 --- a/src/chttpd_httpd_handlers_test_util.erl +++ b/src/chttpd_httpd_handlers_test_util.erl @@ -21,6 +21,11 @@ %%% 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") -> @@ -48,7 +53,7 @@ endpoints_test(App, Module, Apps) -> "Checking dynamic endpoints", { setup, - fun() -> test_util:start_couch(Apps) end, + fun() -> start(App, Apps) end, fun test_util:stop/1, %% we use instantiator to postpone test instantiation %% so we can detect endpoint overrides @@ -184,3 +189,12 @@ get_handlers(EndpointType, Module) -> 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). From 50307afa3ed18a6d66ca3d0599eeed6b88c7922d Mon Sep 17 00:00:00 2001 From: ILYA Khlopotov Date: Tue, 25 Aug 2015 08:22:12 -0700 Subject: [PATCH 3/5] [fixup] Use atoms instead of strings --- src/chttpd_httpd_handlers_test_util.erl | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/chttpd_httpd_handlers_test_util.erl b/src/chttpd_httpd_handlers_test_util.erl index f93d8d6..e69bae3 100644 --- a/src/chttpd_httpd_handlers_test_util.erl +++ b/src/chttpd_httpd_handlers_test_util.erl @@ -26,11 +26,11 @@ start(App, Apps) -> wait_handlers(App), Ctx. -setup("mocked") -> +setup(mocked) -> fun setup_mocked/1; -setup("not_mocked") -> +setup(not_mocked) -> fun setup_not_mocked/1; -setup("skip") -> +setup(skip) -> fun(_) -> undefined end. setup_mocked({Endpoint, {_Path, Module, Function}}) -> @@ -58,12 +58,12 @@ endpoints_test(App, Module, Apps) -> %% 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} + {mocked, url_handler}, + {mocked, db_handler}, + {mocked, design_handler}, + {not_mocked, url_handler}, + {not_mocked, db_handler}, + {not_mocked, design_handler} ]) end } @@ -74,12 +74,12 @@ check_dynamic_endpoints(Setup, EndpointType, App, Module) -> TestMessage = "Checking '" ++ atom_to_list(App) ++ " -- " ++ atom_to_list(EndpointType) - ++ "' [" ++ Setup ++ "] dynamic endpoints", + ++ "' [" ++ 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(skip, EndpointType, Spec) || Spec <- Skips ] }. @@ -101,9 +101,9 @@ make_tests(App, Module, Casses) -> || {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. +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), From cabd486f69d37003d8aced24888862c601b19f6e Mon Sep 17 00:00:00 2001 From: ILYA Khlopotov Date: Wed, 26 Aug 2015 07:17:26 -0700 Subject: [PATCH 4/5] Add an ability to introspect endpoints COUCHDB-2789 --- src/chttpd_handlers.erl | 24 +++++++++++++++++++++++- src/chttpd_httpd_handlers.erl | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) 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..1b196bb 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,36 @@ 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">> + ]. + From 91c19e90637272c6e9c99d7d62a247974aa37704 Mon Sep 17 00:00:00 2001 From: ILYA Khlopotov Date: Wed, 26 Aug 2015 07:18:26 -0700 Subject: [PATCH 5/5] Add test suite for endpoints COUCHDB-2789 --- src/chttpd_httpd_handlers.erl | 8 ++++ src/chttpd_httpd_handlers_test_util.erl | 10 +++-- test/chttpd_httpd_handlers_tests.erl | 54 ------------------------- 3 files changed, 15 insertions(+), 57 deletions(-) delete mode 100644 test/chttpd_httpd_handlers_tests.erl diff --git a/src/chttpd_httpd_handlers.erl b/src/chttpd_httpd_handlers.erl index 1b196bb..2f25c99 100644 --- a/src/chttpd_httpd_handlers.erl +++ b/src/chttpd_httpd_handlers.erl @@ -74,3 +74,11 @@ endpoints(design_handler) -> <<"_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 index e69bae3..18bd241 100644 --- a/src/chttpd_httpd_handlers_test_util.erl +++ b/src/chttpd_httpd_handlers_test_util.erl @@ -170,7 +170,10 @@ handler(EndpointType, HandlerKey) -> chttpd_handlers:EndpointType(HandlerKey, default_handler(EndpointType)). get_active_handler(EndpointType, HandlerKey) -> - Info = erlang:fun_info(handler(EndpointType, HandlerKey)), + fun2spec(HandlerKey, handler(EndpointType, HandlerKey)). + +fun2spec(HandlerKey, Fun) -> + Info = erlang:fun_info(Fun), { HandlerKey, proplists:get_value(module, Info), @@ -182,10 +185,11 @@ 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 = Module:handlers(EndpointType), + Handlers = chttpd_handlers:handlers(Module, EndpointType), + HandlerSpecs = [fun2spec(HandlerKey, Fun) || {HandlerKey, Fun} <- Handlers], lists:partition(fun(Spec) -> is_active(EndpointType, Spec) - end, Handlers). + end, HandlerSpecs). is_active(EndpointType, {Path, _Module, _Function} = Spec) -> get_active_handler(EndpointType, Path) == Spec. diff --git a/test/chttpd_httpd_handlers_tests.erl b/test/chttpd_httpd_handlers_tests.erl deleted file mode 100644 index 8a7a78d..0000000 --- a/test/chttpd_httpd_handlers_tests.erl +++ /dev/null @@ -1,54 +0,0 @@ -% 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_tests). - --export([handlers/1]). - --include_lib("couch/include/couch_eunit.hrl"). - -handlers(url_handler) -> - [ - {<<"">>, chttpd_misc, handle_welcome_req}, - {<<"favicon.ico">>, chttpd_misc, handle_favicon_req}, - {<<"_utils">>, chttpd_misc, handle_utils_dir_req}, - {<<"_all_dbs">>, chttpd_misc, handle_all_dbs_req}, - {<<"_active_tasks">>, chttpd_misc, handle_task_status_req}, - {<<"_node">>, chttpd_misc, handle_node_req}, - {<<"_reload_query_servers">>, chttpd_misc, handle_reload_query_servers_req}, - {<<"_replicate">>, chttpd_misc, handle_replicate_req}, - {<<"_uuids">>, chttpd_misc, handle_uuids_req}, - {<<"_session">>, chttpd_auth, handle_session_req}, - {<<"_up">>, chttpd_misc, handle_up_req}, - {<<"anything">>, chttpd_db, handle_request} - ]; -handlers(db_handler) -> - [ - {<<"_view_cleanup">>, chttpd_db, handle_view_cleanup_req}, - {<<"_compact">>, chttpd_db, handle_compact_req}, - {<<"_design">>, chttpd_db, handle_design_req}, - {<<"_temp_view">>, chttpd_view, handle_temp_view_req}, - {<<"_changes">>, chttpd_db, handle_changes_req} - ]; -handlers(design_handler) -> - [ - {<<"_view">>, chttpd_view, handle_view_req}, - {<<"_show">>, chttpd_show, handle_doc_show_req}, - {<<"_list">>, chttpd_show, handle_view_list_req}, - {<<"_update">>, chttpd_show, handle_doc_update_req}, - {<<"_info">>, chttpd_db, handle_design_info_req}, - {<<"_rewrite">>, chttpd_rewrite, handle_rewrite_req} - ]. - -chttpd_endpoints_test_() -> - Apps = [couch_epi, chttpd], - chttpd_httpd_handlers_test_util:endpoints_test(chttpd, ?MODULE, Apps).