diff options
author | Alexander Shorin <kxepal@apache.org> | 2014-06-04 12:54:44 +0400 |
---|---|---|
committer | Alexander Shorin <kxepal@apache.org> | 2015-12-03 00:51:55 +0300 |
commit | 3e545431dc1bfb751a10a731514dd9d5dd0726d1 (patch) | |
tree | 1140f4597c70f6197eb29faa6682a1c63105b0fc | |
parent | 96b15f9e03a65696d4a359d996f6c652481fd202 (diff) | |
download | couchdb-3e545431dc1bfb751a10a731514dd9d5dd0726d1.tar.gz |
Port 180-http-proxy.t etap test suite to eunit
-rw-r--r-- | test/couchdb/Makefile.am | 5 | ||||
-rw-r--r-- | test/couchdb/couchdb_http_proxy_tests.erl | 554 | ||||
-rw-r--r-- | test/couchdb/test_web.erl (renamed from test/etap/test_web.erl) | 35 | ||||
-rw-r--r-- | test/etap/180-http-proxy.ini | 20 | ||||
-rwxr-xr-x | test/etap/180-http-proxy.t | 376 | ||||
-rw-r--r-- | test/etap/Makefile.am | 5 |
6 files changed, 583 insertions, 412 deletions
diff --git a/test/couchdb/Makefile.am b/test/couchdb/Makefile.am index 95f1b7797..d664a58ea 100644 --- a/test/couchdb/Makefile.am +++ b/test/couchdb/Makefile.am @@ -19,7 +19,8 @@ all: mkdir -p temp/ $(ERLC) -Wall -I$(top_srcdir)/src -I$(top_srcdir)/test/couchdb/include \ -o $(top_builddir)/test/couchdb/ebin/ $(ERLC_FLAGS) ${TEST} \ - $(top_srcdir)/test/couchdb/test_request.erl + $(top_srcdir)/test/couchdb/test_request.erl \ + $(top_srcdir)/test/couchdb/test_web.erl chmod +x run chmod +x $(top_builddir)/test/couchdb/fixtures/os_daemon_configer.escript @@ -41,12 +42,14 @@ eunit_files = \ couch_work_queue_tests.erl \ couchdb_attachments_tests.erl \ couchdb_file_compression_tests.erl \ + couchdb_http_proxy_tests.erl \ couchdb_modules_load_tests.erl \ couchdb_os_daemons_tests.erl \ couchdb_update_conflicts_tests.erl \ couchdb_vhosts_tests.erl \ couchdb_views_tests.erl \ test_request.erl \ + test_web.erl \ include/couch_eunit.hrl fixture_files = \ diff --git a/test/couchdb/couchdb_http_proxy_tests.erl b/test/couchdb/couchdb_http_proxy_tests.erl new file mode 100644 index 000000000..03ceca7c2 --- /dev/null +++ b/test/couchdb/couchdb_http_proxy_tests.erl @@ -0,0 +1,554 @@ +% 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(couchdb_http_proxy_tests). + +-include("couch_eunit.hrl"). + +-record(req, {method=get, path="", headers=[], body="", opts=[]}). + +-define(CONFIG_FIXTURE_TEMP, + begin + FileName = filename:join([?TEMPDIR, ?tempfile() ++ ".ini"]), + {ok, Fd} = file:open(FileName, write), + ok = file:truncate(Fd), + ok = file:close(Fd), + FileName + end). +-define(TIMEOUT, 5000). + + +start() -> + % we have to write any config changes to temp ini file to not loose them + % when supervisor will kill all children due to reaching restart threshold + % (each httpd_global_handlers changes causes couch_httpd restart) + couch_server_sup:start_link(?CONFIG_CHAIN ++ [?CONFIG_FIXTURE_TEMP]), + % 49151 is IANA Reserved, let's assume no one is listening there + with_process_restart(couch_httpd, fun() -> + couch_config:set("httpd_global_handlers", "_error", + "{couch_httpd_proxy, handle_proxy_req, <<\"http://127.0.0.1:49151/\">>}" + ) + end), + ok. + +stop(_) -> + couch_server_sup:stop(), + ok. + +setup() -> + {ok, Pid} = test_web:start_link(), + Value = lists:flatten(io_lib:format( + "{couch_httpd_proxy, handle_proxy_req, ~p}", + [list_to_binary(proxy_url())])), + with_process_restart(couch_httpd, fun() -> + couch_config:set("httpd_global_handlers", "_test", Value) + end), + Pid. + +teardown(Pid) -> + erlang:monitor(process, Pid), + test_web:stop(), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, test_web_stop}) + end. + + +http_proxy_test_() -> + { + "HTTP Proxy handler tests", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_proxy_basic_request/1, + fun should_return_alternative_status/1, + fun should_respect_trailing_slash/1, + fun should_proxy_headers/1, + fun should_proxy_host_header/1, + fun should_pass_headers_back/1, + fun should_use_same_protocol_version/1, + fun should_proxy_body/1, + fun should_proxy_body_back/1, + fun should_proxy_chunked_body/1, + fun should_proxy_chunked_body_back/1, + fun should_rewrite_location_header/1, + fun should_not_rewrite_external_locations/1, + fun should_rewrite_relative_location/1, + fun should_refuse_connection_to_backend/1 + ] + } + + } + }. + + +should_proxy_basic_request(_) -> + Remote = fun(Req) -> + 'GET' = Req:get(method), + "/" = Req:get(path), + 0 = Req:get(body_length), + <<>> = Req:recv_body(), + {ok, {200, [{"Content-Type", "text/plain"}], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + ?_test(check_request(#req{}, Remote, Local)). + +should_return_alternative_status(_) -> + Remote = fun(Req) -> + "/alternate_status" = Req:get(path), + {ok, {201, [], "ok"}} + end, + Local = fun + ({ok, "201", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{path = "/alternate_status"}, + ?_test(check_request(Req, Remote, Local)). + +should_respect_trailing_slash(_) -> + Remote = fun(Req) -> + "/trailing_slash/" = Req:get(path), + {ok, {200, [], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{path="/trailing_slash/"}, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_headers(_) -> + Remote = fun(Req) -> + "/passes_header" = Req:get(path), + "plankton" = Req:get_header_value("X-CouchDB-Ralph"), + {ok, {200, [], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + path="/passes_header", + headers=[{"X-CouchDB-Ralph", "plankton"}] + }, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_host_header(_) -> + Remote = fun(Req) -> + "/passes_host_header" = Req:get(path), + "www.google.com" = Req:get_header_value("Host"), + {ok, {200, [], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + path="/passes_host_header", + headers=[{"Host", "www.google.com"}] + }, + ?_test(check_request(Req, Remote, Local)). + +should_pass_headers_back(_) -> + Remote = fun(Req) -> + "/passes_header_back" = Req:get(path), + {ok, {200, [{"X-CouchDB-Plankton", "ralph"}], "ok"}} + end, + Local = fun + ({ok, "200", Headers, "ok"}) -> + lists:member({"X-CouchDB-Plankton", "ralph"}, Headers); + (_) -> + false + end, + Req = #req{path="/passes_header_back"}, + ?_test(check_request(Req, Remote, Local)). + +should_use_same_protocol_version(_) -> + Remote = fun(Req) -> + "/uses_same_version" = Req:get(path), + {1, 0} = Req:get(version), + {ok, {200, [], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + path="/uses_same_version", + opts=[{http_vsn, {1, 0}}] + }, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_body(_) -> + Remote = fun(Req) -> + 'PUT' = Req:get(method), + "/passes_body" = Req:get(path), + <<"Hooray!">> = Req:recv_body(), + {ok, {201, [], "ok"}} + end, + Local = fun + ({ok, "201", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + method=put, + path="/passes_body", + body="Hooray!" + }, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_body_back(_) -> + BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], + Remote = fun(Req) -> + 'GET' = Req:get(method), + "/passes_eof_body" = Req:get(path), + {raw, {200, [{"Connection", "close"}], BodyChunks}} + end, + Local = fun + ({ok, "200", _, "foobarbazinga"}) -> + true; + (_) -> + false + end, + Req = #req{path="/passes_eof_body"}, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_chunked_body(_) -> + BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], + Remote = fun(Req) -> + 'POST' = Req:get(method), + "/passes_chunked_body" = Req:get(path), + RecvBody = fun + ({Length, Chunk}, [Chunk | Rest]) -> + Length = size(Chunk), + Rest; + ({0, []}, []) -> + ok + end, + ok = Req:stream_body(1024 * 1024, RecvBody, BodyChunks), + {ok, {201, [], "ok"}} + end, + Local = fun + ({ok, "201", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + method=post, + path="/passes_chunked_body", + headers=[{"Transfer-Encoding", "chunked"}], + body=chunked_body(BodyChunks) + }, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_chunked_body_back(_) -> + ?_test(begin + Remote = fun(Req) -> + 'GET' = Req:get(method), + "/passes_chunked_body_back" = Req:get(path), + BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], + {chunked, {200, [{"Transfer-Encoding", "chunked"}], BodyChunks}} + end, + Req = #req{ + path="/passes_chunked_body_back", + opts=[{stream_to, self()}] + }, + + Resp = check_request(Req, Remote, no_local), + ?assertMatch({ibrowse_req_id, _}, Resp), + {_, ReqId} = Resp, + + % Grab headers from response + receive + {ibrowse_async_headers, ReqId, "200", Headers} -> + ?assertEqual("chunked", + proplists:get_value("Transfer-Encoding", Headers)), + ibrowse:stream_next(ReqId) + after 1000 -> + throw({error, timeout}) + end, + + ?assertEqual(<<"foobarbazinga">>, recv_body(ReqId, [])), + ?assertEqual(was_ok, test_web:check_last()) + end). + +should_refuse_connection_to_backend(_) -> + Local = fun + ({ok, "500", _, _}) -> + true; + (_) -> + false + end, + Req = #req{opts=[{url, server_url("/_error")}]}, + ?_test(check_request(Req, no_remote, Local)). + +should_rewrite_location_header(_) -> + { + "Testing location header rewrites", + do_rewrite_tests([ + {"Location", proxy_url() ++ "/foo/bar", + server_url() ++ "/foo/bar"}, + {"Content-Location", proxy_url() ++ "/bing?q=2", + server_url() ++ "/bing?q=2"}, + {"Uri", proxy_url() ++ "/zip#frag", + server_url() ++ "/zip#frag"}, + {"Destination", proxy_url(), + server_url() ++ "/"} + ]) + }. + +should_not_rewrite_external_locations(_) -> + { + "Testing no rewrite of external locations", + do_rewrite_tests([ + {"Location", external_url() ++ "/search", + external_url() ++ "/search"}, + {"Content-Location", external_url() ++ "/s?q=2", + external_url() ++ "/s?q=2"}, + {"Uri", external_url() ++ "/f#f", + external_url() ++ "/f#f"}, + {"Destination", external_url() ++ "/f?q=2#f", + external_url() ++ "/f?q=2#f"} + ]) + }. + +should_rewrite_relative_location(_) -> + { + "Testing relative rewrites", + do_rewrite_tests([ + {"Location", "/foo", + server_url() ++ "/foo"}, + {"Content-Location", "bar", + server_url() ++ "/bar"}, + {"Uri", "/zing?q=3", + server_url() ++ "/zing?q=3"}, + {"Destination", "bing?q=stuff#yay", + server_url() ++ "/bing?q=stuff#yay"} + ]) + }. + + +do_rewrite_tests(Tests) -> + lists:map(fun({Header, Location, Url}) -> + should_rewrite_header(Header, Location, Url) + end, Tests). + +should_rewrite_header(Header, Location, Url) -> + Remote = fun(Req) -> + "/rewrite_test" = Req:get(path), + {ok, {302, [{Header, Location}], "ok"}} + end, + Local = fun + ({ok, "302", Headers, "ok"}) -> + ?assertEqual(Url, couch_util:get_value(Header, Headers)), + true; + (E) -> + ?debugFmt("~p", [E]), + false + end, + Req = #req{path="/rewrite_test"}, + {Header, ?_test(check_request(Req, Remote, Local))}. + + +server_url() -> + server_url("/_test"). + +server_url(Resource) -> + Addr = couch_config:get("httpd", "bind_address"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + lists:concat(["http://", Addr, ":", Port, Resource]). + +proxy_url() -> + "http://127.0.0.1:" ++ integer_to_list(test_web:get_port()). + +external_url() -> + "https://google.com". + +check_request(Req, Remote, Local) -> + case Remote of + no_remote -> + ok; + _ -> + test_web:set_assert(Remote) + end, + Url = case proplists:lookup(url, Req#req.opts) of + none -> + server_url() ++ Req#req.path; + {url, DestUrl} -> + DestUrl + end, + Opts = [{headers_as_is, true} | Req#req.opts], + Resp =ibrowse:send_req( + Url, Req#req.headers, Req#req.method, Req#req.body, Opts + ), + %?debugFmt("ibrowse response: ~p", [Resp]), + case Local of + no_local -> + ok; + _ -> + ?assert(Local(Resp)) + end, + case {Remote, Local} of + {no_remote, _} -> + ok; + {_, no_local} -> + ok; + _ -> + ?assertEqual(was_ok, test_web:check_last()) + end, + Resp. + +chunked_body(Chunks) -> + chunked_body(Chunks, []). + +chunked_body([], Acc) -> + iolist_to_binary(lists:reverse(Acc, "0\r\n\r\n")); +chunked_body([Chunk | Rest], Acc) -> + Size = to_hex(size(Chunk)), + chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]). + +to_hex(Val) -> + to_hex(Val, []). + +to_hex(0, Acc) -> + Acc; +to_hex(Val, Acc) -> + to_hex(Val div 16, [hex_char(Val rem 16) | Acc]). + +hex_char(V) when V < 10 -> $0 + V; +hex_char(V) -> $A + V - 10. + +recv_body(ReqId, Acc) -> + receive + {ibrowse_async_response, ReqId, Data} -> + recv_body(ReqId, [Data | Acc]); + {ibrowse_async_response_end, ReqId} -> + iolist_to_binary(lists:reverse(Acc)); + Else -> + throw({error, unexpected_mesg, Else}) + after ?TIMEOUT -> + throw({error, timeout}) + end. + + +%% Copy from couch test_util @ master branch + +now_us() -> + {MegaSecs, Secs, MicroSecs} = now(), + (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs. + +stop_sync(Name) -> + stop_sync(Name, shutdown). +stop_sync(Name, Reason) -> + stop_sync(Name, Reason, 5000). +stop_sync(Name, Reason, Timeout) when is_atom(Name) -> + stop_sync(whereis(Name), Reason, Timeout); +stop_sync(Pid, Reason, Timeout) when is_atom(Reason) and is_pid(Pid) -> + stop_sync(Pid, fun() -> exit(Pid, Reason) end, Timeout); +stop_sync(Pid, Fun, Timeout) when is_function(Fun) and is_pid(Pid) -> + MRef = erlang:monitor(process, Pid), + try + begin + catch unlink(Pid), + Res = (catch Fun()), + receive + {'DOWN', MRef, _, _, _} -> + Res + after Timeout -> + timeout + end + end + after + erlang:demonitor(MRef, [flush]) + end; +stop_sync(_, _, _) -> error(badarg). + +stop_sync_throw(Name, Error) -> + stop_sync_throw(Name, shutdown, Error). +stop_sync_throw(Name, Reason, Error) -> + stop_sync_throw(Name, Reason, Error, 5000). +stop_sync_throw(Pid, Fun, Error, Timeout) -> + case stop_sync(Pid, Fun, Timeout) of + timeout -> + throw(Error); + Else -> + Else + end. + +with_process_restart(Name) -> + {Pid, true} = with_process_restart( + fun() -> exit(whereis(Name), shutdown) end, Name), + Pid. +with_process_restart(Name, Fun) -> + with_process_restart(Name, Fun, 5000). +with_process_restart(Name, Fun, Timeout) -> + ok = stop_sync(Name, Fun), + case wait_process(Name, Timeout) of + timeout -> + timeout; + Pid -> + Pid + end. + +wait_process(Name) -> + wait_process(Name, 5000). +wait_process(Name, Timeout) -> + wait(fun() -> + case whereis(Name) of + undefined -> + wait; + Pid -> + Pid + end + end, Timeout). + +wait(Fun) -> + wait(Fun, 5000, 50). +wait(Fun, Timeout) -> + wait(Fun, Timeout, 50). +wait(Fun, Timeout, Delay) -> + Now = now_us(), + wait(Fun, Timeout * 1000, Delay, Now, Now). +wait(_Fun, Timeout, _Delay, Started, Prev) when Prev - Started > Timeout -> + timeout; +wait(Fun, Timeout, Delay, Started, _Prev) -> + case Fun() of + wait -> + ok = timer:sleep(Delay), + wait(Fun, Timeout, Delay, Started, now_us()); + Else -> + Else + end. diff --git a/test/etap/test_web.erl b/test/couchdb/test_web.erl index ed78651f1..1de2cd1c3 100644 --- a/test/etap/test_web.erl +++ b/test/couchdb/test_web.erl @@ -13,12 +13,15 @@ -module(test_web). -behaviour(gen_server). --export([start_link/0, loop/1, get_port/0, set_assert/1, check_last/0]). +-include("couch_eunit.hrl"). + +-export([start_link/0, stop/0, loop/1, get_port/0, set_assert/1, check_last/0]). -export([init/1, terminate/2, code_change/3]). -export([handle_call/3, handle_cast/2, handle_info/2]). -define(SERVER, test_web_server). -define(HANDLER, test_web_handler). +-define(DELAY, 500). start_link() -> gen_server:start({local, ?HANDLER}, ?MODULE, [], []), @@ -29,7 +32,7 @@ start_link() -> ]). loop(Req) -> - %etap:diag("Handling request: ~p", [Req]), + %?debugFmt("Handling request: ~p", [Req]), case gen_server:call(?HANDLER, {check_request, Req}) of {ok, RespInfo} -> {ok, Req:respond(RespInfo)}; @@ -40,12 +43,12 @@ loop(Req) -> {ok, Resp}; {chunked, {Status, Headers, BodyChunks}} -> Resp = Req:respond({Status, Headers, chunked}), - timer:sleep(500), + timer:sleep(?DELAY), lists:foreach(fun(C) -> Resp:write_chunk(C) end, BodyChunks), Resp:write_chunk([]), {ok, Resp}; {error, Reason} -> - etap:diag("Error: ~p", [Reason]), + ?debugFmt("Error: ~p", [Reason]), Body = lists:flatten(io_lib:format("Error: ~p", [Reason])), {ok, Req:respond({200, [], Body})} end. @@ -54,7 +57,7 @@ get_port() -> mochiweb_socket_server:get(?SERVER, port). set_assert(Fun) -> - ok = gen_server:call(?HANDLER, {set_assert, Fun}). + ?assertEqual(ok, gen_server:call(?HANDLER, {set_assert, Fun})). check_last() -> gen_server:call(?HANDLER, last_status). @@ -65,12 +68,20 @@ init(_) -> terminate(_Reason, _State) -> ok. +stop() -> + gen_server:cast(?SERVER, stop). + + handle_call({check_request, Req}, _From, State) when is_function(State, 1) -> Resp2 = case (catch State(Req)) of - {ok, Resp} -> {reply, {ok, Resp}, was_ok}; - {raw, Resp} -> {reply, {raw, Resp}, was_ok}; - {chunked, Resp} -> {reply, {chunked, Resp}, was_ok}; - Error -> {reply, {error, Error}, not_ok} + {ok, Resp} -> + {reply, {ok, Resp}, was_ok}; + {raw, Resp} -> + {reply, {raw, Resp}, was_ok}; + {chunked, Resp} -> + {reply, {chunked, Resp}, was_ok}; + Error -> + {reply, {error, Error}, not_ok} end, Req:cleanup(), Resp2; @@ -87,12 +98,14 @@ handle_call({set_assert, _}, _From, State) -> handle_call(Msg, _From, State) -> {reply, {ignored, Msg}, State}. +handle_cast(stop, State) -> + {stop, normal, State}; handle_cast(Msg, State) -> - etap:diag("Ignoring cast message: ~p", [Msg]), + ?debugFmt("Ignoring cast message: ~p", [Msg]), {noreply, State}. handle_info(Msg, State) -> - etap:diag("Ignoring info message: ~p", [Msg]), + ?debugFmt("Ignoring info message: ~p", [Msg]), {noreply, State}. code_change(_OldVsn, State, _Extra) -> diff --git a/test/etap/180-http-proxy.ini b/test/etap/180-http-proxy.ini deleted file mode 100644 index 3e2ba1379..000000000 --- a/test/etap/180-http-proxy.ini +++ /dev/null @@ -1,20 +0,0 @@ -; Licensed to the Apache Software Foundation (ASF) under one -; or more contributor license agreements. See the NOTICE file -; distributed with this work for additional information -; regarding copyright ownership. The ASF licenses this file -; to you 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. - -; 49151 is IANA Reserved, let's assume no one is listening there -[httpd_global_handlers] -_error = {couch_httpd_proxy, handle_proxy_req, <<"http://127.0.0.1:49151/">>} diff --git a/test/etap/180-http-proxy.t b/test/etap/180-http-proxy.t deleted file mode 100755 index da6760364..000000000 --- a/test/etap/180-http-proxy.t +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/env escript -% 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. - --record(req, {method=get, path="", headers=[], body="", opts=[]}). - -server() -> - lists:concat([ - "http://127.0.0.1:", - mochiweb_socket_server:get(couch_httpd, port), - "/_test/" - ]). - -proxy() -> - "http://127.0.0.1:" ++ integer_to_list(test_web:get_port()) ++ "/". - -external() -> "https://www.google.com/". - -main(_) -> - test_util:init_code_path(), - - etap:plan(61), - case (catch test()) of - ok -> - etap:end_tests(); - Other -> - etap:diag("Test died abnormally: ~p", [Other]), - etap:bail("Bad return value.") - end, - ok. - -check_request(Name, Req, Remote, Local) -> - case Remote of - no_remote -> ok; - _ -> test_web:set_assert(Remote) - end, - Url = case proplists:lookup(url, Req#req.opts) of - none -> server() ++ Req#req.path; - {url, DestUrl} -> DestUrl - end, - Opts = [{headers_as_is, true} | Req#req.opts], - Resp =ibrowse:send_req( - Url, Req#req.headers, Req#req.method, Req#req.body, Opts - ), - %etap:diag("ibrowse response: ~p", [Resp]), - case Local of - no_local -> ok; - _ -> etap:fun_is(Local, Resp, Name) - end, - case {Remote, Local} of - {no_remote, _} -> - ok; - {_, no_local} -> - ok; - _ -> - etap:is(test_web:check_last(), was_ok, Name ++ " - request handled") - end, - Resp. - -test() -> - ExtraConfig = [test_util:source_file("test/etap/180-http-proxy.ini")], - couch_server_sup:start_link(test_util:config_files() ++ ExtraConfig), - ibrowse:start(), - crypto:start(), - - % start the test_web server on a random port - test_web:start_link(), - Url = lists:concat([ - "{couch_httpd_proxy, handle_proxy_req, <<\"http://127.0.0.1:", - test_web:get_port(), - "/\">>}" - ]), - couch_config:set("httpd_global_handlers", "_test", Url, false), - - % let couch_httpd restart - timer:sleep(100), - - test_basic(), - test_alternate_status(), - test_trailing_slash(), - test_passes_header(), - test_passes_host_header(), - test_passes_header_back(), - test_rewrites_location_headers(), - test_doesnt_rewrite_external_locations(), - test_rewrites_relative_location(), - test_uses_same_version(), - test_passes_body(), - test_passes_eof_body_back(), - test_passes_chunked_body(), - test_passes_chunked_body_back(), - - test_connect_error(), - - ok. - -test_basic() -> - Remote = fun(Req) -> - 'GET' = Req:get(method), - "/" = Req:get(path), - 0 = Req:get(body_length), - <<>> = Req:recv_body(), - {ok, {200, [{"Content-Type", "text/plain"}], "ok"}} - end, - Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end, - check_request("Basic proxy test", #req{}, Remote, Local). - -test_alternate_status() -> - Remote = fun(Req) -> - "/alternate_status" = Req:get(path), - {ok, {201, [], "ok"}} - end, - Local = fun({ok, "201", _, "ok"}) -> true; (_) -> false end, - Req = #req{path="alternate_status"}, - check_request("Alternate status", Req, Remote, Local). - -test_trailing_slash() -> - Remote = fun(Req) -> - "/trailing_slash/" = Req:get(path), - {ok, {200, [], "ok"}} - end, - Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end, - Req = #req{path="trailing_slash/"}, - check_request("Trailing slash", Req, Remote, Local). - -test_passes_header() -> - Remote = fun(Req) -> - "/passes_header" = Req:get(path), - "plankton" = Req:get_header_value("X-CouchDB-Ralph"), - {ok, {200, [], "ok"}} - end, - Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end, - Req = #req{ - path="passes_header", - headers=[{"X-CouchDB-Ralph", "plankton"}] - }, - check_request("Passes header", Req, Remote, Local). - -test_passes_host_header() -> - Remote = fun(Req) -> - "/passes_host_header" = Req:get(path), - "www.google.com" = Req:get_header_value("Host"), - {ok, {200, [], "ok"}} - end, - Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end, - Req = #req{ - path="passes_host_header", - headers=[{"Host", "www.google.com"}] - }, - check_request("Passes host header", Req, Remote, Local). - -test_passes_header_back() -> - Remote = fun(Req) -> - "/passes_header_back" = Req:get(path), - {ok, {200, [{"X-CouchDB-Plankton", "ralph"}], "ok"}} - end, - Local = fun - ({ok, "200", Headers, "ok"}) -> - lists:member({"X-CouchDB-Plankton", "ralph"}, Headers); - (_) -> - false - end, - Req = #req{path="passes_header_back"}, - check_request("Passes header back", Req, Remote, Local). - -test_rewrites_location_headers() -> - etap:diag("Testing location header rewrites."), - do_rewrite_tests([ - {"Location", proxy() ++ "foo/bar", server() ++ "foo/bar"}, - {"Content-Location", proxy() ++ "bing?q=2", server() ++ "bing?q=2"}, - {"Uri", proxy() ++ "zip#frag", server() ++ "zip#frag"}, - {"Destination", proxy(), server()} - ]). - -test_doesnt_rewrite_external_locations() -> - etap:diag("Testing no rewrite of external locations."), - do_rewrite_tests([ - {"Location", external() ++ "search", external() ++ "search"}, - {"Content-Location", external() ++ "s?q=2", external() ++ "s?q=2"}, - {"Uri", external() ++ "f#f", external() ++ "f#f"}, - {"Destination", external() ++ "f?q=2#f", external() ++ "f?q=2#f"} - ]). - -test_rewrites_relative_location() -> - etap:diag("Testing relative rewrites."), - do_rewrite_tests([ - {"Location", "/foo", server() ++ "foo"}, - {"Content-Location", "bar", server() ++ "bar"}, - {"Uri", "/zing?q=3", server() ++ "zing?q=3"}, - {"Destination", "bing?q=stuff#yay", server() ++ "bing?q=stuff#yay"} - ]). - -do_rewrite_tests(Tests) -> - lists:foreach(fun({Header, Location, Url}) -> - do_rewrite_test(Header, Location, Url) - end, Tests). - -do_rewrite_test(Header, Location, Url) -> - Remote = fun(Req) -> - "/rewrite_test" = Req:get(path), - {ok, {302, [{Header, Location}], "ok"}} - end, - Local = fun - ({ok, "302", Headers, "ok"}) -> - etap:is( - couch_util:get_value(Header, Headers), - Url, - "Header rewritten correctly." - ), - true; - (_) -> - false - end, - Req = #req{path="rewrite_test"}, - Label = "Rewrite test for ", - check_request(Label ++ Header, Req, Remote, Local). - -test_uses_same_version() -> - Remote = fun(Req) -> - "/uses_same_version" = Req:get(path), - {1, 0} = Req:get(version), - {ok, {200, [], "ok"}} - end, - Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end, - Req = #req{ - path="uses_same_version", - opts=[{http_vsn, {1, 0}}] - }, - check_request("Uses same version", Req, Remote, Local). - -test_passes_body() -> - Remote = fun(Req) -> - 'PUT' = Req:get(method), - "/passes_body" = Req:get(path), - <<"Hooray!">> = Req:recv_body(), - {ok, {201, [], "ok"}} - end, - Local = fun({ok, "201", _, "ok"}) -> true; (_) -> false end, - Req = #req{ - method=put, - path="passes_body", - body="Hooray!" - }, - check_request("Passes body", Req, Remote, Local). - -test_passes_eof_body_back() -> - BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], - Remote = fun(Req) -> - 'GET' = Req:get(method), - "/passes_eof_body" = Req:get(path), - {raw, {200, [{"Connection", "close"}], BodyChunks}} - end, - Local = fun({ok, "200", _, "foobarbazinga"}) -> true; (_) -> false end, - Req = #req{path="passes_eof_body"}, - check_request("Passes eof body", Req, Remote, Local). - -test_passes_chunked_body() -> - BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], - Remote = fun(Req) -> - 'POST' = Req:get(method), - "/passes_chunked_body" = Req:get(path), - RecvBody = fun - ({Length, Chunk}, [Chunk | Rest]) -> - Length = size(Chunk), - Rest; - ({0, []}, []) -> - ok - end, - ok = Req:stream_body(1024*1024, RecvBody, BodyChunks), - {ok, {201, [], "ok"}} - end, - Local = fun({ok, "201", _, "ok"}) -> true; (_) -> false end, - Req = #req{ - method=post, - path="passes_chunked_body", - headers=[{"Transfer-Encoding", "chunked"}], - body=mk_chunked_body(BodyChunks) - }, - check_request("Passes chunked body", Req, Remote, Local). - -test_passes_chunked_body_back() -> - Name = "Passes chunked body back", - Remote = fun(Req) -> - 'GET' = Req:get(method), - "/passes_chunked_body_back" = Req:get(path), - BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], - {chunked, {200, [{"Transfer-Encoding", "chunked"}], BodyChunks}} - end, - Req = #req{ - path="passes_chunked_body_back", - opts=[{stream_to, self()}] - }, - - Resp = check_request(Name, Req, Remote, no_local), - - etap:fun_is( - fun({ibrowse_req_id, _}) -> true; (_) -> false end, - Resp, - "Received an ibrowse request id." - ), - {_, ReqId} = Resp, - - % Grab headers from response - receive - {ibrowse_async_headers, ReqId, "200", Headers} -> - etap:is( - proplists:get_value("Transfer-Encoding", Headers), - "chunked", - "Response included the Transfer-Encoding: chunked header" - ), - ibrowse:stream_next(ReqId) - after 1000 -> - throw({error, timeout}) - end, - - % Check body received - % TODO: When we upgrade to ibrowse >= 2.0.0 this check needs to - % check that the chunks returned are what we sent from the - % Remote test. - etap:diag("TODO: UPGRADE IBROWSE"), - etap:is(recv_body(ReqId, []), <<"foobarbazinga">>, "Decoded chunked body."), - - % Check test_web server. - etap:is(test_web:check_last(), was_ok, Name ++ " - request handled"). - -test_connect_error() -> - Local = fun({ok, "500", _Headers, _Body}) -> true; (_) -> false end, - Url = lists:concat([ - "http://127.0.0.1:", - mochiweb_socket_server:get(couch_httpd, port), - "/_error" - ]), - Req = #req{opts=[{url, Url}]}, - check_request("Connect error", Req, no_remote, Local). - - -mk_chunked_body(Chunks) -> - mk_chunked_body(Chunks, []). - -mk_chunked_body([], Acc) -> - iolist_to_binary(lists:reverse(Acc, "0\r\n\r\n")); -mk_chunked_body([Chunk | Rest], Acc) -> - Size = to_hex(size(Chunk)), - mk_chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]). - -to_hex(Val) -> - to_hex(Val, []). - -to_hex(0, Acc) -> - Acc; -to_hex(Val, Acc) -> - to_hex(Val div 16, [hex_char(Val rem 16) | Acc]). - -hex_char(V) when V < 10 -> $0 + V; -hex_char(V) -> $A + V - 10. - -recv_body(ReqId, Acc) -> - receive - {ibrowse_async_response, ReqId, Data} -> - recv_body(ReqId, [Data | Acc]); - {ibrowse_async_response_end, ReqId} -> - iolist_to_binary(lists:reverse(Acc)); - Else -> - throw({error, unexpected_mesg, Else}) - after 5000 -> - throw({error, timeout}) - end. diff --git a/test/etap/Makefile.am b/test/etap/Makefile.am index 05a7870ca..b92faf13c 100644 --- a/test/etap/Makefile.am +++ b/test/etap/Makefile.am @@ -11,7 +11,7 @@ ## the License. noinst_SCRIPTS = run -noinst_DATA = test_util.beam test_web.beam +noinst_DATA = test_util.beam %.beam: %.erl $(ERLC) $< @@ -32,8 +32,6 @@ fixture_files = \ fixtures/test.couch tap_files = \ - 180-http-proxy.ini \ - 180-http-proxy.t \ 190-json-stream-parse.t \ 200-view-group-no-db-leaks.t \ 201-view-group-shutdown.t \ @@ -45,6 +43,5 @@ tap_files = \ EXTRA_DIST = \ run.tpl \ - test_web.erl \ $(fixture_files) \ $(tap_files) |