diff options
author | Juanjo Rodriguez <jjrodrig@gmail.com> | 2020-02-22 22:21:39 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-22 22:21:39 +0100 |
commit | 93cf4d328e26940d7bc49ca3a6f3abbf9fd07b1e (patch) | |
tree | e12c50abadee6c11825c7425a42541b38099b533 | |
parent | 26f93667c9f65f5f977ab7eabd5a65fbe93d07b6 (diff) | |
parent | 2ef656efa1dc33b40894a125b5f57f9ac4bbc87a (diff) | |
download | couchdb-93cf4d328e26940d7bc49ca3a6f3abbf9fd07b1e.tar.gz |
Merge pull request #2586 from jjrodrig/port-elixir-changes
Port changes.js test suite into elixir
-rw-r--r-- | test/elixir/test/changes_async_test.exs | 545 | ||||
-rw-r--r-- | test/elixir/test/changes_test.exs | 440 | ||||
-rw-r--r-- | test/javascript/tests/changes.js | 7 |
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}}); |