summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoan Touzet <wohali@users.noreply.github.com>2019-03-09 13:58:45 -0500
committerGitHub <noreply@github.com>2019-03-09 13:58:45 -0500
commitdf07960f3f26253cae1485ee5d2a19f0f46b3adc (patch)
tree7859ad4fa37f407a4713e5ec2f3fa3d0951afd55
parent46d8c1ae44b11bc83f3a32e6d728f380a76b7352 (diff)
parentd98fd880bfa97f799c1bc6a05f2c888160616c53 (diff)
downloadcouchdb-improve-rfc-template.tar.gz
Merge branch 'master' into improve-rfc-templateimprove-rfc-template
-rw-r--r--src/chttpd/src/chttpd.erl45
-rw-r--r--src/chttpd/src/chttpd_db.erl15
-rw-r--r--src/chttpd/test/chttpd_socket_buffer_size_test.erl152
-rw-r--r--src/couch/priv/stats_descriptions.cfg48
-rw-r--r--src/mango/src/mango_error.erl16
-rw-r--r--src/mango/src/mango_httpd.erl9
-rw-r--r--src/mango/src/mango_idx.erl27
-rw-r--r--test/elixir/README.md12
-rw-r--r--test/elixir/test/attachments.exs482
-rw-r--r--test/elixir/test/multiple_rows_test.exs136
-rw-r--r--test/elixir/test/partition_mango_test.exs27
-rw-r--r--test/javascript/tests/etags_head.js4
12 files changed, 862 insertions, 111 deletions
diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl
index 631eb77c9..1e1d638be 100644
--- a/src/chttpd/src/chttpd.erl
+++ b/src/chttpd/src/chttpd.erl
@@ -693,10 +693,16 @@ etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) ->
etag_match(Req, binary_to_list(CurrentEtag));
etag_match(Req, CurrentEtag) ->
- EtagsToMatch = string:tokens(
+ EtagsToMatch0 = string:tokens(
chttpd:header_value(Req, "If-None-Match", ""), ", "),
+ EtagsToMatch = lists:map(fun strip_weak_prefix/1, EtagsToMatch0),
lists:member(CurrentEtag, EtagsToMatch).
+strip_weak_prefix([$W, $/ | Etag]) ->
+ Etag;
+strip_weak_prefix(Etag) ->
+ Etag.
+
etag_respond(Req, CurrentEtag, RespFun) ->
case etag_match(Req, CurrentEtag) of
true ->
@@ -810,7 +816,8 @@ send_delayed_error(#delayed_resp{req=Req,resp=nil}=DelayedResp, Reason) ->
{Code, ErrorStr, ReasonStr} = error_info(Reason),
{ok, Resp} = send_error(Req, Code, ErrorStr, ReasonStr),
{ok, DelayedResp#delayed_resp{resp=Resp}};
-send_delayed_error(#delayed_resp{resp=Resp}, Reason) ->
+send_delayed_error(#delayed_resp{resp=Resp, req=Req}, Reason) ->
+ update_timeout_stats(Reason, Req),
log_error_with_stack_trace(Reason),
throw({http_abort, Resp, Reason}).
@@ -1025,12 +1032,15 @@ error_headers(_, Code, _, _) ->
send_error(_Req, {already_sent, Resp, _Error}) ->
{ok, Resp};
-send_error(Req, Error) ->
+send_error(#httpd{} = Req, Error) ->
+ update_timeout_stats(Error, Req),
+
{Code, ErrorStr, ReasonStr} = error_info(Error),
{Code1, Headers} = error_headers(Req, Code, ErrorStr, ReasonStr),
send_error(Req, Code1, Headers, ErrorStr, ReasonStr, json_stack(Error)).
-send_error(Req, Code, ErrorStr, ReasonStr) ->
+send_error(#httpd{} = Req, Code, ErrorStr, ReasonStr) ->
+ update_timeout_stats(ErrorStr, Req),
send_error(Req, Code, [], ErrorStr, ReasonStr, []).
send_error(Req, Code, Headers, ErrorStr, ReasonStr, []) ->
@@ -1045,6 +1055,33 @@ send_error(Req, Code, Headers, ErrorStr, ReasonStr, Stack) ->
case Stack of [] -> []; _ -> [{<<"ref">>, stack_hash(Stack)}] end
]}).
+update_timeout_stats(<<"timeout">>, #httpd{requested_path_parts = PathParts}) ->
+ update_timeout_stats(PathParts);
+update_timeout_stats(timeout, #httpd{requested_path_parts = PathParts}) ->
+ update_timeout_stats(PathParts);
+update_timeout_stats(_, _) ->
+ ok.
+
+update_timeout_stats([_, <<"_partition">>, _, <<"_design">>, _,
+ <<"_view">> | _]) ->
+ couch_stats:increment_counter([couchdb, httpd, partition_view_timeouts]);
+update_timeout_stats([_, <<"_partition">>, _, <<"_find">>| _]) ->
+ couch_stats:increment_counter([couchdb, httpd, partition_find_timeouts]);
+update_timeout_stats([_, <<"_partition">>, _, <<"_explain">>| _]) ->
+ couch_stats:increment_counter([couchdb, httpd, partition_explain_timeouts]);
+update_timeout_stats([_, <<"_partition">>, _, <<"_all_docs">> | _]) ->
+ couch_stats:increment_counter([couchdb, httpd, partition_all_docs_timeouts]);
+update_timeout_stats([_, <<"_design">>, _, <<"_view">> | _]) ->
+ couch_stats:increment_counter([couchdb, httpd, view_timeouts]);
+update_timeout_stats([_, <<"_find">>| _]) ->
+ couch_stats:increment_counter([couchdb, httpd, find_timeouts]);
+update_timeout_stats([_, <<"_explain">>| _]) ->
+ couch_stats:increment_counter([couchdb, httpd, explain_timeouts]);
+update_timeout_stats([_, <<"_all_docs">> | _]) ->
+ couch_stats:increment_counter([couchdb, httpd, all_docs_timeouts]);
+update_timeout_stats(_) ->
+ ok.
+
% give the option for list functions to output html or other raw errors
send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) ->
send_chunk(Resp, Reason),
diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl
index 5a0911559..c6404b04d 100644
--- a/src/chttpd/src/chttpd_db.erl
+++ b/src/chttpd/src/chttpd_db.erl
@@ -298,6 +298,7 @@ handle_partition_req(#httpd{path_parts=[DbName, _, PartId | Rest]}=Req, Db) ->
path_parts = [DbName | Rest],
qs = NewQS
},
+ update_partition_stats(Rest),
case Rest of
[OP | _] when OP == <<"_all_docs">> orelse ?IS_MANGO(OP) ->
case chttpd_handlers:db_handler(OP, fun db_req/2) of
@@ -318,6 +319,20 @@ handle_partition_req(#httpd{path_parts=[DbName, _, PartId | Rest]}=Req, Db) ->
handle_partition_req(Req, _Db) ->
chttpd:send_error(Req, not_found).
+update_partition_stats(PathParts) ->
+ case PathParts of
+ [<<"_design">> | _] ->
+ couch_stats:increment_counter([couchdb, httpd, partition_view_requests]);
+ [<<"_all_docs">> | _] ->
+ couch_stats:increment_counter([couchdb, httpd, partition_all_docs_requests]);
+ [<<"_find">> | _] ->
+ couch_stats:increment_counter([couchdb, httpd, partition_find_requests]);
+ [<<"_explain">> | _] ->
+ couch_stats:increment_counter([couchdb, httpd, partition_explain_requests]);
+ _ ->
+ ok % ignore path that do not match
+ end.
+
handle_design_req(#httpd{
path_parts=[_DbName, _Design, Name, <<"_",_/binary>> = Action | _Rest]
diff --git a/src/chttpd/test/chttpd_socket_buffer_size_test.erl b/src/chttpd/test/chttpd_socket_buffer_size_test.erl
index 650bf9b0b..937880621 100644
--- a/src/chttpd/test/chttpd_socket_buffer_size_test.erl
+++ b/src/chttpd/test/chttpd_socket_buffer_size_test.erl
@@ -21,115 +21,69 @@
-define(CONTENT_JSON, {"Content-Type", "application/json"}).
-setup() ->
- Hashed = couch_passwords:hash_admin_password(?PASS),
- ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false),
- SocketOptions = config:get("chttpd", "socket_options"),
+setup(SocketOpts) ->
+ StartCtx = start_couch_with_cfg(SocketOpts),
Db = ?tempdb(),
- Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
- Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)),
- Url = "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(Db),
- create_db(Url),
- {Db, SocketOptions}.
+ create_db(url(Db)),
+ {StartCtx, Db}.
-teardown({Db, SocketOptions}) ->
+teardown(_, {StartCtx, Db}) ->
delete_db(url(Db)),
- ok = config:delete("chttpd", "socket_options", _Persist=false),
ok = config:delete("admins", ?USER, _Persist=false),
- case SocketOptions of
- undefined ->
- ok;
- _ ->
- ok = config:set("chttpd", "socket_options", SocketOptions)
- end.
+ test_util:stop_couch(StartCtx).
socket_buffer_size_test_() ->
{
"chttpd socket_buffer_size_test",
{
- setup,
- fun chttpd_test_util:start_couch/0,
- fun chttpd_test_util:stop_couch/1,
- {
- foreach,
- fun setup/0, fun teardown/1,
- [
- fun buffer_too_small_url_fails/1,
- fun buffer_too_small_header_fails/1,
- fun recbuf_too_small_url_fails/1,
- fun recbuf_too_small_header_fails/1,
- fun default_buffer_settings_work/1
- ]
- }
+ foreachx,
+ fun setup/1, fun teardown/2,
+ [
+ {"[{recbuf, undefined}]", fun default_buffer/2},
+ {"[{recbuf, 1024}]", fun small_recbuf/2},
+ {"[{buffer, 1024}]", fun small_buffer/2}
+ ]
}
}.
-buffer_too_small_url_fails({Db, _}) ->
- ?_test(begin
- restart_chttpd("[{buffer, 1024}]"),
- Id = data(1500),
- Status1 = put_req(url(Db) ++ "/" ++ Id, "{}"),
- ?assertEqual(400, Status1),
- restart_chttpd("[{buffer, 2048}]"),
- Status2 = put_req(url(Db) ++ "/" ++ Id, "{}"),
- ?assert(Status2 =:= 201 orelse Status2 =:= 202)
- end).
-
-
-buffer_too_small_header_fails({Db, _}) ->
- ?_test(begin
- restart_chttpd("[{buffer, 1024}]"),
- Headers = [{"Blah", data(1500)}],
- Status1 = put_req(url(Db) ++ "/d", Headers, "{}"),
- ?assertEqual(400, Status1),
- restart_chttpd("[{buffer, 2048}]"),
- Status2 = put_req(url(Db) ++ "/d", Headers, "{}"),
- ?assert(Status2 =:= 201 orelse Status2 =:= 202)
- end).
-
-
-recbuf_too_small_url_fails({Db, _}) ->
- ?_test(begin
- restart_chttpd("[{recbuf, 1024}]"),
- Id = data(1500),
- Status1 = put_req(url(Db) ++ "/" ++ Id, "{}"),
- ?assertEqual(400, Status1),
- restart_chttpd("[{recbuf, 2048}]"),
- Status2 = put_req(url(Db) ++ "/" ++ Id, "{}"),
- ?assert(Status2 =:= 201 orelse Status2 =:= 202)
- end).
-
-
-recbuf_too_small_header_fails({Db, _}) ->
- ?_test(begin
- restart_chttpd("[{recbuf, 1024}]"),
- Headers = [{"Blah", data(1500)}],
- Status1 = put_req(url(Db) ++ "/d", Headers, "{}"),
- ?assertEqual(400, Status1),
- restart_chttpd("[{recbuf, 2048}]"),
- Status2 = put_req(url(Db) ++ "/d", Headers, "{}"),
- ?assert(Status2 =:= 201 orelse Status2 =:= 202)
- end).
-
-
-default_buffer_settings_work({Db, _}) ->
- ?_test(begin
- restart_chttpd("[{recbuf, undefined}]"),
+small_recbuf(_, {_, Db}) ->
+ {timeout, 30, ?_test(begin
+ Id = data(2048),
+ Response = put_req(url(Db) ++ "/" ++ Id, "{}"),
+ ?assert(Response =:= 400 orelse Response =:= request_failed)
+ end)}.
+
+
+small_buffer(_, {_, Db}) ->
+ {timeout, 30, ?_test(begin
+ Id = data(2048),
+ Response = put_req(url(Db) ++ "/" ++ Id, "{}"),
+ ?assert(Response =:= 400 orelse Response =:= request_failed)
+ end)}.
+
+
+default_buffer(_, {_, Db}) ->
+ {timeout, 30, ?_test(begin
Id = data(7000),
- Status = put_req(url(Db) ++ "/" ++ Id, "{}"),
+ Headers = [{"Blah", data(7000)}],
+ Status = put_req(url(Db) ++ "/" ++ Id, Headers, "{}"),
?assert(Status =:= 201 orelse Status =:= 202)
- end).
+ end)}.
% Helper functions
-url(Db) ->
+url() ->
Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)),
- "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(Db).
+ "http://" ++ Addr ++ ":" ++ Port.
+
+
+url(Db) ->
+ url() ++ "/" ++ ?b2l(Db).
create_db(Url) ->
@@ -147,17 +101,27 @@ put_req(Url, Body) ->
put_req(Url, Headers, Body) ->
AllHeaders = Headers ++ [?CONTENT_JSON, ?AUTH],
- {ok, Status, _, _} = test_request:put(Url, AllHeaders, Body),
- Status.
+ case test_request:put(Url, AllHeaders, Body) of
+ {ok, Status, _, _} -> Status;
+ {error, Error} -> Error
+ end.
data(Size) ->
string:copies("x", Size).
-restart_chttpd(ServerOptions) ->
- ok = application:stop(chttpd),
- ok = application:stop(mochiweb),
- config:set("chttpd", "server_options", ServerOptions, _Persist=false),
- ok = application:start(mochiweb),
- ok = application:start(chttpd).
+append_to_cfg_chain(Cfg) ->
+ CfgDir = filename:dirname(lists:last(?CONFIG_CHAIN)),
+ CfgFile = filename:join([CfgDir, "chttpd_socket_buffer_extra_cfg.ini"]),
+ CfgSect = io_lib:format("[chttpd]~nserver_options = ~s~n", [Cfg]),
+ ok = file:write_file(CfgFile, CfgSect),
+ ?CONFIG_CHAIN ++ [CfgFile].
+
+
+start_couch_with_cfg(Cfg) ->
+ CfgChain = append_to_cfg_chain(Cfg),
+ StartCtx = test_util:start_couch(CfgChain, [chttpd]),
+ Hashed = couch_passwords:hash_admin_password(?PASS),
+ ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false),
+ StartCtx.
diff --git a/src/couch/priv/stats_descriptions.cfg b/src/couch/priv/stats_descriptions.cfg
index e5ac9d722..0e2271350 100644
--- a/src/couch/priv/stats_descriptions.cfg
+++ b/src/couch/priv/stats_descriptions.cfg
@@ -78,6 +78,54 @@
{type, counter},
{desc, <<"number of HTTP requests">>}
]}.
+{[couchdb, httpd, view_timeouts], [
+ {type, counter},
+ {desc, <<"number of HTTP view timeouts">>}
+]}.
+{[couchdb, httpd, find_timeouts], [
+ {type, counter},
+ {desc, <<"number of HTTP find timeouts">>}
+]}.
+{[couchdb, httpd, explain_timeouts], [
+ {type, counter},
+ {desc, <<"number of HTTP _explain timeouts">>}
+]}.
+{[couchdb, httpd, all_docs_timeouts], [
+ {type, counter},
+ {desc, <<"number of HTTP all_docs timeouts">>}
+]}.
+{[couchdb, httpd, partition_view_requests], [
+ {type, counter},
+ {desc, <<"number of partition HTTP view requests">>}
+]}.
+{[couchdb, httpd, partition_find_requests], [
+ {type, counter},
+ {desc, <<"number of partition HTTP _find requests">>}
+]}.
+{[couchdb, httpd, partition_explain_requests], [
+ {type, counter},
+ {desc, <<"number of partition HTTP _explain requests">>}
+]}.
+{[couchdb, httpd, partition_all_docs_requests], [
+ {type, counter},
+ {desc, <<"number of partition HTTP _all_docs requests">>}
+]}.
+{[couchdb, httpd, partition_view_timeouts], [
+ {type, counter},
+ {desc, <<"number of partition HTTP view timeouts">>}
+]}.
+{[couchdb, httpd, partition_find_timeouts], [
+ {type, counter},
+ {desc, <<"number of partition HTTP find timeouts">>}
+]}.
+{[couchdb, httpd, partition_explain_timeouts], [
+ {type, counter},
+ {desc, <<"number of partition HTTP _explain timeouts">>}
+]}.
+{[couchdb, httpd, partition_all_docs_timeouts], [
+ {type, counter},
+ {desc, <<"number of partition HTTP all_docs timeouts">>}
+]}.
{[couchdb, httpd, temporary_view_reads], [
{type, counter},
{desc, <<"number of temporary view reads">>}
diff --git a/src/mango/src/mango_error.erl b/src/mango/src/mango_error.erl
index dcf4b9a7e..2f22552c9 100644
--- a/src/mango/src/mango_error.erl
+++ b/src/mango/src/mango_error.erl
@@ -25,7 +25,21 @@ info(mango_idx, {no_usable_index, missing_sort_index}) ->
{
400,
<<"no_usable_index">>,
- <<"No index exists for this sort, try indexing by the sort fields.">>
+ <<"No index exists for this sort, "
+ "try indexing by the sort fields.">>
+ };
+info(mango_idx, {no_usable_index, missing_sort_index_partitioned}) ->
+ {
+ 400,
+ <<"no_usable_index">>,
+ <<"No partitioned index exists for this sort, "
+ "try indexing by the sort fields.">>
+ };
+info(mango_idx, {no_usable_index, missing_sort_index_global}) ->
+ {
+ 400,
+ <<"no_usable_index">>,
+ <<"No global index exists for this sort, try indexing by the sort fields.">>
};
info(mango_json_bookmark, {invalid_bookmark, BadBookmark}) ->
{
diff --git a/src/mango/src/mango_httpd.erl b/src/mango/src/mango_httpd.erl
index d73ec6cb5..379d2e127 100644
--- a/src/mango/src/mango_httpd.erl
+++ b/src/mango/src/mango_httpd.erl
@@ -186,8 +186,13 @@ handle_find_req(#httpd{method='POST'}=Req, Db) ->
{ok, Opts0} = mango_opts:validate_find(Body),
{value, {selector, Sel}, Opts} = lists:keytake(selector, 1, Opts0),
{ok, Resp0} = start_find_resp(Req),
- {ok, AccOut} = run_find(Resp0, Db, Sel, Opts),
- end_find_resp(AccOut);
+ case run_find(Resp0, Db, Sel, Opts) of
+ {ok, AccOut} ->
+ end_find_resp(AccOut);
+ {error, Error} ->
+ chttpd:send_error(Req, Error)
+ end;
+
handle_find_req(Req, _Db) ->
chttpd:send_method_not_allowed(Req, "POST").
diff --git a/src/mango/src/mango_idx.erl b/src/mango/src/mango_idx.erl
index 6e2abca5c..c2c26958c 100644
--- a/src/mango/src/mango_idx.erl
+++ b/src/mango/src/mango_idx.erl
@@ -72,12 +72,23 @@ get_usable_indexes(Db, Selector, Opts) ->
case lists:filter(UsableFilter, UsableIndexes1) of
[] ->
- ?MANGO_ERROR({no_usable_index, missing_sort_index});
+ mango_sort_error(Db, Opts);
UsableIndexes ->
UsableIndexes
end.
+mango_sort_error(Db, Opts) ->
+ case {fabric_util:is_partitioned(Db), is_opts_partitioned(Opts)} of
+ {false, _} ->
+ ?MANGO_ERROR({no_usable_index, missing_sort_index});
+ {true, true} ->
+ ?MANGO_ERROR({no_usable_index, missing_sort_index_partitioned});
+ {true, false} ->
+ ?MANGO_ERROR({no_usable_index, missing_sort_index_global})
+ end.
+
+
recover(Db) ->
{ok, DDocs0} = mango_util:open_ddocs(Db),
Pred = fun({Props}) ->
@@ -410,12 +421,20 @@ get_idx_partitioned(Db, DDocProps) ->
Default
end.
+is_opts_partitioned(Opts) ->
+ case couch_util:get_value(partition, Opts, <<>>) of
+ <<>> ->
+ false;
+ Partition when is_binary(Partition) ->
+ true
+ end.
+
filter_partition_indexes(Indexes, Opts) ->
- PFilt = case couch_util:get_value(partition, Opts) of
- <<>> ->
+ PFilt = case is_opts_partitioned(Opts) of
+ false ->
fun(#idx{partitioned = P}) -> not P end;
- Partition when is_binary(Partition) ->
+ true ->
fun(#idx{partitioned = P}) -> P end
end,
Filt = fun(Idx) -> type(Idx) == <<"special">> orelse PFilt(Idx) end,
diff --git a/test/elixir/README.md b/test/elixir/README.md
index 8300fa101..883afb512 100644
--- a/test/elixir/README.md
+++ b/test/elixir/README.md
@@ -33,13 +33,13 @@ X means done, - means partially
- [ ] Port attachment_views.js
- [ ] Port auth_cache.js
- [X] Port basics.js
- - [ ] Port batch_save.js
- - [ ] Port bulk_docs.js
+ - [X] Port batch_save.js
+ - [X] Port bulk_docs.js
- [X] Port changes.js
- [X] Port coffee.js
- - [ ] Port compact.js
+ - [X] Port compact.js
- [X] Port config.js
- - [ ] Port conflicts.js
+ - [X] Port conflicts.js
- [ ] Port cookie_auth.js
- [ ] Port copy_doc.js
- [X] Port delayed_commits.js
@@ -68,7 +68,7 @@ X means done, - means partially
- [ ] Port reduce_false.js
- [ ] Port reduce_false_temp.js
- [X] Port reduce.js
- - [-] Port replication.js
+ - [X] Port replication.js
- [ ] Port replicator_db_bad_rep_id.js
- [ ] Port replicator_db_by_doc_id.js
- [ ] Port replicator_db_compact_rep_db.js
@@ -90,7 +90,7 @@ X means done, - means partially
- [ ] Port rev_stemming.js
- [X] Port rewrite.js
- [ ] Port rewrite_js.js
- - [ ] Port security_validation.js
+ - [X] Port security_validation.js
- [ ] Port show_documents.js
- [ ] Port stats.js
- [ ] Port update_documents.js
diff --git a/test/elixir/test/attachments.exs b/test/elixir/test/attachments.exs
new file mode 100644
index 000000000..7f235213e
--- /dev/null
+++ b/test/elixir/test/attachments.exs
@@ -0,0 +1,482 @@
+defmodule AttachmentsTest do
+ use CouchTestCase
+
+ @moduletag :attachments
+
+ # MD5 Digests of compressible attachments and therefore Etags
+ # will vary depending on platform gzip implementation.
+ # These MIME types are defined in [attachments] compressible_types
+ @bin_att_doc %{
+ _id: "bin_doc",
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "application/octet-stream",
+ data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+
+ @moduledoc """
+ Test CouchDB attachments
+ This is a port of the attachments.js suite
+ """
+
+ @tag :with_db
+ test "saves attachment successfully", 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]
+ assert resp.body["ok"]
+ end
+
+ @tag :with_db
+ test "errors for bad attachment", context do
+ db_name = context[:db_name]
+
+ bad_att_doc = %{
+ _id: "bad_doc",
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "text/plain",
+ data: "notBase64Encoded="
+ }
+ }
+ }
+
+ resp = Couch.put("/#{db_name}/bad_doc", body: bad_att_doc, query: %{w: 3})
+ assert resp.status_code == 400
+ end
+
+ @tag :with_db
+ test "reads attachment successfully", 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]
+
+ 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==\""
+ end
+
+ @tag :with_db
+ test "update attachment", context do
+ db_name = context[:db_name]
+
+ bin_att_doc2 = %{
+ _id: "bin_doc2",
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "text/plain",
+ data: ""
+ }
+ }
+ }
+
+ resp = Couch.put("/#{db_name}/bin_doc2", body: bin_att_doc2, query: %{w: 3})
+ assert resp.status_code in [201, 202]
+ rev = resp.body["rev"]
+
+ resp = Couch.get("/#{db_name}/bin_doc2/foo.txt")
+
+ assert resp.headers["Content-Type"] == "text/plain"
+ assert resp.body == ""
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc2/foo2.txt",
+ query: %{rev: rev, w: 3},
+ body: "This is no base64 encoded text",
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert resp.status_code in [201, 202]
+ assert Regex.match?(~r/bin_doc2\/foo2.txt/, resp.headers["location"])
+ end
+
+ @tag :with_db
+ test "delete attachment", 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", query: %{w: 3})
+
+ assert resp.status_code == 409
+
+ resp = Couch.delete("/#{db_name}/bin_doc/foo.txt", query: %{w: 3, rev: rev})
+ assert resp.status_code == 200
+ assert resp.headers["location"] == nil
+ end
+
+ @tag :with_db
+ test "saves binary", context do
+ db_name = context[:db_name]
+
+ bin_data = "JHAPDO*AU£PN ){(3u[d 93DQ9¡€])} ææøo'∂ƒæ≤çæππ•¥∫¶®#†π¶®¥π€ª®˙π8np"
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc3/attachment.txt",
+ body: bin_data,
+ headers: ["Content-Type": "text/plain;charset=utf-8"],
+ query: %{w: 3}
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+
+ rev = resp.body["rev"]
+
+ resp = Couch.get("/#{db_name}/bin_doc3/attachment.txt")
+ assert resp.body == bin_data
+
+ resp =
+ Couch.put("/#{db_name}/bin_doc3/attachment.txt", body: bin_data, query: %{w: 3})
+
+ assert resp.status_code == 409
+
+ # non-existent rev
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc3/attachment.txt",
+ query: %{rev: "1-adae8575ecea588919bd08eb020c708e", w: 3},
+ headers: ["Content-Type": "text/plain;charset=utf-8"],
+ body: bin_data
+ )
+
+ assert resp.status_code == 409
+
+ # current rev
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc3/attachment.txt",
+ query: %{rev: rev, w: 3},
+ headers: ["Content-Type": "text/plain;charset=utf-8"],
+ body: bin_data
+ )
+
+ assert resp.status_code == 201
+
+ 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 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 resp.body == bin_data
+
+ resp = Couch.delete("/#{db_name}/bin_doc3/attachment.txt", query: %{rev: rev, w: 3})
+ assert resp.status_code == 200
+
+ resp = Couch.get("/#{db_name}/bin_doc3/attachment.txt")
+ 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 resp.body == bin_data
+ end
+
+ @tag :with_db
+ test "empty attachments", context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc4/attachment.txt",
+ body: "",
+ headers: ["Content-Type": "text/plain;charset=utf-8"],
+ query: %{w: 3}
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+
+ rev = resp.body["rev"]
+
+ resp = Couch.get("/#{db_name}/bin_doc4/attachment.txt")
+ assert resp.status_code == 200
+ assert resp.body == ""
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc4/attachment.txt",
+ query: %{rev: rev, w: 3},
+ headers: ["Content-Type": "text/plain;charset=utf-8"],
+ body: "This is a string"
+ )
+
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/bin_doc4/attachment.txt")
+ assert resp.status_code == 200
+ assert resp.body == "This is a string"
+ end
+
+ @tag :with_db
+ test "large attachments COUCHDB-366", context do
+ db_name = context[:db_name]
+
+ lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
+ range = 1..10
+
+ large_att = Enum.reduce(range, lorem, fn _, acc -> lorem <> acc end)
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc5/attachment.txt",
+ body: large_att,
+ query: %{w: 3},
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert resp.status_code in [201, 202]
+ 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 resp.body == large_att
+
+ lorem_b64 =
+ "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4g"
+
+ range = 1..10
+
+ large_b64_att = Enum.reduce(range, lorem_b64, fn _, acc -> lorem_b64 <> acc end)
+
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc5",
+ query: %{attachments: true},
+ headers: [Accept: "application/json"]
+ )
+
+ assert large_b64_att == resp.body["_attachments"]["attachment.txt"]["data"]
+ end
+
+ @tag :with_db
+ test "etags for attachments", context do
+ db_name = context[:db_name]
+
+ lorem_att = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc6/attachment.txt",
+ body: lorem_att,
+ headers: ["Content-Type": "text/plain;charset=utf-8"],
+ query: %{w: 3}
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+
+ resp = Couch.get("/#{db_name}/bin_doc6/attachment.txt")
+ assert resp.status_code == 200
+ etag = resp.headers["etag"]
+
+ resp =
+ Couch.get("/#{db_name}/bin_doc6/attachment.txt", headers: ["if-none-match": etag])
+
+ assert resp.status_code == 304
+ end
+
+ @tag :with_db
+ test "test COUCHDB-497 - empty attachments", context do
+ db_name = context[:db_name]
+
+ lorem_att = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc7/attachment.txt",
+ body: lorem_att,
+ headers: ["Content-Type": "text/plain;charset=utf-8"],
+ query: %{w: 3}
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+
+ rev = resp.body["rev"]
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc7/empty.txt",
+ query: %{rev: rev, w: 3},
+ body: "",
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert resp.status_code == 201
+ rev = resp.body["rev"]
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc7/empty.txt",
+ query: %{rev: rev, w: 3},
+ body: "",
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert resp.status_code == 201
+ end
+
+ @tag :with_db
+ test "implicit doc creation allows creating docs with a reserved id. COUCHDB-565",
+ context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.put(
+ "/#{db_name}/_nonexistant/attachment.txt",
+ body: "ATTACHMENT INFO",
+ headers: ["Content-Type": "text/plain;charset=utf-8"],
+ query: %{w: 3}
+ )
+
+ assert resp.status_code == 400
+ end
+
+ @tag :with_db
+ test "COUCHDB-809 - stubs should only require the 'stub' field", context do
+ db_name = context[:db_name]
+
+ stub_doc = %{
+ _id: "stub_doc",
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "text/plain",
+ data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+
+ resp =
+ Couch.put(
+ "/#{db_name}/stub_doc",
+ body: stub_doc,
+ query: %{w: 3}
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+
+ rev = resp.body["rev"]
+
+ stub_doc =
+ Map.merge(stub_doc, %{
+ _rev: rev,
+ _attachments: %{"foo.txt": %{stub: true}}
+ })
+
+ resp =
+ Couch.put(
+ "/#{db_name}/stub_doc",
+ query: %{rev: rev, w: 3},
+ body: stub_doc
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+
+ rev = resp.body["rev"]
+
+ stub_doc =
+ Map.merge(stub_doc, %{
+ _rev: rev,
+ _attachments: %{"foo.txt": %{stub: true, revpos: 10}}
+ })
+
+ resp =
+ Couch.put(
+ "/#{db_name}/stub_doc",
+ query: %{rev: rev},
+ body: stub_doc
+ )
+
+ assert resp.status_code == 412
+ assert resp.body["error"] == "missing_stub"
+ end
+
+ @tag :with_db
+ test "md5 header for attachments", context do
+ db_name = context[:db_name]
+ md5 = "MntvB0NYESObxH4VRDUycw=="
+
+ bin_data = "foo bar"
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc8/attachment.txt",
+ body: bin_data,
+ headers: ["Content-Type": "application/octet-stream", "Content-MD5": md5],
+ query: %{w: 3}
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+
+ resp = Couch.get("/#{db_name}/bin_doc8/attachment.txt")
+ assert resp.status_code == 200
+ assert md5 == resp.headers["Content-MD5"]
+ end
+
+ @tag :with_db
+ test "attachment via multipart/form-data", context do
+ db_name = context[:db_name]
+
+ form_data_doc = %{
+ _id: "form-data-doc"
+ }
+
+ resp =
+ Couch.put(
+ "/#{db_name}/form_data_doc",
+ body: form_data_doc,
+ query: %{w: 3}
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+ rev = resp.body["rev"]
+
+ body =
+ "------TF\r\n" <>
+ "Content-Disposition: form-data; name=\"_rev\"\r\n\r\n" <>
+ rev <>
+ "\r\n" <>
+ "------TF\r\n" <>
+ "Content-Disposition: form-data; name=\"_attachments\"; filename=\"file.txt\"\r\n" <>
+ "Content-Type: text/plain\r\n\r\n" <>
+ "contents of file.txt\r\n\r\n" <> "------TF--"
+
+ resp =
+ Couch.post(
+ "/#{db_name}/form_data_doc",
+ body: body,
+ query: %{w: 3},
+ headers: [
+ Referer: "http://127.0.0.1:15984",
+ "Content-Type": "multipart/form-data; boundary=----TF",
+ "Content-Length": byte_size(body)
+ ]
+ )
+
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+
+ resp = Couch.get("/#{db_name}/form_data_doc")
+ assert resp.status_code == 200
+
+ doc = resp.body
+ assert doc["_attachments"]["file.txt"]["length"] == 22
+ end
+end
diff --git a/test/elixir/test/multiple_rows_test.exs b/test/elixir/test/multiple_rows_test.exs
new file mode 100644
index 000000000..646682823
--- /dev/null
+++ b/test/elixir/test/multiple_rows_test.exs
@@ -0,0 +1,136 @@
+defmodule MultipleRowsTest do
+ use CouchTestCase
+
+ @moduletag :multiple_rows
+
+ @north_carolina_cities ["Charlotte", "Raleigh"]
+ @massachussets_cities ["Boston", "Lowell", "Worcester", "Cambridge", "Springfield"]
+ @florida_cities ["Miami", "Tampa", "Orlando", "Springfield"]
+
+ @moduledoc """
+ Test checking multiple rows
+ This is a port of the multiple_rows.js suite
+ """
+
+ @tag :with_db
+ test "multiple rows", context do
+ db_name = context[:db_name]
+
+ resp1 =
+ Couch.put(
+ "/#{db_name}/NC",
+ body: %{:_id => "NC", :cities => @north_carolina_cities}
+ ).body
+
+ resp2 =
+ Couch.put(
+ "/#{db_name}/MA",
+ body: %{
+ :_id => "MA",
+ :cities => @massachussets_cities
+ }
+ ).body
+
+ resp3 =
+ Couch.put("/#{db_name}/FL", body: %{:_id => "FL", :cities => @florida_cities}).body
+
+ assert resp1["ok"]
+ assert resp2["ok"]
+ assert resp3["ok"]
+
+ %{"rows" => rows, "total_rows" => total_rows} = query_list_cities_and_state(db_name)
+
+ assert Enum.at(rows, 0)["key"] == "Boston, MA"
+ assert Enum.at(rows, 1)["key"] == "Cambridge, MA"
+ assert Enum.at(rows, 2)["key"] == "Charlotte, NC"
+ assert Enum.at(rows, 3)["key"] == "Lowell, MA"
+ assert Enum.at(rows, 4)["key"] == "Miami, FL"
+ assert Enum.at(rows, 5)["key"] == "Orlando, FL"
+ assert Enum.at(rows, 6)["key"] == "Raleigh, NC"
+ assert Enum.at(rows, 7)["key"] == "Springfield, FL"
+ assert Enum.at(rows, 8)["key"] == "Springfield, MA"
+ assert Enum.at(rows, 9)["key"] == "Tampa, FL"
+ assert Enum.at(rows, 10)["key"] == "Worcester, MA"
+
+ assert total_rows === 11
+
+ new_insert_resp =
+ Couch.put(
+ "/#{db_name}/NC",
+ body: %{
+ :id => "NC",
+ :cities => List.insert_at(@north_carolina_cities, -1, "Wilmington"),
+ :_rev => resp1["rev"]
+ }
+ ).body
+
+ assert new_insert_resp["ok"]
+
+ %{"rows" => rows, "total_rows" => total_rows} = query_list_cities_and_state(db_name)
+
+ assert Enum.at(rows, 0)["key"] == "Boston, MA"
+ assert Enum.at(rows, 1)["key"] == "Cambridge, MA"
+ assert Enum.at(rows, 2)["key"] == "Charlotte, NC"
+ assert Enum.at(rows, 3)["key"] == "Lowell, MA"
+ assert Enum.at(rows, 4)["key"] == "Miami, FL"
+ assert Enum.at(rows, 5)["key"] == "Orlando, FL"
+ assert Enum.at(rows, 6)["key"] == "Raleigh, NC"
+ assert Enum.at(rows, 7)["key"] == "Springfield, FL"
+ assert Enum.at(rows, 8)["key"] == "Springfield, MA"
+ assert Enum.at(rows, 9)["key"] == "Tampa, FL"
+ assert Enum.at(rows, 10)["key"] == "Wilmington, NC"
+ assert Enum.at(rows, 11)["key"] == "Worcester, MA"
+
+ assert total_rows === 12
+
+ delete_resp = Couch.delete("/#{db_name}/MA", query: %{:rev => resp2["rev"]}).body
+ assert delete_resp["ok"]
+
+ %{"rows" => rows, "total_rows" => total_rows} = query_list_cities_and_state(db_name)
+
+ assert Enum.at(rows, 0)["key"] == "Charlotte, NC"
+ assert Enum.at(rows, 1)["key"] == "Miami, FL"
+ assert Enum.at(rows, 2)["key"] == "Orlando, FL"
+ assert Enum.at(rows, 3)["key"] == "Raleigh, NC"
+ assert Enum.at(rows, 4)["key"] == "Springfield, FL"
+ assert Enum.at(rows, 5)["key"] == "Tampa, FL"
+ assert Enum.at(rows, 6)["key"] == "Wilmington, NC"
+
+ assert total_rows === 7
+ end
+
+ def query_list_cities_and_state(db_name) do
+ design_doc = %{
+ :_id => "_design/list_cities_and_state",
+ :language => "javascript",
+ :views => %{
+ :view => %{
+ :map => """
+ function(doc) {
+ for (var i = 0; i < doc.cities.length; i++)
+ emit(doc.cities[i] + \", \" + doc._id, null);
+ }
+ """
+ }
+ }
+ }
+
+ design_resp =
+ Couch.put(
+ "/#{db_name}/_design/list_cities_and_state",
+ body: design_doc,
+ query: %{w: 3}
+ )
+
+ assert design_resp.status_code in [201, 202]
+
+ %{:body => result} = Couch.get("/#{db_name}/_design/list_cities_and_state/_view/view")
+
+ Couch.delete(
+ "/#{db_name}/_design/list_cities_and_state",
+ query: %{rev: design_resp.body["rev"]}
+ )
+
+ result
+ end
+end
diff --git a/test/elixir/test/partition_mango_test.exs b/test/elixir/test/partition_mango_test.exs
index 5a5978915..3fd38d52b 100644
--- a/test/elixir/test/partition_mango_test.exs
+++ b/test/elixir/test/partition_mango_test.exs
@@ -633,4 +633,31 @@ defmodule PartitionMangoTest do
%{:body => %{"reason" => reason}} = resp
assert Regex.match?(~r/Partition must not start/, reason)
end
+
+ @tag :with_partitioned_db
+ test "partitioned query sends correct errors for sort errors", context do
+ db_name = context[:db_name]
+ create_partition_docs(db_name)
+
+ url = "/#{db_name}/_partition/foo/_find"
+
+ selector = %{
+ selector: %{
+ some: "field"
+ },
+ sort: ["some"],
+ limit: 50
+ }
+
+ resp = Couch.post(url, body: selector)
+ assert resp.status_code == 400
+ %{:body => %{"reason" => reason}} = resp
+ assert Regex.match?(~r/No partitioned index exists for this sort/, reason)
+
+ url = "/#{db_name}/_find"
+ resp = Couch.post(url, body: selector)
+ assert resp.status_code == 400
+ %{:body => %{"reason" => reason}} = resp
+ assert Regex.match?(~r/No global index exists for this sort/, reason)
+ end
end
diff --git a/test/javascript/tests/etags_head.js b/test/javascript/tests/etags_head.js
index ab5476921..9faca4af6 100644
--- a/test/javascript/tests/etags_head.js
+++ b/test/javascript/tests/etags_head.js
@@ -63,6 +63,10 @@ couchTests.etags_head = function(debug) {
headers: {"if-none-match": etag}
});
T(xhr.status == 304);
+ xhr = CouchDB.request("GET", "/" + db_name + "/1", {
+ headers: {"if-none-match": "W/" + etag}
+ });
+ T(xhr.status == 304);
// fail to delete a doc
xhr = CouchDB.request("DELETE", "/" + db_name + "/1", {