diff options
author | Joan Touzet <wohali@users.noreply.github.com> | 2019-03-09 13:58:45 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-09 13:58:45 -0500 |
commit | df07960f3f26253cae1485ee5d2a19f0f46b3adc (patch) | |
tree | 7859ad4fa37f407a4713e5ec2f3fa3d0951afd55 | |
parent | 46d8c1ae44b11bc83f3a32e6d728f380a76b7352 (diff) | |
parent | d98fd880bfa97f799c1bc6a05f2c888160616c53 (diff) | |
download | couchdb-improve-rfc-template.tar.gz |
Merge branch 'master' into improve-rfc-templateimprove-rfc-template
-rw-r--r-- | src/chttpd/src/chttpd.erl | 45 | ||||
-rw-r--r-- | src/chttpd/src/chttpd_db.erl | 15 | ||||
-rw-r--r-- | src/chttpd/test/chttpd_socket_buffer_size_test.erl | 152 | ||||
-rw-r--r-- | src/couch/priv/stats_descriptions.cfg | 48 | ||||
-rw-r--r-- | src/mango/src/mango_error.erl | 16 | ||||
-rw-r--r-- | src/mango/src/mango_httpd.erl | 9 | ||||
-rw-r--r-- | src/mango/src/mango_idx.erl | 27 | ||||
-rw-r--r-- | test/elixir/README.md | 12 | ||||
-rw-r--r-- | test/elixir/test/attachments.exs | 482 | ||||
-rw-r--r-- | test/elixir/test/multiple_rows_test.exs | 136 | ||||
-rw-r--r-- | test/elixir/test/partition_mango_test.exs | 27 | ||||
-rw-r--r-- | test/javascript/tests/etags_head.js | 4 |
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", { |