diff options
author | Juanjo Rodriguez <juanjo@apache.org> | 2020-07-15 23:12:47 +0200 |
---|---|---|
committer | Juanjo Rodriguez <jjrodrig@gmail.com> | 2020-09-11 09:07:08 +0200 |
commit | ac33e853cef2a6a108aa64269eb196d32b235529 (patch) | |
tree | f0b8f77ecb645c147095da13eba75648c6623995 /test/elixir | |
parent | 1c6a7386f1204d4af38816034ef9e971b5ff19af (diff) | |
download | couchdb-ac33e853cef2a6a108aa64269eb196d32b235529.tar.gz |
Port view_conflicts.js, view_errors.js and view_include_docs.js into elixir·
Diffstat (limited to 'test/elixir')
-rw-r--r-- | test/elixir/README.md | 6 | ||||
-rw-r--r-- | test/elixir/test/view_conflicts_test.exs | 74 | ||||
-rw-r--r-- | test/elixir/test/view_errors_test.exs | 300 | ||||
-rw-r--r-- | test/elixir/test/view_include_docs_test.exs | 263 |
4 files changed, 640 insertions, 3 deletions
diff --git a/test/elixir/README.md b/test/elixir/README.md index 38c85a5e8..52ce45a75 100644 --- a/test/elixir/README.md +++ b/test/elixir/README.md @@ -101,9 +101,9 @@ X means done, - means partially - [X] Port view_collation.js - [X] Port view_collation_raw.js - [X] Port view_compaction.js - - [ ] Port view_conflicts.js - - [ ] Port view_errors.js - - [ ] Port view_include_docs.js + - [X] Port view_conflicts.js + - [X] Port view_errors.js + - [X] Port view_include_docs.js - [X] Port view_multi_key_all_docs.js - [X] Port view_multi_key_design.js - [ ] ~~Port view_multi_key_temp.js~~ diff --git a/test/elixir/test/view_conflicts_test.exs b/test/elixir/test/view_conflicts_test.exs new file mode 100644 index 000000000..9261b1ef0 --- /dev/null +++ b/test/elixir/test/view_conflicts_test.exs @@ -0,0 +1,74 @@ +defmodule ViewConflictsTest do + use CouchTestCase + + @moduletag kind: :single_node + + setup_all do + db_name_a = random_db_name() + db_name_b = random_db_name() + + {:ok, _} = create_db(db_name_a) + {:ok, _} = create_db(db_name_b) + + on_exit(fn -> delete_db(db_name_a) end) + on_exit(fn -> delete_db(db_name_b) end) + {:ok, [db_name_a: db_name_a, db_name_b: db_name_b]} + end + + test "view conflict", context do + db_name_a = context[:db_name_a] + db_name_b = context[:db_name_b] + + create_doc(db_name_a, %{_id: "foo", bar: 42}) + replicate(db_name_a, db_name_b) + + resp = Couch.get("/#{db_name_b}/foo") + + docb = + resp.body + |> Map.put("bar", 43) + + docb = save(db_name_b, docb) + + resp = Couch.get("/#{db_name_a}/foo") + + doca = + resp.body + |> Map.put("bar", 41) + + doca = save(db_name_a, doca) + + replicate(db_name_a, db_name_b) + + resp = Couch.get("/#{db_name_b}/foo", query: [conflicts: true]) + doc = resp.body + assert length(resp.body["_conflicts"]) == 1 + + conflict_rev = Enum.at(resp.body["_conflicts"], 0) + + case doc["bar"] do + 41 -> assert conflict_rev == docb["_rev"] + 43 -> assert conflict_rev == doca["_rev"] + _ -> assert false + end + + map_fun = """ + function(doc) { + if (doc._conflicts) { + emit(doc._id, doc._conflicts); + } + } + """ + + results = query(db_name_b, map_fun) + + rev = + results + |> Map.get("rows") + |> Enum.at(0) + |> Map.get("value") + |> Enum.at(0) + + assert conflict_rev == rev + end +end diff --git a/test/elixir/test/view_errors_test.exs b/test/elixir/test/view_errors_test.exs new file mode 100644 index 000000000..80067ec6c --- /dev/null +++ b/test/elixir/test/view_errors_test.exs @@ -0,0 +1,300 @@ +defmodule ViewErrorsTest do + use CouchTestCase + + @moduletag kind: :single_node + + @document %{integer: 1, string: "1", array: [1, 2, 3]} + + @tag :with_db + test "emit undefined key results as null", context do + db_name = context[:db_name] + {:ok, _} = create_doc(db_name, @document) + + map_fun = """ + function(doc) { + emit(doc.undef, null); + } + """ + + # emitting a key value that is undefined should result in that row + # being included in the view results as null + results = query(db_name, map_fun) + assert results["total_rows"] == 1 + assert Enum.at(results["rows"], 0)["key"] == :null + end + + @tag :with_db + test "exception in map function", context do + db_name = context[:db_name] + {:ok, _} = create_doc(db_name, @document) + + map_fun = """ + function(doc) { + doc.undef(); // throws an error + } + """ + + # if a view function throws an exception, its results are not included in + # the view index, but the view does not itself raise an error + results = query(db_name, map_fun) + assert results["total_rows"] == 0 + end + + @tag :with_db + test "emit undefined value results as null", context do + db_name = context[:db_name] + {:ok, _} = create_doc(db_name, @document) + + map_fun = """ + function(doc) { + emit([doc._id, doc.undef], null); + } + """ + + # if a view function includes an undefined value in the emitted key or + # value, it is treated as null + results = query(db_name, map_fun) + assert results["total_rows"] == 1 + + key = + results["rows"] + |> Enum.at(0) + |> Map.get("key") + |> Enum.at(1) + + assert key == :null + end + + @tag :with_db + test "query view with invalid params", context do + db_name = context[:db_name] + {:ok, _} = create_doc(db_name, @document) + + body = %{ + language: "javascript", + map: "function(doc){emit(doc.integer)}" + } + + # querying a view with invalid params should give a resonable error message + resp = + Couch.post("/#{db_name}/_all_docs?startkey=foo", + headers: ["Content-Type": "application/json"], + body: body + ) + + assert resp.body["error"] == "bad_request" + + resp = + Couch.post("/#{db_name}/_all_docs", + headers: ["Content-Type": "application/x-www-form-urlencoded"], + body: body + ) + + assert resp.status_code == 415 + end + + @tag :with_db + test "query parse error", context do + db_name = context[:db_name] + + map_fun = """ + function(doc) { + emit(doc.integer, doc.integer); + } + """ + + ddoc_name = create_view(db_name, map_fun) + + resp = Couch.get("/#{db_name}/#{ddoc_name}/_view/view", query: [group: true]) + assert resp.status_code == 400 + assert resp.body["error"] == "query_parse_error" + + map_fun = "function() {emit(null, null)}" + ddoc_name = create_view(db_name, map_fun) + + resp = + Couch.get("/#{db_name}/#{ddoc_name}/_view/view", query: [startkey: 2, endkey: 1]) + + assert resp.status_code == 400 + assert resp.body["error"] == "query_parse_error" + assert String.contains?(resp.body["reason"], "No rows can match") + + design_doc = %{ + _id: "_design/test", + language: "javascript", + views: %{ + no_reduce: %{map: "function(doc) {emit(doc._id, null);}"}, + with_reduce: %{ + map: "function (doc) {emit(doc.integer, doc.integer)};", + reduce: "function (keys, values) { return sum(values); };" + } + } + } + + {:ok, _} = create_doc(db_name, design_doc) + + resp = Couch.get("/#{db_name}/_design/test/_view/no_reduce", query: [group: true]) + assert resp.status_code == 400 + assert resp.body["error"] == "query_parse_error" + + resp = Couch.get("/#{db_name}/_design/test/_view/no_reduce", query: [group_level: 1]) + assert resp.status_code == 400 + assert resp.body["error"] == "query_parse_error" + + resp = Couch.get("/#{db_name}/_design/test/_view/no_reduce", query: [reduce: true]) + assert resp.status_code == 400 + assert resp.body["error"] == "query_parse_error" + + resp = Couch.get("/#{db_name}/_design/test/_view/no_reduce", query: [reduce: false]) + assert resp.status_code == 200 + + resp = + Couch.get("/#{db_name}/_design/test/_view/with_reduce", + query: [group: true, reduce: false] + ) + + assert resp.status_code == 400 + assert resp.body["error"] == "query_parse_error" + + resp = + Couch.get("/#{db_name}/_design/test/_view/with_reduce", + query: [group_level: 1, reduce: false] + ) + + assert resp.status_code == 400 + assert resp.body["error"] == "query_parse_error" + end + + @tag :with_db + test "infinite loop", context do + db_name = context[:db_name] + {:ok, _} = create_doc(db_name, @document) + + design_doc3 = %{ + _id: "_design/infinite", + language: "javascript", + views: %{ + infinite_loop: %{ + map: "function(doc) {while(true){emit(doc,doc);}};" + } + } + } + + {:ok, _} = create_doc(db_name, design_doc3) + + resp = Couch.get("/#{db_name}/_design/infinite/_view/infinite_loop") + assert resp.status_code == 500 + # This test has two different races. The first is whether + # the while loop exhausts the JavaScript RAM limits before + # timing. The second is a race between which of two timeouts + # fires first. The first timeout is the couch_os_process + # waiting for data back from couchjs. The second is the + # gen_server call to couch_os_process. + assert resp.body["error"] == "os_process_error" or resp.body["error"] == "timeout" + end + + @tag :with_db + test "error responses for invalid multi-get bodies", context do + db_name = context[:db_name] + + design_doc = %{ + _id: "_design/test", + language: "javascript", + views: %{ + no_reduce: %{map: "function(doc) {emit(doc._id, null);}"}, + with_reduce: %{ + map: "function (doc) {emit(doc.integer, doc.integer)};", + reduce: "function (keys, values) { return sum(values); };" + } + } + } + + {:ok, _} = create_doc(db_name, design_doc) + + resp = + Couch.post("/#{db_name}/_design/test/_view/no_reduce", + body: "[]" + ) + + assert resp.status_code == 400 + assert resp.body["error"] == "bad_request" + assert resp.body["reason"] == "Request body must be a JSON object" + + resp = + Couch.post("/#{db_name}/_design/test/_view/no_reduce", + body: %{keys: 1} + ) + + assert resp.status_code == 400 + assert resp.body["error"] == "bad_request" + assert resp.body["reason"] == "`keys` member must be an array." + end + + @tag :with_db + test "reduce overflow error", context do + db_name = context[:db_name] + {:ok, _} = create_doc(db_name, @document) + + design_doc2 = %{ + _id: "_design/testbig", + language: "javascript", + views: %{ + reduce_too_big: %{ + map: "function (doc) {emit(doc.integer, doc.integer)};", + reduce: + "function (keys, values) { var chars = []; for (var i=0; i < 1000; i++) {chars.push('wazzap');};return chars; };" + } + } + } + + {:ok, _} = create_doc(db_name, design_doc2) + + resp = Couch.get("/#{db_name}/_design/testbig/_view/reduce_too_big") + assert resp.status_code == 200 + # if the reduce grows to fast, throw an overflow error + assert Enum.at(resp.body["rows"], 0)["error"] == "reduce_overflow_error" + end + + @tag :with_db + test "temporary view should give error message", context do + db_name = context[:db_name] + + resp = + Couch.post("/#{db_name}/_temp_view", + headers: ["Content-Type": "application/json"], + body: %{ + language: "javascript", + map: "function(doc){emit(doc.integer)}" + } + ) + + assert resp.status_code == 410 + assert resp.body["error"] == "gone" + assert resp.body["reason"] == "Temporary views are not supported in CouchDB" + end + + defp create_view(db_name, map_fun) do + ddoc_name = "_design/temp_#{now(:ms)}" + + ddoc = %{ + _id: ddoc_name, + language: "javascript", + views: %{ + view: %{map: map_fun} + } + } + + {:ok, _} = create_doc(db_name, ddoc) + ddoc_name + end + + defp now(:ms) do + case elem(:os.type(), 0) do + :win32 -> + div(:erlang.system_time(), 1_000) + + _ -> + div(:erlang.system_time(), 1_000_000) + end + end +end diff --git a/test/elixir/test/view_include_docs_test.exs b/test/elixir/test/view_include_docs_test.exs new file mode 100644 index 000000000..a77753058 --- /dev/null +++ b/test/elixir/test/view_include_docs_test.exs @@ -0,0 +1,263 @@ +defmodule ViewIncludeDocsTest do + use CouchTestCase + + @moduletag kind: :single_node + + @ddoc %{ + _id: "_design/test", + language: "javascript", + views: %{ + all_docs: %{ + map: "function(doc) { emit(doc.integer, doc.string) }" + }, + with_prev: %{ + map: + "function(doc){if(doc.prev) emit(doc._id,{'_rev':doc.prev}); else emit(doc._id,{'_rev':doc._rev});}" + }, + with_id: %{ + map: + "function(doc) {if(doc.link_id) { var value = {'_id':doc.link_id}; if (doc.link_rev) {value._rev = doc.link_rev}; emit(doc._id, value);}};" + }, + summate: %{ + map: + "function (doc) { if (typeof doc.integer === 'number') {emit(doc.integer, doc.integer)};}", + reduce: "function (keys, values) { return sum(values); };" + } + } + } + + setup_all do + db_name = random_db_name() + {:ok, _} = create_db(db_name) + on_exit(fn -> delete_db(db_name) end) + + bulk_save(db_name, make_docs(0..99)) + + create_doc(db_name, @ddoc) + + {:ok, [db_name: db_name]} + end + + test "include docs in view", context do + db_name = context[:db_name] + resp = view(db_name, "test/all_docs", %{include_docs: true, limit: 2}) + assert length(resp.body["rows"]) == 2 + row0 = Enum.at(resp.body["rows"], 0) + assert row0["id"] == "0" + assert row0["doc"]["_id"] == "0" + row1 = Enum.at(resp.body["rows"], 1) + assert row1["id"] == "1" + assert row1["doc"]["_id"] == "1" + + resp = view(db_name, "test/all_docs", %{include_docs: true}, [29, 74]) + assert length(resp.body["rows"]) == 2 + row0 = Enum.at(resp.body["rows"], 0) + assert row0["doc"]["_id"] == "29" + row1 = Enum.at(resp.body["rows"], 1) + assert row1["doc"]["integer"] == 74 + end + + test "include docs in all_docs", context do + db_name = context[:db_name] + + resp = + Couch.get("/#{db_name}/_all_docs", + query: [limit: 2, skip: 1, include_docs: true] + ) + + assert length(resp.body["rows"]) == 2 + row0 = Enum.at(resp.body["rows"], 0) + row1 = Enum.at(resp.body["rows"], 1) + assert row0["doc"]["integer"] == 1 + assert row1["doc"]["integer"] == 10 + + resp = + Couch.post("/#{db_name}/_all_docs", + query: [include_docs: true], + headers: ["Content-Type": "application/json"], + body: %{"keys" => ["not_a_doc"]} + ) + + assert length(resp.body["rows"]) == 1 + row0 = Enum.at(resp.body["rows"], 0) + assert not Map.has_key?(row0, "doc") + + resp = + Couch.post("/#{db_name}/_all_docs", + query: [include_docs: true], + headers: ["Content-Type": "application/json"], + body: %{"keys" => ["1", "foo"]} + ) + + assert length(resp.body["rows"]) == 2 + row0 = Enum.at(resp.body["rows"], 0) + row1 = Enum.at(resp.body["rows"], 1) + assert row0["doc"]["integer"] == 1 + assert not Map.has_key?(row1, "doc") + + resp = + Couch.get("/#{db_name}/_all_docs", + query: [limit: 0, include_docs: true] + ) + + assert Enum.empty?(resp.body["rows"]) + end + + test "no reduce support", context do + db_name = context[:db_name] + + resp = + Couch.get("/#{db_name}/_design/test/_view/summate", query: [include_docs: true]) + + assert resp.status_code == 400 + assert resp.body["error"] == "query_parse_error" + end + + test "Reduce support when reduce=false", context do + db_name = context[:db_name] + + resp = + Couch.get("/#{db_name}/_design/test/_view/summate", + query: [reduce: false, include_docs: true] + ) + + assert length(resp.body["rows"]) == 100 + end + + test "Not an error with include_docs=false&reduce=true", context do + db_name = context[:db_name] + + resp = + Couch.get("/#{db_name}/_design/test/_view/summate", + query: [reduce: true, include_docs: false] + ) + + assert length(resp.body["rows"]) == 1 + row0 = Enum.at(resp.body["rows"], 0) + assert row0["value"] == 4950 + end + + @tag :with_db + test "link to another doc from a value", context do + db_name = context[:db_name] + + bulk_save(db_name, make_docs(0..99)) + create_doc(db_name, @ddoc) + + doc_link = %{ + _id: "link-to-10", + link_id: "10" + } + + {:ok, _} = create_doc(db_name, doc_link) + resp = view(db_name, "test/with_id", %{key: ~s("link-to-10")}) + assert length(resp.body["rows"]) == 1 + row0 = Enum.at(resp.body["rows"], 0) + assert row0["key"] == "link-to-10" + assert row0["value"]["_id"] == "10" + + resp = view(db_name, "test/with_id", %{key: ~s("link-to-10"), include_docs: true}) + assert length(resp.body["rows"]) == 1 + row0 = Enum.at(resp.body["rows"], 0) + assert row0["value"]["_id"] == "10" + assert row0["doc"]["_id"] == "10" + end + + @tag :with_db + test "emitted _rev controls things", context do + db_name = context[:db_name] + + bulk_save(db_name, make_docs(0..99)) + create_doc(db_name, @ddoc) + + resp = + Couch.post("/#{db_name}/_all_docs", + query: [include_docs: true], + headers: ["Content-Type": "application/json"], + body: %{"keys" => ["0"]} + ) + + doc_before = Enum.at(resp.body["rows"], 0)["doc"] + + resp = Couch.get("/#{db_name}/0") + assert resp.status_code == 200 + prev = resp.body["_rev"] + + doc_after = + resp.body + |> Map.put("integer", 100) + |> Map.put("prev", prev) + + saved_doc = save(db_name, doc_after) + + resp = Couch.get("/#{db_name}/0") + assert resp.status_code == 200 + doc_after = resp.body + assert doc_after["_rev"] == saved_doc["_rev"] + assert doc_after["_rev"] != doc_after["prev"] + assert doc_after["integer"] == 100 + + resp = view(db_name, "test/with_prev", %{include_docs: true}, ["0"]) + row0 = Enum.at(resp.body["rows"], 0)["doc"] + assert row0["_id"] == "0" + assert row0["_rev"] == doc_before["_rev"] + assert not Map.has_key?(row0, "prev") + assert assert row0["integer"] == 0 + end + + test "COUCHDB-549 - include_docs=true with conflicts=true" do + db_name_a = random_db_name() + db_name_b = random_db_name() + 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) + + ddoc = %{ + _id: "_design/mydesign", + language: "javascript", + views: %{ + myview: %{ + map: """ + function(doc) { + emit(doc.value, 1); + } + """ + } + } + } + + {:ok, _} = create_doc(db_name_a, ddoc) + + doc1a = %{_id: "foo", value: 1, str: "1"} + {:ok, _} = create_doc(db_name_a, doc1a) + + doc1b = %{_id: "foo", value: 1, str: "666"} + {:ok, _} = create_doc(db_name_b, doc1b) + + doc2 = %{_id: "bar", value: 2, str: "2"} + {:ok, _} = create_doc(db_name_a, doc2) + + replicate(db_name_a, db_name_b) + + resp = Couch.get("/#{db_name_b}/foo", query: [conflicts: true]) + assert resp.status_code == 200 + doc1b = resp.body + assert Map.has_key?(doc1b, "_conflicts") + assert length(doc1b["_conflicts"]) == 1 + conflict_rev = Enum.at(doc1b["_conflicts"], 0) + + resp = Couch.get("/#{db_name_b}/bar", query: [conflicts: true]) + assert resp.status_code == 200 + doc2 = resp.body + assert not Map.has_key?(doc2, "_conflicts") + + resp = view(db_name_b, "mydesign/myview", %{include_docs: true, conflicts: true}) + assert length(resp.body["rows"]) == 2 + row0 = Enum.at(resp.body["rows"], 0)["doc"] + assert length(row0["_conflicts"]) == 1 + assert Enum.at(row0["_conflicts"], 0) == conflict_rev + row1 = Enum.at(resp.body["rows"], 1)["doc"] + assert not Map.has_key?(row1, "_conflicts") + end +end |