blob: d81b335ecf791ace4b777360925c5ac3443c760e [file] [log] [blame]
defmodule BulkDocsTest do
use CouchTestCase
@moduletag :bulk_docs
@moduledoc """
Test CouchDB bulk docs
This is a port of bulk_docs.js
"""
@doc_range 1..5
@tag :with_db
test "bulk docs can create, update, & delete many docs per request", ctx do
db = ctx[:db_name]
docs = create_docs(@doc_range)
resp = bulk_post(docs, db)
assert revs_start_with(resp.body, "1-")
docs = rev(docs, resp.body)
# Modify each doc's `string` field and re-post
docs =
Enum.map(docs, fn doc = %{string: string} ->
%{doc | string: string <> ".00"}
end)
resp = bulk_post(docs, db)
assert revs_start_with(resp.body, "2-")
docs = rev(docs, resp.body)
# Confirm changes were applied for each doc
assert Enum.all?(docs, fn doc ->
String.ends_with?(Couch.get("/#{db}/#{doc._id}").body["string"], ".00")
end)
docs = Enum.map(docs, &Map.put(&1, :_deleted, true))
resp = bulk_post(docs, db)
assert revs_start_with(resp.body, "3-")
# Confirm docs were deleted
assert Enum.all?(docs, fn doc ->
resp = Couch.get("/#{db}/#{doc._id}")
assert resp.status_code == 404
assert resp.body["error"] == "not_found"
assert resp.body["reason"] == "deleted"
end)
end
@tag :with_db
@tag :skip_on_jenkins
test "bulk docs can detect conflicts", ctx do
db = ctx[:db_name]
docs = create_docs(@doc_range)
resp = bulk_post(docs, db)
assert revs_start_with(resp.body, "1-")
docs = rev(docs, resp.body)
# Update just the first doc to create a conflict in subsequent bulk update
doc = hd(docs)
resp = Couch.put("/#{db}/#{doc._id}", body: doc)
assert resp.status_code in [201, 202]
# Attempt to delete all docs
docs = Enum.map(docs, fn doc -> Map.put(doc, :_deleted, true) end)
retry_until(fn ->
resp = bulk_post(docs, db)
# Confirm first doc not updated, and result has no rev field
res = hd(resp.body)
assert res["id"] == "1" and res["error"] == "conflict"
assert Map.get(res, "rev") == nil
# Confirm other docs updated normally
assert revs_start_with(tl(resp.body), "2-")
end)
end
@tag :with_db
test "bulk docs supplies `id` if not provided in doc", ctx do
docs = [%{foo: "bar"}]
res = hd(bulk_post(docs, ctx[:db_name]).body)
assert res["id"]
assert res["rev"]
end
@tag :with_db
test "bulk docs raises error for `all_or_nothing` option", ctx do
opts = [body: %{docs: create_docs(@doc_range), all_or_nothing: true}]
resp = Couch.post("/#{ctx[:db_name]}/_bulk_docs", opts)
assert resp.status_code == 417
assert Enum.all?(resp.body, &(Map.get(&1, "error") == "not_implemented"))
expected_reason = "all_or_nothing is not supported"
assert Enum.all?(resp.body, &(Map.get(&1, "reason") == expected_reason))
end
@tag :with_db
test "bulk docs raises conflict error for combined update & delete", ctx do
db = ctx[:db_name]
doc = %{_id: "id", val: "val"}
resp = Couch.put("/#{db}/#{doc._id}", body: doc)
doc = rev(doc, resp.body)
update = %{doc | val: "newval"}
delete = Map.put(doc, :_deleted, true)
body = bulk_post([update, delete], db).body
assert Enum.count(body, &(Map.get(&1, "error") == "conflict")) == 1
assert Enum.count(body, &Map.get(&1, "rev")) == 1
end
@tag :with_db
test "bulk docs raises error for missing `docs` parameter", ctx do
docs = [%{foo: "bar"}]
resp = Couch.post("/#{ctx[:db_name]}/_bulk_docs", body: %{doc: docs})
assert_bad_request(resp, "POST body must include `docs` parameter.")
end
@tag :with_db
test "bulk docs raises error for invalid `docs` parameter", ctx do
resp = Couch.post("/#{ctx[:db_name]}/_bulk_docs", body: %{docs: "foo"})
assert_bad_request(resp, "`docs` parameter must be an array.")
end
@tag :with_db
test "bulk docs raises error for invalid `new_edits` parameter", ctx do
opts = [body: %{docs: [], new_edits: 0}]
resp = Couch.post("/#{ctx[:db_name]}/_bulk_docs", opts)
assert_bad_request(resp, "`new_edits` parameter must be a boolean.")
end
@tag :with_db
test "bulk docs emits conflict error for duplicate doc `_id`s", ctx do
docs = [%{_id: "0", a: 0}, %{_id: "1", a: 1}, %{_id: "1", a: 2}, %{_id: "3", a: 3}]
rows = bulk_post(docs, ctx[:db_name]).body
assert Enum.at(rows, 1)["id"] == "1"
assert Enum.at(rows, 1)["ok"]
assert Enum.at(rows, 2)["error"] == "conflict"
end
defp bulk_post(docs, db) do
retry_until(fn ->
resp = Couch.post("/#{db}/_bulk_docs", body: %{docs: docs})
assert resp.status_code in [201, 202] and length(resp.body) == length(docs), """
Expected 201 and the same number of response rows as in request, but got
#{pretty_inspect(resp)}
"""
resp
end)
end
defp revs_start_with(rows, prefix) do
Enum.all?(rows, fn %{"rev" => rev} -> String.starts_with?(rev, prefix) end)
end
defp assert_bad_request(resp, reason) do
assert resp.status_code == 400
assert resp.body["error"] == "bad_request"
assert resp.body["reason"] == reason
end
end