blob: e3e8ba7848f5012b6f46db92fe404d153076ef50 [file] [log] [blame]
defmodule ChangesTest do
use CouchTestCase
@moduletag :changes
@moduledoc """
Test CouchDB /{db}/_changes
"""
@tag :with_db
test "Changes feed negative heartbeat", context do
db_name = context[:db_name]
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)."
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"
}
)
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)."
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 "changes filtering on custom filter", context do
db_name = context[:db_name]
create_filters_view(db_name)
resp = Couch.post("/#{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"]
create_doc(db_name, %{bop: false})
resp = Couch.post("/#{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
resp = Couch.post("/#{db_name}/_changes?filter=changes_filter/bop",
body: %{doc_ids: ["doc1", "doc3", "doc4"]},
headers: ["Content-Type": "application/json"]
)
assert length(resp.body["results"]) == 1
change_rev = get_change_rev_at(resp.body["results"], 0)
assert change_rev == rev
end
@tag :with_db
test "changes fail on invalid payload", context do
db_name = context[:db_name]
create_filters_view(db_name)
resp = Couch.post("/#{db_name}/_changes?filter=changes_filter/bop",
body: "[\"doc1\"]",
headers: ["Content-Type": "application/json"]
)
assert resp.status_code == 400
assert resp.body["error"] == "bad_request"
assert resp.body["reason"] == "Request body must be a JSON object"
resp = Couch.post("/#{db_name}/_changes?filter=changes_filter/bop",
body: "{\"doc_ids\": [\"doc1\",",
headers: ["Content-Type": "application/json"]
)
assert resp.status_code == 400
assert resp.body["error"] == "bad_request"
assert resp.body["reason"] == "invalid UTF-8 JSON"
set_config({"chttpd", "max_http_request_size", "16"})
resp = Couch.post("/#{db_name}/_changes?filter=changes_filter/bop",
body: %{doc_ids: ["doc1", "doc3", "doc4"]},
headers: ["Content-Type": "application/json"]
)
assert resp.status_code == 413
assert resp.body["error"] == "too_large"
assert resp.body["reason"] == "the request entity is too large"
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