summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuanjo Rodriguez <jjrodrig@gmail.com>2020-03-04 07:39:14 +0100
committerGitHub <noreply@github.com>2020-03-04 07:39:14 +0100
commit0aa37dc8b8acd04542b92694e2681512dcba0fef (patch)
tree28e7f22d39c994cd741a9b10ef6f65e0a80ef461
parente6e4c0376ea09f346e3655ce2410d66bd0cf2f20 (diff)
downloadcouchdb-0aa37dc8b8acd04542b92694e2681512dcba0fef.tar.gz
Port changes, _design_docs, design_options and design_paths tests from js to elixir (3.0.x) (#2629)
* Port _design_docs tests, design_options and design_paths from js to elixir (#2596) * Port changes.js test suite into elixir
-rw-r--r--test/elixir/README.md5
-rw-r--r--test/elixir/test/changes_async_test.exs545
-rw-r--r--test/elixir/test/changes_test.exs440
-rw-r--r--test/elixir/test/design_docs_query_test.exs273
-rw-r--r--test/elixir/test/design_docs_test.exs108
-rw-r--r--test/elixir/test/design_options_test.exs74
-rw-r--r--test/elixir/test/design_paths_test.exs76
-rw-r--r--test/javascript/tests/changes.js7
-rw-r--r--test/javascript/tests/design_docs_query.js2
-rw-r--r--test/javascript/tests/design_options.js3
-rw-r--r--test/javascript/tests/design_paths.js1
11 files changed, 1404 insertions, 130 deletions
diff --git a/test/elixir/README.md b/test/elixir/README.md
index 5aa70bb9b..6a19c246d 100644
--- a/test/elixir/README.md
+++ b/test/elixir/README.md
@@ -43,8 +43,9 @@ X means done, - means partially
- [X] Port cookie_auth.js
- [X] Port copy_doc.js
- [ ] Port design_docs.js
- - [ ] Port design_options.js
- - [ ] Port design_paths.js
+ - [X] Port design_docs_query.js
+ - [X] Port design_options.js
+ - [X] Port design_paths.js
- [X] Port erlang_views.js
- [X] Port etags_head.js
- [ ] ~~Port etags_views.js~~ (skipped in js test suite)
diff --git a/test/elixir/test/changes_async_test.exs b/test/elixir/test/changes_async_test.exs
new file mode 100644
index 000000000..07afcdc7c
--- /dev/null
+++ b/test/elixir/test/changes_async_test.exs
@@ -0,0 +1,545 @@
+defmodule ChangesAsyncTest do
+ use CouchTestCase
+
+ @moduletag :changes
+
+ @moduledoc """
+ Test CouchDB /{db}/_changes
+ """
+
+ @tag :with_db
+ test "live changes", context do
+ db_name = context[:db_name]
+ test_changes(db_name, "live")
+ end
+
+ @tag :with_db
+ test "continuous changes", context do
+ db_name = context[:db_name]
+ test_changes(db_name, "continuous")
+ end
+
+ @tag :with_db
+ test "longpoll changes", context do
+ db_name = context[:db_name]
+
+ check_empty_db(db_name)
+
+ create_doc(db_name, sample_doc_foo())
+
+ req_id =
+ Couch.get("/#{db_name}/_changes?feed=longpoll",
+ stream_to: self()
+ )
+
+ changes = process_response(req_id.id, &parse_chunk/1)
+ {changes_length, last_seq_prefix} = parse_changes_response(changes)
+ assert changes_length == 1, "db should not be empty"
+ assert last_seq_prefix == "1-", "seq must start with 1-"
+
+ last_seq = changes["last_seq"]
+ {:ok, worker_pid} = HTTPotion.spawn_link_worker_process(Couch.process_url(""))
+
+ req_id =
+ Couch.get("/#{db_name}/_changes?feed=longpoll&since=#{last_seq}",
+ stream_to: self(),
+ direct: worker_pid
+ )
+
+ :ok = wait_for_headers(req_id.id, 200)
+
+ create_doc_bar(db_name, "bar")
+
+ {changes_length, last_seq_prefix} =
+ req_id.id
+ |> process_response(&parse_chunk/1)
+ |> parse_changes_response()
+
+ assert changes_length == 1, "should return one change"
+ assert last_seq_prefix == "2-", "seq must start with 2-"
+
+ req_id =
+ Couch.get("/#{db_name}/_changes?feed=longpoll&since=now",
+ stream_to: self(),
+ direct: worker_pid
+ )
+
+ :ok = wait_for_headers(req_id.id, 200)
+
+ create_doc_bar(db_name, "barzzzz")
+
+ changes = process_response(req_id.id, &parse_chunk/1)
+ {changes_length, last_seq_prefix} = parse_changes_response(changes)
+ assert changes_length == 1, "should return one change"
+ assert Enum.at(changes["results"], 0)["id"] == "barzzzz"
+ assert last_seq_prefix == "3-", "seq must start with 3-"
+ end
+
+ @tag :with_db
+ test "eventsource changes", context do
+ db_name = context[:db_name]
+
+ check_empty_db(db_name)
+
+ create_doc(db_name, sample_doc_foo())
+ {:ok, worker_pid} = HTTPotion.spawn_link_worker_process(Couch.process_url(""))
+
+ req_id =
+ Rawresp.get("/#{db_name}/_changes?feed=eventsource&timeout=500",
+ stream_to: self(),
+ direct: worker_pid
+ )
+
+ :ok = wait_for_headers(req_id.id, 200)
+
+ create_doc_bar(db_name, "bar")
+
+ changes = process_response(req_id.id, &parse_event/1)
+
+ assert length(changes) == 2
+ assert Enum.at(changes, 0)["id"] == "foo"
+ assert Enum.at(changes, 1)["id"] == "bar"
+
+ HTTPotion.stop_worker_process(worker_pid)
+ end
+
+ @tag :with_db
+ test "eventsource heartbeat", context do
+ db_name = context[:db_name]
+
+ {:ok, worker_pid} = HTTPotion.spawn_link_worker_process(Couch.process_url(""))
+
+ req_id =
+ Rawresp.get("/#{db_name}/_changes?feed=eventsource&heartbeat=10",
+ stream_to: {self(), :once},
+ direct: worker_pid
+ )
+
+ :ok = wait_for_headers(req_id.id, 200)
+ beats = wait_for_heartbeats(req_id.id, 0, 3)
+ assert beats == 3
+ HTTPotion.stop_worker_process(worker_pid)
+ end
+
+ @tag :with_db
+ test "longpoll filtered changes", context do
+ db_name = context[:db_name]
+ create_filters_view(db_name)
+
+ create_doc(db_name, %{bop: "foom"})
+ create_doc(db_name, %{bop: false})
+
+ req_id =
+ Couch.get("/#{db_name}/_changes?feed=longpoll&filter=changes_filter/bop",
+ stream_to: self()
+ )
+
+ changes = process_response(req_id.id, &parse_chunk/1)
+ {changes_length, last_seq_prefix} = parse_changes_response(changes)
+ assert changes_length == 1, "db should not be empty"
+ assert last_seq_prefix == "3-", "seq must start with 3-"
+
+ last_seq = changes["last_seq"]
+ # longpoll waits until a matching change before returning
+ {:ok, worker_pid} = HTTPotion.spawn_link_worker_process(Couch.process_url(""))
+
+ req_id =
+ Couch.get(
+ "/#{db_name}/_changes?feed=longpoll&filter=changes_filter/bop&since=#{last_seq}",
+ stream_to: self(),
+ direct: worker_pid
+ )
+
+ :ok = wait_for_headers(req_id.id, 200)
+ create_doc(db_name, %{_id: "falsy", bop: ""})
+ # Doc doesn't match the filter
+ changes = process_response(req_id.id, &parse_chunk/1)
+ assert changes == :timeout
+
+ # Doc matches the filter
+ create_doc(db_name, %{_id: "bingo", bop: "bingo"})
+ changes = process_response(req_id.id, &parse_chunk/1)
+ {changes_length, last_seq_prefix} = parse_changes_response(changes)
+ assert changes_length == 1, "db should not be empty"
+ assert last_seq_prefix == "5-", "seq must start with 5-"
+ assert Enum.at(changes["results"], 0)["id"] == "bingo"
+ end
+
+ @tag :with_db
+ test "continuous filtered changes", context do
+ db_name = context[:db_name]
+ create_filters_view(db_name)
+
+ create_doc(db_name, %{bop: false})
+ create_doc(db_name, %{_id: "bingo", bop: "bingo"})
+
+ {:ok, worker_pid} = HTTPotion.spawn_link_worker_process(Couch.process_url(""))
+
+ req_id =
+ Rawresp.get(
+ "/#{db_name}/_changes?feed=continuous&filter=changes_filter/bop&timeout=500",
+ stream_to: self(),
+ direct: worker_pid
+ )
+
+ :ok = wait_for_headers(req_id.id, 200)
+ create_doc(db_name, %{_id: "rusty", bop: "plankton"})
+
+ changes = process_response(req_id.id, &parse_changes_line_chunk/1)
+
+ changes_ids =
+ changes
+ |> Enum.filter(fn p -> Map.has_key?(p, "id") end)
+ |> Enum.map(fn p -> p["id"] end)
+
+ assert Enum.member?(changes_ids, "bingo")
+ assert Enum.member?(changes_ids, "rusty")
+ assert length(changes_ids) == 2
+ end
+
+ @tag :with_db
+ test "continuous filtered changes with doc ids", context do
+ db_name = context[:db_name]
+ doc_ids = %{doc_ids: ["doc1", "doc3", "doc4"]}
+
+ create_doc(db_name, %{_id: "doc1", value: 1})
+ create_doc(db_name, %{_id: "doc2", value: 2})
+
+ {:ok, worker_pid} = HTTPotion.spawn_link_worker_process(Couch.process_url(""))
+
+ req_id =
+ Rawresp.post(
+ "/#{db_name}/_changes?feed=continuous&timeout=500&filter=_doc_ids",
+ body: doc_ids,
+ headers: ["Content-Type": "application/json"],
+ stream_to: self(),
+ direct: worker_pid
+ )
+
+ :ok = wait_for_headers(req_id.id, 200)
+ create_doc(db_name, %{_id: "doc3", value: 3})
+
+ changes = process_response(req_id.id, &parse_changes_line_chunk/1)
+
+ changes_ids =
+ changes
+ |> Enum.filter(fn p -> Map.has_key?(p, "id") end)
+ |> Enum.map(fn p -> p["id"] end)
+
+ assert Enum.member?(changes_ids, "doc1")
+ assert Enum.member?(changes_ids, "doc3")
+ assert length(changes_ids) == 2
+ end
+
+ @tag :with_db
+ test "COUCHDB-1852", context do
+ db_name = context[:db_name]
+
+ create_doc(db_name, %{bop: "foom"})
+ create_doc(db_name, %{bop: "foom"})
+ create_doc(db_name, %{bop: "foom"})
+ create_doc(db_name, %{bop: "foom"})
+
+ resp = Couch.get("/#{db_name}/_changes")
+ assert length(resp.body["results"]) == 4
+ seq = Enum.at(resp.body["results"], 1)["seq"]
+
+ {:ok, worker_pid} = HTTPotion.spawn_link_worker_process(Couch.process_url(""))
+
+ # simulate an EventSource request with a Last-Event-ID header
+ req_id =
+ Rawresp.get(
+ "/#{db_name}/_changes?feed=eventsource&timeout=100&since=0",
+ headers: [Accept: "text/event-stream", "Last-Event-ID": seq],
+ stream_to: self(),
+ direct: worker_pid
+ )
+
+ changes = process_response(req_id.id, &parse_event/1)
+ assert length(changes) == 2
+ end
+
+ defp wait_for_heartbeats(id, beats, expexted_beats) do
+ if beats < expexted_beats do
+ :ibrowse.stream_next(id)
+ is_heartbeat = process_response(id, &parse_heartbeat/1)
+
+ case is_heartbeat do
+ :heartbeat -> wait_for_heartbeats(id, beats + 1, expexted_beats)
+ :timeout -> beats
+ _ -> wait_for_heartbeats(id, beats, expexted_beats)
+ end
+ else
+ beats
+ end
+ end
+
+ defp wait_for_headers(id, status, timeout \\ 1000) do
+ receive do
+ %HTTPotion.AsyncHeaders{id: ^id, status_code: ^status} ->
+ :ok
+
+ _ ->
+ wait_for_headers(id, status, timeout)
+ after
+ timeout -> :timeout
+ end
+ end
+
+ defp process_response(id, chunk_parser, timeout \\ 1000) do
+ receive do
+ %HTTPotion.AsyncChunk{id: ^id} = msg ->
+ chunk_parser.(msg)
+
+ _ ->
+ process_response(id, chunk_parser, timeout)
+ after
+ timeout -> :timeout
+ end
+ end
+
+ defp parse_chunk(msg) do
+ msg.chunk |> IO.iodata_to_binary() |> :jiffy.decode([:return_maps])
+ end
+
+ defp parse_event(msg) do
+ captures = Regex.scan(~r/data: (.*)/, msg.chunk)
+
+ captures
+ |> Enum.map(fn p -> Enum.at(p, 1) end)
+ |> Enum.filter(fn p -> String.trim(p) != "" end)
+ |> Enum.map(fn p ->
+ p
+ |> IO.iodata_to_binary()
+ |> :jiffy.decode([:return_maps])
+ end)
+ end
+
+ defp parse_heartbeat(msg) do
+ is_heartbeat = Regex.match?(~r/event: heartbeat/, msg.chunk)
+
+ if is_heartbeat do
+ :heartbeat
+ else
+ :other
+ end
+ end
+
+ defp parse_changes_response(changes) do
+ {length(changes["results"]), String.slice(changes["last_seq"], 0..1)}
+ end
+
+ defp check_empty_db(db_name) do
+ resp = Couch.get("/#{db_name}/_changes")
+ assert resp.body["results"] == [], "db must be empty"
+ assert String.at(resp.body["last_seq"], 0) == "0", "seq must start with 0"
+ end
+
+ defp test_changes(db_name, feed) do
+ check_empty_db(db_name)
+ {_, resp} = create_doc(db_name, sample_doc_foo())
+ rev = resp.body["rev"]
+
+ # TODO: retry_part
+ resp = Couch.get("/#{db_name}/_changes")
+ assert length(resp.body["results"]) == 1, "db must not be empty"
+ assert String.at(resp.body["last_seq"], 0) == "1", "seq must start with 1"
+
+ # increase timeout to 100 to have enough time 2 assemble
+ # (seems like too little timeouts kill
+ resp = Rawresp.get("/#{db_name}/_changes?feed=#{feed}&timeout=100")
+ changes = parse_changes_line(resp.body)
+
+ change = Enum.at(changes, 0)
+ assert Enum.at(change["changes"], 0)["rev"] == rev
+
+ # the sequence is not fully ordered and a complex structure now
+ change = Enum.at(changes, 1)
+ assert String.at(change["last_seq"], 0) == "1"
+
+ # create_doc_bar(db_name,"bar")
+ {:ok, worker_pid} = HTTPotion.spawn_worker_process(Couch.process_url(""))
+
+ %HTTPotion.AsyncResponse{id: req_id} =
+ Rawresp.get("/#{db_name}/_changes?feed=#{feed}&timeout=500",
+ stream_to: self(),
+ direct: worker_pid
+ )
+
+ :ok = wait_for_headers(req_id, 200)
+ create_doc_bar(db_name, "bar")
+
+ changes = process_response(req_id, &parse_changes_line_chunk/1)
+ assert length(changes) == 3
+
+ HTTPotion.stop_worker_process(worker_pid)
+ end
+
+ def create_doc_bar(db_name, id) do
+ create_doc(db_name, %{:_id => id, :bar => 1})
+ end
+
+ defp parse_changes_line_chunk(msg) do
+ parse_changes_line(msg.chunk)
+ end
+
+ defp parse_changes_line(body) do
+ body_lines = String.split(body, "\n")
+
+ body_lines
+ |> Enum.filter(fn line -> line != "" end)
+ |> Enum.map(fn line ->
+ line |> IO.iodata_to_binary() |> :jiffy.decode([:return_maps])
+ end)
+ end
+
+ defp create_filters_view(db_name) do
+ dynamic_fun = """
+ function(doc, req) {
+ var field = req.query.field;
+ return doc[field];
+ }
+ """
+
+ userctx_fun = """
+ function(doc, req) {
+ var field = req.query.field;
+ return doc[field];
+ }
+ """
+
+ blah_fun = """
+ function(doc) {
+ if (doc._id == "blah") {
+ emit(null, null);
+ }
+ }
+ """
+
+ ddoc = %{
+ _id: "_design/changes_filter",
+ filters: %{
+ bop: "function(doc, req) { return (doc.bop);}",
+ dynamic: dynamic_fun,
+ userCtx: userctx_fun,
+ conflicted: "function(doc, req) { return (doc._conflicts);}"
+ },
+ options: %{
+ local_seq: true
+ },
+ views: %{
+ local_seq: %{
+ map: "function(doc) {emit(doc._local_seq, null)}"
+ },
+ blah: %{
+ map: blah_fun
+ }
+ }
+ }
+
+ 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/changes_test.exs b/test/elixir/test/changes_test.exs
index b5545087b..5bb376b9c 100644
--- a/test/elixir/test/changes_test.exs
+++ b/test/elixir/test/changes_test.exs
@@ -11,33 +11,441 @@ defmodule ChangesTest do
test "Changes feed negative heartbeat", context do
db_name = context[:db_name]
- resp = Couch.get(
- "/#{db_name}/_changes",
- query: %{
- :feed => "continuous",
- :heartbeat => -1000
- }
- )
+ resp =
+ Couch.get(
+ "/#{db_name}/_changes",
+ query: %{
+ :feed => "continuous",
+ :heartbeat => -1000
+ }
+ )
assert resp.status_code == 400
assert resp.body["error"] == "bad_request"
- assert resp.body["reason"] == "The heartbeat value should be a positive integer (in milliseconds)."
+
+ assert resp.body["reason"] ==
+ "The heartbeat value should be a positive integer (in milliseconds)."
end
@tag :with_db
test "Changes feed non-integer heartbeat", context do
db_name = context[:db_name]
- resp = Couch.get(
- "/#{db_name}/_changes",
- query: %{
- :feed => "continuous",
- :heartbeat => "a1000"
- }
- )
+ resp =
+ Couch.get(
+ "/#{db_name}/_changes",
+ query: %{
+ :feed => "continuous",
+ :heartbeat => "a1000"
+ }
+ )
assert resp.status_code == 400
assert resp.body["error"] == "bad_request"
- assert resp.body["reason"] == "Invalid heartbeat value. Expecting a positive integer value (in milliseconds)."
+
+ assert resp.body["reason"] ==
+ "Invalid heartbeat value. Expecting a positive integer value (in milliseconds)."
+ end
+
+ @tag :with_db
+ test "function filtered changes", context do
+ db_name = context[:db_name]
+ create_filters_view(db_name)
+
+ resp = Couch.get("/#{db_name}/_changes?filter=changes_filter/bop")
+ assert Enum.empty?(resp.body["results"]), "db must be empty"
+
+ {:ok, doc_resp} = create_doc(db_name, %{bop: "foom"})
+ rev = doc_resp.body["rev"]
+ id = doc_resp.body["id"]
+ create_doc(db_name, %{bop: false})
+
+ resp = Couch.get("/#{db_name}/_changes?filter=changes_filter/bop")
+ assert length(resp.body["results"]) == 1
+ change_rev = get_change_rev_at(resp.body["results"], 0)
+ assert change_rev == rev
+
+ doc = open_doc(db_name, id)
+ doc = Map.put(doc, "newattr", "a")
+
+ doc = save_doc(db_name, doc)
+
+ resp = Couch.get("/#{db_name}/_changes?filter=changes_filter/bop")
+ assert length(resp.body["results"]) == 1
+ new_change_rev = get_change_rev_at(resp.body["results"], 0)
+ assert new_change_rev == doc["_rev"]
+ assert new_change_rev != change_rev
+
+ resp = Couch.get("/#{db_name}/_changes?filter=changes_filter/dynamic&field=woox")
+ assert Enum.empty?(resp.body["results"]), "db must be empty"
+
+ resp = Couch.get("/#{db_name}/_changes?filter=changes_filter/dynamic&field=bop")
+ assert length(resp.body["results"]) == 1, "db must have one change"
+ new_change_rev = get_change_rev_at(resp.body["results"], 0)
+ assert new_change_rev == doc["_rev"]
+ end
+
+ @tag :with_db
+ test "non-existing desing doc for filtered changes", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_changes?filter=nothingtosee/bop")
+ assert resp.status_code == 404
+ end
+
+ @tag :with_db
+ test "non-existing function for filtered changes", context do
+ db_name = context[:db_name]
+ create_filters_view(db_name)
+ resp = Couch.get("/#{db_name}/_changes?filter=changes_filter/movealong")
+ assert resp.status_code == 404
+ end
+
+ @tag :with_db
+ test "non-existing desing doc and funcion for filtered changes", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_changes?filter=nothingtosee/movealong")
+ assert resp.status_code == 404
+ end
+
+ @tag :with_db
+ test "map function filtered changes", context do
+ db_name = context[:db_name]
+ create_filters_view(db_name)
+ create_doc(db_name, %{_id: "blah", bop: "plankton"})
+ resp = Couch.get("/#{db_name}/_changes?filter=_view&view=changes_filter/blah")
+ assert length(resp.body["results"]) == 1
+ assert Enum.at(resp.body["results"], 0)["id"] == "blah"
+ end
+
+ @tag :with_db
+ test "changes limit", context do
+ db_name = context[:db_name]
+
+ create_doc(db_name, %{_id: "blah", bop: "plankton"})
+ create_doc(db_name, %{_id: "blah2", bop: "plankton"})
+ create_doc(db_name, %{_id: "blah3", bop: "plankton"})
+
+ resp = Couch.get("/#{db_name}/_changes?limit=1")
+ assert length(resp.body["results"]) == 1
+
+ resp = Couch.get("/#{db_name}/_changes?limit=2")
+ assert length(resp.body["results"]) == 2
+ end
+
+ @tag :with_db
+ test "erlang function filtered changes", context do
+ db_name = context[:db_name]
+ create_erlang_filters_view(db_name)
+
+ resp = Couch.get("/#{db_name}/_changes?filter=erlang/foo")
+ assert Enum.empty?(resp.body["results"])
+
+ create_doc(db_name, %{_id: "doc1", value: 1})
+ create_doc(db_name, %{_id: "doc2", value: 2})
+ create_doc(db_name, %{_id: "doc3", value: 3})
+ create_doc(db_name, %{_id: "doc4", value: 4})
+
+ resp = Couch.get("/#{db_name}/_changes?filter=erlang/foo")
+
+ changes_ids =
+ resp.body["results"]
+ |> Enum.map(fn p -> p["id"] end)
+
+ assert Enum.member?(changes_ids, "doc2")
+ assert Enum.member?(changes_ids, "doc4")
+ assert length(resp.body["results"]) == 2
+ end
+
+ @tag :with_db
+ test "changes filtering on docids", context do
+ db_name = context[:db_name]
+ doc_ids = %{doc_ids: ["doc1", "doc3", "doc4"]}
+
+ resp =
+ Couch.post("/#{db_name}/_changes?filter=_doc_ids",
+ body: doc_ids,
+ headers: ["Content-Type": "application/json"]
+ )
+
+ assert Enum.empty?(resp.body["results"])
+
+ create_doc(db_name, %{_id: "doc1", value: 1})
+ create_doc(db_name, %{_id: "doc2", value: 2})
+
+ resp =
+ Couch.post("/#{db_name}/_changes?filter=_doc_ids",
+ body: doc_ids,
+ headers: ["Content-Type": "application/json"]
+ )
+
+ assert length(resp.body["results"]) == 1
+ assert Enum.at(resp.body["results"], 0)["id"] == "doc1"
+
+ create_doc(db_name, %{_id: "doc3", value: 3})
+
+ resp =
+ Couch.post("/#{db_name}/_changes?filter=_doc_ids",
+ body: doc_ids,
+ headers: ["Content-Type": "application/json"]
+ )
+
+ assert length(resp.body["results"]) == 2
+
+ changes_ids =
+ resp.body["results"]
+ |> Enum.map(fn p -> p["id"] end)
+
+ assert Enum.member?(changes_ids, "doc1")
+ assert Enum.member?(changes_ids, "doc3")
+
+ encoded_doc_ids = doc_ids.doc_ids |> :jiffy.encode()
+
+ resp =
+ Couch.get("/#{db_name}/_changes",
+ query: %{filter: "_doc_ids", doc_ids: encoded_doc_ids}
+ )
+
+ assert length(resp.body["results"]) == 2
+
+ changes_ids =
+ resp.body["results"]
+ |> Enum.map(fn p -> p["id"] end)
+
+ assert Enum.member?(changes_ids, "doc1")
+ assert Enum.member?(changes_ids, "doc3")
+ end
+
+ @tag :with_db
+ test "changes filtering on design docs", context do
+ db_name = context[:db_name]
+
+ create_erlang_filters_view(db_name)
+ create_doc(db_name, %{_id: "doc1", value: 1})
+
+ resp = Couch.get("/#{db_name}/_changes?filter=_design")
+ assert length(resp.body["results"]) == 1
+ assert Enum.at(resp.body["results"], 0)["id"] == "_design/erlang"
+ end
+
+ @tag :with_db
+ test "COUCHDB-1037-empty result for ?limit=1&filter=foo/bar in some cases",
+ context do
+ db_name = context[:db_name]
+
+ filter_fun = """
+ function(doc, req) {
+ return (typeof doc.integer === "number");
+ }
+ """
+
+ ddoc = %{
+ _id: "_design/testdocs",
+ language: "javascript",
+ filters: %{
+ testdocsonly: filter_fun
+ }
+ }
+
+ create_doc(db_name, ddoc)
+
+ ddoc = %{
+ _id: "_design/foobar",
+ foo: "bar"
+ }
+
+ create_doc(db_name, ddoc)
+ bulk_save(db_name, make_docs(0..4))
+
+ resp = Couch.get("/#{db_name}/_changes")
+ assert length(resp.body["results"]) == 7
+
+ resp = Couch.get("/#{db_name}/_changes?limit=1&filter=testdocs/testdocsonly")
+ assert length(resp.body["results"]) == 1
+ # we can't guarantee ordering
+ assert Regex.match?(~r/[0-4]/, Enum.at(resp.body["results"], 0)["id"])
+
+ resp = Couch.get("/#{db_name}/_changes?limit=2&filter=testdocs/testdocsonly")
+ assert length(resp.body["results"]) == 2
+ # we can't guarantee ordering
+ assert Regex.match?(~r/[0-4]/, Enum.at(resp.body["results"], 0)["id"])
+ assert Regex.match?(~r/[0-4]/, Enum.at(resp.body["results"], 1)["id"])
+ end
+
+ @tag :with_db
+ test "COUCHDB-1256", context do
+ db_name = context[:db_name]
+ {:ok, resp} = create_doc(db_name, %{_id: "foo", a: 123})
+ create_doc(db_name, %{_id: "bar", a: 456})
+ foo_rev = resp.body["rev"]
+
+ Couch.put("/#{db_name}/foo?new_edits=false",
+ headers: ["Content-Type": "application/json"],
+ body: %{_rev: foo_rev, a: 456}
+ )
+
+ resp = Couch.get("/#{db_name}/_changes?style=all_docs")
+ assert length(resp.body["results"]) == 2
+
+ resp =
+ Couch.get("/#{db_name}/_changes",
+ query: %{style: "all_docs", since: Enum.at(resp.body["results"], 0)["seq"]}
+ )
+
+ assert length(resp.body["results"]) == 1
+ end
+
+ @tag :with_db
+ test "COUCHDB-1923", context do
+ db_name = context[:db_name]
+ attachment_data = "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+
+ docs =
+ make_docs(20..29, %{
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "text/plain",
+ data: attachment_data
+ },
+ "bar.txt": %{
+ content_type: "text/plain",
+ data: attachment_data
+ }
+ }
+ })
+
+ bulk_save(db_name, docs)
+
+ resp = Couch.get("/#{db_name}/_changes?include_docs=true")
+ assert length(resp.body["results"]) == 10
+
+ first_doc = Enum.at(resp.body["results"], 0)["doc"]
+
+ assert first_doc["_attachments"]["foo.txt"]["stub"]
+ assert not Enum.member?(first_doc["_attachments"]["foo.txt"], "data")
+ assert not Enum.member?(first_doc["_attachments"]["foo.txt"], "encoding")
+ assert not Enum.member?(first_doc["_attachments"]["foo.txt"], "encoded_length")
+ assert first_doc["_attachments"]["bar.txt"]["stub"]
+ assert not Enum.member?(first_doc["_attachments"]["bar.txt"], "data")
+ assert not Enum.member?(first_doc["_attachments"]["bar.txt"], "encoding")
+ assert not Enum.member?(first_doc["_attachments"]["bar.txt"], "encoded_length")
+
+ resp = Couch.get("/#{db_name}/_changes?include_docs=true&attachments=true")
+ assert length(resp.body["results"]) == 10
+
+ first_doc = Enum.at(resp.body["results"], 0)["doc"]
+
+ assert not Enum.member?(first_doc["_attachments"]["foo.txt"], "stub")
+ assert first_doc["_attachments"]["foo.txt"]["data"] == attachment_data
+ assert not Enum.member?(first_doc["_attachments"]["foo.txt"], "encoding")
+ assert not Enum.member?(first_doc["_attachments"]["foo.txt"], "encoded_length")
+
+ assert not Enum.member?(first_doc["_attachments"]["bar.txt"], "stub")
+ assert first_doc["_attachments"]["bar.txt"]["data"] == attachment_data
+ assert not Enum.member?(first_doc["_attachments"]["bar.txt"], "encoding")
+ assert not Enum.member?(first_doc["_attachments"]["bar.txt"], "encoded_length")
+
+ resp = Couch.get("/#{db_name}/_changes?include_docs=true&att_encoding_info=true")
+ assert length(resp.body["results"]) == 10
+
+ first_doc = Enum.at(resp.body["results"], 0)["doc"]
+
+ assert first_doc["_attachments"]["foo.txt"]["stub"]
+ assert not Enum.member?(first_doc["_attachments"]["foo.txt"], "data")
+ assert first_doc["_attachments"]["foo.txt"]["encoding"] == "gzip"
+ assert first_doc["_attachments"]["foo.txt"]["encoded_length"] == 47
+ assert first_doc["_attachments"]["bar.txt"]["stub"]
+ assert not Enum.member?(first_doc["_attachments"]["bar.txt"], "data")
+ assert first_doc["_attachments"]["bar.txt"]["encoding"] == "gzip"
+ assert first_doc["_attachments"]["bar.txt"]["encoded_length"] == 47
+ end
+
+ defp create_erlang_filters_view(db_name) do
+ erlang_fun = """
+ fun({Doc}, Req) ->
+ case couch_util:get_value(<<"value">>, Doc) of
+ undefined -> false;
+ Value -> (Value rem 2) =:= 0;
+ _ -> false
+ end
+ end.
+ """
+
+ ddoc = %{
+ _id: "_design/erlang",
+ language: "erlang",
+ filters: %{
+ foo: erlang_fun
+ }
+ }
+
+ create_doc(db_name, ddoc)
+ end
+
+ defp create_filters_view(db_name) do
+ dynamic_fun = """
+ function(doc, req) {
+ var field = req.query.field;
+ return doc[field];
+ }
+ """
+
+ userctx_fun = """
+ function(doc, req) {
+ var field = req.query.field;
+ return doc[field];
+ }
+ """
+
+ blah_fun = """
+ function(doc) {
+ if (doc._id == "blah") {
+ emit(null, null);
+ }
+ }
+ """
+
+ ddoc = %{
+ _id: "_design/changes_filter",
+ filters: %{
+ bop: "function(doc, req) { return (doc.bop);}",
+ dynamic: dynamic_fun,
+ userCtx: userctx_fun,
+ conflicted: "function(doc, req) { return (doc._conflicts);}"
+ },
+ options: %{
+ local_seq: true
+ },
+ views: %{
+ local_seq: %{
+ map: "function(doc) {emit(doc._local_seq, null)}"
+ },
+ blah: %{
+ map: blah_fun
+ }
+ }
+ }
+
+ create_doc(db_name, ddoc)
+ end
+
+ defp get_change_rev_at(results, idx) do
+ results
+ |> Enum.at(idx)
+ |> Map.fetch!("changes")
+ |> Enum.at(0)
+ |> Map.fetch!("rev")
+ end
+
+ defp open_doc(db_name, id) do
+ resp = Couch.get("/#{db_name}/#{id}")
+ assert resp.status_code == 200
+ resp.body
+ end
+
+ defp save_doc(db_name, body) do
+ resp = Couch.put("/#{db_name}/#{body["_id"]}", body: body)
+ assert resp.status_code in [201, 202]
+ assert resp.body["ok"]
+ Map.put(body, "_rev", resp.body["rev"])
end
end
diff --git a/test/elixir/test/design_docs_query_test.exs b/test/elixir/test/design_docs_query_test.exs
new file mode 100644
index 000000000..b439a2e02
--- /dev/null
+++ b/test/elixir/test/design_docs_query_test.exs
@@ -0,0 +1,273 @@
+defmodule DesignDocsQueryTest do
+ use CouchTestCase
+
+ @moduletag :design_docs
+
+ @moduledoc """
+ Test CouchDB /{db}/_design_docs
+ """
+
+ 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(1..5))
+
+ Enum.each(1..5, fn x -> create_ddoc(db_name, x) end)
+
+ {:ok, [db_name: db_name]}
+ end
+
+ defp create_ddoc(db_name, idx) do
+ ddoc = %{
+ _id: "_design/ddoc0#{idx}",
+ views: %{
+ testing: %{
+ map: "function(){emit(1,1)}"
+ }
+ }
+ }
+
+ create_doc(db_name, ddoc)
+ end
+
+ test "query _design_docs (GET with no parameters)", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_design_docs")
+ assert resp.status_code == 200, "standard get should be 200"
+ assert resp.body["total_rows"] == 5, "total_rows mismatch"
+ assert length(resp.body["rows"]) == 5, "amount of rows mismatch"
+ end
+
+ test "query _design_docs with single key", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_design_docs?key=\"_design/ddoc03\"")
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 1, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 0)["key"] == "_design/ddoc03"
+ end
+
+ test "query _design_docs with multiple key", context do
+ resp =
+ Couch.get(
+ "/#{context[:db_name]}/_design_docs",
+ query: %{
+ :keys => "[\"_design/ddoc02\", \"_design/ddoc03\"]"
+ }
+ )
+
+ assert resp.status_code == 200
+ assert length(Map.get(resp, :body)["rows"]) == 2
+ end
+
+ test "POST with empty body", context do
+ resp =
+ Couch.post(
+ "/#{context[:db_name]}/_design_docs",
+ body: %{}
+ )
+
+ assert resp.status_code == 200
+ assert length(Map.get(resp, :body)["rows"]) == 5
+ end
+
+ test "POST with keys and limit", context do
+ resp =
+ Couch.post(
+ "/#{context[:db_name]}/_design_docs",
+ body: %{
+ :keys => ["_design/ddoc02", "_design/ddoc03"],
+ :limit => 1
+ }
+ )
+
+ assert resp.status_code == 200
+ assert length(Map.get(resp, :body)["rows"]) == 1
+ end
+
+ test "POST with query parameter and JSON body", context do
+ resp =
+ Couch.post(
+ "/#{context[:db_name]}/_design_docs",
+ query: %{
+ :limit => 1
+ },
+ body: %{
+ :keys => ["_design/ddoc02", "_design/ddoc03"]
+ }
+ )
+
+ assert resp.status_code == 200
+ assert length(Map.get(resp, :body)["rows"]) == 1
+ end
+
+ test "POST edge case with colliding parameters - query takes precedence", context do
+ resp =
+ Couch.post(
+ "/#{context[:db_name]}/_design_docs",
+ query: %{
+ :limit => 0
+ },
+ body: %{
+ :keys => ["_design/ddoc02", "_design/ddoc03"],
+ :limit => 2
+ }
+ )
+
+ assert resp.status_code == 200
+ assert Enum.empty?(Map.get(resp, :body)["rows"])
+ end
+
+ test "query _design_docs descending=true", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_design_docs?descending=true")
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 5, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 0)["key"] == "_design/ddoc05"
+ end
+
+ test "query _design_docs descending=false", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_design_docs?descending=false")
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 5, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 0)["key"] == "_design/ddoc01"
+ end
+
+ test "query _design_docs end_key", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_design_docs?end_key=\"_design/ddoc03\"")
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 3, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 2)["key"] == "_design/ddoc03"
+ end
+
+ test "query _design_docs endkey", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_design_docs?endkey=\"_design/ddoc03\"")
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 3, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 2)["key"] == "_design/ddoc03"
+ end
+
+ test "query _design_docs start_key", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_design_docs?start_key=\"_design/ddoc03\"")
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 3, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 0)["key"] == "_design/ddoc03"
+ end
+
+ test "query _design_docs startkey", context do
+ db_name = context[:db_name]
+ resp = Couch.get("/#{db_name}/_design_docs?startkey=\"_design/ddoc03\"")
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 3, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 0)["key"] == "_design/ddoc03"
+ end
+
+ test "query _design_docs end_key inclusive_end=true", context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.get("/#{db_name}/_design_docs",
+ query: [end_key: "\"_design/ddoc03\"", inclusive_end: true]
+ )
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 3, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 2)["key"] == "_design/ddoc03"
+ end
+
+ test "query _design_docs end_key inclusive_end=false", context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.get("/#{db_name}/_design_docs",
+ query: [end_key: "\"_design/ddoc03\"", inclusive_end: false]
+ )
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 2, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 1)["key"] == "_design/ddoc02"
+ end
+
+ test "query _design_docs end_key inclusive_end=false descending", context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.get("/#{db_name}/_design_docs",
+ query: [end_key: "\"_design/ddoc03\"", inclusive_end: false, descending: true]
+ )
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 2, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 1)["key"] == "_design/ddoc04"
+ end
+
+ test "query _design_docs end_key limit", context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.get("/#{db_name}/_design_docs",
+ query: [end_key: "\"_design/ddoc05\"", limit: 2]
+ )
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 2, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 1)["key"] == "_design/ddoc02"
+ end
+
+ test "query _design_docs end_key skip", context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.get("/#{db_name}/_design_docs",
+ query: [end_key: "\"_design/ddoc05\"", skip: 2]
+ )
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 3, "amount of rows mismatch"
+ assert Enum.at(resp.body["rows"], 0)["key"] == "_design/ddoc03"
+ assert Enum.at(resp.body["rows"], 2)["key"] == "_design/ddoc05"
+ end
+
+ test "query _design_docs update_seq", context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.get("/#{db_name}/_design_docs",
+ query: [end_key: "\"_design/ddoc05\"", update_seq: true]
+ )
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert Map.has_key?(resp.body, "update_seq")
+ end
+
+ test "query _design_docs post with keys", context do
+ db_name = context[:db_name]
+
+ resp =
+ Couch.post("/#{db_name}/_design_docs",
+ headers: ["Content-Type": "application/json"],
+ body: %{keys: ["_design/ddoc02", "_design/ddoc03"]}
+ )
+
+ keys =
+ resp.body["rows"]
+ |> Enum.map(fn p -> p["key"] end)
+
+ assert resp.status_code == 200, "standard get should be 200"
+ assert length(resp.body["rows"]) == 2, "amount of rows mismatch"
+ assert Enum.member?(keys, "_design/ddoc03")
+ assert Enum.member?(keys, "_design/ddoc02")
+ end
+end
diff --git a/test/elixir/test/design_docs_test.exs b/test/elixir/test/design_docs_test.exs
deleted file mode 100644
index ed0a0dfb5..000000000
--- a/test/elixir/test/design_docs_test.exs
+++ /dev/null
@@ -1,108 +0,0 @@
-defmodule DesignDocsTest do
- use CouchTestCase
-
- @moduletag :design_docs
-
- @moduledoc """
- Test CouchDB /{db}/_design_docs
- """
-
- 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,
- %{
- _id: "_design/foo",
- bar: "baz"
- }
- )
-
- {:ok, _} = create_doc(
- db_name,
- %{
- _id: "_design/foo2",
- bar: "baz2"
- }
- )
-
- {:ok, [db_name: db_name]}
- end
-
- test "GET with no parameters", context do
- resp = Couch.get(
- "/#{context[:db_name]}/_design_docs"
- )
-
- assert resp.status_code == 200
- assert length(Map.get(resp, :body)["rows"]) == 2
- end
-
- test "GET with multiple keys", context do
- resp = Couch.get(
- "/#{context[:db_name]}/_design_docs",
- query: %{
- :keys => "[\"_design/foo\", \"_design/foo2\"]",
- }
- )
-
- assert resp.status_code == 200
- assert length(Map.get(resp, :body)["rows"]) == 2
- end
-
- test "POST with empty body", context do
- resp = Couch.post(
- "/#{context[:db_name]}/_design_docs",
- body: %{}
- )
-
- assert resp.status_code == 200
- assert length(Map.get(resp, :body)["rows"]) == 2
- end
-
- test "POST with keys and limit", context do
- resp = Couch.post(
- "/#{context[:db_name]}/_design_docs",
- body: %{
- :keys => ["_design/foo", "_design/foo2"],
- :limit => 1
- }
- )
-
- assert resp.status_code == 200
- assert length(Map.get(resp, :body)["rows"]) == 1
- end
-
- test "POST with query parameter and JSON body", context do
- resp = Couch.post(
- "/#{context[:db_name]}/_design_docs",
- query: %{
- :limit => 1
- },
- body: %{
- :keys => ["_design/foo", "_design/foo2"]
- }
- )
-
- assert resp.status_code == 200
- assert length(Map.get(resp, :body)["rows"]) == 1
- end
-
- test "POST edge case with colliding parameters - query takes precedence", context do
- resp = Couch.post(
- "/#{context[:db_name]}/_design_docs",
- query: %{
- :limit => 0
- },
- body: %{
- :keys => ["_design/foo", "_design/foo2"],
- :limit => 2
- }
- )
-
- assert resp.status_code == 200
- assert Enum.empty?(Map.get(resp, :body)["rows"])
- end
-end
diff --git a/test/elixir/test/design_options_test.exs b/test/elixir/test/design_options_test.exs
new file mode 100644
index 000000000..95a938e38
--- /dev/null
+++ b/test/elixir/test/design_options_test.exs
@@ -0,0 +1,74 @@
+defmodule DesignOptionsTest do
+ use CouchTestCase
+
+ @moduletag :design_docs
+
+ @moduledoc """
+ Test CouchDB design documents options include_design and local_seq
+ """
+ @tag :with_db
+ test "design doc options - include_desing=true", context do
+ db_name = context[:db_name]
+
+ create_test_view(db_name, "_design/fu", %{include_design: true})
+
+ resp = Couch.get("/#{db_name}/_design/fu/_view/data")
+ assert resp.status_code == 200
+ assert length(Map.get(resp, :body)["rows"]) == 1
+ assert Enum.at(resp.body["rows"], 0)["value"] == "_design/fu"
+ end
+
+ @tag :with_db
+ test "design doc options - include_desing=false", context do
+ db_name = context[:db_name]
+
+ create_test_view(db_name, "_design/bingo", %{include_design: false})
+
+ resp = Couch.get("/#{db_name}/_design/bingo/_view/data")
+ assert resp.status_code == 200
+ assert Enum.empty?(Map.get(resp, :body)["rows"])
+ end
+
+ @tag :with_db
+ test "design doc options - include_design default value", context do
+ db_name = context[:db_name]
+
+ create_test_view(db_name, "_design/bango", %{})
+
+ resp = Couch.get("/#{db_name}/_design/bango/_view/data")
+ assert resp.status_code == 200
+ assert Enum.empty?(Map.get(resp, :body)["rows"])
+ end
+
+ @tag :with_db
+ test "design doc options - local_seq=true", context do
+ db_name = context[:db_name]
+
+ create_test_view(db_name, "_design/fu", %{include_design: true, local_seq: true})
+ create_doc(db_name, %{})
+ resp = Couch.get("/#{db_name}/_design/fu/_view/with_seq")
+
+ row_with_key =
+ resp.body["rows"]
+ |> Enum.filter(fn p -> p["key"] != :null end)
+
+ assert length(row_with_key) == 2
+ end
+
+ defp create_test_view(db_name, id, options) do
+ map = "function (doc) {emit(null, doc._id);}"
+ withseq = "function(doc) {emit(doc._local_seq, null)}"
+
+ design_doc = %{
+ _id: id,
+ language: "javascript",
+ options: options,
+ views: %{
+ data: %{map: map},
+ with_seq: %{map: withseq}
+ }
+ }
+
+ create_doc(db_name, design_doc)
+ end
+end
diff --git a/test/elixir/test/design_paths_test.exs b/test/elixir/test/design_paths_test.exs
new file mode 100644
index 000000000..b3e10c165
--- /dev/null
+++ b/test/elixir/test/design_paths_test.exs
@@ -0,0 +1,76 @@
+defmodule DesignPathTest do
+ use CouchTestCase
+
+ @moduletag :design_docs
+
+ @moduledoc """
+ Test CouchDB design documents path
+ """
+ @tag :with_db
+ test "design doc path", context do
+ db_name = context[:db_name]
+ ddoc_path_test(db_name)
+ end
+
+ @tag :with_db_name
+ test "design doc path with slash in db name", context do
+ db_name = URI.encode_www_form(context[:db_name] <> "/with_slashes")
+ create_db(db_name)
+ ddoc_path_test(db_name)
+ end
+
+ defp ddoc_path_test(db_name) do
+ create_test_view(db_name, "_design/test")
+
+ resp = Couch.get("/#{db_name}/_design/test")
+ assert resp.body["_id"] == "_design/test"
+
+ resp =
+ Couch.get(Couch.process_url("/#{db_name}/_design%2Ftest"),
+ follow_redirects: true
+ )
+
+ assert resp.body["_id"] == "_design/test"
+
+ resp = Couch.get("/#{db_name}/_design/test/_view/testing")
+ assert Enum.empty?(Map.get(resp, :body)["rows"])
+
+ design_doc2 = %{
+ _id: "_design/test2",
+ views: %{
+ testing: %{
+ map: "function(){emit(1,1)}"
+ }
+ }
+ }
+
+ resp = Couch.put("/#{db_name}/_design/test2", body: design_doc2)
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/_design/test2")
+ assert resp.body["_id"] == "_design/test2"
+
+ resp =
+ Couch.get(Couch.process_url("/#{db_name}/_design%2Ftest2"),
+ follow_redirects: true
+ )
+
+ assert resp.body["_id"] == "_design/test2"
+
+ resp = Couch.get("/#{db_name}/_design/test2/_view/testing")
+ assert Enum.empty?(Map.get(resp, :body)["rows"])
+ end
+
+ defp create_test_view(db_name, id) do
+ design_doc = %{
+ _id: id,
+ views: %{
+ testing: %{
+ map: "function(){emit(1,1)}"
+ }
+ }
+ }
+
+ create_doc(db_name, design_doc)
+ end
+end
diff --git a/test/javascript/tests/changes.js b/test/javascript/tests/changes.js
index d312edc41..d98e37cc8 100644
--- a/test/javascript/tests/changes.js
+++ b/test/javascript/tests/changes.js
@@ -11,6 +11,7 @@
// the License.
function jsonp(obj) {
+ return console.log('done in test/elixir/test/changes_test.exs and changes_async_test.exs');
T(jsonp_flag == 0);
T(obj.results.length == 1 && obj.last_seq == 1, "jsonp");
jsonp_flag = 1;
@@ -359,7 +360,7 @@ couchTests.changes = function(debug) {
resp = JSON.parse(req.responseText);
T(resp.results.length == 1, "changes_filter/dynamic&field=bop");
T(resp.results[0].changes[0].rev == docres1.rev, "filtered/dynamic&field=bop rev");
-
+
// these will NEVER run as we're always in navigator == undefined
if (!is_safari && xhr) { // full test requires parallel connections
// filter with longpoll
@@ -708,7 +709,7 @@ couchTests.changes = function(debug) {
db = new CouchDB(db_name, {"X-Couch-Full-Commit":"true"}, {"w": 3});
T(db.createDb());
- // create 4 documents... this assumes the update sequnce will start from 0 and then do sth in the cluster
+ // create 4 documents... this assumes the update sequnce will start from 0 and then do sth in the cluster
db.save({"bop" : "foom"});
db.save({"bop" : "foom"});
db.save({"bop" : "foom"});
@@ -717,7 +718,7 @@ couchTests.changes = function(debug) {
req = CouchDB.request("GET", "/" + db_name + "/_changes");
// simulate an EventSource request with a Last-Event-ID header
- // increase timeout to 100 to have enough time 2 assemble (seems like too little timeouts kill
+ // increase timeout to 100 to have enough time 2 assemble (seems like too little timeouts kill
req = CouchDB.request("GET", "/" + db_name + "/_changes?feed=eventsource&timeout=100&since=0",
{"headers": {"Accept": "text/event-stream", "Last-Event-ID": JSON.parse(req.responseText).results[1].seq}});
diff --git a/test/javascript/tests/design_docs_query.js b/test/javascript/tests/design_docs_query.js
index 07e6577ab..2aefe49b4 100644
--- a/test/javascript/tests/design_docs_query.js
+++ b/test/javascript/tests/design_docs_query.js
@@ -11,6 +11,8 @@
// the License.
couchTests.design_docs_query = function(debug) {
+ return console.log('done in test/elixir/test/design_docs_query_test.exs');
+
var db_name = get_random_db_name();
var db = new CouchDB(db_name, {"X-Couch-Full-Commit":"false"});
db.createDb();
diff --git a/test/javascript/tests/design_options.js b/test/javascript/tests/design_options.js
index cc2571f6b..d3f8594d4 100644
--- a/test/javascript/tests/design_options.js
+++ b/test/javascript/tests/design_options.js
@@ -11,6 +11,7 @@
// the License.
couchTests.design_options = function(debug) {
+ return console.log('done in test/elixir/test/design_options.exs');
var db_name = get_random_db_name();
var db = new CouchDB(db_name, {"X-Couch-Full-Commit":"false"});
db.createDb();
@@ -36,7 +37,7 @@ couchTests.design_options = function(debug) {
T(db.save(designDoc).ok);
// should work for temp views
- // no more there on cluster - pointless test
+ // no more there on cluster - pointless test
//var rows = db.query(map, null, {options:{include_design: true}}).rows;
//T(rows.length == 1);
//T(rows[0].value == "_design/fu");
diff --git a/test/javascript/tests/design_paths.js b/test/javascript/tests/design_paths.js
index 6e816991a..b85426acf 100644
--- a/test/javascript/tests/design_paths.js
+++ b/test/javascript/tests/design_paths.js
@@ -11,6 +11,7 @@
// the License.
couchTests.design_paths = function(debug) {
+ return console.log('done in test/elixir/test/design_paths.exs');
if (debug) debugger;
var db_name = get_random_db_name()
var dbNames = [db_name, db_name + "/with_slashes"];