summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuanjo Rodriguez <jjrodrig@gmail.com>2020-02-20 23:53:51 +0100
committerJuanjo Rodriguez <jjrodrig@gmail.com>2020-02-22 22:38:37 +0100
commitbc0d4d7bb3f8237b9678d3f6ecb938a3cc29ac28 (patch)
tree348ac45ce5023f86e5a7811e431f287d9b21ce36
parentdc6a06c836a5ecae8920b6c7eca1cfec55fcfb33 (diff)
downloadcouchdb-bc0d4d7bb3f8237b9678d3f6ecb938a3cc29ac28.tar.gz
Port changes.js test suite into elixir
-rw-r--r--test/elixir/test/changes_async_test.exs545
-rw-r--r--test/elixir/test/changes_test.exs440
-rw-r--r--test/javascript/tests/changes.js7
3 files changed, 973 insertions, 19 deletions
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/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}});