This is an automated email from the ASF dual-hosted git repository.
nickva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git
The following commit(s) were added to refs/heads/main by this push:
new 78395184b Use gun in tests
78395184b is described below
commit 78395184b02cdc9573c3536ec72721fb353d5442
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Fri Aug 21 17:30:33 2026 -0400
Use gun in tests
Since we have gun and it's proven its worth in nouveau let's use for tests
as
well. The bigger idea is to use for replication but let's make a smaller
step
first and see how it fares in unit and elixir tests.
To help with unit tests created a simpler compatibility couch_gun module.
Dependency-wise we just have to ensure gun is started but otherwise it's
already present there for nouveau so we don't need to bring anything new in,
which is nice.
Elixir tests used httpotion based on ibrowse. That client is deprecated we
have
been getting "httpotion is unmaintained" warnings for a while now (this was
another reason to attempt this PR). In many ways gun provides a nicer
abstraction to streaming, so we could also simplify some of the worker
start/stop and direct or pid:once hacks. Dependency-wise we don't have to do
anything gun and cowlib are already present we can just clean up httpotion
and
ibrowse from elixir deps.
---
.credo.exs | 1 -
.gitignore | 1 -
mix.exs | 9 +-
src/chttpd/test/eunit/chttpd_bulk_get_test.erl | 9 +-
src/chttpd/test/eunit/chttpd_csp_tests.erl | 24 +-
.../test/eunit/chttpd_db_attachment_size_tests.erl | 5 +-
src/chttpd/test/eunit/chttpd_db_doc_get_tests.erl | 9 +-
src/chttpd/test/eunit/chttpd_dbs_info_test.erl | 13 +-
src/couch/src/couch.app.src | 1 +
src/couch/src/couch_gun.erl | 439 +++++++++++++++++++
src/couch/src/test_request.erl | 78 +++-
src/couch/src/test_util.erl | 5 +-
src/couch/test/eunit/couchdb_attachments_tests.erl | 10 +-
.../test/eunit/couchdb_cookie_domain_tests.erl | 12 +-
src/couch/test/eunit/couchdb_cors_tests.erl | 48 +--
.../test/eunit/couchdb_location_header_tests.erl | 8 +-
src/couch/test/eunit/couchdb_mrview_cors_tests.erl | 4 +-
src/couch/test/eunit/same_site_cookie_tests.erl | 2 +-
.../test/eunit/couch_prometheus_e2e_tests.erl | 2 +-
test/elixir/lib/couch.ex | 133 ++----
test/elixir/lib/couch/dbtest.ex | 12 +-
test/elixir/lib/couch/http.ex | 465 +++++++++++++++++++++
test/elixir/lib/couch_raw.ex | 123 ++----
test/elixir/lib/step/start.ex | 2 +-
test/elixir/test/attachment_names_test.exs | 4 +-
test/elixir/test/attachment_paths_test.exs | 8 +-
test/elixir/test/attachment_ranges_test.exs | 24 +-
test/elixir/test/attachments_multipart_test.exs | 43 +-
test/elixir/test/attachments_test.exs | 26 +-
test/elixir/test/basics_test.exs | 13 +-
test/elixir/test/changes_async_test.exs | 162 ++++---
test/elixir/test/design_paths_test.exs | 16 +-
test/elixir/test/etags_head_test.exs | 22 +-
test/elixir/test/http_test.exs | 12 +-
test/elixir/test/jsonp_test.exs | 9 +-
test/elixir/test/list_views_test.exs | 4 +-
test/elixir/test/replication_test.exs | 57 ++-
test/elixir/test/rewrite_js_test.exs | 10 +-
test/elixir/test/rewrite_test.exs | 8 +-
test/elixir/test/show_documents_test.exs | 24 +-
test/elixir/test/update_documents_test.exs | 10 +-
test/elixir/test/uuids_test.exs | 6 +-
42 files changed, 1326 insertions(+), 547 deletions(-)
diff --git a/.credo.exs b/.credo.exs
index a07bb48a3..384b19155 100644
--- a/.credo.exs
+++ b/.credo.exs
@@ -30,7 +30,6 @@
~r"/src/fast_pbkdf2/",
~r"/src/jason",
~r"/src/hackney",
- ~r"/src/httpotion",
~r"/src/file_system",
~r"/src/credo",
~r"/src/idna",
diff --git a/.gitignore b/.gitignore
index 28dcadf05..cb42cdb75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,7 +130,6 @@ test/javascript/junit.xml
/_build/
/src/bunt
/src/credo/
-/src/httpotion/
/src/jason/
/src/junit_formatter/
diff --git a/mix.exs b/mix.exs
index 701bef5f6..cb6962c27 100644
--- a/mix.exs
+++ b/mix.exs
@@ -73,7 +73,7 @@ defmodule CouchDBTest.Mixfile do
end
# Run "mix help compile.app" to learn about applications.
- def application, do: [applications: [:logger, :httpotion]]
+ def application, do: [applications: [:logger]]
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["test/elixir/lib",
"test/elixir/test/support"]
@@ -84,9 +84,7 @@ defmodule CouchDBTest.Mixfile do
defp deps() do
deps1 = [
{:junit_formatter, "~> 3.4", only: [:dev, :test, :integration]},
- {:httpotion, ">= 3.2.0", only: [:dev, :test, :integration], runtime:
false},
{:excoveralls, "~> 0.18.5", only: :test},
- {:ibrowse, path: path("ibrowse"), override: true},
{:credo, "== 1.7.19", only: [:dev, :test, :integration], runtime: false}
]
@@ -95,14 +93,14 @@ defmodule CouchDBTest.Mixfile do
deps_list = deps1 ++ deps2
- [:config, :couch, :fabric]
+ [:config, :couch, :fabric, :gun, :cowlib]
|> Enum.map(&path("#{&1}/ebin"))
|> Enum.map(&String.to_charlist/1)
|> Enum.each(&:code.add_patha/1)
# Some deps may be missing during source check
# Besides we don't want to spend time checking them anyway
- List.foldl([:ibrowse | extra_deps], deps_list, fn dep, acc ->
+ List.foldl(extra_deps, deps_list, fn dep, acc ->
if File.dir?(acc[dep][:path]) do
acc
else
@@ -133,7 +131,6 @@ defmodule CouchDBTest.Mixfile do
"credo",
"excoveralls",
"hackney",
- "httpotion",
"ibrowse",
"idna",
"jason",
diff --git a/src/chttpd/test/eunit/chttpd_bulk_get_test.erl
b/src/chttpd/test/eunit/chttpd_bulk_get_test.erl
index b3a01bc30..9b0b8654f 100644
--- a/src/chttpd/test/eunit/chttpd_bulk_get_test.erl
+++ b/src/chttpd/test/eunit/chttpd_bulk_get_test.erl
@@ -1524,11 +1524,11 @@ req_mp(Method, Url, #{} = Body, MpType) ->
req_mp(Method, Url, Body, MpType) ->
Headers = [?JSON, ?AUTH, MpType],
{ok, Code, ResHeaders, Res} = test_request:request(Method, Url, Headers,
Body),
- CType = header_value("Content-Type", ResHeaders),
+ CType = header_value(~"content-type", ResHeaders),
case CType of
- "application/json" ->
+ ~"application/json" ->
{Code, json_decode(Res)};
- "multipart/" ++ _ ->
+ <<"multipart/", _/binary>> ->
Chunks = split(Res, CType),
{Code, lists:map(fun chunk_parse_fun/1, Chunks)}
end.
@@ -1574,8 +1574,7 @@ header_value(Key, Headers) ->
header_value(Key, Headers, undefined).
header_value(Key, Headers, Default) ->
- Headers1 = [{string:to_lower(K), V} || {K, V} <- Headers],
- case lists:keyfind(string:to_lower(Key), 1, Headers1) of
+ case lists:keyfind(Key, 1, Headers) of
{_, Value} -> Value;
_ -> Default
end.
diff --git a/src/chttpd/test/eunit/chttpd_csp_tests.erl
b/src/chttpd/test/eunit/chttpd_csp_tests.erl
index 567b1906f..7a88a1451 100644
--- a/src/chttpd/test/eunit/chttpd_csp_tests.erl
+++ b/src/chttpd/test/eunit/chttpd_csp_tests.erl
@@ -118,35 +118,39 @@ should_not_return_any_csp_headers_when_disabled(_DbName)
->
ok = config:set("csp", "utils_enable", "false", false),
ok = config:set("csp", "enable", "false", false),
{ok, _, Headers, _} = test_request:get(base_url() ++ "/_utils/"),
- proplists:get_value("Content-Security-Policy", Headers)
+ proplists:get_value(~"content-security-policy", Headers)
end
).
should_apply_default_policy(_DbName) ->
?_assertEqual(
- "child-src 'self' data: blob:; default-src 'self'; img-src 'self'
data:; font-src 'self'; "
- "script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';
frame-src https://blog.couchdb.org;",
+ <<
+ "child-src 'self' data: blob:; default-src 'self'; img-src 'self'
data:; font-src 'self'; "
+ "script-src 'self' 'unsafe-eval'; style-src 'self'
'unsafe-inline'; frame-src https://blog.couchdb.org;"
+ >>,
begin
{ok, _, Headers, _} = test_request:get(base_url() ++ "/_utils/"),
- proplists:get_value("Content-Security-Policy", Headers)
+ proplists:get_value(~"content-security-policy", Headers)
end
).
should_apply_default_policy_with_legacy_config(_DbName) ->
?_assertEqual(
- "child-src 'self' data: blob:; default-src 'self'; img-src 'self'
data:; font-src 'self'; "
- "script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';
frame-src https://blog.couchdb.org;",
+ <<
+ "child-src 'self' data: blob:; default-src 'self'; img-src 'self'
data:; font-src 'self'; "
+ "script-src 'self' 'unsafe-eval'; style-src 'self'
'unsafe-inline'; frame-src https://blog.couchdb.org;"
+ >>,
begin
ok = config:set("csp", "utils_enable", "false", false),
ok = config:set("csp", "enable", "true", false),
{ok, _, Headers, _} = test_request:get(base_url() ++ "/_utils/"),
- proplists:get_value("Content-Security-Policy", Headers)
+ proplists:get_value(~"content-security-policy", Headers)
end
).
should_return_custom_policy(_DbName) ->
?_assertEqual(
- "default-src 'http://example.com';",
+ ~"default-src 'http://example.com';",
begin
ok = config:set(
"csp",
@@ -155,7 +159,7 @@ should_return_custom_policy(_DbName) ->
false
),
{ok, _, Headers, _} = test_request:get(base_url() ++ "/_utils/"),
- proplists:get_value("Content-Security-Policy", Headers)
+ proplists:get_value(~"content-security-policy", Headers)
end
).
@@ -273,4 +277,4 @@ req(Method, {_, _} = Auth, Url, ContentType, #{} = Body) ->
{Code, is_sandboxed(RespHdrs)}.
is_sandboxed(Headers) ->
- lists:member({"Content-Security-Policy", "sandbox"}, Headers).
+ lists:member({~"content-security-policy", ~"sandbox"}, Headers).
diff --git a/src/chttpd/test/eunit/chttpd_db_attachment_size_tests.erl
b/src/chttpd/test/eunit/chttpd_db_attachment_size_tests.erl
index 420291616..1b49beb92 100644
--- a/src/chttpd/test/eunit/chttpd_db_attachment_size_tests.erl
+++ b/src/chttpd/test/eunit/chttpd_db_attachment_size_tests.erl
@@ -394,8 +394,9 @@ req(Method, Url, Headers, Body) ->
{ok, Code, _, Res} = test_request:request(Method, Url, Headers1, Body),
{Code, json_decode(Res)}.
-% Data streaming generator for ibrowse client. ibrowse will repeatedly call the
-% function with State and it should return {ok, Data, NewState} or eof at end.
+% Data streaming generator for the test http client, which repeatedly calls
+% the function with State; it should return {ok, Data, NewState} or eof at
+% the end.
data_stream_fun(Size) ->
Fun = fun
(0) -> eof;
diff --git a/src/chttpd/test/eunit/chttpd_db_doc_get_tests.erl
b/src/chttpd/test/eunit/chttpd_db_doc_get_tests.erl
index 899b52862..b2b132c2e 100644
--- a/src/chttpd/test/eunit/chttpd_db_doc_get_tests.erl
+++ b/src/chttpd/test/eunit/chttpd_db_doc_get_tests.erl
@@ -540,11 +540,11 @@ get_doc_mp(DbUrl, DocId, Params) ->
get_mp(Url, MpType) ->
Headers = [?JSON_CT, ?AUTH, MpType],
{ok, Code, ResHeaders, Res} = test_request:request(get, Url, Headers),
- CType = header_value("Content-Type", ResHeaders),
+ CType = header_value(~"content-type", ResHeaders),
case CType of
- ?JSON ->
+ ~"application/json" ->
{Code, json_decode(Res)};
- "multipart/" ++ _ ->
+ <<"multipart/", _/binary>> ->
Chunks = split(Res, CType),
{Code, lists:map(fun chunk_parse_fun/1, Chunks)}
end.
@@ -590,8 +590,7 @@ header_value(Key, Headers) ->
header_value(Key, Headers, undefined).
header_value(Key, Headers, Default) ->
- Headers1 = [{string:to_lower(K), V} || {K, V} <- Headers],
- case lists:keyfind(string:to_lower(Key), 1, Headers1) of
+ case lists:keyfind(Key, 1, Headers) of
{_, Value} -> Value;
_ -> Default
end.
diff --git a/src/chttpd/test/eunit/chttpd_dbs_info_test.erl
b/src/chttpd/test/eunit/chttpd_dbs_info_test.erl
index a53442f98..0749c2f70 100644
--- a/src/chttpd/test/eunit/chttpd_dbs_info_test.erl
+++ b/src/chttpd/test/eunit/chttpd_dbs_info_test.erl
@@ -21,7 +21,7 @@
-define(CONTENT_JSON, {"Content-Type", "application/json"}).
start() ->
- Ctx = test_util:start_couch([inets, chttpd]),
+ Ctx = test_util:start_couch([chttpd]),
DbDir = config:get("couchdb", "database_dir"),
Suffix = ?b2l(couch_uuids:random()),
test_util:with_couch_server_restart(fun() ->
@@ -183,17 +183,12 @@
should_return_nothing_when_db_not_exist_for_get_dbs_info(_) ->
should_return_500_time_out_when_time_is_not_enough_for_get_dbs_info(_) ->
mock_timeout(),
- Auth = base64:encode_to_string(?USER ++ ":" ++ ?PASS),
- Headers = [{"Authorization", "Basic " ++ Auth}],
- Request = {dbs_info_url("buffer_response=true"), Headers},
+ Url = dbs_info_url("buffer_response=true"),
{Props} =
test_util:wait(
fun() ->
- % Use httpc to avoid ibrowse returning {error,
- % retry_later} in some cases, causing test_request to
- % sleep and retry, resulting in timeout failures.
- case httpc:request(get, Request, [], []) of
- {ok, {{_, Code, _}, _, Body}} ->
+ case test_request:get(Url, [?CONTENT_JSON, ?AUTH]) of
+ {ok, Code, _, Body} ->
?assertEqual(500, Code),
jiffy:decode(Body);
_ ->
diff --git a/src/couch/src/couch.app.src b/src/couch/src/couch.app.src
index 5f1fb9800..140140f82 100644
--- a/src/couch/src/couch.app.src
+++ b/src/couch/src/couch.app.src
@@ -36,6 +36,7 @@
os_mon,
% Upstream deps
+ gun,
ibrowse,
mochiweb,
diff --git a/src/couch/src/couch_gun.erl b/src/couch/src/couch_gun.erl
new file mode 100644
index 000000000..e207283ce
--- /dev/null
+++ b/src/couch/src/couch_gun.erl
@@ -0,0 +1,439 @@
+% 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.
+
+% Helper module to use gun instead of other http clients we had.
+%
+% Functions:
+%
+% * req/3,4,5: For basic synchronous requests
+%
+% * parse_url/1: Helper parser to turn urls with possible auth bits
+% embedded into a gun uri map
+%
+% * open/3, close/1: Open/close connections
+%
+% * send/5,6: Send requests on opened connections. Can take {Fun, State} for
+% a streaming body.
+%
+% * await/3: Wait for response a send.
+%
+% * headers/1, method/1, basic_auth/2: convert existing calling conventions
+% to gun's format. These are helpers to avoid modifying all the call sites
+% right off the bat. For example, the codebase expects headers to be
+% strings and methods to be atom so we transform them here accordingly.
+%
+% * norm_error/1: shorten/normalize gun's error reason
+
+-module(couch_gun).
+
+-export([
+ req/3,
+ req/4,
+ req/5,
+ parse_url/1,
+ open/3,
+ close/1,
+ send/5,
+ send/6,
+ await/3,
+ headers/1,
+ method/1,
+ basic_auth/2,
+ norm_error/1
+]).
+
+-define(DEFAULT_TIMEOUT, 30000).
+
+req(Method, Url, Headers) ->
+ req(Method, Url, Headers, <<>>, #{}).
+
+req(Method, Url, Headers, Body) ->
+ req(Method, Url, Headers, Body, #{}).
+
+req(Method, Url, Headers, Body, #{} = Opts) when is_atom(Method), is_list(Url)
->
+ case parse_url(Url) of
+ {ok, #{transport := Transport, host := Host, port := Port} = Parsed} ->
+ #{path := Path, userinfo := UserInfo} = Parsed,
+ Timeout = maps:get(timeout, Opts, ?DEFAULT_TIMEOUT),
+ Headers1 = auth_headers(headers(Headers), UserInfo, Opts),
+ OpenOpts = maps:with([tls_opts, tcp_opts, gun_opts], Opts),
+ case open(Host, Port, OpenOpts#{transport => Transport}) of
+ {ok, Pid} ->
+ try
+ case gun:await_up(Pid, Timeout) of
+ {ok, _} ->
+ Ref = send(Pid, Method, Path, Headers1, Body),
+ await(Pid, Ref, Timeout);
+ {error, Reason} ->
+ {error, norm_error(Reason)}
+ end
+ after
+ close(Pid)
+ end;
+ {error, Reason} ->
+ {error, norm_error(Reason)}
+ end;
+ {error, _} = Error ->
+ Error
+ end.
+
+parse_url("http://" ++ Rest) ->
+ parse_auth(tcp, 80, Rest);
+parse_url("https://" ++ Rest) ->
+ parse_auth(tls, 443, Rest);
+parse_url(_) ->
+ {error, invalid_uri}.
+
+parse_auth(Transport, DefaultPort, Rest) ->
+ {Auth, Path} =
+ case lists:splitwith(fun(C) -> C /= $/ andalso C /= $? end, Rest) of
+ {A, ""} -> {A, "/"};
+ {A, "?" ++ _ = Query} -> {A, "/" ++ Query};
+ {A, P} -> {A, P}
+ end,
+ {UserInfo, HostPort} =
+ case string:split(Auth, "@", trailing) of
+ [Creds, HP] -> {Creds, HP};
+ [HP] -> {undefined, HP}
+ end,
+ case parse_host_port(HostPort, DefaultPort) of
+ {error, _} = Error ->
+ Error;
+ {Host, Port} ->
+ {ok, #{
+ transport => Transport,
+ host => Host,
+ port => Port,
+ path => Path,
+ userinfo => UserInfo
+ }}
+ end.
+
+parse_host_port("", _DefaultPort) ->
+ {error, invalid_uri};
+parse_host_port("[" ++ Rest, DefaultPort) ->
+ % ipv6 with brackets [...]
+ case string:split(Rest, "]") of
+ ["", _] ->
+ {error, invalid_uri};
+ [Host, ""] ->
+ {Host, DefaultPort};
+ [Host, ":" ++ PortStr] ->
+ case string:to_integer(PortStr) of
+ {Port, ""} when is_integer(Port) -> {Host, Port};
+ _ -> {error, invalid_uri}
+ end;
+ _ ->
+ {error, invalid_uri}
+ end;
+parse_host_port(HostPort, DefaultPort) ->
+ case string:split(HostPort, ":", trailing) of
+ ["", _] ->
+ {error, invalid_uri};
+ [Host, PortStr] ->
+ case string:to_integer(PortStr) of
+ {Port, ""} when is_integer(Port) -> {Host, Port};
+ _ -> {error, invalid_uri}
+ end;
+ [Host] ->
+ {Host, DefaultPort}
+ end.
+
+% Spawn an http/1.1 gun connection process. Opts is a map that can have
+% these fields:
+%
+% transport - tcp | tls (default is tcp)
+% tls_opts - tls client opts, default is []
+% tcp_opts - gen_tcp options
+% gun_opts - Other gun options (see gun:open/3 docs)
+%
+% Host could be a string or address tuple. We don't wait for the connection to
+% be up a caller may do that with gun:wait_up/2 to get connection errors
+% earlier than during the first send.
+%
+open(Host, Port, #{} = Opts) ->
+ Transport = maps:get(transport, Opts, tcp),
+ OpenOpts0 = #{transport => Transport, protocols => [http], retry => 0},
+ OpenOpts1 =
+ case Opts of
+ #{tcp_opts := TcpOpts} -> OpenOpts0#{tcp_opts => TcpOpts};
+ #{} -> OpenOpts0
+ end,
+ OpenOpts2 =
+ case Transport of
+ tls -> OpenOpts1#{tls_opts => maps:get(tls_opts, Opts, [])};
+ tcp -> OpenOpts1
+ end,
+ OpenOpts = maps:merge(OpenOpts2, maps:get(gun_opts, Opts, #{})),
+ gun:open(host(Host), Port, OpenOpts).
+
+close(Pid) when is_pid(Pid) ->
+ try
+ gun:close(Pid)
+ catch
+ _:_ -> ok
+ end,
+ ok.
+
+% Send a request and get back a stream ref. Body may be a {Fun, State} tuple.
+% Then Fun(State) should return {ok, Data, NewState} and then return eof at the
+% end. Data will be sent chunked unless a content-length header is set.
+send(Pid, Method, Path, Headers, Body) ->
+ send(Pid, Method, Path, Headers, Body, #{}).
+
+send(Pid, Method, Path, Headers, {Fun, State}, ReqOpts) when is_function(Fun,
1) ->
+ SRef = gun:headers(Pid, method(Method), Path, headers(Headers), ReqOpts),
+ ok = send_body(Pid, SRef, Fun, State),
+ SRef;
+send(Pid, Method, Path, Headers, Body, ReqOpts) when is_pid(Pid) ->
+ gun:request(Pid, method(Method), Path, headers(Headers), body(Body),
ReqOpts).
+
+body([]) ->
+ <<>>;
+body(Body) ->
+ Body.
+
+send_body(Pid, SRef, Fun, State) ->
+ case Fun(State) of
+ {ok, Data, State1} ->
+ send_body(Pid, SRef, Fun, State1, Data);
+ eof ->
+ ok = gun:data(Pid, SRef, fin, <<>>)
+ end.
+
+send_body(Pid, SRef, Fun, State, Data0) ->
+ case Fun(State) of
+ {ok, Data, State1} ->
+ % Send pending data before sending the next.
+ % We're doing one chunk at a time here
+ ok = gun:data(Pid, SRef, nofin, Data0),
+ send_body(Pid, SRef, Fun, State1, Data);
+ eof ->
+ ok = gun:data(Pid, SRef, fin, Data0)
+ end.
+
+% Wait for a response. First wait for status + headers then body. 1xx info
+% responses are skipped and we don't care about trailers either. If we got a
+% bad connection and didn't find out until calling send and await we'll get the
+% error here.
+await(Pid, SRef, Timeout) when is_pid(Pid) ->
+ MRef = monitor(process, Pid),
+ try await_headers(Pid, SRef, Timeout, MRef) of
+ {response, fin, Code, RespHeaders} ->
+ {ok, Code, RespHeaders, <<>>};
+ {response, nofin, Code, RespHeaders} ->
+ case gun:await_body(Pid, SRef, Timeout, MRef) of
+ {ok, RespBody} -> {ok, Code, RespHeaders, RespBody};
+ {ok, RespBody, _Trailers} -> {ok, Code, RespHeaders, RespBody};
+ {error, Reason} -> {error, norm_error(Reason)}
+ end;
+ {error, Reason} ->
+ {error, norm_error(Reason)}
+ after
+ demonitor(MRef, [flush])
+ end.
+
+await_headers(Pid, SRef, Timeout, MRef) ->
+ case gun:await(Pid, SRef, Timeout, MRef) of
+ {inform, _Status, _Headers} -> await_headers(Pid, SRef, Timeout, MRef);
+ Other -> Other
+ end.
+
+% Transform our request headers into gun's lowercase binary shape. Previous
+% http client accepted special atom headers like {basic_auth, {User, Pass}} and
+% {cookie, Value}, {content_type, Type}, {content_length, Len}. We handle those
+% here to avoid modifying all the call sites.
+
+headers(Headers) ->
+ [header(H) || H <- Headers].
+
+header({basic_auth, {User, Pass}}) ->
+ basic_auth(User, Pass);
+header({cookie, Cookie}) ->
+ {~"cookie", to_bin(Cookie)};
+header({content_type, Value}) ->
+ {~"content-type", to_bin(Value)};
+header({content_length, Value}) ->
+ {~"content-length", to_bin(Value)};
+header({Name, Value}) ->
+ {string:lowercase(to_bin(Name)), to_bin(Value)}.
+
+method(Method) when is_atom(Method) ->
+ string:uppercase(atom_to_binary(Method, utf8)).
+
+basic_auth(User, Pass) ->
+ UserPass = base64:encode(iolist_to_binary([User, $:, Pass])),
+ {~"authorization", <<"Basic ", UserPass/binary>>}.
+
+% If headers already have authorization set use that, otherwise take from the
+% userinfo field from the url
+auth_headers(Headers, UserInfo, Opts) ->
+ case lists:keymember(~"authorization", 1, Headers) of
+ true ->
+ Headers;
+ false ->
+ case {Opts, UserInfo} of
+ {#{basic_auth := {User, Pass}}, _} ->
+ [basic_auth(User, Pass) | Headers];
+ {#{}, undefined} ->
+ Headers;
+ {#{}, UserInfo} ->
+ case string:split(UserInfo, ":") of
+ [User, Pass] -> [basic_auth(User, Pass) | Headers];
+ [User] -> [basic_auth(User, "") | Headers]
+ end
+ end
+ end.
+
+to_bin(V) when is_atom(V) ->
+ atom_to_binary(V, utf8);
+to_bin(V) ->
+ iolist_to_binary(V).
+
+% gun expects IPs as address tuples
+host(Host) when is_list(Host) ->
+ case inet:parse_strict_address(Host) of
+ {ok, Ip} -> Ip;
+ {error, _} -> Host
+ end;
+host(Host) ->
+ Host.
+
+% To simplify error handling return {error, Reason} to make it easier for
+% callers to handle it instead of the multi-level nested error shapes from gun.
+norm_error({stream_error, Reason}) ->
+ norm_error(Reason);
+norm_error({connection_error, Reason}) ->
+ norm_error(Reason);
+norm_error({down, {shutdown, Reason}}) ->
+ norm_error(Reason);
+norm_error({down, Reason}) ->
+ norm_error(Reason);
+norm_error({shutdown, Reason}) ->
+ norm_error(Reason);
+norm_error(Reason) ->
+ Reason.
+
+-ifdef(TEST).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+parse_url_test() ->
+ ?assertEqual(
+ {ok, #{transport => tcp, host => "h", port => 80, path => "/",
userinfo => undefined}},
+ parse_url("http://h")
+ ),
+ ?assertEqual(
+ {ok, #{
+ transport => tcp, host => "h", port => 5984, path => "/db?a=b",
userinfo => undefined
+ }},
+ parse_url("http://h:5984/db?a=b")
+ ),
+ ?assertEqual(
+ {ok, #{transport => tls, host => "h", port => 443, path => "/",
userinfo => undefined}},
+ parse_url("https://h")
+ ),
+ ?assertEqual(
+ {ok, #{
+ transport => tcp, host => "127.0.0.1", port => 80, path => "/",
userinfo => undefined
+ }},
+ parse_url("http://127.0.0.1")
+ ),
+ ?assertEqual(
+ {ok, #{transport => tcp, host => "::1", port => 5984, path => "/db",
userinfo => undefined}},
+ parse_url("http://[::1]:5984/db")
+ ),
+ ?assertEqual(
+ {ok, #{transport => tcp, host => "::1", port => 80, path => "/",
userinfo => undefined}},
+ parse_url("http://[::1]")
+ ),
+ ?assertEqual(
+ {ok, #{transport => tcp, host => "h", port => 80, path => "/",
userinfo => "u:p"}},
+ parse_url("http://u:p@h")
+ ),
+ ?assertEqual(
+ {ok, #{
+ transport => tcp,
+ host => "h",
+ port => 15984,
+ path => "/_dbs_info?startkey=\"db1\"&endkey=\"db2\"",
+ userinfo => undefined
+ }},
+ parse_url("http://h:15984/_dbs_info?startkey=\"db1\"&endkey=\"db2\"")
+ ),
+ ?assertEqual(
+ {ok, #{transport => tcp, host => "h", port => 80, path => "/?q=1",
userinfo => undefined}},
+ parse_url("http://h?q=1")
+ ),
+ ?assertEqual({error, invalid_uri}, parse_url("a potato")),
+ ?assertEqual({error, invalid_uri}, parse_url("ftp://h/")),
+ ?assertEqual({error, invalid_uri}, parse_url("http://")),
+ ?assertEqual({error, invalid_uri}, parse_url("http://:80/")),
+ ?assertEqual({error, invalid_uri}, parse_url("http://h:x/")),
+ ?assertEqual({error, invalid_uri}, parse_url("http://[::1")),
+ ?assertEqual({error, invalid_uri}, parse_url("http://[]:80/")).
+
+host_test() ->
+ ?assertEqual("cdb.example.com", host("cdb.example.com")),
+ ?assertEqual({127, 0, 0, 1}, host("127.0.0.1")),
+ ?assertEqual({0, 0, 0, 0, 0, 0, 0, 1}, host("::1")),
+ ?assertEqual({1, 2, 3, 4}, host({1, 2, 3, 4})).
+
+headers_test() ->
+ ?assertEqual([], headers([])),
+ ?assertEqual(
+ [{~"content-type", ~"application/json"}],
+ headers([{"Content-Type", "application/json"}])
+ ),
+ ?assertEqual(
+ [{~"x-foo", ~"1"}, {~"accept", ~"*/*"}],
+ headers([{'X-Foo', "1"}, {~"Accept", ~"*/*"}])
+ ),
+ ?assertEqual(
+ [basic_auth("u", "p"), {~"cookie", ~"k=v"}],
+ headers([{basic_auth, {"u", "p"}}, {cookie, "k=v"}])
+ ),
+ ?assertEqual(
+ [{~"content-type", ~"text/plain"}, {~"content-length", ~"3"}],
+ headers([{content_type, "text/plain"}, {content_length, "3"}])
+ ).
+
+method_test() ->
+ ?assertEqual(~"GET", method(get)),
+ ?assertEqual(~"COPY", method(copy)),
+ ?assertEqual(~"DELETE", method('Delete')).
+
+basic_auth_test() ->
+ ?assertEqual(
+ {~"authorization", <<"Basic ", (base64:encode(~"u:p"))/binary>>},
+ basic_auth("u", "p")
+ ),
+ ?assertEqual(basic_auth("u", "p"), basic_auth(~"u", ~"p")).
+
+auth_headers_test() ->
+ Auth = basic_auth("u", "p"),
+ Override = [{~"authorization", ~"Bearer dabears"}],
+ ?assertEqual([], auth_headers([], undefined, #{})),
+ ?assertEqual([Auth], auth_headers([], undefined, #{basic_auth => {"u",
"p"}})),
+ ?assertEqual([Auth], auth_headers([], "u:p", #{})),
+ ?assertEqual([basic_auth("u", "")], auth_headers([], "u", #{})),
+ ?assertEqual([Auth], auth_headers([], "x:y", #{basic_auth => {"u", "p"}})),
+ ?assertEqual(Override, auth_headers(Override, "x:y", #{basic_auth => {"u",
"p"}})).
+
+norm_error_test() ->
+ ?assertEqual(econnrefused, norm_error({down, {shutdown, econnrefused}})),
+ ?assertEqual(closed, norm_error({stream_error, closed})),
+ ?assertEqual(closed, norm_error({connection_error, closed})),
+ ?assertEqual(timeout, norm_error(timeout)),
+ ?assertEqual(normal, norm_error({down, normal})).
+
+-endif.
diff --git a/src/couch/src/test_request.erl b/src/couch/src/test_request.erl
index d7364012f..a55fcb82a 100644
--- a/src/couch/src/test_request.erl
+++ b/src/couch/src/test_request.erl
@@ -20,6 +20,8 @@
-export([options/1, options/2, options/3]).
-export([request/3, request/4, request/5]).
+-define(TIMEOUT, 30000).
+
copy(Url) ->
copy(Url, []).
@@ -86,25 +88,65 @@ request(Method, Url, Headers, Body, Opts) ->
request(_Method, _Url, _Headers, _Body, _Opts, 0) ->
{error, request_failed};
request(Method, Url, Headers, Body, Opts, N) ->
- case code:is_loaded(ibrowse) of
- false ->
- {ok, _} = ibrowse:start();
- _ ->
- ok
- end,
- case ibrowse:send_req(Url, Headers, Method, Body, Opts) of
- {ok, Code0, RespHeaders, RespBody0} ->
- Code = list_to_integer(Code0),
- RespBody = iolist_to_binary(RespBody0),
- {ok, Code, RespHeaders, RespBody};
- {error, {'EXIT', {normal, _}}} ->
- % Connection closed right after a successful request that
- % used the same connection.
- request(Method, Url, Headers, Body, Opts, N - 1);
- {error, retry_later} ->
- % CouchDB is busy, let’s wait a bit
- timer:sleep(3000 div N),
+ {ok, _} = application:ensure_all_started(gun),
+ Headers1 = headers(Headers, Opts),
+ ReqOpts = #{timeout => ?TIMEOUT, tls_opts => [{verify, verify_none}]},
+ case couch_gun:req(Method, Url, Headers1, Body, ReqOpts) of
+ {ok, _Code, _RespHeaders, _RespBody} = Resp ->
+ Resp;
+ {error, closed} ->
+ % Retry. Possible race with the server starting.
request(Method, Url, Headers, Body, Opts, N - 1);
Error ->
Error
end.
+
+headers(Headers, Opts) ->
+ lists:foldl(fun apply_opt/2, couch_gun:headers(Headers), Opts).
+
+apply_opt({host_header, Value}, Headers) ->
+ [Host] = couch_gun:headers([{host, Value}]),
+ lists:keystore(~"host", 1, Headers, Host);
+apply_opt({basic_auth, {User, Pass}}, Headers) ->
+ Auth = couch_gun:basic_auth(User, Pass),
+ lists:keystore(~"authorization", 1, Headers, Auth);
+apply_opt(_Other, Headers) ->
+ Headers.
+
+-ifdef(TEST).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+headers_test() ->
+ ?assertEqual([], headers([], [])),
+ ?assertEqual(
+ [{~"content-type", ~"application/json"}],
+ headers([{"Content-Type", "application/json"}], [])
+ ),
+ Auth = couch_gun:basic_auth("u", "p"),
+ ?assertEqual(
+ [{~"authorization", ~"Basic dTpw"}],
+ headers([{basic_auth, {"u", "p"}}], [])
+ ),
+ ?assertEqual(
+ [{~"cookie", ~"k=v"}],
+ headers([{cookie, "k=v"}], [])
+ ),
+ ?assertEqual(
+ [{~"accept", ~"*/*"}, {~"host", ~"potato.local"}],
+ headers([{"Accept", "*/*"}], [{host_header, "potato.local"}])
+ ),
+ ?assertEqual(
+ [{~"host", ~"b"}],
+ headers([{"Host", "a"}], [{host_header, "b"}])
+ ),
+ ?assertEqual(
+ [Auth],
+ headers([], [{basic_auth, {"u", "p"}}])
+ ),
+ ?assertEqual(
+ [Auth],
+ headers([{basic_auth, {"x", "y"}}], [{basic_auth, {"u", "p"}}])
+ ).
+
+-endif.
diff --git a/src/couch/src/test_util.erl b/src/couch/src/test_util.erl
index fd5364fb7..dabbb3382 100644
--- a/src/couch/src/test_util.erl
+++ b/src/couch/src/test_util.erl
@@ -46,7 +46,7 @@
-record(test_context, {mocked = [], started = [], module}).
--define(DEFAULT_APPS, [inets, ibrowse, ssl, config, couch_epi, couch_event,
couch]).
+-define(DEFAULT_APPS, [inets, gun, ssl, config, couch_epi, couch_event,
couch]).
srcdir() ->
code:priv_dir(couch) ++ "/../../".
@@ -58,7 +58,8 @@ init_code_path() ->
Paths = [
"couchdb",
"jiffy",
- "ibrowse",
+ "gun",
+ "cowlib",
"mochiweb",
"snappy"
],
diff --git a/src/couch/test/eunit/couchdb_attachments_tests.erl
b/src/couch/test/eunit/couchdb_attachments_tests.erl
index 103a02326..eacf2852d 100644
--- a/src/couch/test/eunit/couchdb_attachments_tests.erl
+++ b/src/couch/test/eunit/couchdb_attachments_tests.erl
@@ -319,7 +319,7 @@ should_get_att_without_accept_gzip_encoding(_, {Data, {_,
_, AttUrl}}) ->
?_test(begin
{ok, Code, Headers, Body} = test_request:get(AttUrl),
?assertEqual(200, Code),
- ?assertNot(lists:member({"Content-Encoding", "gzip"}, Headers)),
+ ?assertNot(lists:member({~"content-encoding", ~"gzip"}, Headers)),
?assertEqual(Data, iolist_to_binary(Body))
end).
@@ -329,7 +329,7 @@ should_get_att_with_accept_gzip_encoding(compressed, {Data,
{_, _, AttUrl}}) ->
AttUrl, [{"Accept-Encoding", "gzip"}]
),
?assertEqual(200, Code),
- ?assert(lists:member({"Content-Encoding", "gzip"}, Headers)),
+ ?assert(lists:member({~"content-encoding", ~"gzip"}, Headers)),
?assertEqual(Data, zlib:gunzip(iolist_to_binary(Body)))
end);
should_get_att_with_accept_gzip_encoding({text, _}, {Data, {_, _, AttUrl}}) ->
@@ -338,7 +338,7 @@ should_get_att_with_accept_gzip_encoding({text, _}, {Data,
{_, _, AttUrl}}) ->
AttUrl, [{"Accept-Encoding", "gzip"}]
),
?assertEqual(200, Code),
- ?assert(lists:member({"Content-Encoding", "gzip"}, Headers)),
+ ?assert(lists:member({~"content-encoding", ~"gzip"}, Headers)),
?assertEqual(Data, zlib:gunzip(iolist_to_binary(Body)))
end);
should_get_att_with_accept_gzip_encoding({binary, _}, {Data, {_, _, AttUrl}})
->
@@ -349,7 +349,7 @@ should_get_att_with_accept_gzip_encoding({binary, _},
{Data, {_, _, AttUrl}}) ->
?assertEqual(200, Code),
?assertEqual(
undefined,
- couch_util:get_value("Content-Encoding", Headers)
+ couch_util:get_value(~"content-encoding", Headers)
),
?assertEqual(Data, iolist_to_binary(Body))
end).
@@ -362,7 +362,7 @@ should_get_att_with_accept_deflate_encoding(_, {Data, {_,
_, AttUrl}}) ->
?assertEqual(200, Code),
?assertEqual(
undefined,
- couch_util:get_value("Content-Encoding", Headers)
+ couch_util:get_value(~"content-encoding", Headers)
),
?assertEqual(Data, iolist_to_binary(Body))
end).
diff --git a/src/couch/test/eunit/couchdb_cookie_domain_tests.erl
b/src/couch/test/eunit/couchdb_cookie_domain_tests.erl
index 17c41dafe..dbe9e4ef1 100755
--- a/src/couch/test/eunit/couchdb_cookie_domain_tests.erl
+++ b/src/couch/test/eunit/couchdb_cookie_domain_tests.erl
@@ -60,8 +60,8 @@ should_set_cookie_domain(Url, ContentType, Payload) ->
),
{ok, Code, Headers, _} = test_request:post(Url, ContentType, Payload),
?assertEqual(200, Code),
- Cookie = proplists:get_value("Set-Cookie", Headers),
- ?assert(string:str(Cookie, "; Domain=example.com") > 0)
+ Cookie = proplists:get_value(~"set-cookie", Headers),
+ ?assertNotEqual(nomatch, string:find(Cookie, "; Domain=example.com"))
end).
should_not_set_cookie_domain(Url, ContentType, Payload) ->
@@ -69,8 +69,8 @@ should_not_set_cookie_domain(Url, ContentType, Payload) ->
ok = config:set("couch_httpd_auth", "cookie_domain", "", false),
{ok, Code, Headers, _} = test_request:post(Url, ContentType, Payload),
?assertEqual(200, Code),
- Cookie = proplists:get_value("Set-Cookie", Headers),
- ?assertEqual(0, string:str(Cookie, "; Domain="))
+ Cookie = proplists:get_value(~"set-cookie", Headers),
+ ?assertEqual(nomatch, string:find(Cookie, "; Domain="))
end).
should_delete_cookie_domain(Url, ContentType, Payload) ->
@@ -83,6 +83,6 @@ should_delete_cookie_domain(Url, ContentType, Payload) ->
),
{ok, Code, Headers, _} = test_request:delete(Url, ContentType,
Payload),
?assertEqual(200, Code),
- Cookie = proplists:get_value("Set-Cookie", Headers),
- ?assert(string:str(Cookie, "; Domain=example.com") > 0)
+ Cookie = proplists:get_value(~"set-cookie", Headers),
+ ?assertNotEqual(nomatch, string:find(Cookie, "; Domain=example.com"))
end).
diff --git a/src/couch/test/eunit/couchdb_cors_tests.erl
b/src/couch/test/eunit/couchdb_cors_tests.erl
index dce07fd28..b6baf09b8 100644
--- a/src/couch/test/eunit/couchdb_cors_tests.erl
+++ b/src/couch/test/eunit/couchdb_cors_tests.erl
@@ -155,7 +155,7 @@ should_not_allow_origin(_, {_, _, Url, Headers0}) ->
[{"Origin", "http://127.0.0.1"}] ++
Headers1,
{ok, _, Resp, _} = test_request:get(Url, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
@@ -170,7 +170,7 @@ should_not_allow_origin_with_port_mismatch({_, VHost}, {_,
_, Url, _}) ->
] ++
maybe_append_vhost(VHost),
{ok, _, Resp, _} = test_request:options(Url, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
@@ -185,7 +185,7 @@ should_not_allow_origin_with_scheme_mismatch({_, VHost},
{_, _, Url, _}) ->
] ++
maybe_append_vhost(VHost),
{ok, _, Resp, _} = test_request:options(Url, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
@@ -200,7 +200,7 @@ should_not_all_origin_due_case_mismatch({_, VHost}, {_, _,
Url, _}) ->
] ++
maybe_append_vhost(VHost),
{ok, _, Resp, _} = test_request:options(Url, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
@@ -209,15 +209,15 @@ should_make_simple_request(_, {_, _, Url,
DefaultHeaders}) ->
{ok, _, Resp, _} = test_request:get(Url, DefaultHeaders),
?assertEqual(
undefined,
- proplists:get_value("Access-Control-Allow-Credentials", Resp)
+ proplists:get_value(~"access-control-allow-credentials", Resp)
),
?assertEqual(
- "http://example.com",
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ ~"http://example.com",
+ proplists:get_value(~"access-control-allow-origin", Resp)
),
?assertEqualLists(
?COUCH_HEADERS ++ list_simple_headers(Resp),
- split_list(proplists:get_value("Access-Control-Expose-Headers",
Resp))
+ split_list(proplists:get_value(~"access-control-expose-headers",
Resp))
)
end).
@@ -229,13 +229,13 @@ should_make_preflight_request(_, {_, _, Url,
DefaultHeaders}) ->
DefaultHeaders ++
[{"Access-Control-Request-Method", "GET"}],
{ok, _, Resp, _} = test_request:options(Url, Headers),
- split_list(proplists:get_value("Access-Control-Allow-Methods",
Resp))
+ split_list(proplists:get_value(~"access-control-allow-methods",
Resp))
end
).
should_make_prefligh_request_with_port({_, VHost}, {_, _, Url, _}) ->
?_assertEqual(
- "http://example.com:5984",
+ ~"http://example.com:5984",
begin
config:set(
"cors",
@@ -250,13 +250,13 @@ should_make_prefligh_request_with_port({_, VHost}, {_, _,
Url, _}) ->
] ++
maybe_append_vhost(VHost),
{ok, _, Resp, _} = test_request:options(Url, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
should_make_prefligh_request_with_scheme({_, VHost}, {_, _, Url, _}) ->
?_assertEqual(
- "https://example.com:5984",
+ ~"https://example.com:5984",
begin
config:set(
"cors",
@@ -271,13 +271,13 @@ should_make_prefligh_request_with_scheme({_, VHost}, {_,
_, Url, _}) ->
] ++
maybe_append_vhost(VHost),
{ok, _, Resp, _} = test_request:options(Url, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
should_make_prefligh_request_with_wildcard_origin({_, VHost}, {_, _, Url, _})
->
?_assertEqual(
- "https://example.com:5984",
+ ~"https://example.com:5984",
begin
config:set("cors", "origins", "*", false),
Headers =
@@ -287,23 +287,23 @@ should_make_prefligh_request_with_wildcard_origin({_,
VHost}, {_, _, Url, _}) ->
] ++
maybe_append_vhost(VHost),
{ok, _, Resp, _} = test_request:options(Url, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
should_make_request_with_credentials(_, {_, _, Url, DefaultHeaders}) ->
?_assertEqual(
- "true",
+ ~"true",
begin
ok = config:set("cors", "credentials", "true", false),
{ok, _, Resp, _} = test_request:options(Url, DefaultHeaders),
- proplists:get_value("Access-Control-Allow-Credentials", Resp)
+ proplists:get_value(~"access-control-allow-credentials", Resp)
end
).
should_make_origin_request_with_auth(_, {_, _, Url, DefaultHeaders}) ->
?_assertEqual(
- "http://example.com",
+ ~"http://example.com",
begin
Hashed = couch_passwords:hash_admin_password(<<"test">>),
config:set("admins", "test", ?b2l(Hashed), false),
@@ -311,7 +311,7 @@ should_make_origin_request_with_auth(_, {_, _, Url,
DefaultHeaders}) ->
Url, DefaultHeaders, [{basic_auth, {"test", "test"}}]
),
config:delete("admins", "test", false),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
@@ -328,7 +328,7 @@ should_make_preflight_request_with_auth(_, {_, _, Url,
DefaultHeaders}) ->
Url, Headers, [{basic_auth, {"test", "test"}}]
),
config:delete("admins", "test", false),
- split_list(proplists:get_value("Access-Control-Allow-Methods",
Resp))
+ split_list(proplists:get_value(~"access-control-allow-methods",
Resp))
end
).
@@ -338,7 +338,7 @@ should_not_return_cors_headers_for_invalid_origin({Host,
_}) ->
begin
Headers = [{"Origin", "http://127.0.0.1"}],
{ok, _, Resp, _} = test_request:get(Host, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
@@ -351,7 +351,7 @@
should_not_return_cors_headers_for_invalid_origin_preflight({Host, _}) ->
{"Access-Control-Request-Method", "GET"}
],
{ok, _, Resp, _} = test_request:options(Host, Headers),
- proplists:get_value("Access-Control-Allow-Origin", Resp)
+ proplists:get_value(~"access-control-allow-origin", Resp)
end
).
@@ -407,7 +407,7 @@ should_make_request_with_if_none_match_header({Host,
DbName}) ->
Url ++ "/doc", [{"Content-Type", "application/json"}], "{}"
),
?assert(Code0 =:= 201),
- ETag = proplists:get_value("ETag", Headers0),
+ ETag = proplists:get_value(~"etag", Headers0),
{ok, Code, _, _} = test_request:get(
Url ++ "/doc", [
{"Origin", "http://example.com"},
@@ -427,5 +427,5 @@ split_list(S) ->
re:split(S, "\\s*,\\s*", [trim, {return, list}]).
list_simple_headers(Headers) ->
- LCHeaders = [string:to_lower(K) || {K, _V} <- Headers],
+ LCHeaders = [binary_to_list(K) || {K, _V} <- Headers],
lists:filter(fun(H) -> lists:member(H, ?SIMPLE_HEADERS) end, LCHeaders).
diff --git a/src/couch/test/eunit/couchdb_location_header_tests.erl
b/src/couch/test/eunit/couchdb_location_header_tests.erl
index 08870f8c8..320a88817 100644
--- a/src/couch/test/eunit/couchdb_location_header_tests.erl
+++ b/src/couch/test/eunit/couchdb_location_header_tests.erl
@@ -54,14 +54,14 @@ should_work_with_newlines_in_docs({Host, DbName}) ->
Url = Host ++ "/" ++ DbName ++ "/docid%0A",
{"COUCHDB-708",
?_assertEqual(
- Url,
+ list_to_binary(Url),
begin
{ok, _, Headers, _} = test_request:put(
Url,
[{"Content-Type", "application/json"}],
"{}"
),
- proplists:get_value("Location", Headers)
+ proplists:get_value(~"location", Headers)
end
)}.
@@ -70,7 +70,7 @@ should_work_with_newlines_in_attachments({Host, DbName}) ->
AttUrl = Url ++ "/docid%0A/readme.txt",
{"COUCHDB-708",
?_assertEqual(
- AttUrl,
+ list_to_binary(AttUrl),
begin
Body = "We all live in a yellow submarine!",
Headers0 = [
@@ -78,6 +78,6 @@ should_work_with_newlines_in_attachments({Host, DbName}) ->
{"Content-Type", "text/plain"}
],
{ok, _, Headers, _} = test_request:put(AttUrl, Headers0, Body),
- proplists:get_value("Location", Headers)
+ proplists:get_value(~"location", Headers)
end
)}.
diff --git a/src/couch/test/eunit/couchdb_mrview_cors_tests.erl
b/src/couch/test/eunit/couchdb_mrview_cors_tests.erl
index 9822542f3..64e5986c7 100644
--- a/src/couch/test/eunit/couchdb_mrview_cors_tests.erl
+++ b/src/couch/test/eunit/couchdb_mrview_cors_tests.erl
@@ -91,8 +91,8 @@ should_make_shows_request(_, {Host, DbName}) ->
?AUTH
],
{ok, _, Resp, Body} = test_request:get(ReqUrl, Headers),
- Origin = proplists:get_value("Access-Control-Allow-Origin", Resp),
- ?assertEqual("http://example.com", Origin),
+ Origin = proplists:get_value(~"access-control-allow-origin", Resp),
+ ?assertEqual(~"http://example.com", Origin),
?assertEqual(<<"<h1>wosh</h1>">>, Body)
end).
diff --git a/src/couch/test/eunit/same_site_cookie_tests.erl
b/src/couch/test/eunit/same_site_cookie_tests.erl
index 9ae56b27e..c0e4dc050 100644
--- a/src/couch/test/eunit/same_site_cookie_tests.erl
+++ b/src/couch/test/eunit/same_site_cookie_tests.erl
@@ -37,7 +37,7 @@ get_cookie(BaseUri, User, Password) ->
[{"Content-Type", "application/json"}],
couch_util:json_encode(#{username => ?l2b(User), password =>
?l2b(Password)})
),
- proplists:get_value("Set-Cookie", Headers).
+ proplists:get_value(~"set-cookie", Headers).
same_site_cookie_test_() ->
{
diff --git a/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl
b/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl
index 913b80834..1d4e44447 100644
--- a/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl
+++ b/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl
@@ -105,7 +105,7 @@ t_prometheus_port(_) ->
t_reject_prometheus_port(Port) ->
Response = test_request:get(node_local_url(Port), [?CONTENT_JSON, ?AUTH]),
- ?assertEqual({error, {conn_failed, {error, econnrefused}}}, Response).
+ ?assertEqual({error, econnrefused}, Response).
t_no_duplicate_metrics(Port) ->
Url = node_local_url(Port),
diff --git a/test/elixir/lib/couch.ex b/test/elixir/lib/couch.ex
index a119095a9..effaba054 100644
--- a/test/elixir/lib/couch.ex
+++ b/test/elixir/lib/couch.ex
@@ -42,9 +42,12 @@ defmodule Couch.Session do
# if the need arises.
def go(%Couch.Session{} = sess, method, url, opts) do
parse_response = Keyword.get(opts, :parse_response, true)
- opts = opts
- |> Keyword.merge(cookie: sess.cookie)
- |> Keyword.delete(:parse_response)
+
+ opts =
+ opts
+ |> Keyword.merge(cookie: sess.cookie)
+ |> Keyword.delete(:parse_response)
+
if parse_response do
Couch.request(method, url, opts)
else
@@ -54,9 +57,12 @@ defmodule Couch.Session do
def go!(%Couch.Session{} = sess, method, url, opts) do
parse_response = Keyword.get(opts, :parse_response, true)
- opts = opts
- |> Keyword.merge(cookie: sess.cookie)
- |> Keyword.delete(:parse_response)
+
+ opts =
+ opts
+ |> Keyword.merge(cookie: sess.cookie)
+ |> Keyword.delete(:parse_response)
+
if parse_response do
Couch.request!(method, url, opts)
else
@@ -66,112 +72,37 @@ defmodule Couch.Session do
end
defmodule Couch do
- use HTTPotion.Base
-
@moduledoc """
CouchDB library to power test suite.
"""
- # These constants are supplied to the underlying HTTP client and control
- # how long we will wait before timing out a test. The inactivity timeout
- # specifically fires during an active HTTP response and defaults to 10_000
- # if not specified. We're defining it to a different value than the
- # request_timeout largely just so we know which timeout fired.
- @request_timeout 60_000
- @inactivity_timeout 55_000
-
- def process_url("http://" <> _ = url) do
- url
- end
-
- def process_url(url) do
- base_url = System.get_env("EX_COUCH_URL") || "http://127.0.0.1:15984"
- base_url <> url
- end
-
- def process_request_headers(headers, _body, options) do
- headers = Keyword.put(headers, :"User-Agent", "couch-potion")
-
- headers =
- if headers[:"Content-Type"] do
- headers
- else
- Keyword.put(headers, :"Content-Type", "application/json")
- end
-
- case Keyword.get(options, :cookie) do
- nil ->
- headers
-
- cookie ->
- Keyword.put(headers, :Cookie, cookie)
- end
- end
-
- def process_options(options) do
- options
- |> set_auth_options()
- |> set_inactivity_timeout()
- |> set_request_timeout()
- end
+ defdelegate process_url(url), to: Couch.Http
- def process_request_body(body) do
- if is_map(body) do
- :jiffy.encode(body, [:use_nil])
- else
- body
- end
- end
+ def get(url, opts \\ []), do: request(:get, url, opts)
+ def get!(url, opts \\ []), do: request!(:get, url, opts)
+ def put(url, opts \\ []), do: request(:put, url, opts)
+ def put!(url, opts \\ []), do: request!(:put, url, opts)
+ def post(url, opts \\ []), do: request(:post, url, opts)
+ def post!(url, opts \\ []), do: request!(:post, url, opts)
+ def delete(url, opts \\ []), do: request(:delete, url, opts)
+ def delete!(url, opts \\ []), do: request!(:delete, url, opts)
+ def head(url, opts \\ []), do: request(:head, url, opts)
+ def head!(url, opts \\ []), do: request!(:head, url, opts)
- def process_response_body(_headers, body) when body == [] do
- ""
+ def request(method, url, opts \\ []) do
+ Couch.Http.request(method, url, opts, :json)
end
- def process_response_body(headers, body) do
- content_type = headers[:"Content-Type"]
-
- if !!content_type and String.match?(content_type, ~r/application\/json/) do
- body |> IO.iodata_to_binary() |> :jiffy.decode([:return_maps, :use_nil])
- else
- process_response_body(body)
- end
- end
+ def request!(method, url, opts \\ []) do
+ case request(method, url, opts) do
+ %Couch.ErrorResponse{message: message} ->
+ raise "HTTP request failed: #{method} #{url}: #{message}"
- def set_auth_options(options) do
- cond do
- Keyword.get(options, :no_auth, false) ->
- options
- Keyword.get(options, :cookie) == nil ->
- headers = Keyword.get(options, :headers, [])
- if headers[:basic_auth] != nil or headers[:authorization] != nil
- or List.keymember?(headers, :"X-Auth-CouchDB-UserName", 0) do
- options
- else
- username = System.get_env("EX_USERNAME") || "adm"
- password = System.get_env("EX_PASSWORD") || "pass"
- Keyword.put(options, :basic_auth, {username, password})
- end
- true ->
- options
+ resp ->
+ resp
end
end
- def set_inactivity_timeout(options) do
- Keyword.update(
- options,
- :ibrowse,
- [{:inactivity_timeout, @inactivity_timeout}],
- fn ibrowse ->
- Keyword.put_new(ibrowse, :inactivity_timeout, @inactivity_timeout)
- end
- )
- end
-
- def set_request_timeout(options) do
- timeout = Application.get_env(:httpotion, :default_timeout,
@request_timeout)
- Keyword.put_new(options, :timeout, timeout)
- end
-
def login(userinfo) do
[user, pass] = String.split(userinfo, ":", parts: 2)
login(user, pass)
@@ -182,7 +113,7 @@ defmodule Couch do
if expect == :success do
true = resp.body["ok"]
- cookie = resp.headers[:"set-cookie"]
+ cookie = resp.headers["set-cookie"]
[token | _] = String.split(cookie, ";")
%Couch.Session{cookie: token}
else
diff --git a/test/elixir/lib/couch/dbtest.ex b/test/elixir/lib/couch/dbtest.ex
index 693e6f0f3..d221151a3 100644
--- a/test/elixir/lib/couch/dbtest.ex
+++ b/test/elixir/lib/couch/dbtest.ex
@@ -63,7 +63,7 @@ defmodule Couch.DBTest do
on_exit(fn ->
query = %{:rev => user["_rev"]}
resp = Couch.delete("/_users/#{user["_id"]}", query: query)
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
end)
context = Map.put(context, :user, user)
@@ -159,7 +159,7 @@ defmodule Couch.DBTest do
end
resp = Couch.post("/_users", body: user_doc)
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["ok"]
Map.put(user_doc, "_rev", resp.body["rev"])
end
@@ -337,7 +337,7 @@ defmodule Couch.DBTest do
options = Map.put(options, :body, body)
resp = Couch.post("/_replicate", Enum.to_list(options))
- assert HTTPotion.Response.success?(resp), "#{inspect(resp)}"
+ assert Couch.Response.success?(resp), "#{inspect(resp)}"
resp.body
end
@@ -562,7 +562,7 @@ defmodule Couch.DBTest do
defp restart_node(node, port) do
url = "http://127.0.0.1:#{port}/_node/#{node}/_restart"
resp = Couch.post(url)
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["ok"]
# make sure node went down. we assuming the node can't bounce quick
# enough to inroduce a race here
@@ -575,7 +575,7 @@ defmodule Couch.DBTest do
url = "http://127.0.0.1:#{port}/_up"
resp = Couch.get(url)
- case HTTPotion.Response.success?(resp) do
+ case Couch.Response.success?(resp) do
true -> resp.status_code in 200..399
false -> false
end
@@ -584,7 +584,7 @@ defmodule Couch.DBTest do
defp node_to_port(node) do
url = "/_node/#{node}/_config/chttpd/port"
resp = Couch.get(url)
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
resp.body
end
end
diff --git a/test/elixir/lib/couch/http.ex b/test/elixir/lib/couch/http.ex
new file mode 100644
index 000000000..687afe99d
--- /dev/null
+++ b/test/elixir/lib/couch/http.ex
@@ -0,0 +1,465 @@
+defmodule Couch.Response do
+ @moduledoc """
+ Response to request
+ """
+ defstruct status_code: nil, headers: %{}, body: ""
+ def success?(%__MODULE__{status_code: code}), do: code in 200..299
+ def success?(_), do: false
+end
+
+defmodule Couch.ErrorResponse do
+ @moduledoc """
+ Error response
+ """
+ defstruct message: ""
+end
+
+defmodule Couch.AsyncResponse do
+ @moduledoc """
+ Response to streaming request iniated by the stream_to: pid option
+ """
+ defstruct [:id]
+end
+
+defmodule Couch.AsyncHeaders do
+ @moduledoc """
+ Streaming header response
+ """
+ defstruct [:id, :status_code, :headers]
+end
+
+defmodule Couch.AsyncChunk do
+ @moduledoc """
+ Streaming body chunk
+ """
+ defstruct [:id, :chunk]
+end
+
+defmodule Couch.AsyncEnd do
+ @moduledoc """
+ Stream end
+ """
+ defstruct [:id]
+end
+
+defmodule Couch.Http do
+ @moduledoc """
+
+ Small http client built on gun. It looks a bit odd because it's trying to
+ mimick the now removed httpotion client shape a bit. Some of the patterns
+ here are also copied from couch_gun.erl. The one differense if we keep a
+ connected process cached in the process dict to speed up test runs here.
+
+ Normal requests return Couch.Response | Couch.ErrorResponse results.
+
+ Response headers are a plain map with gun's lowercase binary header names
+ as keys; a repeated header collects its values into a list, in arrival
+ order.
+
+ Streaming requests should pass `stream_to: pid` as the option. Their response
+ will be Couch.AsyncResponse then followed by Couch.AsyncChunk messages and
+ finally Couch.AsyncEnd.
+
+ Some requests options are:
+ :body
+ :headers
+ :query
+ :timeout
+ :cookie
+ :no_auth
+ """
+
+ @request_timeout 60_000
+ @inactivity_timeout 55_000
+ @attempts 3
+
+ def base_url do
+ System.get_env("EX_COUCH_URL") || "http://127.0.0.1:15984"
+ end
+
+ def process_url("http://" <> _ = url), do: url
+ def process_url("https://" <> _ = url), do: url
+ def process_url(url), do: base_url() <> url
+
+ def request(method, url, options, body_mode) when body_mode in [:json, :raw]
do
+ ensure_gun_started()
+ url = url |> to_string() |> process_url()
+ url = append_query(url, Keyword.get(options, :query))
+ method = method |> to_string() |> String.upcase()
+ headers = build_headers(options)
+ body = encode_body(Keyword.get(options, :body, ""))
+
+ case Keyword.get(options, :stream_to) do
+ nil ->
+ sync_request(method, url, headers, body, options, body_mode)
+
+ target when is_pid(target) ->
+ async_request(method, url, headers, body, target)
+ end
+ end
+
+ defp sync_request(method, url, headers, body, options, body_mode) do
+ with {:ok, origin, path} <- parse_url(url),
+ {:ok, status, resp_headers, resp_body} <-
+ do_sync_request(origin, method, path, headers, body, options, 1) do
+ resp_headers = headers_map(resp_headers)
+
+ %Couch.Response{
+ status_code: status,
+ headers: resp_headers,
+ body: process_body(body_mode, resp_headers, resp_body)
+ }
+ else
+ {:error, reason} -> %Couch.ErrorResponse{message: error_message(reason)}
+ end
+ end
+
+ defp do_sync_request(origin, method, path, headers, body, options, attempt)
do
+ timeout = Keyword.get(options, :timeout, @request_timeout)
+ deadline = now_ms() + timeout
+ conn = cached_conn(origin)
+ stream = :gun.request(conn, method, path, headers, body, %{})
+ mref = Process.monitor(conn)
+ result = await_response(conn, stream, mref, deadline)
+ Process.demonitor(mref, [:flush])
+
+ case result do
+ {:ok, _status, resp_headers, _body} = ok ->
+ # If server closed the connection we drop it as well
+ if close_after?(resp_headers), do: invalidate(origin, conn)
+ ok
+
+ {:error, reason} ->
+ reason = norm_error(reason)
+ # Teardown the cached connection on error and start fresh
+ invalidate(origin, conn)
+
+ if conn_lost?(reason) and attempt < @attempts do
+ # Retry a few times on times or server start/stop race
+ do_sync_request(origin, method, path, headers, body, options,
attempt + 1)
+ else
+ {:error, reason}
+ end
+ end
+ end
+
+ defp await_response(conn, stream, mref, deadline) do
+ case gun_await(conn, stream, mref, deadline) do
+ {:inform, _status, _headers} ->
+ # These are 1xx bits and such and we don't care about them
+ await_response(conn, stream, mref, deadline)
+
+ {:response, :fin, status, headers} ->
+ {:ok, status, headers, ""}
+
+ {:response, :nofin, status, headers} ->
+ collect_body(conn, stream, mref, deadline, status, headers, [])
+
+ {:error, _} = error ->
+ error
+ end
+ end
+
+ defp collect_body(conn, stream, mref, deadline, status, headers, acc) do
+ case gun_await(conn, stream, mref, deadline) do
+ {:data, :nofin, data} ->
+ collect_body(conn, stream, mref, deadline, status, headers, [acc |
data])
+
+ {:data, :fin, data} ->
+ {:ok, status, headers, IO.iodata_to_binary([acc | data])}
+
+ {:trailers, _} ->
+ # We don't care about trailers
+ {:ok, status, headers, IO.iodata_to_binary(acc)}
+
+ {:error, _} = error ->
+ error
+ end
+ end
+
+ defp gun_await(conn, stream, mref, deadline) do
+ remaining = deadline - now_ms()
+
+ if remaining <= 0 do
+ {:error, :req_timedout}
+ else
+ :gun.await(conn, stream, min(remaining, @inactivity_timeout), mref)
+ end
+ end
+
+ # Streaming stuff
+ #
+ # A helper relay process opens the connection, makes the request forward
+ # response to the target as Couch.Async* messages.
+
+ defp async_request(method, url, headers, body, target) do
+ case parse_url(url) do
+ {:ok, origin, path} ->
+ relay = spawn(fn -> relay_init(target, origin, method, path, headers,
body) end)
+ %Couch.AsyncResponse{id: relay}
+
+ {:error, reason} ->
+ %Couch.ErrorResponse{message: error_message(reason)}
+ end
+ end
+
+ defp relay_init(target, origin, method, path, headers, body) do
+ tref = Process.monitor(target)
+ conn = open_conn(origin)
+ cref = Process.monitor(conn)
+ stream = :gun.request(conn, method, path, headers, body, %{})
+ relay_loop(%{target: target, conn: conn, stream: stream, tref: tref, cref:
cref})
+ end
+
+ defp relay_loop(state) do
+ %{target: target, conn: conn, stream: stream} = state
+
+ receive do
+ {:gun_inform, ^conn, ^stream, _status, _headers} ->
+ # skip 1xx stuff
+ relay_loop(state)
+
+ {:gun_response, ^conn, ^stream, fin, status, headers} ->
+ async_headers = %Couch.AsyncHeaders{
+ id: self(),
+ status_code: status,
+ headers: headers_map(headers)
+ }
+
+ send(target, async_headers)
+ if fin == :fin, do: relay_done(state), else: relay_loop(state)
+
+ {:gun_data, ^conn, ^stream, fin, data} ->
+ send(target, %Couch.AsyncChunk{id: self(), chunk: data})
+ if fin == :fin, do: relay_done(state), else: relay_loop(state)
+
+ {:gun_trailers, ^conn, ^stream, _trailers} ->
+ # don't care about trailers
+ relay_done(state)
+
+ {:gun_error, ^conn, ^stream, _reason} ->
+ relay_done(state)
+
+ {:gun_error, ^conn, _reason} ->
+ relay_done(state)
+
+ {:DOWN, mref, :process, _pid, _reason} ->
+ cond do
+ mref == state.cref ->
+ # connection died
+ send(target, %Couch.AsyncEnd{id: self()})
+ :ok
+
+ mref == state.tref ->
+ # target (test) process died, clean up
+ close_conn(conn)
+ :ok
+
+ true ->
+ relay_loop(state)
+ end
+ end
+ end
+
+ defp relay_done(state) do
+ send(state.target, %Couch.AsyncEnd{id: self()})
+ close_conn(state.conn)
+ :ok
+ end
+
+ # Connection handling. This works for the test with one
+ # test client and one server decently enough.
+ defp cached_conn(origin) do
+ key = {:couch_http_conn, origin}
+
+ case Process.get(key) do
+ pid when is_pid(pid) ->
+ if Process.alive?(pid) do
+ pid
+ else
+ Process.delete(key)
+ cached_conn(origin)
+ end
+
+ nil ->
+ conn = open_conn(origin)
+ Process.put(key, conn)
+ conn
+ end
+ end
+
+ defp invalidate(origin, conn) do
+ key = {:couch_http_conn, origin}
+ if Process.get(key) == conn, do: Process.delete(key)
+ close_conn(conn)
+ end
+
+ defp open_conn({transport, host, port}) do
+ host_chars = String.to_charlist(host)
+ # gun expects IP address tuples
+ host_addr =
+ case :inet.parse_strict_address(host_chars) do
+ {:ok, addr} -> addr
+ {:error, _} -> host_chars
+ end
+
+ opts = %{transport: transport, protocols: [:http], retry: 0}
+
+ opts =
+ case transport do
+ :tls -> Map.put(opts, :tls_opts, [{:verify, :verify_none}])
+ :tcp -> opts
+ end
+
+ {:ok, conn} = :gun.open(host_addr, port, opts)
+ conn
+ end
+
+ defp close_conn(conn) do
+ try do
+ :gun.close(conn)
+ catch
+ _, _ -> :ok
+ end
+ end
+
+ defp ensure_gun_started() do
+ case Process.get(:couch_http_gun_started) do
+ true ->
+ :ok
+
+ _ ->
+ {:ok, _} = Application.ensure_all_started(:gun)
+ Process.put(:couch_http_gun_started, true)
+ :ok
+ end
+ end
+
+ defp parse_url(url) do
+ case URI.parse(url) do
+ %URI{scheme: scheme, host: host} = uri
+ when scheme in ["http", "https"] and is_binary(host) and host != "" ->
+ transport = if scheme == "https", do: :tls, else: :tcp
+ path = uri.path || "/"
+ path = if uri.query, do: path <> "?" <> uri.query, else: path
+ {:ok, {transport, host, uri.port}, path}
+
+ _ ->
+ {:error, :invalid_uri}
+ end
+ end
+
+ defp append_query(url, query) when query == nil or query == [] or query ==
%{} do
+ url
+ end
+
+ defp append_query(url, query) do
+ sep = if String.contains?(url, "?"), do: "&", else: "?"
+ url <> sep <> URI.encode_query(query)
+ end
+
+ defp encode_body(nil), do: ""
+ defp encode_body(body) when is_map(body), do: :jiffy.encode(body, [:use_nil])
+ defp encode_body(body), do: body
+
+ defp build_headers(options) do
+ headers =
+ for {k, v} <- Keyword.get(options, :headers, []) do
+ {k |> to_string() |> String.downcase(), to_string(v)}
+ end
+
+ headers =
+ headers
+ |> put_new_header("user-agent", "couch-potion")
+ |> put_new_header("content-type", "application/json")
+
+ case Keyword.get(options, :cookie) do
+ nil -> set_auth(headers, options)
+ cookie -> put_new_header(headers, "cookie", cookie)
+ end
+ end
+
+ # Auth may come from the environtment test setup
+ defp set_auth(headers, options) do
+ conf_auth? =
+ List.keymember?(headers, "authorization", 0) or
+ List.keymember?(headers, "x-auth-couchdb-username", 0)
+
+ if Keyword.get(options, :no_auth, false) or conf_auth? do
+ headers
+ else
+ username = System.get_env("EX_USERNAME") || "adm"
+ password = System.get_env("EX_PASSWORD") || "pass"
+ credentials = Base.encode64("#{username}:#{password}")
+ [{"authorization", "Basic #{credentials}"} | headers]
+ end
+ end
+
+ defp put_new_header(headers, key, value) do
+ if List.keymember?(headers, key, 0) do
+ headers
+ else
+ [{key, value} | headers]
+ end
+ end
+
+ # Response stuff
+
+ defp process_body(:raw, _headers, body), do: body
+
+ defp process_body(:json, headers, body) do
+ content_type = headers["content-type"]
+
+ json? =
+ is_binary(content_type) and
+ String.match?(content_type, ~r/application\/json/)
+
+ if json? and body != "" do
+ :jiffy.decode(body, [:return_maps, :use_nil])
+ else
+ body
+ end
+ end
+
+ # Errors
+
+ defp headers_map(headers) when is_list(headers) do
+ Enum.reduce(headers, %{}, fn {name, value}, acc ->
+ Map.update(acc, name, value, fn
+ values when is_list(values) -> values ++ [value]
+ value0 -> [value0, value]
+ end)
+ end)
+ end
+
+ defp norm_error({:stream_error, reason}), do: norm_error(reason)
+ defp norm_error({:connection_error, reason}), do: norm_error(reason)
+ defp norm_error({:down, {:shutdown, reason}}), do: norm_error(reason)
+ defp norm_error({:down, reason}), do: norm_error(reason)
+ defp norm_error({:shutdown, reason}), do: norm_error(reason)
+ defp norm_error(reason), do: reason
+
+ defp conn_lost?(:closed), do: true
+ defp conn_lost?({:closed, _}), do: true
+ defp conn_lost?(:normal), do: true
+ defp conn_lost?(:shutdown), do: true
+ defp conn_lost?(:noproc), do: true
+ defp conn_lost?(:einval), do: true
+ defp conn_lost?(:socket_closed_remotely), do: true
+ defp conn_lost?(_), do: false
+
+ defp close_after?(headers) do
+ case List.keyfind(headers, "connection", 0) do
+ {_, value} -> String.downcase(value) == "close"
+ nil -> false
+ end
+ end
+
+ defp error_message(:timeout), do: "req_timedout"
+ defp error_message(:req_timedout), do: "req_timedout"
+ defp error_message(reason) when is_atom(reason), do: Atom.to_string(reason)
+ defp error_message(reason), do: inspect(reason)
+
+ # Helpers
+ defp now_ms(), do: System.monotonic_time(:millisecond)
+end
diff --git a/test/elixir/lib/couch_raw.ex b/test/elixir/lib/couch_raw.ex
index 641612c9c..afe5f85b9 100644
--- a/test/elixir/lib/couch_raw.ex
+++ b/test/elixir/lib/couch_raw.ex
@@ -1,105 +1,32 @@
defmodule Rawresp do
- use HTTPotion.Base
-
@moduledoc """
- HTTP client that provides raw response as result
+ HTTP client that provides raw response as result. Same as `Couch` but
+ response bodies are returned as-is, without JSON decoding.
"""
- @request_timeout 60_000
- @inactivity_timeout 55_000
-
- def process_url("http://" <> _ = url) do
- url
- end
-
- def process_url(url) do
- base_url = System.get_env("EX_COUCH_URL") || "http://127.0.0.1:15984"
- base_url <> url
- end
-
- def process_request_headers(headers, _body, options) do
- headers =
- headers
- |> Keyword.put(:"User-Agent", "couch-potion")
-
- headers =
- if headers[:"Content-Type"] do
- headers
- else
- Keyword.put(headers, :"Content-Type", "application/json")
- end
-
- case Keyword.get(options, :cookie) do
- nil ->
- headers
-
- cookie ->
- Keyword.put(headers, :Cookie, cookie)
- end
- end
-
- def process_options(options) do
- options
- |> set_auth_options()
- |> set_inactivity_timeout()
- |> set_request_timeout()
- end
-
- def process_request_body(body) do
- if is_map(body) do
- :jiffy.encode(body, [:use_nil])
- else
- body
- end
- end
-
- def set_auth_options(options) do
- if Keyword.get(options, :cookie) == nil do
- headers = Keyword.get(options, :headers, [])
-
- if headers[:basic_auth] != nil or headers[:authorization] != nil do
- options
- else
- username = System.get_env("EX_USERNAME") || "adm"
- password = System.get_env("EX_PASSWORD") || "pass"
- Keyword.put(options, :basic_auth, {username, password})
- end
- else
- options
- end
- end
-
- def set_inactivity_timeout(options) do
- Keyword.update(
- options,
- :ibrowse,
- [{:inactivity_timeout, @inactivity_timeout}],
- fn ibrowse ->
- Keyword.put_new(ibrowse, :inactivity_timeout, @inactivity_timeout)
- end
- )
- end
-
- def set_request_timeout(options) do
- timeout = Application.get_env(:httpotion, :default_timeout,
@request_timeout)
- Keyword.put_new(options, :timeout, timeout)
- end
-
- def login(userinfo) do
- [user, pass] = String.split(userinfo, ":", parts: 2)
- login(user, pass)
- end
-
- def login(user, pass, expect \\ :success) do
- resp = Couch.post("/_session", body: %{:username => user, :password =>
pass})
- if expect == :success do
- true = resp.body["ok"]
- cookie = resp.headers[:"set-cookie"]
- [token | _] = String.split(cookie, ";")
- %Couch.Session{cookie: token}
- else
- true = Map.has_key?(resp.body, "error")
- %Couch.Session{error: resp.body["error"]}
+ def get(url, opts \\ []), do: request(:get, url, opts)
+ def get!(url, opts \\ []), do: request!(:get, url, opts)
+ def put(url, opts \\ []), do: request(:put, url, opts)
+ def put!(url, opts \\ []), do: request!(:put, url, opts)
+ def post(url, opts \\ []), do: request(:post, url, opts)
+ def post!(url, opts \\ []), do: request!(:post, url, opts)
+ def delete(url, opts \\ []), do: request(:delete, url, opts)
+ def delete!(url, opts \\ []), do: request!(:delete, url, opts)
+ def head(url, opts \\ []), do: request(:head, url, opts)
+ def head!(url, opts \\ []), do: request!(:head, url, opts)
+ def options(url, opts \\ []), do: request(:options, url, opts)
+
+ def request(method, url, opts \\ []) do
+ Couch.Http.request(method, url, opts, :raw)
+ end
+
+ def request!(method, url, opts \\ []) do
+ case request(method, url, opts) do
+ %Couch.ErrorResponse{message: message} ->
+ raise "HTTP request failed: #{method} #{url}: #{message}"
+
+ resp ->
+ resp
end
end
end
diff --git a/test/elixir/lib/step/start.ex b/test/elixir/lib/step/start.ex
index b86b14a4f..1f1ca2ac9 100644
--- a/test/elixir/lib/step/start.ex
+++ b/test/elixir/lib/step/start.ex
@@ -4,7 +4,7 @@ defmodule Couch.Test.Setup.Step.Start do
list of applications from DEFAULT_APPS macro defined in `test_util.erl`.
At the time of writing this list included:
- inets
- - ibrowse
+ - gun
- ssl
- config
- couch_epi
diff --git a/test/elixir/test/attachment_names_test.exs
b/test/elixir/test/attachment_names_test.exs
index a89b26548..af142998d 100644
--- a/test/elixir/test/attachment_names_test.exs
+++ b/test/elixir/test/attachment_names_test.exs
@@ -54,8 +54,8 @@ defmodule AttachmentNamesTest do
resp = Couch.get("/#{db_name}/good_doc/#{filename}")
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "application/octet-stream"
- assert resp.headers["Etag"] == ~s("aEI7pOYCRBLTRQvvqYrrJQ==")
+ assert resp.headers["content-type"] == "application/octet-stream"
+ assert resp.headers["etag"] == ~s("aEI7pOYCRBLTRQvvqYrrJQ==")
resp = Couch.post("/#{db_name}", body: @bin_att_doc)
assert(resp.status_code == 201)
diff --git a/test/elixir/test/attachment_paths_test.exs
b/test/elixir/test/attachment_paths_test.exs
index b776feabf..a32d743a5 100644
--- a/test/elixir/test/attachment_paths_test.exs
+++ b/test/elixir/test/attachment_paths_test.exs
@@ -60,12 +60,12 @@ defmodule AttachmentPathsTest do
resp = Couch.get("/#{db_name}/bin_doc/foo/bar.txt")
assert resp.status_code == 200
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
resp = Couch.get("/#{db_name}/bin_doc/foo%2Fbar.txt")
assert resp.status_code == 200
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
resp = Couch.get("/#{db_name}/bin_doc/foo/baz.txt")
assert resp.status_code == 404
@@ -126,12 +126,12 @@ defmodule AttachmentPathsTest do
resp = Couch.get("/#{db_name}/_design/bin_doc/foo/bar.txt")
assert resp.status_code == 200
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
resp = Couch.get("/#{db_name}/_design/bin_doc/foo%2Fbar.txt")
assert resp.status_code == 200
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
resp = Couch.get("/#{db_name}/_design/bin_doc/foo/baz.txt")
assert resp.status_code == 404
diff --git a/test/elixir/test/attachment_ranges_test.exs
b/test/elixir/test/attachment_ranges_test.exs
index 01c1239bc..73715d19e 100644
--- a/test/elixir/test/attachment_ranges_test.exs
+++ b/test/elixir/test/attachment_ranges_test.exs
@@ -32,8 +32,8 @@ defmodule AttachmentRangesTest do
assert(resp.status_code == 206)
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Range"] == "bytes 0-28/29"
- assert resp.headers["Content-Length"] == "29"
+ assert resp.headers["content-range"] == "bytes 0-28/29"
+ assert resp.headers["content-length"] == "29"
# Fetch the whole entity without an end offset is a 200
resp =
@@ -44,8 +44,8 @@ defmodule AttachmentRangesTest do
assert(resp.status_code == 200)
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Range"] == nil
- assert resp.headers["Content-Length"] == "29"
+ assert resp.headers["content-range"] == nil
+ assert resp.headers["content-length"] == "29"
# Even if you ask multiple times.
resp =
@@ -74,8 +74,8 @@ defmodule AttachmentRangesTest do
assert(resp.status_code == 206)
assert resp.body == "is is a base64 encoded text"
- assert resp.headers["Content-Range"] == "bytes 2-28/29"
- assert resp.headers["Content-Length"] == "27"
+ assert resp.headers["content-range"] == "bytes 2-28/29"
+ assert resp.headers["content-length"] == "27"
# Fetch first part of entity is a 206
resp =
@@ -86,8 +86,8 @@ defmodule AttachmentRangesTest do
assert(resp.status_code == 206)
assert resp.body == "This"
- assert resp.headers["Content-Range"] == "bytes 0-3/29"
- assert resp.headers["Content-Length"] == "4"
+ assert resp.headers["content-range"] == "bytes 0-3/29"
+ assert resp.headers["content-length"] == "4"
# Fetch middle of entity is also a 206
resp =
@@ -98,8 +98,8 @@ defmodule AttachmentRangesTest do
assert(resp.status_code == 206)
assert resp.body == "base64"
- assert resp.headers["Content-Range"] == "bytes 10-15/29"
- assert resp.headers["Content-Length"] == "6"
+ assert resp.headers["content-range"] == "bytes 10-15/29"
+ assert resp.headers["content-length"] == "6"
# Fetch end of entity is also a 206
resp =
@@ -110,8 +110,8 @@ defmodule AttachmentRangesTest do
assert(resp.status_code == 206)
assert resp.body == "ext"
- assert resp.headers["Content-Range"] == "bytes 26-28/29"
- assert resp.headers["Content-Length"] == "3"
+ assert resp.headers["content-range"] == "bytes 26-28/29"
+ assert resp.headers["content-length"] == "3"
# backward range is 416
resp =
diff --git a/test/elixir/test/attachments_multipart_test.exs
b/test/elixir/test/attachments_multipart_test.exs
index f940bae10..a17ef0d43 100644
--- a/test/elixir/test/attachments_multipart_test.exs
+++ b/test/elixir/test/attachments_multipart_test.exs
@@ -131,7 +131,7 @@ defmodule AttachmentMultipartTest do
)
assert resp.status_code == 200
- assert resp.headers["Content-length"] == "790"
+ assert resp.headers["content-length"] == "790"
# parse out the multipart
sections = parse_multipart(resp)
@@ -139,17 +139,17 @@ defmodule AttachmentMultipartTest do
# The first section is the json doc. Check it's content-type.
# Each part carries their own meta data.
- assert Enum.at(sections, 0).headers["Content-Type"] == "application/json"
- assert Enum.at(sections, 1).headers["Content-Type"] == "application/test"
- assert Enum.at(sections, 2).headers["Content-Type"] == "application/test"
+ assert Enum.at(sections, 0).headers["content-type"] == "application/json"
+ assert Enum.at(sections, 1).headers["content-type"] == "application/test"
+ assert Enum.at(sections, 2).headers["content-type"] == "application/test"
- assert Enum.at(sections, 1).headers["Content-Length"] == "21"
- assert Enum.at(sections, 2).headers["Content-Length"] == "18"
+ assert Enum.at(sections, 1).headers["content-length"] == "21"
+ assert Enum.at(sections, 2).headers["content-length"] == "18"
- assert Enum.at(sections, 1).headers["Content-Disposition"] ==
+ assert Enum.at(sections, 1).headers["content-disposition"] ==
~s(attachment; filename="foo.txt")
- assert Enum.at(sections, 2).headers["Content-Disposition"] ==
+ assert Enum.at(sections, 2).headers["content-disposition"] ==
~s(attachment; filename="bar.txt")
doc = :jiffy.decode(Enum.at(sections, 0).body, [:return_maps, :use_nil])
@@ -198,13 +198,13 @@ defmodule AttachmentMultipartTest do
# 1 section, with a multipart/related Content-Type
assert length(sections) == 1
- ctype_value = Enum.at(sections, 0).headers["Content-Type"]
+ ctype_value = Enum.at(sections, 0).headers["content-type"]
assert String.starts_with?(ctype_value, "multipart/related;") == true
inner_sections = parse_multipart(Enum.at(sections, 0))
# 2 inner sections: a document body section plus an attachment data section
assert length(inner_sections) == 2
- assert Enum.at(inner_sections, 0).headers["Content-Type"] ==
"application/json"
+ assert Enum.at(inner_sections, 0).headers["content-type"] ==
"application/json"
doc = :jiffy.decode(Enum.at(inner_sections, 0).body, [:return_maps,
:use_nil])
assert doc["_attachments"]["foo.txt"]["stub"] == true
@@ -265,6 +265,7 @@ defmodule AttachmentMultipartTest do
att_data = String.duplicate("x", 100_000)
att_len = byte_size(att_data)
+
document = """
{
"body": "This is a body.",
@@ -312,14 +313,11 @@ defmodule AttachmentMultipartTest do
end
defp put_multipart_new_edits_false(db_name, rev, multipart_data) do
- # Help ensure we're re-using client connections
- ibrowse_opts = [{:max_sessions, 1}, {:max_pipeline_size, 1}]
resp =
Couch.put(
"/#{db_name}/multipart_replicated_changes?new_edits=false&rev=#{rev}",
body: multipart_data,
- headers: ["Content-Type": "multipart/related;boundary=\"abc123\""],
- ibrowse: ibrowse_opts
+ headers: ["Content-Type": "multipart/related;boundary=\"abc123\""]
)
assert resp.status_code in [201, 202]
@@ -369,13 +367,13 @@ defmodule AttachmentMultipartTest do
sections = parse_multipart(resp)
# 1 section, with a multipart/related Content-Type
assert length(sections) == 1
- ctype_value = Enum.at(sections, 0).headers["Content-Type"]
+ ctype_value = Enum.at(sections, 0).headers["content-type"]
assert String.starts_with?(ctype_value, "multipart/related;") == true
inner_sections = parse_multipart(Enum.at(sections, 0))
# 3 inner sections: a document body section plus 2 attachment data sections
assert length(inner_sections) == 3
- assert Enum.at(inner_sections, 0).headers["Content-Type"] ==
"application/json"
+ assert Enum.at(inner_sections, 0).headers["content-type"] ==
"application/json"
doc = :jiffy.decode(Enum.at(inner_sections, 0).body, [:return_maps,
:use_nil])
assert doc["_attachments"]["lorem.txt"]["follows"] == true
@@ -406,13 +404,13 @@ defmodule AttachmentMultipartTest do
# 1 section, with a multipart/related Content-Type
assert length(sections) == 1
- ctype_value = Enum.at(sections, 0).headers["Content-Type"]
+ ctype_value = Enum.at(sections, 0).headers["content-type"]
assert String.starts_with?(ctype_value, "multipart/related;") == true
inner_sections = parse_multipart(Enum.at(sections, 0))
# 2 inner sections: a document body section plus 1 attachment data section
assert length(inner_sections) == 2
- assert Enum.at(inner_sections, 0).headers["Content-Type"] ==
"application/json"
+ assert Enum.at(inner_sections, 0).headers["content-type"] ==
"application/json"
doc = :jiffy.decode(Enum.at(inner_sections, 0).body, [:return_maps,
:use_nil])
assert doc["_attachments"]["lorem.txt"]["follows"] == true
assert doc["_attachments"]["lorem.txt"]["encoding"] == "gzip"
@@ -420,7 +418,7 @@ defmodule AttachmentMultipartTest do
end
def get_boundary(response) do
- ctype = response.headers["Content-Type"]
+ ctype = response.headers["content-type"]
ctype_args = String.split(ctype, "; ")
ctype_args = Enum.slice(ctype_args, 1, length(ctype_args))
@@ -459,7 +457,12 @@ defmodule AttachmentMultipartTest do
Enum.reduce(raw_headers, %{}, fn raw_header, acc ->
if raw_header != "" do
header_parts = String.split(raw_header, ": ")
- Map.put(acc, Enum.at(header_parts, 0), Enum.at(header_parts, 1))
+
+ Map.put(
+ acc,
+ String.downcase(Enum.at(header_parts, 0)),
+ Enum.at(header_parts, 1)
+ )
else
acc
end
diff --git a/test/elixir/test/attachments_test.exs
b/test/elixir/test/attachments_test.exs
index 2d3251b19..27457a717 100644
--- a/test/elixir/test/attachments_test.exs
+++ b/test/elixir/test/attachments_test.exs
@@ -58,8 +58,8 @@ defmodule AttachmentsTest do
resp = Couch.get("/#{db_name}/bin_doc/foo.txt", body: @bin_att_doc)
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "application/octet-stream"
- assert resp.headers["Etag"] == "\"aEI7pOYCRBLTRQvvqYrrJQ==\""
+ assert resp.headers["content-type"] == "application/octet-stream"
+ assert resp.headers["etag"] == "\"aEI7pOYCRBLTRQvvqYrrJQ==\""
end
@tag :with_db
@@ -82,7 +82,7 @@ defmodule AttachmentsTest do
resp = Couch.get("/#{db_name}/bin_doc2/foo.txt")
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
assert resp.body == ""
resp =
@@ -124,17 +124,23 @@ defmodule AttachmentsTest do
end
@tag :with_db
- test "delete attachment request with a payload should not block following
requests", context do
+ test "delete attachment request with a payload should not block following
requests",
+ context do
db_name = context[:db_name]
resp = Couch.put("/#{db_name}/bin_doc", body: @bin_att_doc, query: %{w: 3})
assert resp.status_code in [201, 202]
rev = resp.body["rev"]
- resp = Couch.delete("/#{db_name}/bin_doc/foo.txt", body: "some payload",
query: %{w: 3, rev: rev}, ibrowse: [{:max_sessions, 1}, {:max_pipeline_size,
1}])
+ resp =
+ Couch.delete("/#{db_name}/bin_doc/foo.txt",
+ body: "some payload",
+ query: %{w: 3, rev: rev}
+ )
+
assert resp.status_code == 200
- resp = Couch.get("/", timeout: 1000, ibrowse: [{:max_sessions, 1},
{:max_pipeline_size, 1}])
+ resp = Couch.get("/", timeout: 1000)
assert resp.status_code == 200
end
@@ -190,11 +196,11 @@ defmodule AttachmentsTest do
rev = resp.body["rev"]
resp = Couch.get("/#{db_name}/bin_doc3/attachment.txt")
- assert String.downcase(resp.headers["Content-Type"]) ==
"text/plain;charset=utf-8"
+ assert String.downcase(resp.headers["content-type"]) ==
"text/plain;charset=utf-8"
assert resp.body == bin_data
resp = Couch.get("/#{db_name}/bin_doc3/attachment.txt", query: %{rev: rev})
- assert String.downcase(resp.headers["Content-Type"]) ==
"text/plain;charset=utf-8"
+ assert String.downcase(resp.headers["content-type"]) ==
"text/plain;charset=utf-8"
assert resp.body == bin_data
resp = Couch.delete("/#{db_name}/bin_doc3/attachment.txt", query: %{rev:
rev, w: 3})
@@ -204,7 +210,7 @@ defmodule AttachmentsTest do
assert resp.status_code == 404
resp = Couch.get("/#{db_name}/bin_doc3/attachment.txt", query: %{rev: rev})
- assert String.downcase(resp.headers["Content-Type"]) ==
"text/plain;charset=utf-8"
+ assert String.downcase(resp.headers["content-type"]) ==
"text/plain;charset=utf-8"
assert resp.body == bin_data
end
@@ -265,7 +271,7 @@ defmodule AttachmentsTest do
assert resp.body["ok"]
resp = Couch.get("/#{db_name}/bin_doc5/attachment.txt")
- assert String.downcase(resp.headers["Content-Type"]) ==
"text/plain;charset=utf-8"
+ assert String.downcase(resp.headers["content-type"]) ==
"text/plain;charset=utf-8"
assert resp.body == large_att
lorem_b64 =
diff --git a/test/elixir/test/basics_test.exs b/test/elixir/test/basics_test.exs
index f685f4b52..af10aa3a9 100644
--- a/test/elixir/test/basics_test.exs
+++ b/test/elixir/test/basics_test.exs
@@ -65,7 +65,7 @@ defmodule BasicsTest do
assert db_count > 0
assert Couch.get("/_all_dbs?limit=0").body == []
assert not Enum.empty?(Couch.get("/_all_dbs?limit=1").body)
- assert length(Couch.get("/_all_dbs?skip=1").body) == (db_count - 1)
+ assert length(Couch.get("/_all_dbs?skip=1").body) == db_count - 1
assert [db] == Couch.get("/_all_dbs?start_key=\"#{db}\"&limit=1").body
end
@@ -158,7 +158,7 @@ defmodule BasicsTest do
etag = ~s("#{resp.body["rev"]}")
resp = Couch.get("/#{db_name}/foo", headers: ["If-None-Match": etag])
assert resp.status_code == 304, "Should be 304 Not Modified"
- assert resp.headers[:"Content-Length"] == "0", "Should have zero content
length"
+ assert resp.headers["content-length"] == "0", "Should have zero content
length"
assert resp.body == "", "Should have an empty body"
end
@@ -267,7 +267,7 @@ defmodule BasicsTest do
db_name = context[:db_name]
resp = Couch.post("/#{db_name}", body: %{:foo => :bar})
assert resp.body["ok"]
- loc = resp.headers["Location"]
+ loc = resp.headers["location"]
assert loc, "should have a Location header"
locs = Enum.reverse(String.split(loc, "/"))
assert hd(locs) == resp.body["id"]
@@ -378,15 +378,15 @@ defmodule BasicsTest do
post_response = Couch.post("/#{db_name}", body: %{:foo => :bar})
id = post_response.body["id"]
head_response = Couch.head("/#{db_name}/#{id}?open_revs=all")
- assert head_response.headers["X-Couch-Request-ID"]
- assert head_response.headers["X-CouchDB-Body-Time"]
+ assert head_response.headers["x-couch-request-id"]
+ assert head_response.headers["x-couchdb-body-time"]
end
@tag :with_db
test "request ID can be specified at the client", _context do
uuid = "E7498DE1-B661-42FA-943D-17F890143068"
resp = Couch.get("/", headers: ["X-Couch-Request-ID": uuid])
- assert resp.headers["X-Couch-Request-ID"] == uuid
+ assert resp.headers["x-couch-request-id"] == uuid
end
test "_all_dbs/_all_docs is not found", _context do
@@ -400,5 +400,4 @@ defmodule BasicsTest do
assert resp.status_code == 404
assert resp.body["error"] == "not_found"
end
-
end
diff --git a/test/elixir/test/changes_async_test.exs
b/test/elixir/test/changes_async_test.exs
index 75362d8a9..97a4b2ebb 100644
--- a/test/elixir/test/changes_async_test.exs
+++ b/test/elixir/test/changes_async_test.exs
@@ -38,12 +38,10 @@ defmodule ChangesAsyncTest do
assert last_seq_prefix == "1-", "seq must start with 1-"
last_seq = changes["last_seq"]
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
req_id =
Couch.get("/#{db_name}/_changes?feed=longpoll&since=#{last_seq}",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
@@ -60,8 +58,7 @@ defmodule ChangesAsyncTest do
req_id =
Couch.get("/#{db_name}/_changes?feed=longpoll&since=now",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
@@ -82,12 +79,10 @@ defmodule ChangesAsyncTest do
check_empty_db(db_name)
create_doc(db_name, sample_doc_foo())
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
req_id =
Rawresp.get("/#{db_name}/_changes?feed=eventsource&timeout=500",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
@@ -99,8 +94,6 @@ defmodule ChangesAsyncTest do
assert length(changes) == 2
assert Enum.at(changes, 0)["id"] == "foo"
assert Enum.at(changes, 1)["id"] == "bar"
-
- HTTPotion.stop_worker_process(worker_pid)
end
@tag :with_db
@@ -110,12 +103,10 @@ defmodule ChangesAsyncTest do
check_empty_db(db_name)
create_doc(db_name, sample_doc_foo())
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
req_id =
Rawresp.get("/#{db_name}/_changes?feed=eventsource&limit=1",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
@@ -125,8 +116,6 @@ defmodule ChangesAsyncTest do
changes = process_response(req_id.id, &parse_event/1)
assert length(changes) == 1
assert Enum.at(changes, 0)["id"] == "foo"
-
- HTTPotion.stop_worker_process(worker_pid)
end
@tag :with_db
@@ -136,12 +125,10 @@ defmodule ChangesAsyncTest do
check_empty_db(db_name)
create_doc(db_name, sample_doc_foo())
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
req_id =
Rawresp.get("/#{db_name}/_changes?feed=eventsource&limit=2",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
@@ -152,8 +139,6 @@ defmodule ChangesAsyncTest do
assert length(changes) == 2
assert Enum.at(changes, 0)["id"] == "foo"
assert Enum.at(changes, 1)["id"] == "bar"
-
- HTTPotion.stop_worker_process(worker_pid)
end
@tag :with_db
@@ -166,12 +151,9 @@ defmodule ChangesAsyncTest do
t0 = :erlang.monotonic_time(:millisecond)
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
-
req_id =
Rawresp.get("/#{db_name}/_changes?feed=eventsource&timeout=1100&limit=2",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
changes = process_response(req_id.id, &parse_event/1, 5000)
@@ -181,8 +163,6 @@ defmodule ChangesAsyncTest do
assert length(changes) == 1
assert Enum.at(changes, 0)["id"] == "foo"
assert dt_msec > 1000
-
- HTTPotion.stop_worker_process(worker_pid)
end
@tag :with_db
@@ -197,34 +177,31 @@ defmodule ChangesAsyncTest do
lines = String.split(resp.body, "\n")
- all_lines = lines
- |> Enum.map(fn p -> Enum.at(String.split(p, ":"), 0) end)
+ all_lines =
+ lines
+ |> Enum.map(fn p -> Enum.at(String.split(p, ":"), 0) end)
allowed = ["", "data", "id", "event"]
- allowed_lines = all_lines
- |> Enum.filter(fn p -> Enum.member?(allowed, p) end)
+ allowed_lines =
+ all_lines
+ |> Enum.filter(fn p -> Enum.member?(allowed, p) end)
assert length(all_lines) == length(allowed_lines)
-
end
@tag :with_db
test "eventsource heartbeat", context do
db_name = context[:db_name]
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
-
req_id =
Rawresp.get("/#{db_name}/_changes?feed=eventsource&heartbeat=10",
- stream_to: {self(), :once},
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
beats = wait_for_heartbeats(req_id.id, 0, 3)
assert beats == 3
- HTTPotion.stop_worker_process(worker_pid)
end
@tag :with_db
@@ -247,13 +224,11 @@ defmodule ChangesAsyncTest do
last_seq = changes["last_seq"]
# longpoll waits until a matching change before returning
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
req_id =
Couch.get(
"/#{db_name}/_changes?feed=longpoll&filter=changes_filter/bop&since=#{last_seq}",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
@@ -279,20 +254,17 @@ defmodule ChangesAsyncTest do
create_doc(db_name, %{bop: false})
create_doc(db_name, %{_id: "bingo", bop: "bingo"})
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
-
req_id =
Rawresp.get(
"/#{db_name}/_changes?feed=continuous&filter=changes_filter/bop&timeout=500",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
create_doc(db_name, %{_id: "rusty", bop: "plankton"})
retry_until(fn ->
- changes = process_response(req_id.id, &parse_changes_line_chunk/1)
+ changes = process_response(req_id.id, &parse_changes_line/1)
changes_ids =
changes
@@ -300,8 +272,8 @@ defmodule ChangesAsyncTest do
|> Enum.map(fn p -> p["id"] end)
Enum.member?(changes_ids, "bingo") and
- Enum.member?(changes_ids, "rusty") and
- length(changes_ids) == 2
+ Enum.member?(changes_ids, "rusty") and
+ length(changes_ids) == 2
end)
end
@@ -313,21 +285,18 @@ defmodule ChangesAsyncTest do
create_doc(db_name, %{_id: "doc1", value: 1})
create_doc(db_name, %{_id: "doc2", value: 2})
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
-
req_id =
Rawresp.post(
"/#{db_name}/_changes?feed=continuous&timeout=500&filter=_doc_ids",
body: doc_ids,
headers: ["Content-Type": "application/json"],
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id.id, 200)
create_doc(db_name, %{_id: "doc3", value: 3})
- changes = process_response(req_id.id, &parse_changes_line_chunk/1)
+ changes = process_response(req_id.id, &parse_changes_line/1)
changes_ids =
changes
@@ -352,15 +321,12 @@ defmodule ChangesAsyncTest do
assert length(resp.body["results"]) == 4
seq = Enum.at(resp.body["results"], 1)["seq"]
- {:ok, worker_pid} =
HTTPotion.spawn_link_worker_process(Couch.process_url(""))
-
# simulate an EventSource request with a Last-Event-ID header
req_id =
Rawresp.get(
"/#{db_name}/_changes?feed=eventsource&timeout=100&since=0",
headers: [Accept: "text/event-stream", "Last-Event-ID": seq],
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
changes = process_response(req_id.id, &parse_event/1)
@@ -369,13 +335,16 @@ defmodule ChangesAsyncTest do
defp wait_for_heartbeats(id, beats, expexted_beats) do
if beats < expexted_beats do
- :ibrowse.stream_next(id)
- is_heartbeat = process_response(id, &parse_heartbeat/1)
-
- case is_heartbeat do
- :heartbeat -> wait_for_heartbeats(id, beats + 1, expexted_beats)
- :timeout -> beats
- _ -> wait_for_heartbeats(id, beats, expexted_beats)
+ case next_chunk(id) do
+ :timeout ->
+ beats
+
+ chunk ->
+ if Regex.match?(~r/event: heartbeat/, chunk) do
+ wait_for_heartbeats(id, beats + 1, expexted_beats)
+ else
+ wait_for_heartbeats(id, beats, expexted_beats)
+ end
end
else
beats
@@ -384,7 +353,7 @@ defmodule ChangesAsyncTest do
defp wait_for_headers(id, status, timeout \\ 1000) do
receive do
- %HTTPotion.AsyncHeaders{id: ^id, status_code: ^status} ->
+ %Couch.AsyncHeaders{id: ^id, status_code: ^status} ->
:ok
_ ->
@@ -394,24 +363,51 @@ defmodule ChangesAsyncTest do
end
end
- defp process_response(id, chunk_parser, timeout \\ 3000) do
+ # Gather response until stream ends and also handles timeouts as we'd expect
+ # them in the _changes feeds responses normally and we have tests for those
+ defp process_response(id, parser, timeout \\ 3000) do
+ acc = Process.delete({:chunk_acc, id}) || []
+ case gather_response(id, acc, timeout) do
+ {:done, body} ->
+ parser.(body)
+ {:timeout, []} ->
+ :timeout
+ {:timeout, acc} ->
+ Process.put({:chunk_acc, id}, acc)
+ :timeout
+ end
+ end
+
+ defp gather_response(id, acc, timeout) do
receive do
- %HTTPotion.AsyncChunk{id: ^id} = msg ->
- chunk_parser.(msg)
+ %Couch.AsyncChunk{id: ^id, chunk: chunk} ->
+ gather_response(id, [acc | chunk], timeout)
+ %Couch.AsyncEnd{id: ^id} ->
+ if acc == [], do: {:timeout, []}, else: {:done,
IO.iodata_to_binary(acc)}
+ _ ->
+ gather_response(id, acc, timeout)
+ after
+ timeout -> {:timeout, acc}
+ end
+ end
+ defp next_chunk(id, timeout \\ 3000) do
+ receive do
+ %Couch.AsyncChunk{id: ^id, chunk: chunk} ->
+ chunk
_ ->
- process_response(id, chunk_parser, timeout)
+ next_chunk(id, timeout)
after
timeout -> :timeout
end
end
- defp parse_chunk(msg) do
- msg.chunk |> IO.iodata_to_binary() |> :jiffy.decode([:return_maps,
:use_nil])
+ defp parse_chunk(body) do
+ :jiffy.decode(body, [:return_maps, :use_nil])
end
- defp parse_event(msg) do
- captures = Regex.scan(~r/data: (.*)/, msg.chunk)
+ defp parse_event(body) do
+ captures = Regex.scan(~r/data: (.*)/, body)
captures
|> Enum.map(fn p -> Enum.at(p, 1) end)
@@ -423,16 +419,6 @@ defmodule ChangesAsyncTest do
end)
end
- defp parse_heartbeat(msg) do
- is_heartbeat = Regex.match?(~r/event: heartbeat/, msg.chunk)
-
- if is_heartbeat do
- :heartbeat
- else
- :other
- end
- end
-
defp parse_changes_response(changes) do
{length(changes["results"]), String.slice(changes["last_seq"], 0..1)}
end
@@ -466,31 +452,23 @@ defmodule ChangesAsyncTest do
assert String.at(change["last_seq"], 0) == "1"
# create_doc_bar(db_name,"bar")
- {:ok, worker_pid} = HTTPotion.spawn_worker_process(Couch.process_url(""))
- %HTTPotion.AsyncResponse{id: req_id} =
+ %Couch.AsyncResponse{id: req_id} =
Rawresp.get("/#{db_name}/_changes?feed=#{feed}&timeout=500",
- stream_to: self(),
- direct: worker_pid
+ stream_to: self()
)
:ok = wait_for_headers(req_id, 200)
create_doc_bar(db_name, "bar")
- changes = process_response(req_id, &parse_changes_line_chunk/1)
+ changes = process_response(req_id, &parse_changes_line/1)
assert length(changes) == 3
-
- HTTPotion.stop_worker_process(worker_pid)
end
def create_doc_bar(db_name, id) do
create_doc(db_name, %{:_id => id, :bar => 1})
end
- defp parse_changes_line_chunk(msg) do
- parse_changes_line(msg.chunk)
- end
-
defp parse_changes_line(body) do
body_lines = String.split(body, "\n")
diff --git a/test/elixir/test/design_paths_test.exs
b/test/elixir/test/design_paths_test.exs
index b3e10c165..1485eb878 100644
--- a/test/elixir/test/design_paths_test.exs
+++ b/test/elixir/test/design_paths_test.exs
@@ -25,11 +25,9 @@ defmodule DesignPathTest do
resp = Couch.get("/#{db_name}/_design/test")
assert resp.body["_id"] == "_design/test"
- resp =
- Couch.get(Couch.process_url("/#{db_name}/_design%2Ftest"),
- follow_redirects: true
- )
-
+ resp = Couch.get("/#{db_name}/_design%2Ftest")
+ assert resp.status_code == 301
+ resp = Couch.get(resp.headers["location"])
assert resp.body["_id"] == "_design/test"
resp = Couch.get("/#{db_name}/_design/test/_view/testing")
@@ -50,11 +48,9 @@ defmodule DesignPathTest do
resp = Couch.get("/#{db_name}/_design/test2")
assert resp.body["_id"] == "_design/test2"
- resp =
- Couch.get(Couch.process_url("/#{db_name}/_design%2Ftest2"),
- follow_redirects: true
- )
-
+ resp = Couch.get("/#{db_name}/_design%2Ftest2")
+ assert resp.status_code == 301
+ resp = Couch.get(resp.headers["location"])
assert resp.body["_id"] == "_design/test2"
resp = Couch.get("/#{db_name}/_design/test2/_view/testing")
diff --git a/test/elixir/test/etags_head_test.exs
b/test/elixir/test/etags_head_test.exs
index 9b9ff8bb0..4979af151 100644
--- a/test/elixir/test/etags_head_test.exs
+++ b/test/elixir/test/etags_head_test.exs
@@ -14,7 +14,7 @@ defmodule EtagsHeadTest do
)
assert resp.status_code == 201
- assert Map.has_key?(resp.headers.hdrs, "etag")
+ assert Map.has_key?(resp.headers, "etag")
end
@tag :with_db
@@ -27,11 +27,11 @@ defmodule EtagsHeadTest do
body: %{}
)
- etag = resp.headers.hdrs["etag"]
+ etag = resp.headers["etag"]
# get the doc and verify the headers match
resp = Couch.get("/#{db_name}/1")
- assert etag == resp.headers.hdrs["etag"]
+ assert etag == resp.headers["etag"]
# 'head' the doc and verify the headers match
resp =
@@ -39,7 +39,7 @@ defmodule EtagsHeadTest do
headers: ["if-none-match": "s"]
)
- assert etag == resp.headers.hdrs["etag"]
+ assert etag == resp.headers["etag"]
end
@tag :with_db
@@ -52,7 +52,7 @@ defmodule EtagsHeadTest do
body: %{}
)
- etag = resp.headers.hdrs["etag"]
+ etag = resp.headers["etag"]
# 'head' the doc and verify the headers match
resp =
@@ -60,7 +60,7 @@ defmodule EtagsHeadTest do
headers: ["if-none-match": "s"]
)
- assert etag == resp.headers.hdrs["etag"]
+ assert etag == resp.headers["etag"]
end
@tag :with_db
@@ -74,13 +74,13 @@ defmodule EtagsHeadTest do
)
assert resp.status_code == 201
- assert Map.has_key?(resp.headers.hdrs, "etag")
+ assert Map.has_key?(resp.headers, "etag")
- etag = resp.headers.hdrs["etag"]
+ etag = resp.headers["etag"]
# get the doc and verify the headers match
resp = Couch.get("/#{db_name}/1")
- assert etag == resp.headers.hdrs["etag"]
+ assert etag == resp.headers["etag"]
# 'head' the doc and verify the headers match
resp =
@@ -88,7 +88,7 @@ defmodule EtagsHeadTest do
headers: ["if-none-match": "s"]
)
- assert etag == resp.headers.hdrs["etag"]
+ assert etag == resp.headers["etag"]
# replace a doc
resp =
@@ -101,7 +101,7 @@ defmodule EtagsHeadTest do
# extract the new ETag value
previous_etag = etag
- etag = resp.headers.hdrs["etag"]
+ etag = resp.headers["etag"]
# fail to replace a doc
resp =
diff --git a/test/elixir/test/http_test.exs b/test/elixir/test/http_test.exs
index 4f4cf26d6..f2ca386b7 100644
--- a/test/elixir/test/http_test.exs
+++ b/test/elixir/test/http_test.exs
@@ -8,7 +8,7 @@ defmodule HttpTest do
db_name = context[:db_name]
resp = Couch.put("/#{db_name}/test", body: %{})
db_url = Couch.process_url("/" <> db_name)
- assert resp.headers.hdrs["location"] == db_url <> "/test"
+ assert resp.headers["location"] == db_url <> "/test"
end
@tag :with_db
@@ -21,7 +21,7 @@ defmodule HttpTest do
headers: ["X-Forwarded-Host": "mysite.com"]
)
- assert resp.headers.hdrs["location"] ==
"http://mysite.com/#{db_name}/test2"
+ assert resp.headers["location"] == "http://mysite.com/#{db_name}/test2"
end
@tag :with_db
@@ -43,7 +43,7 @@ defmodule HttpTest do
headers: ["X-Host": "mysite2.com"]
)
- assert resp.headers.hdrs["location"] ==
"http://mysite2.com/#{db_name}/test3"
+ assert resp.headers["location"] == "http://mysite2.com/#{db_name}/test3"
end)
end
@@ -58,7 +58,7 @@ defmodule HttpTest do
)
db_url = Couch.process_url("/" <> db_name)
- assert resp.headers.hdrs["location"] == db_url <>
"/docid%0A/attachment.txt"
+ assert resp.headers["location"] == db_url <> "/docid%0A/attachment.txt"
resp =
Couch.put("/#{db_name}/docidtest%0A",
@@ -67,7 +67,7 @@ defmodule HttpTest do
)
db_url = Couch.process_url("/" <> db_name)
- assert resp.headers.hdrs["location"] == db_url <> "/docidtest%0A"
+ assert resp.headers["location"] == db_url <> "/docidtest%0A"
resp =
Couch.post("/#{db_name}/",
@@ -76,6 +76,6 @@ defmodule HttpTest do
)
db_url = Couch.process_url("/" <> db_name)
- assert resp.headers.hdrs["location"] == db_url <> "/docidtestpost%250A"
+ assert resp.headers["location"] == db_url <> "/docidtestpost%250A"
end
end
diff --git a/test/elixir/test/jsonp_test.exs b/test/elixir/test/jsonp_test.exs
index 0dbb59b78..ae4b9d472 100644
--- a/test/elixir/test/jsonp_test.exs
+++ b/test/elixir/test/jsonp_test.exs
@@ -10,7 +10,7 @@ defmodule JsonpTest do
resp = Couch.get("/#{db_name}/0?callback=jsonp_no_chunk")
assert resp.status_code == 200
- assert resp.headers.hdrs["content-type"] == "application/json"
+ assert resp.headers["content-type"] == "application/json"
end
@tag :with_db
@@ -31,7 +31,7 @@ defmodule JsonpTest do
resp = Couch.get("/#{db_name}/0?callback=jsonp_no_chunk")
assert resp.status_code == 200
- assert resp.headers.hdrs["content-type"] == "application/javascript"
+ assert resp.headers["content-type"] == "application/javascript"
{callback_fun, callback_param} = parse_callback(resp.body)
@@ -71,7 +71,7 @@ defmodule JsonpTest do
run_on_modified_server(server_config, fn ->
resp =
Couch.get("/#{db_name}/_design/test/_view/all_docs?callback=jsonp_chunk")
assert resp.status_code == 200
- assert resp.headers.hdrs["content-type"] == "application/javascript"
+ assert resp.headers["content-type"] == "application/javascript"
{callback_fun, callback_param} = parse_callback(resp.body)
@@ -83,12 +83,11 @@ defmodule JsonpTest do
resp = Couch.get("/#{db_name}/_changes?callback=jsonp_chunk")
assert resp.status_code == 200
- assert resp.headers.hdrs["content-type"] == "application/javascript"
+ assert resp.headers["content-type"] == "application/javascript"
{callback_fun, callback_param} = parse_callback(resp.body)
assert callback_fun == "jsonp_chunk"
assert length(callback_param["results"]) == 3
-
end)
end
diff --git a/test/elixir/test/list_views_test.exs
b/test/elixir/test/list_views_test.exs
index 8e6314dfb..b824ef1f8 100644
--- a/test/elixir/test/list_views_test.exs
+++ b/test/elixir/test/list_views_test.exs
@@ -464,7 +464,7 @@ defmodule ListViewsTest do
headers: [Accept: "text/html"]
)
- assert resp.headers["Content-Type"] == "text/html; charset=utf-8"
+ assert resp.headers["content-type"] == "text/html; charset=utf-8"
assert String.match?(resp.body, ~r/HTML/)
assert String.match?(resp.body, ~r/Value/)
end
@@ -561,7 +561,7 @@ defmodule ListViewsTest do
resp =
Rawresp.get("/#{db_name}/_design/lists/_list/setHeaderAfterGotRow/basicView")
assert resp.status_code == 400
- assert resp.headers["X-My-Header"] == "MyHeader"
+ assert resp.headers["x-my-header"] == "MyHeader"
assert String.match?(resp.body, ~r/^bad request$/)
end
diff --git a/test/elixir/test/replication_test.exs
b/test/elixir/test/replication_test.exs
index 75e69ed2d..4649b4a41 100644
--- a/test/elixir/test/replication_test.exs
+++ b/test/elixir/test/replication_test.exs
@@ -87,7 +87,7 @@ defmodule ReplicationTest do
opts = [headers: [Accept: "application/json"], query: query]
resp = Couch.get("/#{tgt_db_name}/#{doc["_id"]}", opts)
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert is_map(resp.body)
refute Map.has_key?(resp.body, "_conflicts")
refute Map.has_key?(resp.body, "_deleted_conflicts")
@@ -190,16 +190,16 @@ defmodule ReplicationTest do
test "default headers returned for _scheduler/jobs" do
resp = Couch.get("/_scheduler/jobs")
- assert resp.headers["Content-Type"] == "application/json"
- assert resp.headers["X-Couch-Request-ID"]
- assert resp.headers["X-CouchDB-Body-Time"]
+ assert resp.headers["content-type"] == "application/json"
+ assert resp.headers["x-couch-request-id"]
+ assert resp.headers["x-couchdb-body-time"]
end
test "default headers returned for _scheduler/docs " do
resp = Couch.get("/_scheduler/docs")
- assert resp.headers["Content-Type"] == "application/json"
- assert resp.headers["X-Couch-Request-ID"]
- assert resp.headers["X-CouchDB-Body-Time"]
+ assert resp.headers["content-type"] == "application/json"
+ assert resp.headers["x-couch-request-id"]
+ assert resp.headers["x-couchdb-body-time"]
end
Enum.each(@db_pairs_prefixes, fn {name, src_prefix, tgt_prefix} ->
@@ -904,11 +904,11 @@ defmodule ReplicationTest do
assert history["doc_write_failures"] == 0
resp = Couch.get!("/#{tgt_db_name}/foo1")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["value"] == 1
resp = Couch.get!("/#{tgt_db_name}/foo2")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["value"] == 2
resp = Couch.get!("/#{tgt_db_name}/foo3")
@@ -932,23 +932,23 @@ defmodule ReplicationTest do
assert history["doc_write_failures"] == 0
resp = Couch.get!("/#{tgt_db_name}/foo1")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["value"] == 1
resp = Couch.get!("/#{tgt_db_name}/foo2")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["value"] == 2
resp = Couch.get!("/#{tgt_db_name}/foo3")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["value"] == 3
resp = Couch.get!("/#{tgt_db_name}/foo4")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["value"] == 4
resp = Couch.get!("/#{tgt_db_name}/_design/mydesign")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
end
def run_by_id_repl(src_prefix, tgt_prefix) do
@@ -1046,8 +1046,8 @@ defmodule ReplicationTest do
assert orig.status_code == 404
assert copy.status_code == 404
else
- assert HTTPotion.Response.success?(orig)
- assert HTTPotion.Response.success?(copy)
+ assert Couch.Response.success?(orig)
+ assert Couch.Response.success?(copy)
assert cmp_json(orig.body, copy.body)
end
end)
@@ -1059,7 +1059,7 @@ defmodule ReplicationTest do
is_doc_id = &Enum.member?(doc_ids, &1)
if is_doc_id.(doc["_id"]) or is_doc_id.(encoded_id) do
- assert HTTPotion.Response.success?(copy)
+ assert Couch.Response.success?(copy)
else
assert copy.status_code == 404
end
@@ -1101,8 +1101,8 @@ defmodule ReplicationTest do
assert orig.status_code == 404
assert copy.status_code == 404
else
- assert HTTPotion.Response.success?(orig)
- assert HTTPotion.Response.success?(copy)
+ assert Couch.Response.success?(orig)
+ assert Couch.Response.success?(copy)
assert cmp_json(orig.body, copy.body)
end
end)
@@ -1116,7 +1116,7 @@ defmodule ReplicationTest do
is_doc_id = &Enum.member?(all_doc_ids, &1)
if is_doc_id.(doc["_id"]) or is_doc_id.(encoded_id) do
- assert HTTPotion.Response.success?(copy)
+ assert Couch.Response.success?(copy)
else
assert copy.status_code == 404
end
@@ -1161,7 +1161,7 @@ defmodule ReplicationTest do
query = %{"conflicts" => "true"}
copy = Couch.get!("/#{tgt_db_name}/#{conflict_id}", query: query)
- assert HTTPotion.Response.success?(copy)
+ assert Couch.Response.success?(copy)
assert copy.body["integer"] == 666
assert String.starts_with?(copy.body["_rev"], "4-")
assert not Map.has_key?(doc, "_conflicts")
@@ -1537,7 +1537,7 @@ defmodule ReplicationTest do
if String.starts_with?(doc["_id"], "_design/") do
assert resp.status_code == 404
else
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert cmp_json(doc, resp.body)
end
end)
@@ -1593,11 +1593,10 @@ defmodule ReplicationTest do
def get_db_info(db_name) do
resp = Couch.get("/#{db_name}")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
resp.body
end
-
def cancel_replication(src, tgt) do
body = %{:cancel => true}
@@ -1610,7 +1609,7 @@ defmodule ReplicationTest do
def get_db_changes(db_name, query \\ %{}) do
resp = Couch.get("/#{db_name}/_changes", query: query)
- assert HTTPotion.Response.success?(resp), "#{inspect(resp)}
#{inspect(query)}"
+ assert Couch.Response.success?(resp), "#{inspect(resp)} #{inspect(query)}"
resp.body
end
@@ -1618,7 +1617,7 @@ defmodule ReplicationTest do
query = %{w: 3}
body = %{docs: docs}
resp = Couch.post("/#{db_name}/_bulk_docs", query: query, body: body)
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
for {doc, resp} <- Enum.zip(docs, resp.body) do
assert resp["ok"], "Error saving doc: #{doc["_id"]}"
@@ -1628,7 +1627,7 @@ defmodule ReplicationTest do
def set_security(db_name, sec_props) do
resp = Couch.put("/#{db_name}/_security", body: :jiffy.encode(sec_props,
[:use_nil]))
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert resp.body["ok"]
end
@@ -1652,7 +1651,7 @@ defmodule ReplicationTest do
retry_until(fn ->
resp = Couch.put(uri, headers: headers, query: params, body: att[:body])
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
Map.put(doc, "_rev", resp.body["rev"])
end)
end
@@ -1718,7 +1717,7 @@ defmodule ReplicationTest do
def try_get_task(repl_id) do
resp = Couch.get("/_active_tasks")
- assert HTTPotion.Response.success?(resp)
+ assert Couch.Response.success?(resp)
assert is_list(resp.body)
Enum.find(resp.body, nil, fn task ->
diff --git a/test/elixir/test/rewrite_js_test.exs
b/test/elixir/test/rewrite_js_test.exs
index d793e616a..ef4058603 100644
--- a/test/elixir/test/rewrite_js_test.exs
+++ b/test/elixir/test/rewrite_js_test.exs
@@ -237,11 +237,11 @@ defmodule RewriteJSTest do
# Test simple rewriting
resp = Couch.get("/#{db_name}/_design/test/_rewrite/foo")
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
resp = Couch.get("/#{db_name}/_design/test/_rewrite/foo2")
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
# Test POST, hello update world
resp =
@@ -254,7 +254,7 @@ defmodule RewriteJSTest do
resp = Couch.put("/#{db_name}/_design/test/_rewrite/hello/#{doc_id}")
assert resp.status_code in [201, 202]
assert resp.body == "hello doc"
- assert String.match?(resp.headers["Content-Type"], ~r/charset=utf-8/)
+ assert String.match?(resp.headers["content-type"], ~r/charset=utf-8/)
assert Couch.get("/#{db_name}/#{doc_id}").body["world"] == "hello"
@@ -270,7 +270,7 @@ defmodule RewriteJSTest do
resp = Couch.put("/#{db_name}/_design/test/_rewrite/welcome3/test")
assert resp.status_code in [201, 202]
assert resp.body == "New World"
- assert String.match?(resp.headers["Content-Type"], ~r/charset=utf-8/)
+ assert String.match?(resp.headers["content-type"], ~r/charset=utf-8/)
resp = Couch.get("/#{db_name}/_design/test/_rewrite/welcome3/test")
assert resp.body == "Welcome test"
@@ -337,7 +337,7 @@ defmodule RewriteJSTest do
resp = Couch.get("/#{db_name}/_design/response/_rewrite?status=451")
assert resp.status_code == 451
- assert resp.headers["Content-Type"] == "application/json"
+ assert resp.headers["content-type"] == "application/json"
resp = Couch.get("/#{db_name}/_design/response/_rewrite?status=500")
assert resp.status_code == 500
diff --git a/test/elixir/test/rewrite_test.exs
b/test/elixir/test/rewrite_test.exs
index 4a13803c6..7fa787b14 100644
--- a/test/elixir/test/rewrite_test.exs
+++ b/test/elixir/test/rewrite_test.exs
@@ -298,11 +298,11 @@ defmodule RewriteTest do
# Test simple rewriting
resp = Couch.get("/#{db_name}/_design/test/_rewrite/foo")
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
resp = Couch.get("/#{db_name}/_design/test/_rewrite/foo2")
assert resp.body == "This is a base64 encoded text"
- assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.headers["content-type"] == "text/plain"
# Test POST, hello update world
resp =
@@ -315,7 +315,7 @@ defmodule RewriteTest do
resp = Couch.put("/#{db_name}/_design/test/_rewrite/hello/#{doc_id}")
assert resp.status_code in [201, 202]
assert resp.body == "hello doc"
- assert String.match?(resp.headers["Content-Type"], ~r/charset=utf-8/)
+ assert String.match?(resp.headers["content-type"], ~r/charset=utf-8/)
assert Couch.get("/#{db_name}/#{doc_id}").body["world"] == "hello"
@@ -331,7 +331,7 @@ defmodule RewriteTest do
resp = Couch.put("/#{db_name}/_design/test/_rewrite/welcome3/test")
assert resp.status_code in [201, 202]
assert resp.body == "New World"
- assert String.match?(resp.headers["Content-Type"], ~r/charset=utf-8/)
+ assert String.match?(resp.headers["content-type"], ~r/charset=utf-8/)
resp = Couch.get("/#{db_name}/_design/test/_rewrite/welcome3/test")
assert resp.body == "Welcome test"
diff --git a/test/elixir/test/show_documents_test.exs
b/test/elixir/test/show_documents_test.exs
index a574c72b1..f56dca135 100644
--- a/test/elixir/test/show_documents_test.exs
+++ b/test/elixir/test/show_documents_test.exs
@@ -211,10 +211,10 @@ defmodule ShowDocumentsTest do
resp = Rawresp.get("/#{db_name}/_design/template/_show/hello/test-doc-id")
assert resp.body == "Hello World"
- assert String.match?(resp.headers["Content-Type"], ~r/charset=utf-8/)
+ assert String.match?(resp.headers["content-type"], ~r/charset=utf-8/)
# Fix for COUCHDB-379
- assert String.match?(resp.headers["Server"], ~r/^CouchDB/)
+ assert String.match?(resp.headers["server"], ~r/^CouchDB/)
end
test "show without docid", context do
@@ -276,8 +276,8 @@ defmodule ShowDocumentsTest do
headers: [Accept: "text/html;text/plain;*/*"]
)
- assert String.match?(resp.headers["Content-Type"], ~r/text\/html/)
- assert resp.headers["Vary"] == "Accept"
+ assert String.match?(resp.headers["content-type"], ~r/text\/html/)
+ assert resp.headers["vary"] == "Accept"
etag = resp.headers["etag"]
@@ -287,7 +287,7 @@ defmodule ShowDocumentsTest do
)
assert String.match?(resp.body, ~r/PNG/)
- assert resp.headers["Content-Type"] == "image/png"
+ assert resp.headers["content-type"] == "image/png"
etag2 = resp.headers["etag"]
@@ -337,8 +337,8 @@ defmodule ShowDocumentsTest do
headers: [Accept: "text/html,application/atom+xml; q=0.9"]
)
- assert String.match?(resp.headers["Content-Type"], ~r/text\/html/)
- assert String.match?(resp.headers["Content-Type"], ~r/charset=utf-8/)
+ assert String.match?(resp.headers["content-type"], ~r/text\/html/)
+ assert String.match?(resp.headers["content-type"], ~r/charset=utf-8/)
assert resp.body == "Ha ha, you said \"plankton\"."
end
@@ -350,7 +350,7 @@ defmodule ShowDocumentsTest do
headers: [Accept: "application/x-foo"]
)
- assert resp.headers["Content-Type"] == "application/x-foo"
+ assert resp.headers["content-type"] == "application/x-foo"
assert String.match?(resp.body, ~r/foofoo/)
end
@@ -386,7 +386,7 @@ defmodule ShowDocumentsTest do
resp = Rawresp.get("/#{db_name}/_design/template/_show/list-api/foo")
assert resp.body == "Hey"
- assert resp.headers["X-Couch-Test-Header"] == "Yeah"
+ assert resp.headers["x-couch-test-header"] == "Yeah"
end
test "list() compatible API with provides function", context do
@@ -411,12 +411,12 @@ defmodule ShowDocumentsTest do
resp = Rawresp.get("/#{db_name}/_design/template/_show/list-api-mix/foo")
assert resp.body == "Hey Dude"
- assert resp.headers["X-Couch-Test-Header"] == "Yeah"
+ assert resp.headers["x-couch-test-header"] == "Yeah"
resp =
Rawresp.get("/#{db_name}/_design/template/_show/list-api-mix-with-header/foo")
assert resp.body == "Hey Dude"
- assert resp.headers["X-Couch-Test-Header"] == "Yeah"
- assert resp.headers["X-Couch-Test-Header-Awesome"] == "Oh Yeah!"
+ assert resp.headers["x-couch-test-header"] == "Yeah"
+ assert resp.headers["x-couch-test-header-awesome"] == "Oh Yeah!"
end
test "deleted docs", context do
diff --git a/test/elixir/test/update_documents_test.exs
b/test/elixir/test/update_documents_test.exs
index c29b31a4d..d6b05197c 100644
--- a/test/elixir/test/update_documents_test.exs
+++ b/test/elixir/test/update_documents_test.exs
@@ -125,15 +125,15 @@ defmodule UpdateDocumentsTest do
resp = Couch.put("/#{db_name}/_design/update/_update/hello/#{docid}")
assert resp.status_code == 201
assert resp.body == "<p>hello doc</p>"
- assert String.contains?(resp.headers["Content-Type"], "charset=utf-8")
- assert resp.headers["X-Couch-Id"] == docid
+ assert String.contains?(resp.headers["content-type"], "charset=utf-8")
+ assert resp.headers["x-couch-id"] == docid
resp = Couch.get("/#{db_name}/#{docid}")
assert resp.status_code == 200
assert resp.body["world"] == "hello"
# Fix for COUCHDB-379
- assert String.starts_with?(resp.headers["Server"], "CouchDB")
+ assert String.starts_with?(resp.headers["server"], "CouchDB")
resp = Couch.put("/#{db_name}/_design/update/_update/hello")
assert resp.status_code == 200
@@ -234,7 +234,7 @@ defmodule UpdateDocumentsTest do
headers: ["X-Couch-Full-Commit": "true"]
)
- newrev = resp.headers["X-Couch-Update-NewRev"]
+ newrev = resp.headers["x-couch-update-newrev"]
resp = Couch.get("/#{db_name}/#{docid}")
assert resp.status_code == 200
@@ -309,7 +309,7 @@ defmodule UpdateDocumentsTest do
assert resp.status_code == 201
assert resp.body == "hello world!"
- assert String.contains?(resp.headers["Content-Type"],
"application/octet-stream")
+ assert String.contains?(resp.headers["content-type"],
"application/octet-stream")
end
@tag :with_db
diff --git a/test/elixir/test/uuids_test.exs b/test/elixir/test/uuids_test.exs
index 9a29b328f..14107d266 100644
--- a/test/elixir/test/uuids_test.exs
+++ b/test/elixir/test/uuids_test.exs
@@ -9,9 +9,9 @@ defmodule UUIDsTest do
test "cache busting headers are set" do
resp = Couch.get("/_uuids")
assert resp.status_code == 200
- assert Regex.match?(~r/no-cache/, resp.headers["Cache-Control"])
- assert resp.headers["Pragma"] == "no-cache"
- assert String.length(resp.headers["ETag"]) > 0
+ assert Regex.match?(~r/no-cache/, resp.headers["cache-control"])
+ assert resp.headers["pragma"] == "no-cache"
+ assert String.length(resp.headers["etag"]) > 0
end
test "can return single uuid" do