summaryrefslogtreecommitdiff
path: root/test/elixir/test/bulk_docs_test.exs
blob: d548eefa0ca684b56018d7dbf8e9319e8adb1d22 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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 invlaid `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 invlaid `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

  @tag :with_db
  test "bulk docs raises transaction_too_large error for transaction larger than 10MB", ctx do
    docs = [%{_id: "0", a: random_string(16_000_000)}]
    old_size = Couch.get("/_node/node1@127.0.0.1/_config/couchdb/max_document_size").body
    set_config_raw("couchdb", "max_document_size", "67108864") # 64M
    resp = Couch.post("/#{ctx[:db_name]}/_bulk_docs", body: %{docs: docs})
    set_config_raw("couchdb", "max_document_size", old_size) # set back
    assert resp.status_code == 413
    assert resp.body["error"] == "transaction_too_large"
  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

  defp random_string(length) do
    raw = :crypto.strong_rand_bytes(length)
    raw
    |> Base.url_encode64
    |> binary_part(0, length)
  end
end