summaryrefslogtreecommitdiff
path: root/test/elixir
diff options
context:
space:
mode:
authorJuanjo Rodriguez <juanjo@apache.org>2020-07-15 23:12:47 +0200
committerJuanjo Rodriguez <jjrodrig@gmail.com>2020-09-11 09:07:08 +0200
commitac33e853cef2a6a108aa64269eb196d32b235529 (patch)
treef0b8f77ecb645c147095da13eba75648c6623995 /test/elixir
parent1c6a7386f1204d4af38816034ef9e971b5ff19af (diff)
downloadcouchdb-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.md6
-rw-r--r--test/elixir/test/view_conflicts_test.exs74
-rw-r--r--test/elixir/test/view_errors_test.exs300
-rw-r--r--test/elixir/test/view_include_docs_test.exs263
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