diff --git a/Makefile b/Makefile index 5bef9c761c2..7c35c7178d7 100644 --- a/Makefile +++ b/Makefile @@ -151,7 +151,6 @@ fauxton: share/www check-all-tests: all python-black @$(MAKE) emilio @$(MAKE) eunit - @$(MAKE) javascript @$(MAKE) mango-test @$(MAKE) elixir diff --git a/Makefile.win b/Makefile.win index edfc1bf3bd2..21a34f722ca 100644 --- a/Makefile.win +++ b/Makefile.win @@ -136,7 +136,6 @@ fauxton: share\www check: all python-black @$(MAKE) emilio @$(MAKE) eunit - @$(MAKE) javascript @$(MAKE) mango-test @$(MAKE) elixir diff --git a/src/chttpd/src/chttpd_rewrite.erl b/src/chttpd/src/chttpd_rewrite.erl index 01965137466..1c2c1f33326 100644 --- a/src/chttpd/src/chttpd_rewrite.erl +++ b/src/chttpd/src/chttpd_rewrite.erl @@ -71,8 +71,9 @@ do_rewrite(#httpd{mochi_req=MochiReq}=Req, {Props}=Rewrite) when is_list(Props) undefined -> erlang:get(mochiweb_request_body); B -> B end, + NewMochiReq:cleanup(), case Body of - undefined -> NewMochiReq:cleanup(); + undefined -> []; _ -> erlang:put(mochiweb_request_body, Body) end, couch_log:debug("rewrite to ~p", [Path]), diff --git a/test/elixir/README.md b/test/elixir/README.md index 52ce45a75fe..13d74a46346 100644 --- a/test/elixir/README.md +++ b/test/elixir/README.md @@ -54,7 +54,7 @@ X means done, - means partially - [X] Port invalid_docids.js - [X] Port jsonp.js - [X] Port large_docs.js - - [ ] Port list_views.js + - [X] Port list_views.js - [X] Port lorem_b64.txt - [X] Port lorem.txt - [X] Port lots_of_docs.js @@ -89,13 +89,13 @@ X means done, - means partially - [ ] Port replicator_db_write_auth.js - [X] Port rev_stemming.js - [X] Port rewrite.js - - [ ] Port rewrite_js.js + - [X] Port rewrite_js.js - [X] Port security_validation.js - - [ ] Port show_documents.js + - [X] Port show_documents.js - [ ] Port stats.js - [X] Port update_documents.js - [X] Port users_db.js - - [ ] Port users_db_security.js + - [X] Port users_db_security.js - [X] Port utf8.js - [X] Port uuids.js - [X] Port view_collation.js diff --git a/test/elixir/lib/couch.ex b/test/elixir/lib/couch.ex index 094f275a8de..5928d5dd61d 100644 --- a/test/elixir/lib/couch.ex +++ b/test/elixir/lib/couch.ex @@ -40,15 +40,28 @@ defmodule Couch.Session do # Skipping head/patch/options for YAGNI. Feel free to add # if the need arises. - def go(%Couch.Session{} = sess, method, url, opts) do - opts = Keyword.merge(opts, cookie: sess.cookie, base_url: sess.base_url) - Couch.request(method, url, opts) + parse_response = Keyword.get(opts, :parse_response, true) + opts = opts + |> Keyword.merge(cookie: sess.cookie, base_url: sess.base_url) + |> Keyword.delete(:parse_response) + if parse_response do + Couch.request(method, url, opts) + else + Rawresp.request(method, url, opts) + end end def go!(%Couch.Session{} = sess, method, url, opts) do - opts = Keyword.merge(opts, cookie: sess.cookie, base_url: sess.base_url) - Couch.request!(method, url, opts) + parse_response = Keyword.get(opts, :parse_response, true) + opts = opts + |> Keyword.merge(cookie: sess.cookie, base_url: sess.base_url) + |> Keyword.delete(:parse_response) + if parse_response do + Couch.request!(method, url, opts) + else + Rawresp.request!(method, url, opts) + end end end diff --git a/test/elixir/lib/couch_raw.ex b/test/elixir/lib/couch_raw.ex new file mode 100644 index 00000000000..62a0bbd0ed2 --- /dev/null +++ b/test/elixir/lib/couch_raw.ex @@ -0,0 +1,105 @@ +defmodule Rawresp do + use HTTPotion.Base + + @moduledoc """ + HTTP client that provides raw response as result + """ + @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) + 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"]} + end + end +end diff --git a/test/elixir/test/changes_async_test.exs b/test/elixir/test/changes_async_test.exs index 36876aedf0f..001c5d58c83 100644 --- a/test/elixir/test/changes_async_test.exs +++ b/test/elixir/test/changes_async_test.exs @@ -441,106 +441,3 @@ defmodule ChangesAsyncTest do create_doc(db_name, ddoc) end end - -defmodule Rawresp do - use HTTPotion.Base - - @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) - 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"]} - end - end -end diff --git a/test/elixir/test/list_views_test.exs b/test/elixir/test/list_views_test.exs new file mode 100644 index 00000000000..8e6314dfbc4 --- /dev/null +++ b/test/elixir/test/list_views_test.exs @@ -0,0 +1,581 @@ +defmodule ListViewsTest do + use CouchTestCase + + @moduletag kind: :single_node + + @ddoc %{ + _id: "_design/lists", + language: "javascript", + views: %{ + basicView: %{ + map: """ + function(doc) { + emit(doc.integer, doc.string); + } + """ + }, + withReduce: %{ + map: """ + function(doc) { + emit(doc.integer, doc.string); + } + """, + reduce: """ + function(keys, values, rereduce) { + if (rereduce) { + return sum(values); + } else { + return values.length; + } + } + """ + } + }, + lists: %{ + basicBasic: """ + function(head, req) { + send("head"); + var row; + while(row = getRow()) { + send(row.key); + }; + return "tail"; + } + """, + basicJSON: """ + function(head, req) { + start({"headers":{"Content-Type" : "application/json"}}); + send('{"head":'+toJSON(head)+', '); + send('"req":'+toJSON(req)+', '); + send('"rows":['); + var row, sep = ''; + while (row = getRow()) { + send(sep + toJSON(row)); + sep = ', '; + } + return "]}"; + } + """, + simpleForm: """ + function(head, req) { + send('

FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'

'; + } + """, + acceptSwitch: """ + function(head, req) { + // respondWith takes care of setting the proper headers + provides("html", function() { + send("HTML '; + }); + } + """, + qsParams: """ + function(head, req) { + return toJSON(req.query) + "\\n"; + } + """, + stopIter: """ + function(req) { + send("head"); + var row, row_number = 0; + while(row = getRow()) { + if(row_number > 2) break; + send(" " + row_number); + row_number += 1; + }; + return " tail"; + } + """, + stopIter2: """ + function(head, req) { + provides("html", function() { + send("head"); + var row, row_number = 0; + while(row = getRow()) { + if(row_number > 2) break; + send(" " + row_number); + row_number += 1; + }; + return " tail"; + }); + } + """, + tooManyGetRows: """ + function() { + send("head"); + var row; + while(row = getRow()) { + send(row.key); + }; + getRow(); + getRow(); + getRow(); + row = getRow(); + return "after row: "+toJSON(row); + } + """, + emptyList: """ + function() { + return " "; + } + """, + rowError: """ + function(head, req) { + send("head"); + var row = getRow(); + send(fooBarBam); // intentional error + return "tail"; + } + """, + docReference: """ + function(head, req) { + send("head"); + var row = getRow(); + send(row.doc.integer); + return "tail"; + } + """, + secObj: """ + function(head, req) { + return toJSON(req.secObj); + } + """, + setHeaderAfterGotRow: """ + function(head, req) { + getRow(); + start({ + code: 400, + headers: { + "X-My-Header": "MyHeader" + } + }); + send("bad request"); + } + """, + allDocs: """ + function(head, req){ + start({'headers': {'Content-Type': 'application/json'}}); + var resp = head; + var rows = []; + while(row=getRow()){ + rows.push(row); + } + resp.rows = rows; + return toJSON(resp); + } + """ + } + } + + @view_only_design_doc %{ + _id: "_design/views", + language: "javascript", + views: %{ + basicView: %{ + map: """ + function(doc) { + emit(-doc.integer, doc.string); + } + """ + } + } + } + + @erl_list_doc %{ + _id: "_design/erlang", + language: "erlang", + lists: %{ + simple: """ + fun(Head, {Req}) -> + Send(<<"[">>), + Fun = fun({Row}, Sep) -> + Val = couch_util:get_value(<<"key">>, Row, 23), + Send(list_to_binary(Sep ++ integer_to_list(Val))), + {ok, ","} + end, + {ok, _} = FoldRows(Fun, ""), + Send(<<"]">>) + end. + """ + } + } + + setup_all do + db_name = random_db_name() + {:ok, _} = create_db(db_name) + on_exit(fn -> delete_db(db_name) end) + + {:ok, _} = create_doc(db_name, @ddoc) + bulk_save(db_name, make_docs(0..9)) + + # Check setup + resp = view(db_name, "lists/basicView") + assert resp.body["total_rows"] == 10 + + db_name_cross = "#{db_name}_cross" + {:ok, _} = create_db(db_name_cross) + on_exit(fn -> delete_db(db_name_cross) end) + + {:ok, _} = create_doc(db_name_cross, @ddoc) + {:ok, _} = create_doc(db_name_cross, @view_only_design_doc) + bulk_save(db_name_cross, make_docs(0..9)) + + db_name_erlang = "#{db_name}_erlang" + {:ok, _} = create_db(db_name_erlang) + on_exit(fn -> delete_db(db_name_erlang) end) + + {:ok, _} = create_doc(db_name_erlang, @erl_list_doc) + {:ok, _} = create_doc(db_name_erlang, @view_only_design_doc) + bulk_save(db_name_erlang, make_docs(0..9)) + + {:ok, + [db_name: db_name, db_name_cross: db_name_cross, db_name_erlang: db_name_erlang]} + end + + test "standard GET", context do + db_name = context[:db_name] + resp = Rawresp.get("/#{db_name}/_design/lists/_list/basicBasic/basicView") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/head0123456789tail/) + end + + test "standard OPTIONS", context do + db_name = context[:db_name] + resp = Rawresp.options("/#{db_name}/_design/lists/_list/basicBasic/basicView") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/head0123456789tail/) + end + + test "the richness of the arguments", context do + db_name = context[:db_name] + + resp = + Couch.get("/#{db_name}/_design/lists/_list/basicJSON/basicView?update_seq=true") + + assert resp.status_code == 200 + assert resp.body["head"]["total_rows"] == 10 + assert resp.body["head"]["offset"] == 0 + assert length(resp.body["rows"]) == 10 + assert Enum.at(resp.body["rows"], 0) == %{"id" => "0", "key" => 0, "value" => "0"} + assert resp.body["req"]["info"]["db_name"] == db_name + assert resp.body["req"]["method"] == "GET" + + assert resp.body["req"]["path"] == [ + db_name, + "_design", + "lists", + "_list", + "basicJSON", + "basicView" + ] + + assert Map.has_key?(resp.body["req"]["headers"], "Host") == true + assert Map.has_key?(resp.body["req"]["headers"], "User-Agent") == true + assert Map.has_key?(resp.body["req"], "cookie") + + assert resp.body["req"]["raw_path"] == + "/#{db_name}/_design/lists/_list/basicJSON/basicView?update_seq=true" + end + + test "get with query params", context do + db_name = context[:db_name] + + resp = + Rawresp.get( + "/#{db_name}/_design/lists/_list/simpleForm/basicView?startkey=3&endkey=8" + ) + + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: 1/) + assert String.match?(resp.body, ~r/FirstKey: 3/) + assert String.match?(resp.body, ~r/LastKey: 8/) + end + + test "with 0 rows", context do + db_name = context[:db_name] + + resp = Rawresp.get("/#{db_name}/_design/lists/_list/simpleForm/basicView?startkey=30") + + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/<\/ul>/) + end + + test "too many Get Rows", context do + db_name = context[:db_name] + + resp = Rawresp.get("/#{db_name}/_design/lists/_list/tooManyGetRows/basicView") + + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/9after row: null/) + end + + test "reduce with 0 rows", context do + db_name = context[:db_name] + + resp = + Rawresp.get("/#{db_name}/_design/lists/_list/simpleForm/withReduce?startkey=30") + + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/LastKey: undefined/) + end + + test "when there is a reduce present, but not used", context do + db_name = context[:db_name] + + resp = + Rawresp.get("/#{db_name}/_design/lists/_list/simpleForm/withReduce?reduce=false") + + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Key: 1/) + end + + test "when there is a reduce present, and used", context do + db_name = context[:db_name] + + resp = Rawresp.get("/#{db_name}/_design/lists/_list/simpleForm/withReduce?group=true") + + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Key: 1/) + end + + test "empty list", context do + db_name = context[:db_name] + + resp = Rawresp.get("/#{db_name}/_design/lists/_list/emptyList/basicView") + assert String.match?(resp.body, ~r/^ $/) + + resp = Rawresp.get("/#{db_name}/_design/lists/_list/emptyList/withReduce?group=true") + assert String.match?(resp.body, ~r/^ $/) + end + + test "multi-key fetch with POST", context do + db_name = context[:db_name] + + resp = + Rawresp.post("/#{db_name}/_design/lists/_list/simpleForm/basicView", + body: %{keys: [2, 4, 5, 7]} + ) + + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: 1/) + assert String.match?(resp.body, ~r/Key: 2/) + assert String.match?(resp.body, ~r/FirstKey: 2/) + assert String.match?(resp.body, ~r/LastKey: 7/) + end + + test "multi-key fetch with GET", context do + db_name = context[:db_name] + + resp = + Rawresp.get("/#{db_name}/_design/lists/_list/simpleForm/basicView?keys=[2,4,5,7]") + + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: 1/) + assert String.match?(resp.body, ~r/Key: 2/) + assert String.match?(resp.body, ~r/FirstKey: 2/) + assert String.match?(resp.body, ~r/LastKey: 7/) + end + + test "no multi-key fetch allowed when group=false", context do + db_name = context[:db_name] + + resp = + Rawresp.post("/#{db_name}/_design/lists/_list/simpleForm/withReduce?group=false", + body: %{keys: [2, 4, 5, 7]} + ) + + assert resp.status_code == 400 + assert String.match?(resp.body, ~r/query_parse_error/) + + resp = Rawresp.get("/#{db_name}/_design/lists/_list/rowError/basicView") + assert String.match?(resp.body, ~r/ReferenceError/) + end + + test "with include_docs and a reference to the doc", context do + db_name = context[:db_name] + + resp = + Rawresp.get( + "/#{db_name}/_design/lists/_list/docReference/basicView?include_docs=true" + ) + + assert String.match?(resp.body, ~r/head0tail/) + end + + test "extra qs params", context do + db_name = context[:db_name] + resp = Rawresp.get("/#{db_name}/_design/lists/_list/qsParams/basicView?foo=blam") + assert String.match?(resp.body, ~r/blam/) + end + + test "stop iteration", context do + db_name = context[:db_name] + resp = Rawresp.get("/#{db_name}/_design/lists/_list/stopIter/basicView") + assert String.match?(resp.body, ~r/^head 0 1 2 tail$/) + + resp = + Rawresp.get("/#{db_name}/_design/lists/_list/stopIter2/basicView", + headers: [Accept: "text/html"] + ) + + assert String.match?(resp.body, ~r/^head 0 1 2 tail$/) + end + + test "abort iteration with reduce", context do + db_name = context[:db_name] + + resp = Rawresp.get("/#{db_name}/_design/lists/_list/stopIter/withReduce?group=true") + assert String.match?(resp.body, ~r/^head 0 1 2 tail$/) + + resp = + Rawresp.get("/#{db_name}/_design/lists/_list/stopIter2/withReduce?group=true", + headers: [Accept: "text/html"] + ) + + assert String.match?(resp.body, ~r/^head 0 1 2 tail$/) + end + + test "with accept headers for HTML", context do + db_name = context[:db_name] + + resp = + Rawresp.get("/#{db_name}/_design/lists/_list/acceptSwitch/basicView", + headers: [Accept: "text/html"] + ) + + 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 + + test "we can run lists and views from separate docs", context do + db_name = context[:db_name_cross] + + resp = + Rawresp.get( + "/#{db_name}/_design/lists/_list/simpleForm/views/basicView?startkey=-3" + ) + + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: -4/) + assert String.match?(resp.body, ~r/FirstKey: -3/) + assert String.match?(resp.body, ~r/LastKey: 0/) + end + + test "we do multi-key requests on lists and views in separate docs", context do + db_name = context[:db_name_cross] + + resp = + Rawresp.post( + "/#{db_name}/_design/lists/_list/simpleForm/views/basicView", + body: %{keys: [-2, -4, -5, -7]} + ) + + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: -3/) + assert String.match?(resp.body, ~r/Key: -7/) + assert String.match?(resp.body, ~r/FirstKey: -2/) + assert String.match?(resp.body, ~r/LastKey: -7/) + end + + test "secObj is available", context do + db_name = context[:db_name] + + resp = Couch.get("/#{db_name}/_design/lists/_list/secObj/basicView") + assert resp.status_code == 200 + assert is_map(resp.body) + end + + test "multiple languages in design docs", context do + db_name = context[:db_name_erlang] + + resp = + Couch.get("/#{db_name}/_design/erlang/_list/simple/views/basicView?startkey=-3") + + assert resp.status_code == 200 + assert length(resp.body) == 4 + + for i <- 0..3 do + assert Enum.at(resp.body, i) + 3 == i + end + end + + @tag :with_db + test "COUCHDB-1113", context do + db_name = context[:db_name] + + ddoc = %{ + _id: "_design/test", + views: %{ + me: %{ + map: "function(doc) { emit(null,null)}" + } + }, + lists: %{ + you: """ + function(head, req) { + var row; + while(row = getRow()) { + send(row); + } + } + """ + } + } + + {:ok, _} = create_doc(db_name, ddoc) + + resp = + Couch.get("/#{db_name}/_design/test/_list/you/me", + headers: [ + "Content-Type": "application/x-www-form-urlencoded" + ] + ) + + assert resp.status_code == 200 + end + + test "HTTP header response set after getRow() called in _list function", context do + db_name = context[:db_name] + + resp = Rawresp.get("/#{db_name}/_design/lists/_list/setHeaderAfterGotRow/basicView") + assert resp.status_code == 400 + assert resp.headers["X-My-Header"] == "MyHeader" + assert String.match?(resp.body, ~r/^bad request$/) + end + + test "handling _all_docs by _list functions. the result should be equal", context do + db_name = context[:db_name] + + resp_list = Couch.get("/#{db_name}/_design/lists/_list/allDocs/_all_docs") + assert resp_list.status_code == 200 + + resp_alldocs = Couch.get("/#{db_name}/_all_docs") + + assert resp_list.body["total_rows"] == resp_alldocs.body["total_rows"] + assert resp_list.body["offset"] == resp_alldocs.body["offset"] + assert length(resp_list.body["rows"]) == length(resp_alldocs.body["rows"]) + assert resp_list.body["rows"] == resp_alldocs.body["rows"] + end +end diff --git a/test/elixir/test/rewrite_js_test.exs b/test/elixir/test/rewrite_js_test.exs new file mode 100644 index 00000000000..a3adb3e7d4b --- /dev/null +++ b/test/elixir/test/rewrite_js_test.exs @@ -0,0 +1,411 @@ +defmodule RewriteJSTest do + use CouchTestCase + + @moduletag :js_engine + @moduletag kind: :single_node + + @moduledoc """ + Test CouchDB rewrites JS + This is a port of the rewrite_js.js suite + """ + + @ddoc %{ + _id: "_design/test", + language: "javascript", + _attachments: %{ + "foo.txt": %{ + content_type: "text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + }, + rewrites: """ + function(req) { + prefix = req.path[4]; + if (prefix === 'foo') { + return 'foo.txt'; + } + if (prefix === 'foo2') { + return {path: 'foo.txt', method: 'GET'}; + } + if (prefix === 'hello') { + if (req.method != 'PUT') { + return + } + id = req.path[5]; + return {path: '_update/hello/' + id}; + } + if (prefix === 'welcome') { + if (req.path.length == 6){ + name = req.path[5]; + return {path: '_show/welcome', query: {'name': name}}; + } + return '_show/welcome'; + } + if (prefix === 'welcome2') { + return {path: '_show/welcome', query: {'name': 'user'}}; + } + if (prefix === 'welcome3') { + name = req.path[5]; + if (req.method == 'PUT') { + path = '_update/welcome2/' + name; + } else if (req.method == 'GET') { + path = '_show/welcome2/' + name; + } else { + return; + } + return path; + } + if (prefix === 'welcome4') { + return {path: '_show/welcome3', query: {name: req.path[5]}}; + } + if (prefix === 'welcome5') { + rest = req.path.slice(5).join('/'); + return {path: '_show/' + rest, query: {name: rest}}; + } + if (prefix === 'basicView') { + rest = req.path.slice(5).join('/'); + return {path: '_view/basicView'}; + } + if (req.path.slice(4).join('/') === 'simpleForm/basicView') { + return {path: '_list/simpleForm/basicView'}; + } + if (req.path.slice(4).join('/') === 'simpleForm/basicViewFixed') { + return {path: '_list/simpleForm/basicView', + query: {startkey: '"3"', endkey: '"8"'}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView') { + return {path: '_list/simpleForm/complexView', + query: {key: JSON.stringify([1,2])}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView2') { + return {path: '_list/simpleForm/complexView', + query: {key: JSON.stringify(['test', {}])}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView3') { + return {path: '_list/simpleForm/complexView', + query: {key: JSON.stringify(['test', ['test', 'essai']])}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView4') { + return {path: '_list/simpleForm/complexView2', + query: {key: JSON.stringify({"c": 1})}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/sendBody1') { + return {path: '_list/simpleForm/complexView2', + method: 'POST', + query: {limit: '1'}, + headers:{'Content-type':'application/json'}, + body: JSON.stringify( {keys: [{"c": 1}]} )}; + } + if (req.path.slice(4).join('/') === '/') { + return {path: '_view/basicView'}; + } + if (prefix === 'db') { + return {path: '../../' + req.path.slice(5).join('/')}; + } + } + """, + lists: %{ + simpleForm: """ + function(head, req) { + send('

FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'

'; + } + """ + }, + shows: %{ + welcome: """ + function(doc,req) { + return "Welcome " + req.query["name"]; + } + """, + welcome2: """ + function(doc, req) { + return "Welcome " + doc.name; + } + """, + welcome3: """ + function(doc,req) { + return "Welcome " + req.query["name"]; + } + """ + }, + updates: %{ + hello: """ + function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id : req.id + }, "New World"] + } + return [null, "Empty World"]; + } + doc.world = "hello"; + doc.edited_by = req.userCtx; + return [doc, "hello doc"]; + } + """, + welcome2: """ + function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id: req.id, + name: req.id + }, "New World"] + } + return [null, "Empty World"]; + } + return [doc, "hello doc"]; + } + """ + }, + views: %{ + basicView: %{ + map: """ + function(doc) { + if (doc.integer) { + emit(doc.integer, doc.string); + } + } + """ + }, + complexView: %{ + map: """ + function(doc) { + if (doc.type == "complex") { + emit([doc.a, doc.b], doc.string); + } + } + """ + }, + complexView2: %{ + map: """ + function(doc) { + if (doc.type == "complex") { + emit(doc.a, doc.string); + } + } + """ + }, + complexView3: %{ + map: """ + function(doc) { + if (doc.type == "complex") { + emit(doc.b, doc.string); + } + } + """ + } + } + } + + Enum.each( + ["test_rewrite_suite_db", "test_rewrite_suite_db%2Fwith_slashes"], + fn db_name -> + @tag with_random_db: db_name + test "Test basic js rewrites on #{db_name}", context do + db_name = context[:db_name] + + create_doc(db_name, @ddoc) + + docs1 = make_docs(0..9) + bulk_save(db_name, docs1) + + docs2 = [ + %{"a" => 1, "b" => 1, "string" => "doc 1", "type" => "complex"}, + %{"a" => 1, "b" => 2, "string" => "doc 2", "type" => "complex"}, + %{"a" => "test", "b" => %{}, "string" => "doc 3", "type" => "complex"}, + %{ + "a" => "test", + "b" => ["test", "essai"], + "string" => "doc 4", + "type" => "complex" + }, + %{"a" => %{"c" => 1}, "b" => "", "string" => "doc 5", "type" => "complex"} + ] + + bulk_save(db_name, docs2) + + # 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" + + 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" + + # Test POST, hello update world + resp = + Couch.post("/#{db_name}", body: %{"word" => "plankton", "name" => "Rusty"}).body + + assert resp["ok"] + doc_id = resp["id"] + assert doc_id + + 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 Couch.get("/#{db_name}/#{doc_id}").body["world"] == "hello" + + resp = Couch.get("/#{db_name}/_design/test/_rewrite/welcome?name=user") + assert resp.body == "Welcome user" + + resp = Couch.get("/#{db_name}/_design/test/_rewrite/welcome/user") + assert resp.body == "Welcome user" + + resp = Couch.get("/#{db_name}/_design/test/_rewrite/welcome2") + assert resp.body == "Welcome user" + + 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/) + + resp = Couch.get("/#{db_name}/_design/test/_rewrite/welcome3/test") + assert resp.body == "Welcome test" + + resp = Couch.get("/#{db_name}/_design/test/_rewrite/welcome4/user") + assert resp.body == "Welcome user" + + resp = Couch.get("/#{db_name}/_design/test/_rewrite/welcome5/welcome3") + assert resp.body == "Welcome welcome3" + + resp = Couch.get("/#{db_name}/_design/test/_rewrite/basicView") + assert resp.status_code == 200 + assert resp.body["total_rows"] == 9 + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/complexView") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/FirstKey: [1, 2]/) + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/complexView2") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 3/) + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/complexView3") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 4/) + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/complexView4") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 5/) + + # COUCHDB-1612 - send body rewriting get to post + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/sendBody1") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 5 LineNo: 1/) + + resp = Couch.get("/#{db_name}/_design/test/_rewrite/db/_design/test?meta=true") + assert resp.status_code == 200 + assert resp.body["_id"] == "_design/test" + assert Map.has_key?(resp.body, "_revs_info") + end + + @tag with_random_db: db_name + test "early response on #{db_name}", context do + db_name = context[:db_name] + + ddoc = %{ + _id: "_design/response", + rewrites: """ + function(req){ + status = parseInt(req.query.status); + return {code: status, + body: JSON.stringify({"status": status}), + headers: {'x-foo': 'bar', 'Content-Type': 'application/json'}}; + } + """ + } + + create_doc(db_name, ddoc) + + resp = Couch.get("/#{db_name}/_design/response/_rewrite?status=200") + assert resp.status_code == 200 + assert resp.headers["x-foo"] == "bar" + assert resp.body["status"] == 200 + + resp = Couch.get("/#{db_name}/_design/response/_rewrite?status=451") + assert resp.status_code == 451 + assert resp.headers["Content-Type"] == "application/json" + + resp = Couch.get("/#{db_name}/_design/response/_rewrite?status=500") + assert resp.status_code == 500 + end + + @tag with_random_db: db_name + test "path relative to server on #{db_name}", context do + db_name = context[:db_name] + + ddoc = %{ + _id: "_design/relative", + rewrites: """ + function(req){ + return '../../../_uuids' + } + """ + } + + create_doc(db_name, ddoc) + resp = Couch.get("/#{db_name}/_design/relative/_rewrite/uuids") + assert resp.status_code == 200 + assert length(resp.body["uuids"]) == 1 + end + + @tag with_random_db: db_name + test "loop on #{db_name}", context do + db_name = context[:db_name] + + ddoc_loop = %{ + _id: "_design/loop", + rewrites: """ + function(req) { + return '_rewrite/loop'; + } + """ + } + + create_doc(db_name, ddoc_loop) + resp = Couch.get("/#{db_name}/_design/loop/_rewrite/loop") + assert resp.status_code == 400 + end + + @tag with_random_db: db_name + test "requests with body preserve the query string rewrite on #{db_name}", + context do + db_name = context[:db_name] + + ddoc_qs = %{ + _id: "_design/qs", + rewrites: + "function (r) { return {path: '../../_changes', query: {'filter': '_doc_ids'}};};" + } + + create_doc(db_name, ddoc_qs) + create_doc(db_name, %{_id: "qs1"}) + create_doc(db_name, %{_id: "qs2"}) + + resp = + Couch.post("/#{db_name}/_design/qs/_rewrite", + body: %{doc_ids: ["qs2"]} + ) + + assert resp.status_code == 200 + assert length(resp.body["results"]) == 1 + assert Enum.at(resp.body["results"], 0)["id"] == "qs2" + end + end + ) +end diff --git a/test/elixir/test/rewrite_test.exs b/test/elixir/test/rewrite_test.exs index daa2a80a8a4..75f198568f7 100644 --- a/test/elixir/test/rewrite_test.exs +++ b/test/elixir/test/rewrite_test.exs @@ -349,15 +349,178 @@ defmodule RewriteTest do assert resp.status_code == 200 assert resp.body["total_rows"] == 9 - # TODO: port _list function tests and everything below in rewrite.js - # This is currently broken because _list funcitons default to application/json - # response bodies and my attempts to change the content-type from within the - # _list function have not yet succeeded. - # - # Test GET with query params - # resp = Couch.get("/#{db_name}/_design/test/_rewrite/simpleForm/basicView", query: %{startkey: 3, endkey: 8}) - # Logger.error("GOT RESP: #{inspect resp.body}") - # assert resp.status_code == 200 + resp = + Rawresp.get( + "/#{db_name}/_design/test/_rewrite/simpleForm/basicView?startkey=3&endkey=8" + ) + + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: 1/) + assert String.match?(resp.body, ~r/FirstKey: 3/) + assert String.match?(resp.body, ~r/LastKey: 8/) + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/basicViewFixed") + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: 1/) + assert String.match?(resp.body, ~r/FirstKey: 3/) + assert String.match?(resp.body, ~r/LastKey: 8/) + + resp = + Rawresp.get( + "/#{db_name}/_design/test/_rewrite/simpleForm/basicViewFixed?startkey=4" + ) + + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: 1/) + assert String.match?(resp.body, ~r/FirstKey: 3/) + assert String.match?(resp.body, ~r/LastKey: 8/) + + resp = + Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/basicViewPath/3/8") + + assert resp.status_code == 200 + assert not String.match?(resp.body, ~r/Key: 1/) + assert String.match?(resp.body, ~r/FirstKey: 3/) + assert String.match?(resp.body, ~r/LastKey: 8/) + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/complexView") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/FirstKey: [1, 2]/) + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/complexView2") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 3/) + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/complexView3") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 4/) + + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/simpleForm/complexView4") + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 5/) + + resp = + Rawresp.get( + "/#{db_name}/_design/test/_rewrite/simpleForm/complexView5/test/essai" + ) + + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 4/) + + resp = + Rawresp.get( + "/#{db_name}/_design/test/_rewrite/simpleForm/complexView6?a=test&b=essai" + ) + + assert resp.status_code == 200 + assert String.match?(resp.body, ~r/Value: doc 4/) + + resp = + Rawresp.get( + "/#{db_name}/_design/test/_rewrite/simpleForm/complexView7/test/essai?doc=true" + ) + + assert resp.status_code == 200 + result = resp.body |> IO.iodata_to_binary() |> :jiffy.decode([:return_maps]) + first_row = Enum.at(result["rows"], 0) + assert Map.has_key?(first_row, "doc") + + # COUCHDB-2031 - path normalization versus qs params + resp = Rawresp.get("/#{db_name}/_design/test/_rewrite/db/_design/test?meta=true") + assert resp.status_code == 200 + result = resp.body |> IO.iodata_to_binary() |> :jiffy.decode([:return_maps]) + assert result["_id"] == "_design/test" + assert Map.has_key?(result, "_revs_info") + + ddoc2 = %{ + _id: "_design/test2", + rewrites: [ + %{ + from: "uuids", + to: "../../../_uuids" + } + ] + } + + create_doc(db_name, ddoc2) + resp = Couch.get("/#{db_name}/_design/test2/_rewrite/uuids") + assert resp.status_code == 500 + assert resp.body["error"] == "insecure_rewrite_rule" + end + + @tag with_random_db: db_name + @tag config: [ + {"httpd", "secure_rewrites", "false"} + ] + test "path relative to server on #{db_name}", context do + db_name = context[:db_name] + + ddoc = %{ + _id: "_design/test2", + rewrites: [ + %{ + from: "uuids", + to: "../../../_uuids" + } + ] + } + + create_doc(db_name, ddoc) + + resp = Couch.get("/#{db_name}/_design/test2/_rewrite/uuids") + assert resp.status_code == 200 + assert length(resp.body["uuids"]) == 1 + end + + @tag with_random_db: db_name + @tag config: [ + {"httpd", "rewrite_limit", "2"} + ] + test "loop detection on #{db_name}", context do + db_name = context[:db_name] + + ddoc_loop = %{ + _id: "_design/loop", + rewrites: [%{from: "loop", to: "_rewrite/loop"}] + } + + create_doc(db_name, ddoc_loop) + + resp = Couch.get("/#{db_name}/_design/loop/_rewrite/loop") + assert resp.status_code == 400 + end + + @tag with_random_db: db_name + @tag config: [ + {"httpd", "rewrite_limit", "2"}, + {"httpd", "secure_rewrites", "false"} + ] + test "serial execution is not spuriously counted as loop on #{db_name}", context do + db_name = context[:db_name] + + ddoc = %{ + _id: "_design/test", + language: "javascript", + _attachments: %{ + "foo.txt": %{ + content_type: "text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + }, + rewrites: [ + %{ + from: "foo", + to: "foo.txt" + } + ] + } + + create_doc(db_name, ddoc) + + for _i <- 0..4 do + resp = Couch.get("/#{db_name}/_design/test/_rewrite/foo") + assert resp.status_code == 200 + end end end ) diff --git a/test/elixir/test/security_validation_test.exs b/test/elixir/test/security_validation_test.exs index fda928cde7e..dddf7a7b899 100644 --- a/test/elixir/test/security_validation_test.exs +++ b/test/elixir/test/security_validation_test.exs @@ -21,6 +21,10 @@ defmodule SecurityValidationTest do spike_cat: [ # spike:cat - which is wrong authorization: "Basic c3Bpa2U6Y2F0" + ], + spike: [ + # spike:dog + authorization: "Basic c3Bpa2U6ZG9n" ] } @@ -147,6 +151,15 @@ defmodule SecurityValidationTest do assert resp.body["userCtx"]["roles"] == [] end + @tag :with_db + test "try to set a wrong value for _security", context do + db_name = context[:db_name] + # try to do something lame + resp = Couch.put("/#{db_name}/_security", body: ["foo"]) + assert resp.status_code == 400 + assert resp.body["error"] == "bad_request" + end + @tag :with_db test "Author presence and user security", context do db_name = context[:db_name] @@ -179,6 +192,12 @@ defmodule SecurityValidationTest do assert resp.status_code == 403 assert resp.body["error"] == "forbidden" + # Admin cannot write the document (admin_override = false) + test_doc = Map.put(test_doc, "foo", 3) + resp = Couch.put("/#{db_name}/test_doc", body: test_doc) + assert resp.status_code == 401 + assert resp.body["error"] == "unauthorized" + # Enable admin override for changing author values assert Couch.put("/#{db_name}/_security", body: %{sec_obj | admin_override: true}).body[ "ok" @@ -202,136 +221,103 @@ defmodule SecurityValidationTest do resp = Couch.delete("/#{db_name}/test_doc?rev=#{test_doc["_rev"]}", opts) resp.status_code == 401 and resp.body["error"] == "unauthorized" end) + + # Admin can write the document (admin_override = true) + test_doc = Map.put(test_doc, "foo", 4) + resp = Couch.put("/#{db_name}/test_doc", body: test_doc) + assert resp.body["ok"] + + # Disable admin override + assert Couch.put("/#{db_name}/_security", body: %{sec_obj | admin_override: false}).body[ + "ok" + ] + + docs = [%{_id: "bahbah", author: "jerry", foo: "bar"}, %{_id: "fahfah", foo: "baz"}] + + resp = + Couch.post( + "/#{db_name}/_bulk_docs", + body: %{ + docs: docs + }, + headers: jerry + ) + + assert Enum.at(resp.body, 0)["rev"] + assert !Enum.at(resp.body, 0)["error"] + assert !Enum.at(resp.body, 1)["rev"] + assert Enum.at(resp.body, 1)["error"] == "forbidden" + + resp = Couch.get("/#{db_name}/bahbah") + assert resp.status_code == 200 + + resp = Couch.get("/#{db_name}/fahfah") + assert resp.status_code == 404 end -end -# TODO: port remainder of security_validation.js suite -# remaining bits reproduced below: -# -# // try to do something lame -# try { -# db.setDbProperty("_security", ["foo"]); -# T(false && "can't do this"); -# } catch(e) {} -# -# // go back to normal -# T(db.setDbProperty("_security", {admin_override : false}).ok); -# -# // Now delete document -# T(user2Db.deleteDoc(doc).ok); -# -# // now test bulk docs -# var docs = [{_id:"bahbah",author:"jerry",foo:"bar"},{_id:"fahfah",foo:"baz"}]; -# -# // Create the docs -# var results = db.bulkSave(docs); -# -# T(results[0].rev) -# T(results[0].error == undefined) -# T(results[1].rev === undefined) -# T(results[1].error == "forbidden") -# -# T(db.open("bahbah")); -# T(db.open("fahfah") == null); -# -# -# // now all or nothing with a failure - no more available on cluster -# /* var docs = [ -# {_id:"booboo",author:"Damien Katz",foo:"bar"},{_id:"foofoo",foo:"baz"} -# ]; -# -# // Create the docs -# var results = db.bulkSave(docs, {all_or_nothing:true}); -# -# T(results.errors.length == 1); -# T(results.errors[0].error == "forbidden"); -# T(db.open("booboo") == null); -# T(db.open("foofoo") == null); -# */ -# -# // Now test replication -# var AuthHeaders = {"Authorization": "Basic c3Bpa2U6ZG9n"}; // spike -# adminDbA = new CouchDB("" + db_name + "_a", {"X-Couch-Full-Commit":"false"}); -# adminDbB = new CouchDB("" + db_name + "_b", {"X-Couch-Full-Commit":"false"}); -# var dbA = new CouchDB("" + db_name + "_a", AuthHeaders); -# var dbB = new CouchDB("" + db_name + "_b", AuthHeaders); -# // looping does not really add value as the scenario is the same anyway -# // (there's nothing 2 be gained from it) -# var A = CouchDB.protocol + CouchDB.host + "/" + db_name + "_a"; -# var B = CouchDB.protocol + CouchDB.host + "/" + db_name + "_b"; -# -# // (the databases never exist b4 - and we made sure they're deleted below) -# //adminDbA.deleteDb(); -# adminDbA.createDb(); -# //adminDbB.deleteDb(); -# adminDbB.createDb(); -# -# // save and replicate a documents that will and will not pass our design -# // doc validation function. -# T(dbA.save({_id:"foo1",value:"a",author:"tom"}).ok); -# T(dbA.save({_id:"foo2",value:"a",author:"spike"}).ok); -# T(dbA.save({_id:"bad1",value:"a"}).ok); -# -# T(CouchDB.replicate(A, B, {headers:AuthHeaders}).ok); -# T(CouchDB.replicate(B, A, {headers:AuthHeaders}).ok); -# -# T(dbA.open("foo1")); -# T(dbB.open("foo1")); -# T(dbA.open("foo2")); -# T(dbB.open("foo2")); -# -# // save the design doc to dbA -# delete designDoc._rev; // clear rev from previous saves -# T(adminDbA.save(designDoc).ok); -# -# // no affect on already saved docs -# T(dbA.open("bad1")); -# -# // Update some docs on dbB. Since the design hasn't replicated, anything -# // is allowed. -# -# // this edit will fail validation on replication to dbA (no author) -# T(dbB.save({_id:"bad2",value:"a"}).ok); -# -# // this edit will fail security on replication to dbA (wrong author -# // replicating the change) -# var foo1 = dbB.open("foo1"); -# foo1.value = "b"; -# T(dbB.save(foo1).ok); -# -# // this is a legal edit -# var foo2 = dbB.open("foo2"); -# foo2.value = "b"; -# T(dbB.save(foo2).ok); -# -# var results = CouchDB.replicate({"url": B, "headers": AuthHeaders}, -# {"url": A, "headers": AuthHeaders}, {headers:AuthHeaders}); -# T(results.ok); -# TEquals(1, results.history[0].docs_written); -# TEquals(2, results.history[0].doc_write_failures); -# -# // bad2 should not be on dbA -# T(dbA.open("bad2") == null); -# -# // The edit to foo1 should not have replicated. -# T(dbA.open("foo1").value == "a"); -# -# // The edit to foo2 should have replicated. -# T(dbA.open("foo2").value == "b"); -# }); -# -# // cleanup -# db.deleteDb(); -# if(adminDbA){ -# adminDbA.deleteDb(); -# } -# if(adminDbB){ -# adminDbB.deleteDb(); -# } -# authDb.deleteDb(); -# // have to clean up authDb on the backside :( -# var req = CouchDB.newXhr(); -# req.open("DELETE", "http://127.0.0.1:15986/" + authDb_name, false); -# req.send(""); -# CouchDB.maybeThrowError(req); -# }; + test "Author presence and user security when replicated", _context do + db_name = random_db_name() + db_name_a = "#{db_name}_a" + db_name_b = "#{db_name}_b" + create_db(db_name_a) + create_db(db_name_b) + on_exit(fn -> delete_db(db_name_a) end) + on_exit(fn -> delete_db(db_name_b) end) + + spike = @auth_headers[:spike] + + # save and replicate a documents that will and will not pass our design + # doc validation function. + {:ok, _} = create_doc(db_name_a, %{_id: "foo1", value: "a", author: "tom"}) + {:ok, _} = create_doc(db_name_a, %{_id: "foo2", value: "a", author: "spike"}) + {:ok, _} = create_doc(db_name_a, %{_id: "bad1", value: "a"}) + replicate(db_name_a, db_name_b, headers: spike) + replicate(db_name_b, db_name_a, headers: spike) + + assert Couch.get("/#{db_name_a}/foo1").status_code == 200 + assert Couch.get("/#{db_name_b}/foo1").status_code == 200 + assert Couch.get("/#{db_name_a}/foo2").status_code == 200 + assert Couch.get("/#{db_name_b}/foo2").status_code == 200 + + {:ok, _} = create_doc(db_name_a, @ddoc) + + # no affect on already saved docs + assert Couch.get("/#{db_name_a}/bad1").status_code == 200 + + # Update some docs on dbB. Since the design hasn't replicated, anything + # is allowed. + + # this edit will fail validation on replication to dbA (no author) + assert Couch.post( + "/#{db_name_b}", + body: %{id: "bad2", value: "a"}, + headers: spike + ).body["ok"] + + # this edit will fail security on replication to dbA (wrong author + # replicating the change) + foo1 = Couch.get("/#{db_name_b}/foo1").body + foo1 = Map.put(foo1, "value", "b") + assert Couch.put("/#{db_name_b}/foo1", body: foo1, headers: spike).body["ok"] + + # this is a legal edit + foo2 = Couch.get("/#{db_name_b}/foo2").body + foo2 = Map.put(foo2, "value", "b") + assert Couch.put("/#{db_name_b}/foo2", body: foo2, headers: spike).body["ok"] + + result = replicate(db_name_b, db_name_a, headers: spike) + assert Enum.at(result["history"], 0)["docs_written"] == 1 + assert Enum.at(result["history"], 0)["doc_write_failures"] == 2 + + # bad2 should not be on dbA + assert Couch.get("/#{db_name_a}/bad2").status_code == 404 + + # The edit to foo1 should not have replicated. + resp = Couch.get("/#{db_name_a}/foo1") + assert resp.body["value"] == "a" + + # The edit to foo2 should have replicated. + resp = Couch.get("/#{db_name_a}/foo2") + assert resp.body["value"] == "b" + end +end diff --git a/test/elixir/test/show_documents_test.exs b/test/elixir/test/show_documents_test.exs new file mode 100644 index 00000000000..a574c72b18d --- /dev/null +++ b/test/elixir/test/show_documents_test.exs @@ -0,0 +1,448 @@ +defmodule ShowDocumentsTest do + use CouchTestCase + + @moduletag kind: :single_node + + @ddoc %{ + _id: "_design/template", + language: "javascript", + shows: %{ + hello: """ + function(doc, req) { + if (doc) { + return "Hello World"; + } else { + if(req.id) { + return "New World"; + } else { + return "Empty World"; + } + } + } + """, + "just-name": """ + function(doc, req) { + if (doc) { + return { + body : "Just " + doc.name + }; + } else { + return { + body : "No such doc", + code : 404 + }; + } + } + """, + json: """ + function(doc, req) { + return { + json : doc + } + } + """, + "req-info": """ + function(doc, req) { + return { + json : req + } + } + """, + "show-deleted": """ + function(doc, req) { + if(doc) { + return doc._id; + } else { + return "No doc " + req.id; + } + } + """, + "render-error": """ + function(doc, req) { + return noSuchVariable; + } + """, + empty: """ + function(doc, req) { + return ""; + } + """, + fail: """ + function(doc, req) { + return doc._id; + } + """, + "no-set-etag": """ + function(doc, req) { + return { + headers : { + "Etag" : "skipped" + }, + "body" : "something" + } + } + """, + "list-api": """ + function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey"); + } + """, + "list-api-provides": """ + function(doc, req) { + provides("text", function(){ + send("foo, "); + send("bar, "); + send("baz!"); + }) + } + """, + "list-api-provides-and-return": """ + function(doc, req) { + provides("text", function(){ + send("4, "); + send("5, "); + send("6, "); + return "7!"; + }) + send("1, "); + send("2, "); + return "3, "; + } + """, + "list-api-mix": """ + function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey "); + return "Dude"; + } + """, + "list-api-mix-with-header": """ + function(doc, req) { + start({"X-Couch-Test-Header": "Yeah"}); + send("Hey "); + return { + headers: { + "X-Couch-Test-Header-Awesome": "Oh Yeah!" + }, + body: "Dude" + }; + } + """, + "accept-switch": """ + function(doc, req) { + if (req.headers["Accept"].match(/image/)) { + return { + // a 16x16 px version of the CouchDB logo + "base64" : + ["iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAsV", + "BMVEUAAAD////////////////////////5ur3rEBn////////////////wDBL/", + "AADuBAe9EB3IEBz/7+//X1/qBQn2AgP/f3/ilpzsDxfpChDtDhXeCA76AQH/v7", + "/84eLyWV/uc3bJPEf/Dw/uw8bRWmP1h4zxSlD6YGHuQ0f6g4XyQkXvCA36MDH6", + "wMH/z8/yAwX64ODeh47BHiv/Ly/20dLQLTj98PDXWmP/Pz//39/wGyJ7Iy9JAA", + "AADHRSTlMAbw8vf08/bz+Pv19jK/W3AAAAg0lEQVR4Xp3LRQ4DQRBD0QqTm4Y5", + "zMxw/4OleiJlHeUtv2X6RbNO1Uqj9g0RMCuQO0vBIg4vMFeOpCWIWmDOw82fZx", + "vaND1c8OG4vrdOqD8YwgpDYDxRgkSm5rwu0nQVBJuMg++pLXZyr5jnc1BaH4GT", + "LvEliY253nA3pVhQqdPt0f/erJkMGMB8xucAAAAASUVORK5CYII="].join(''), + headers : { + "Content-Type" : "image/png", + "Vary" : "Accept" // we set this for proxy caches + } + }; + } else { + return { + "body" : "accepting text requests", + headers : { + "Content-Type" : "text/html", + "Vary" : "Accept" + } + }; + } + } + """, + provides: """ + function(doc, req) { + registerType("foo", "application/foo","application/x-foo"); + + provides("html", function() { + return "Ha ha, you said \\"" + doc.word + "\\"."; + }); + + provides("foo", function() { + return "foofoo"; + }); + } + """, + withSlash: """ + function(doc, req) { + return { json: doc } + } + """, + secObj: """ + function(doc, req) { + return { json: req.secObj }; + } + """ + } + } + + setup_all do + db_name = random_db_name() + {:ok, _} = create_db(db_name) + on_exit(fn -> delete_db(db_name) end) + + {:ok, _} = create_doc(db_name, @ddoc) + + create_doc(db_name, %{_id: "test-doc-id", word: "plankton", name: "Rusty"}) + + {:ok, [db_name: db_name]} + end + + test "show error", context do + db_name = context[:db_name] + + resp = Couch.get("/#{db_name}/_design/template/_show/") + assert resp.status_code == 404 + assert resp.body["reason"] == "Invalid path." + end + + test "show with existing doc", context do + db_name = context[:db_name] + + 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/) + + # Fix for COUCHDB-379 + assert String.match?(resp.headers["Server"], ~r/^CouchDB/) + end + + test "show without docid", context do + db_name = context[:db_name] + resp = Rawresp.get("/#{db_name}/_design/template/_show/hello") + assert resp.body == "Empty World" + + resp = Rawresp.get("/#{db_name}/_design/template/_show/empty") + assert resp.body == "" + end + + test "show fail with non-existing docid", context do + db_name = context[:db_name] + resp = Couch.get("/#{db_name}/_design/template/_show/fail/nonExistingDoc") + assert resp.status_code == 404 + assert resp.body["error"] == "not_found" + end + + test "show with doc", context do + db_name = context[:db_name] + resp = Rawresp.get("/#{db_name}/_design/template/_show/just-name/test-doc-id") + assert resp.body == "Just Rusty" + end + + test "show with missing doc", context do + db_name = context[:db_name] + resp = Rawresp.get("/#{db_name}/_design/template/_show/just-name/missingdoc") + assert resp.status_code == 404 + assert resp.body == "No such doc" + end + + test "missing design doc", context do + db_name = context[:db_name] + resp = Couch.get("/#{db_name}/_design/missingddoc/_show/just-name/test-doc-id") + assert resp.status_code == 404 + assert resp.body["error"] == "not_found" + end + + test "show query parameters", context do + db_name = context[:db_name] + + resp = + Couch.get("/#{db_name}/_design/template/_show/req-info/test-doc-id?foo=bar", + headers: [Accept: "text/html;text/plain;*/*", "X-Foo": "bar"] + ) + + assert resp.body["headers"]["X-Foo"] == "bar" + assert resp.body["query"] == %{"foo" => "bar"} + assert resp.body["method"] == "GET" + assert Enum.at(resp.body["path"], 5) == "test-doc-id" + assert resp.body["info"]["db_name"] == db_name + end + + test "accept header switching - different mime has different etag", context do + db_name = context[:db_name] + + resp = + Couch.get("/#{db_name}/_design/template/_show/accept-switch/test-doc-id", + headers: [Accept: "text/html;text/plain;*/*"] + ) + + assert String.match?(resp.headers["Content-Type"], ~r/text\/html/) + assert resp.headers["Vary"] == "Accept" + + etag = resp.headers["etag"] + + resp = + Rawresp.get("/#{db_name}/_design/template/_show/accept-switch/test-doc-id", + headers: [Accept: "image/png;*/*"] + ) + + assert String.match?(resp.body, ~r/PNG/) + assert resp.headers["Content-Type"] == "image/png" + + etag2 = resp.headers["etag"] + + assert etag != etag2 + end + + test "show with doc - etags", context do + db_name = context[:db_name] + + doc = %{"_id" => "test-doc-id2", word: "plankton", name: "Rusty"} + doc = save(db_name, doc) + + resp = Couch.get("/#{db_name}/_design/template/_show/just-name/test-doc-id2") + + etag = resp.headers["etag"] + + resp = + Couch.get("/#{db_name}/_design/template/_show/just-name/test-doc-id2", + headers: ["if-none-match": etag] + ) + + assert resp.status_code == 304 + + doc = Map.put(doc, "name", "Crusty") + save(db_name, doc) + + resp = + Couch.get("/#{db_name}/_design/template/_show/just-name/test-doc-id2", + headers: ["if-none-match": etag] + ) + + assert resp.status_code == 200 + end + + test "JS can't set etag", context do + db_name = context[:db_name] + + resp = Couch.get("/#{db_name}/_design/template/_show/no-set-etag/test-doc-id") + assert resp.headers["etag"] != "skipped" + end + + test "the provides mime matcher", context do + db_name = context[:db_name] + + resp = + Rawresp.get("/#{db_name}/_design/template/_show/provides/test-doc-id", + 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 resp.body == "Ha ha, you said \"plankton\"." + end + + test "registering types works", context do + db_name = context[:db_name] + + resp = + Rawresp.get("/#{db_name}/_design/template/_show/provides/test-doc-id", + headers: [Accept: "application/x-foo"] + ) + + assert resp.headers["Content-Type"] == "application/x-foo" + assert String.match?(resp.body, ~r/foofoo/) + end + + test "the provides mime matcher without a match", context do + db_name = context[:db_name] + + resp = + Couch.get("/#{db_name}/_design/template/_show/provides/test-doc-id", + headers: [Accept: "text/monkeys"] + ) + + assert resp.body["error"] == "not_acceptable" + end + + test "id with slash", context do + db_name = context[:db_name] + + doc3 = %{"_id" => "a/b/c", "a" => 1} + save(db_name, doc3) + resp = Couch.get("/#{db_name}/_design/template/_show/withSlash/a/b/c") + assert resp.status_code == 200 + end + + test "show with non-existing docid", context do + db_name = context[:db_name] + + resp = Rawresp.get("/#{db_name}/_design/template/_show/hello/nonExistingDoc") + assert resp.body == "New World" + end + + test "list() compatible API", context do + db_name = context[:db_name] + + resp = Rawresp.get("/#{db_name}/_design/template/_show/list-api/foo") + assert resp.body == "Hey" + assert resp.headers["X-Couch-Test-Header"] == "Yeah" + end + + test "list() compatible API with provides function", context do + db_name = context[:db_name] + + resp = + Rawresp.get("/#{db_name}/_design/template/_show/list-api-provides/foo?format=text") + + assert resp.body == "foo, bar, baz!" + end + + test "should keep next result order: chunks + return value + provided chunks + provided return value", + context do + db_name = context[:db_name] + + resp = + Rawresp.get( + "/#{db_name}/_design/template/_show/list-api-provides-and-return/foo?format=text" + ) + + assert resp.body == "1, 2, 3, 4, 5, 6, 7!" + + 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" + + 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!" + end + + test "deleted docs", context do + db_name = context[:db_name] + + doc = save(db_name, %{"_id" => "testdoc", "foo" => 1}) + + resp = Rawresp.get("/#{db_name}/_design/template/_show/show-deleted/testdoc") + assert resp.body == "testdoc" + + Couch.delete("/#{db_name}/testdoc?rev=#{doc["_rev"]}") + resp = Rawresp.get("/#{db_name}/_design/template/_show/show-deleted/testdoc") + assert resp.body == "No doc testdoc" + end + + @tag :with_db + test "security object", context do + db_name = context[:db_name] + {:ok, _} = create_doc(db_name, @ddoc) + {:ok, _} = create_doc(db_name, %{_id: "testdoc", foo: 1}) + + Couch.put("/#{db_name}/_security", body: %{foo: true}) + + retry_until(fn -> + resp = Couch.get("/#{db_name}/_design/template/_show/secObj") + assert resp.body["foo"] + end) + end +end diff --git a/test/elixir/test/update_documents_test.exs b/test/elixir/test/update_documents_test.exs index fcbdbeaca89..bd5e0f1ccb5 100644 --- a/test/elixir/test/update_documents_test.exs +++ b/test/elixir/test/update_documents_test.exs @@ -1,6 +1,7 @@ defmodule UpdateDocumentsTest do use CouchTestCase + @moduletag :docs @moduletag kind: :single_node @ddoc %{ diff --git a/test/elixir/test/users_db_security_test.exs b/test/elixir/test/users_db_security_test.exs new file mode 100644 index 00000000000..7b2c97df9fa --- /dev/null +++ b/test/elixir/test/users_db_security_test.exs @@ -0,0 +1,520 @@ +defmodule UsersDbSecurityTest do + use CouchTestCase + + @moduletag :authentication + @moduletag kind: :single_node + + @users_db "_users" + + @login_user %{ + jerry: "apple", + tom: "mp3", + spike: "foobar", + speedy: "test", + silvestre: "anchovy" + } + + setup_all do + # Create db if not exists + Couch.put("/#{@users_db}") + + retry_until(fn -> + resp = + Couch.get( + "/#{@users_db}/_changes", + query: [feed: "longpoll", timeout: 5000, filter: "_design"] + ) + + length(resp.body["results"]) > 0 + end) + + on_exit(&tear_down/0) + + :ok + end + + defp tear_down do + users = Map.keys(@login_user) + Enum.each(users, fn name -> + resp = Couch.get("/#{@users_db}/org.couchdb.user:#{name}") + if resp.status_code == 200 do + rev = resp.body["_rev"] + Couch.delete("/#{@users_db}/org.couchdb.user:#{name}?rev=#{rev}") + end + end) + end + + defp login_as(user, password \\ nil) do + pwd = + case password do + nil -> @login_user[String.to_atom(user)] + _ -> password + end + + sess = Couch.login(user, pwd) + assert sess.cookie, "Login correct is expected" + sess + end + + defp logout(session) do + assert Couch.Session.logout(session).body["ok"] + end + + defp open_as(db_name, doc_id, options) do + use_session = Keyword.get(options, :use_session) + user = Keyword.get(options, :user) + pwd = Keyword.get(options, :pwd) + expect_response = Keyword.get(options, :expect_response, 200) + expect_message = Keyword.get(options, :error_message) + + session = use_session || login_as(user, pwd) + + resp = + Couch.Session.get( + session, + "/#{db_name}/#{URI.encode(doc_id)}" + ) + + if use_session == nil do + logout(session) + end + + assert resp.status_code == expect_response + + if expect_message != nil do + assert resp.body["error"] == expect_message + end + + resp.body + end + + defp save_as(db_name, doc, options) do + use_session = Keyword.get(options, :use_session) + user = Keyword.get(options, :user) + expect_response = Keyword.get(options, :expect_response, [201, 202]) + expect_message = Keyword.get(options, :error_message) + + session = use_session || login_as(user) + + resp = + Couch.Session.put( + session, + "/#{db_name}/#{URI.encode(doc["_id"])}", + body: doc + ) + + if use_session == nil do + logout(session) + end + + if is_list(expect_response) do + assert resp.status_code in expect_response + else + assert resp.status_code == expect_response + end + + if expect_message != nil do + assert resp.body["error"] == expect_message + end + + resp + end + + defp view_as(db_name, view_name, options) do + use_session = Keyword.get(options, :use_session) + user = Keyword.get(options, :user) + pwd = Keyword.get(options, :pwd) + expect_response = Keyword.get(options, :expect_response, 200) + expect_message = Keyword.get(options, :error_message) + + session = use_session || login_as(user, pwd) + + [view_root, view_name] = String.split(view_name, "/") + + resp = + Couch.Session.get(session, "/#{db_name}/_design/#{view_root}/_view/#{view_name}") + + if use_session == nil do + logout(session) + end + + if is_list(expect_response) do + assert resp.status_code in expect_response + else + assert resp.status_code == expect_response + end + + if expect_message != nil do + assert resp.body["error"] == expect_message + end + + resp + end + + defp changes_as(db_name, options) do + use_session = Keyword.get(options, :use_session) + user = Keyword.get(options, :user) + expect_response = Keyword.get(options, :expect_response, [200, 202]) + expect_message = Keyword.get(options, :error_message) + + session = use_session || login_as(user) + + resp = + Couch.Session.get( + session, + "/#{db_name}/_changes" + ) + + if use_session == nil do + logout(session) + end + + if is_list(expect_response) do + assert resp.status_code in expect_response + else + assert resp.status_code == expect_response + end + + if expect_message != nil do + assert resp.body["error"] == expect_message + end + + resp + end + + defp request_raw_as(db_name, path, options) do + use_session = Keyword.get(options, :use_session) + user = Keyword.get(options, :user) + pwd = Keyword.get(options, :pwd) + expect_response = Keyword.get(options, :expect_response, 200) + expect_message = Keyword.get(options, :error_message) + + session = use_session || login_as(user, pwd) + + resp = + Couch.Session.get( + session, + "/#{db_name}/#{path}", + parse_response: false + ) + + if use_session == nil do + logout(session) + end + + if is_list(expect_response) do + assert resp.status_code in expect_response + else + assert resp.status_code == expect_response + end + + if expect_message != nil do + assert resp.body["error"] == expect_message + end + + resp + end + + defp request_as(db_name, path, options) do + use_session = Keyword.get(options, :use_session) + user = Keyword.get(options, :user) + pwd = Keyword.get(options, :pwd) + expect_response = Keyword.get(options, :expect_response, 200) + expect_message = Keyword.get(options, :error_message) + + session = use_session || login_as(user, pwd) + + resp = + Couch.Session.get( + session, + "/#{db_name}/#{path}" + ) + + if use_session == nil do + logout(session) + end + + if is_list(expect_response) do + assert resp.status_code in expect_response + else + assert resp.status_code == expect_response + end + + if expect_message != nil do + assert resp.body["error"] == expect_message + end + + resp + end + + defp set_security(db_name, security, expect_response \\ 200) do + resp = Couch.put("/#{db_name}/_security", body: security) + assert resp.status_code == expect_response + end + + @tag config: [ + { + "couchdb", + "users_db_security_editable", + "true" + }, + { + "couch_httpd_auth", + "iterations", + "1" + }, + { + "admins", + "jerry", + "apple" + } + ] + test "user db security" do + # _users db + # a doc with a field 'password' should be hashed to 'derived_key' + # with salt and salt stored in 'salt', 'password' is set to null. + # Exising 'derived_key' and 'salt' fields are overwritten with new values + # when a non-null 'password' field exists. + # anonymous should be able to create a user document + user_doc = %{ + _id: "org.couchdb.user:tom", + type: "user", + name: "tom", + password: "mp3", + roles: [] + } + + resp = + Couch.post("/#{@users_db}", body: user_doc, headers: [authorization: "annonymous"]) + + assert resp.status_code in [201, 202] + assert resp.body["ok"] + + user_doc = + retry_until(fn -> + user_doc = open_as(@users_db, "org.couchdb.user:tom", user: "tom") + assert !user_doc["password"] + assert String.length(user_doc["derived_key"]) == 40 + assert String.length(user_doc["salt"]) == 32 + user_doc + end) + + # anonymous should not be able to read an existing user's user document + resp = + Couch.get("/#{@users_db}/org.couchdb.user:tom", + headers: [authorization: "annonymous"] + ) + + assert resp.status_code == 404 + + # anonymous should not be able to read /_users/_changes + resp = Couch.get("/#{@users_db}/_changes", headers: [authorization: "annonymous"]) + assert resp.status_code == 401 + assert resp.body["error"] == "unauthorized" + + # user should be able to read their own document + tom_doc = open_as(@users_db, "org.couchdb.user:tom", user: "tom") + assert tom_doc["_id"] == "org.couchdb.user:tom" + + # user should not be able to read /_users/_changes + changes_as(@users_db, + user: "tom", + expect_response: 401, + expect_message: "unauthorized" + ) + + tom_doc = Map.put(tom_doc, "password", "couch") + save_as(@users_db, tom_doc, user: "tom") + + tom_doc = open_as(@users_db, "org.couchdb.user:tom", user: "jerry") + assert !tom_doc["password"] + assert String.length(tom_doc["derived_key"]) == 40 + assert String.length(tom_doc["salt"]) == 32 + assert tom_doc["derived_key"] != user_doc["derived_key"] + assert tom_doc["salt"] != user_doc["salt"] + + # user should not be able to read another user's user document + spike_doc = %{ + _id: "org.couchdb.user:spike", + type: "user", + name: "spike", + password: "foobar", + roles: [] + } + + {:ok, _} = create_doc(@users_db, spike_doc) + + open_as(@users_db, "org.couchdb.user:spike", + user: "tom", + pwd: "couch", + expect_response: 404 + ) + + speedy_doc = %{ + _id: "org.couchdb.user:speedy", + type: "user", + name: "speedy", + password: "test", + roles: ["user_admin"] + } + + {:ok, _} = create_doc(@users_db, speedy_doc) + + security = %{ + admins: %{ + roles: [], + names: ["speedy"] + } + } + + set_security(@users_db, security) + + # user should not be able to read from any view + ddoc = %{ + _id: "_design/user_db_auth", + views: %{ + test: %{ + map: "function(doc) { emit(doc._id, null); }" + } + }, + lists: %{ + names: """ + function(head, req) { + var row; while (row = getRow()) { send(row.key + \"\\n\"); } + } + """ + }, + shows: %{ + name: "function(doc, req) { return doc.name; }" + } + } + + create_doc(@users_db, ddoc) + + resp = + Couch.get("/#{@users_db}/_design/user_db_auth/_view/test", + headers: [authorization: "annonymous"] + ) + + assert resp.body["error"] == "forbidden" + + # admin should be able to read from any view + resp = view_as(@users_db, "user_db_auth/test", user: "jerry") + assert resp.body["total_rows"] == 3 + + # db admin should be able to read from any view + resp = view_as(@users_db, "user_db_auth/test", user: "speedy") + assert resp.body["total_rows"] == 3 + + # non-admins can't read design docs + open_as(@users_db, "_design/user_db_auth", + user: "tom", + pwd: "couch", + expect_response: 403, + expect_message: "forbidden" + ) + + # admin shold be able to read _list + result = + request_raw_as(@users_db, "_design/user_db_auth/_list/names/test", user: "jerry") + + assert result.status_code == 200 + assert length(String.split(result.body, "\n")) == 4 + + # non-admins can't read _list + request_raw_as(@users_db, "_design/user_db_auth/_list/names/test", + user: "tom", + pwd: "couch", + expect_response: 403 + ) + + # admin should be able to read _show + result = + request_raw_as(@users_db, "_design/user_db_auth/_show/name/org.couchdb.user:tom", + user: "jerry" + ) + + assert result.status_code == 200 + assert result.body == "tom" + + # non-admin should be able to access own _show + result = + request_raw_as(@users_db, "_design/user_db_auth/_show/name/org.couchdb.user:tom", + user: "tom", + pwd: "couch" + ) + + assert result.status_code == 200 + assert result.body == "tom" + + # non-admin can't read other's _show + request_raw_as(@users_db, "_design/user_db_auth/_show/name/org.couchdb.user:jerry", + user: "tom", + pwd: "couch", + expect_response: 404 + ) + + # admin should be able to read and edit any user doc + spike_doc = open_as(@users_db, "org.couchdb.user:spike", user: "jerry") + spike_doc = Map.put(spike_doc, "password", "mobile") + save_as(@users_db, spike_doc, user: "jerry") + + # admin should be able to read and edit any user doc + spike_doc = open_as(@users_db, "org.couchdb.user:spike", user: "jerry") + spike_doc = Map.put(spike_doc, "password", "mobile1") + save_as(@users_db, spike_doc, user: "speedy") + + security = %{ + admins: %{ + roles: ["user_admin"], + names: [] + } + } + + set_security(@users_db, security) + + # db admin should be able to read and edit any user doc + spike_doc = open_as(@users_db, "org.couchdb.user:spike", user: "jerry") + spike_doc = Map.put(spike_doc, "password", "mobile2") + save_as(@users_db, spike_doc, user: "speedy") + + # ensure creation of old-style docs still works + silvestre_doc = prepare_user_doc(name: "silvestre", password: "anchovy") + + resp = + Couch.post("/#{@users_db}", + body: silvestre_doc, + headers: [authorization: "annonymous"] + ) + + assert resp.body["ok"] + + run_on_modified_server( + [ + %{ + :section => "couch_httpd_auth", + :key => "public_fields", + :value => "name" + }, + %{ + :section => "couch_httpd_auth", + :key => "users_db_public", + :value => "false" + } + ], + fn -> + request_as(@users_db, "_all_docs?include_docs=true", + user: "tom", + pwd: "couch", + expect_response: 401, + expect_message: "unauthorized" + ) + + # COUCHDB-1888 make sure admins always get all fields + resp = request_as(@users_db, "_all_docs?include_docs=true", user: "jerry") + rows = resp.body["rows"] + assert Enum.at(rows, 2)["doc"]["type"] == "user" + end + ) + end +end diff --git a/test/elixir/test/view_collation_raw_test.exs b/test/elixir/test/view_collation_raw_test.exs index 1bcd397d98a..b9c40f6d88b 100644 --- a/test/elixir/test/view_collation_raw_test.exs +++ b/test/elixir/test/view_collation_raw_test.exs @@ -6,6 +6,7 @@ defmodule ViewCollationRawTest do This is a port of the view_collation_raw.js suite """ + @moduletag :view_collation_raw @moduletag kind: :single_node @values [ diff --git a/test/elixir/test/view_collation_test.exs b/test/elixir/test/view_collation_test.exs index e18e1e26f39..0dabd501beb 100644 --- a/test/elixir/test/view_collation_test.exs +++ b/test/elixir/test/view_collation_test.exs @@ -6,6 +6,7 @@ defmodule ViewCollationTest do This is a port of the view_collation.js suite """ + @moduletag :view_collation @moduletag kind: :single_node @values [ diff --git a/test/elixir/test/view_compaction_test.exs b/test/elixir/test/view_compaction_test.exs index 5bdd2eac1ea..ed7461aa194 100644 --- a/test/elixir/test/view_compaction_test.exs +++ b/test/elixir/test/view_compaction_test.exs @@ -6,6 +6,7 @@ defmodule ViewCompactionTest do This is a port of the view_compaction.js suite """ + @moduletag :view_compaction @moduletag kind: :single_node @num_docs 1000 diff --git a/test/elixir/test/view_conflicts_test.exs b/test/elixir/test/view_conflicts_test.exs index 9261b1ef075..89d9cb4eb23 100644 --- a/test/elixir/test/view_conflicts_test.exs +++ b/test/elixir/test/view_conflicts_test.exs @@ -1,6 +1,7 @@ defmodule ViewConflictsTest do use CouchTestCase + @moduletag :view_conflicts @moduletag kind: :single_node setup_all do diff --git a/test/elixir/test/view_errors_test.exs b/test/elixir/test/view_errors_test.exs index 80067ec6c4c..1e8f880a654 100644 --- a/test/elixir/test/view_errors_test.exs +++ b/test/elixir/test/view_errors_test.exs @@ -1,6 +1,7 @@ defmodule ViewErrorsTest do use CouchTestCase + @moduletag :view_errors @moduletag kind: :single_node @document %{integer: 1, string: "1", array: [1, 2, 3]} diff --git a/test/elixir/test/view_include_docs_test.exs b/test/elixir/test/view_include_docs_test.exs index a7775305840..1c2ab57315b 100644 --- a/test/elixir/test/view_include_docs_test.exs +++ b/test/elixir/test/view_include_docs_test.exs @@ -1,6 +1,7 @@ defmodule ViewIncludeDocsTest do use CouchTestCase + @moduletag :view_include_docs @moduletag kind: :single_node @ddoc %{ diff --git a/test/elixir/test/view_multi_key_all_docs_test.exs b/test/elixir/test/view_multi_key_all_docs_test.exs index 6426eb2c2e6..c983905805f 100644 --- a/test/elixir/test/view_multi_key_all_docs_test.exs +++ b/test/elixir/test/view_multi_key_all_docs_test.exs @@ -1,6 +1,7 @@ defmodule ViewMultiKeyAllDocsTest do use CouchTestCase + @moduletag :view_multi_key_all_docs @moduletag kind: :single_node @keys ["10", "15", "30", "37", "50"] diff --git a/test/elixir/test/view_multi_key_design_test.exs b/test/elixir/test/view_multi_key_design_test.exs index 1ea3fe33431..03cdc574d45 100644 --- a/test/elixir/test/view_multi_key_design_test.exs +++ b/test/elixir/test/view_multi_key_design_test.exs @@ -1,6 +1,7 @@ defmodule ViewMultiKeyDesignTest do use CouchTestCase + @moduletag :view_multi_key_design @moduletag kind: :single_node @keys [10, 15, 30, 37, 50] diff --git a/test/elixir/test/view_sandboxing_test.exs b/test/elixir/test/view_sandboxing_test.exs index 99a9c0de422..02087ac60eb 100644 --- a/test/elixir/test/view_sandboxing_test.exs +++ b/test/elixir/test/view_sandboxing_test.exs @@ -1,6 +1,7 @@ defmodule ViewSandboxingTest do use CouchTestCase + @moduletag :view_sandboxing @moduletag kind: :single_node @document %{integer: 1, string: "1", array: [1, 2, 3]} diff --git a/test/javascript/tests/list_views.js b/test/javascript/tests/list_views.js index e255e15461b..2d74586fe9f 100644 --- a/test/javascript/tests/list_views.js +++ b/test/javascript/tests/list_views.js @@ -9,7 +9,7 @@ // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. - +couchTests.elixir = true; couchTests.list_views = function(debug) { var db_name = get_random_db_name(); diff --git a/test/javascript/tests/rewrite.js b/test/javascript/tests/rewrite.js index a984936d104..88479b87763 100644 --- a/test/javascript/tests/rewrite.js +++ b/test/javascript/tests/rewrite.js @@ -10,7 +10,7 @@ // License for the specific language governing permissions and limitations under // the License. - +couchTests.elixir = true; couchTests.rewrite = function(debug) { if (debug) debugger; diff --git a/test/javascript/tests/rewrite_js.js b/test/javascript/tests/rewrite_js.js index 22de6c940bf..7179fc9f5b2 100644 --- a/test/javascript/tests/rewrite_js.js +++ b/test/javascript/tests/rewrite_js.js @@ -11,7 +11,7 @@ // the License. - +couchTests.elixir = true; couchTests.rewrite = function(debug) { if (debug) debugger; var dbNames = [get_random_db_name(), get_random_db_name() + "test_suite_db/with_slashes"]; @@ -116,7 +116,6 @@ couchTests.rewrite = function(debug) { }), lists: { simpleForm: stringFun(function(head, req) { - log("simpleForm"); send('