summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuanjo Rodriguez <jjrodrig@gmail.com>2019-04-08 11:30:26 +0200
committergarren smith <garren.smith@gmail.com>2019-04-08 11:30:26 +0200
commit5d199268d4330b80ce54d5dad2fe27db4bb81641 (patch)
tree9105813c0db8fb0de168c14d483a63f63efa189a
parenta6db7d54e7651eee796932a60b534b208566f563 (diff)
downloadcouchdb-5d199268d4330b80ce54d5dad2fe27db4bb81641.tar.gz
Port javascript attachment test suite into elixir (#1999)
-rw-r--r--test/elixir/README.md12
-rw-r--r--test/elixir/lib/couch/db_test.ex133
-rw-r--r--test/elixir/test/attachment_names_test.exs97
-rw-r--r--test/elixir/test/attachment_paths_test.exs177
-rw-r--r--test/elixir/test/attachment_ranges_test.exs143
-rw-r--r--test/elixir/test/attachment_views_test.exs142
-rw-r--r--test/elixir/test/attachments_multipart_test.exs409
-rw-r--r--test/elixir/test/attachments_test.exs (renamed from test/elixir/test/attachments.exs)0
8 files changed, 1107 insertions, 6 deletions
diff --git a/test/elixir/README.md b/test/elixir/README.md
index 6f0fa2942..8735e1e71 100644
--- a/test/elixir/README.md
+++ b/test/elixir/README.md
@@ -25,12 +25,12 @@ $ EX_USERNAME=myusername EX_PASSWORD=password EX_COUCH_URL=http://my-couchdb.com
X means done, - means partially
- [X] Port all_docs.js
- - [ ] Port attachment_names.js
- - [ ] Port attachment_paths.js
- - [ ] Port attachment_ranges.js
- - [ ] Port attachments.js
- - [ ] Port attachments_multipart.js
- - [ ] Port attachment_views.js
+ - [X] Port attachment_names.js
+ - [X] Port attachment_paths.js
+ - [X] Port attachment_ranges.js
+ - [X] Port attachments.js
+ - [X] Port attachments_multipart.js
+ - [X] Port attachment_views.js
- [ ] Port auth_cache.js
- [X] Port basics.js
- [X] Port batch_save.js
diff --git a/test/elixir/lib/couch/db_test.ex b/test/elixir/lib/couch/db_test.ex
index d88478a8f..990173a13 100644
--- a/test/elixir/lib/couch/db_test.ex
+++ b/test/elixir/lib/couch/db_test.ex
@@ -182,6 +182,85 @@ defmodule Couch.DBTest do
{:ok, resp}
end
+ def bulk_save(db_name, docs) do
+ resp =
+ Couch.post(
+ "/#{db_name}/_bulk_docs",
+ body: %{
+ docs: docs
+ }
+ )
+
+ assert resp.status_code == 201
+ end
+
+ def query(
+ db_name,
+ map_fun,
+ reduce_fun \\ nil,
+ options \\ nil,
+ keys \\ nil,
+ language \\ "javascript"
+ ) do
+ l_map_function =
+ if language == "javascript" do
+ "#{map_fun} /* avoid race cond #{now(:ms)} */"
+ else
+ map_fun
+ end
+
+ view = %{
+ :map => l_map_function
+ }
+
+ view =
+ if reduce_fun != nil do
+ Map.put(view, :reduce, reduce_fun)
+ else
+ view
+ end
+
+ {view, request_options} =
+ if options != nil and Map.has_key?(options, :options) do
+ {Map.put(view, :options, options.options), Map.delete(options, :options)}
+ else
+ {view, options}
+ end
+
+ ddoc_name = "_design/temp_#{now(:ms)}"
+
+ ddoc = %{
+ _id: ddoc_name,
+ language: language,
+ views: %{
+ view: view
+ }
+ }
+
+ request_options =
+ if keys != nil and is_list(keys) do
+ Map.merge(request_options || %{}, %{:keys => :jiffy.encode(keys)})
+ else
+ request_options
+ end
+
+ resp =
+ Couch.put(
+ "/#{db_name}/#{ddoc_name}",
+ headers: ["Content-Type": "application/json"],
+ body: ddoc
+ )
+
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/#{ddoc_name}/_view/view", query: request_options)
+ assert resp.status_code == 200
+
+ Couch.delete("/#{db_name}/#{ddoc_name}")
+
+ resp.body
+ end
+
def sample_doc_foo do
%{
_id: "foo",
@@ -196,6 +275,13 @@ defmodule Couch.DBTest do
end
end
+ # Generate range of docs based on a template
+ def make_docs(id_range, template_doc) do
+ for id <- id_range, str_id = Integer.to_string(id) do
+ Map.merge(template_doc, %{"_id" => str_id})
+ end
+ end
+
# Generate range of docs with atoms as keys, which are more
# idiomatic, and are encoded by jiffy to binaries
def create_docs(id_range) do
@@ -247,6 +333,53 @@ defmodule Couch.DBTest do
inspect(resp, opts)
end
+ def run_on_modified_server(settings, fun) do
+ resp = Couch.get("/_membership")
+ assert resp.status_code == 200
+ nodes = resp.body["all_nodes"]
+
+ prev_settings =
+ Enum.map(settings, fn setting ->
+ prev_setting_node =
+ Enum.reduce(nodes, %{}, fn node, acc ->
+ resp =
+ Couch.put(
+ "/_node/#{node}/_config/#{setting.section}/#{setting.key}",
+ headers: ["X-Couch-Persist": false],
+ body: :jiffy.encode(setting.value)
+ )
+
+ Map.put(acc, node, resp.body)
+ end)
+
+ Map.put(setting, :nodes, Map.to_list(prev_setting_node))
+ end)
+
+ try do
+ fun.()
+ after
+ Enum.each(prev_settings, fn setting ->
+ Enum.each(setting.nodes, fn node_value ->
+ node = elem(node_value, 0)
+ value = elem(node_value, 1)
+
+ if value == ~s(""\\n) do
+ Couch.delete(
+ "/_node/#{node}/_config/#{setting.section}/#{setting.key}",
+ headers: ["X-Couch-Persist": false]
+ )
+ else
+ Couch.put(
+ "/_node/#{node}/_config/#{setting.section}/#{setting.key}",
+ headers: ["X-Couch-Persist": false],
+ body: :jiffy.encode(value)
+ )
+ end
+ end)
+ end)
+ end
+ end
+
def restart_cluster do
resp = Couch.get("/_membership")
assert resp.status_code == 200
diff --git a/test/elixir/test/attachment_names_test.exs b/test/elixir/test/attachment_names_test.exs
new file mode 100644
index 000000000..ee2f4ba7e
--- /dev/null
+++ b/test/elixir/test/attachment_names_test.exs
@@ -0,0 +1,97 @@
+defmodule AttachmentNamesTest do
+ use CouchTestCase
+
+ @moduletag :attachments
+
+ @good_doc """
+ {
+ "_id": "good_doc",
+ "_attachments": {
+ "Kолян.txt": {
+ "content_type": "application/octet-stream",
+ "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+ """
+
+ @bin_att_doc %{
+ _id: "bin_doc",
+ _attachments: %{
+ footxt: %{
+ content_type: "text/plain",
+ data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+
+ @bin_data "JHAPDO*AU£PN ){(3u[d 93DQ9¡€])} ææøo'∂ƒæ≤çæππ•¥∫¶®#†π¶®¥π€ª®˙π8np"
+
+ @leading_underscores_att """
+ {
+ "_id": "bin_doc2",
+ "_attachments": {
+ "_foo.txt": {
+ "content_type": "text/plain",
+ "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+ """
+
+ @moduledoc """
+ Test CouchDB attachment names
+ This is a port of the attachment_names.js suite
+ """
+
+ @tag :with_db
+ test "saves attachment names successfully", context do
+ db_name = context[:db_name]
+ filename = URI.encode("Kолян.txt", &URI.char_unreserved?(&1))
+ resp = Couch.post("/#{db_name}", body: @good_doc)
+ msg = "Should return 201-Created"
+ assert resp.status_code == 201, msg
+
+ resp = Couch.get("/#{db_name}/good_doc/#{filename}")
+ assert resp.body == "This is a base64 encoded text"
+ assert resp.headers["Content-Type"] == "application/octet-stream"
+ assert resp.headers["Etag"] == ~s("aEI7pOYCRBLTRQvvqYrrJQ==")
+
+ resp = Couch.post("/#{db_name}", body: @bin_att_doc)
+ assert(resp.status_code == 201)
+
+ # standalone docs
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc3/attachmenttxt",
+ body: @bin_data,
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert(resp.status_code == 201)
+
+ # bulk docs
+ docs = %{
+ docs: [@bin_att_doc]
+ }
+
+ resp =
+ Couch.post(
+ "/#{db_name}/_bulk_docs",
+ body: docs
+ )
+
+ assert(resp.status_code == 201)
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc2",
+ body: @leading_underscores_att
+ )
+
+ assert resp.status_code == 400
+
+ assert resp.body["reason"] ==
+ "Attachment name '_foo.txt' starts with prohibited character '_'"
+ end
+end
diff --git a/test/elixir/test/attachment_paths_test.exs b/test/elixir/test/attachment_paths_test.exs
new file mode 100644
index 000000000..9f67f0875
--- /dev/null
+++ b/test/elixir/test/attachment_paths_test.exs
@@ -0,0 +1,177 @@
+defmodule AttachmentPathsTest do
+ use CouchTestCase
+
+ @moduletag :attachments
+
+ @bin_att_doc """
+ {
+ "_id": "bin_doc",
+ "_attachments": {
+ "foo/bar.txt": {
+ "content_type": "text/plain",
+ "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ },
+ "foo%2Fbaz.txt": {
+ "content_type": "text/plain",
+ "data": "V2UgbGlrZSBwZXJjZW50IHR3byBGLg=="
+ }
+ }
+ }
+ """
+
+ @design_att_doc """
+ {
+ "_id": "_design/bin_doc",
+ "_attachments": {
+ "foo/bar.txt": {
+ "content_type": "text/plain",
+ "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ },
+ "foo%2Fbaz.txt": {
+ "content_type": "text/plain",
+ "data": "V2UgbGlrZSBwZXJjZW50IHR3byBGLg=="
+ }
+ }
+ }
+ """
+
+ @moduledoc """
+ Test CouchDB attachment names
+ This is a port of the attachment_names.js suite
+ """
+
+ @tag :with_db_name
+ test "manages attachment paths successfully", context do
+ db_name =
+ URI.encode(
+ "#{context[:db_name]}/with_slashes",
+ &URI.char_unreserved?(&1)
+ )
+
+ create_db(db_name)
+
+ resp = Couch.post("/#{db_name}", body: @bin_att_doc)
+ msg = "Should return 201-Created"
+
+ assert resp.status_code == 201, msg
+
+ rev = resp.body["rev"]
+
+ resp = Couch.get("/#{db_name}/bin_doc/foo/bar.txt")
+ assert resp.status_code == 200
+ assert resp.body == "This is a base64 encoded text"
+ assert resp.headers["Content-Type"] == "text/plain"
+
+ resp = Couch.get("/#{db_name}/bin_doc/foo%2Fbar.txt")
+ assert resp.status_code == 200
+ assert resp.body == "This is a base64 encoded text"
+ assert resp.headers["Content-Type"] == "text/plain"
+
+ resp = Couch.get("/#{db_name}/bin_doc/foo/baz.txt")
+ assert resp.status_code == 404
+
+ resp = Couch.get("/#{db_name}/bin_doc/foo%252Fbaz.txt")
+ assert resp.status_code == 200
+ assert resp.body == "We like percent two F."
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc/foo/attachment.txt",
+ body: "Just some text",
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert resp.status_code == 409
+
+ resp =
+ Couch.put(
+ "/#{db_name}/bin_doc/foo/bar2.txt",
+ query: %{rev: rev},
+ body: "This is no base64 encoded text",
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/bin_doc")
+ assert resp.status_code == 200
+
+ att_doc = resp.body
+
+ assert att_doc["_attachments"]["foo/bar.txt"]
+ assert att_doc["_attachments"]["foo%2Fbaz.txt"]
+ assert att_doc["_attachments"]["foo/bar2.txt"]
+
+ ctype = att_doc["_attachments"]["foo/bar2.txt"]["content_type"]
+ assert ctype == "text/plain;charset=utf-8"
+
+ assert att_doc["_attachments"]["foo/bar2.txt"]["length"] == 30
+ delete_db(db_name)
+ end
+
+ @tag :with_db_name
+ test "manages attachment paths successfully - design docs", context do
+ db_name =
+ URI.encode(
+ "#{context[:db_name]}/with_slashes",
+ &URI.char_unreserved?(&1)
+ )
+
+ create_db(db_name)
+ resp = Couch.post("/#{db_name}", body: @design_att_doc)
+ assert resp.status_code == 201
+
+ rev = resp.body["rev"]
+
+ resp = Couch.get("/#{db_name}/_design/bin_doc/foo/bar.txt")
+ assert resp.status_code == 200
+ assert resp.body == "This is a base64 encoded text"
+ assert resp.headers["Content-Type"] == "text/plain"
+
+ resp = Couch.get("/#{db_name}/_design/bin_doc/foo%2Fbar.txt")
+ assert resp.status_code == 200
+ assert resp.body == "This is a base64 encoded text"
+ assert resp.headers["Content-Type"] == "text/plain"
+
+ resp = Couch.get("/#{db_name}/_design/bin_doc/foo/baz.txt")
+ assert resp.status_code == 404
+
+ resp = Couch.get("/#{db_name}/_design/bin_doc/foo%252Fbaz.txt")
+ assert resp.status_code == 200
+ assert resp.body == "We like percent two F."
+
+ resp =
+ Couch.put(
+ "/#{db_name}/_design/bin_doc/foo/attachment.txt",
+ body: "Just some text",
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert resp.status_code == 409
+
+ resp =
+ Couch.put(
+ "/#{db_name}/_design/bin_doc/foo/bar2.txt",
+ query: %{rev: rev},
+ body: "This is no base64 encoded text",
+ headers: ["Content-Type": "text/plain;charset=utf-8"]
+ )
+
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/_design/bin_doc")
+ assert resp.status_code == 200
+
+ att_doc = resp.body
+
+ assert att_doc["_attachments"]["foo/bar.txt"]
+ assert att_doc["_attachments"]["foo%2Fbaz.txt"]
+ assert att_doc["_attachments"]["foo/bar2.txt"]
+
+ ctype = att_doc["_attachments"]["foo/bar2.txt"]["content_type"]
+ assert ctype == "text/plain;charset=utf-8"
+
+ assert att_doc["_attachments"]["foo/bar2.txt"]["length"] == 30
+ delete_db(db_name)
+ end
+end
diff --git a/test/elixir/test/attachment_ranges_test.exs b/test/elixir/test/attachment_ranges_test.exs
new file mode 100644
index 000000000..01c1239bc
--- /dev/null
+++ b/test/elixir/test/attachment_ranges_test.exs
@@ -0,0 +1,143 @@
+defmodule AttachmentRangesTest do
+ use CouchTestCase
+
+ @moduletag :attachments
+
+ @moduledoc """
+ Test CouchDB attachment range requests
+ This is a port of the attachment_ranges.js suite
+ """
+
+ @tag :with_db
+ test "manages attachment range requests successfully", context do
+ db_name = context[:db_name]
+
+ bin_att_doc = %{
+ _id: "bin_doc",
+ _attachments: %{
+ "foo.txt": %{
+ content_type: "application/octet-stream",
+ data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+ }
+ }
+ }
+
+ create_doc(db_name, bin_att_doc)
+ # Fetching the whole entity is a 206
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=0-28"]
+ )
+
+ assert(resp.status_code == 206)
+ assert resp.body == "This is a base64 encoded text"
+ assert resp.headers["Content-Range"] == "bytes 0-28/29"
+ assert resp.headers["Content-Length"] == "29"
+
+ # Fetch the whole entity without an end offset is a 200
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=0-"]
+ )
+
+ assert(resp.status_code == 200)
+ assert resp.body == "This is a base64 encoded text"
+ assert resp.headers["Content-Range"] == nil
+ assert resp.headers["Content-Length"] == "29"
+
+ # Even if you ask multiple times.
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=0-,0-,0-"]
+ )
+
+ assert(resp.status_code == 200)
+
+ # Badly formed range header is a 200
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes:0-"]
+ )
+
+ assert(resp.status_code == 200)
+
+ # Fetch the end of an entity without an end offset is a 206
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=2-"]
+ )
+
+ assert(resp.status_code == 206)
+ assert resp.body == "is is a base64 encoded text"
+ assert resp.headers["Content-Range"] == "bytes 2-28/29"
+ assert resp.headers["Content-Length"] == "27"
+
+ # Fetch first part of entity is a 206
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=0-3"]
+ )
+
+ assert(resp.status_code == 206)
+ assert resp.body == "This"
+ assert resp.headers["Content-Range"] == "bytes 0-3/29"
+ assert resp.headers["Content-Length"] == "4"
+
+ # Fetch middle of entity is also a 206
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=10-15"]
+ )
+
+ assert(resp.status_code == 206)
+ assert resp.body == "base64"
+ assert resp.headers["Content-Range"] == "bytes 10-15/29"
+ assert resp.headers["Content-Length"] == "6"
+
+ # Fetch end of entity is also a 206
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=-3"]
+ )
+
+ assert(resp.status_code == 206)
+ assert resp.body == "ext"
+ assert resp.headers["Content-Range"] == "bytes 26-28/29"
+ assert resp.headers["Content-Length"] == "3"
+
+ # backward range is 416
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=5-3"]
+ )
+
+ assert(resp.status_code == 416)
+
+ # range completely outside of entity is 416
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=300-310"]
+ )
+
+ assert(resp.status_code == 416)
+
+ # We ignore a Range header with too many ranges
+ resp =
+ Couch.get(
+ "/#{db_name}/bin_doc/foo.txt",
+ headers: [Range: "bytes=0-1,0-1,0-1,0-1,0-1,0-1,0-1,0-1,0-1,0-1"]
+ )
+
+ assert(resp.status_code == 200)
+ end
+end
diff --git a/test/elixir/test/attachment_views_test.exs b/test/elixir/test/attachment_views_test.exs
new file mode 100644
index 000000000..3da62f042
--- /dev/null
+++ b/test/elixir/test/attachment_views_test.exs
@@ -0,0 +1,142 @@
+defmodule AttachmentViewTest do
+ use CouchTestCase
+
+ @moduletag :attachments
+
+ @moduledoc """
+ Test CouchDB attachment views requests
+ This is a port of the attachment_views.js suite
+ """
+
+ @tag :with_db
+ test "manages attachments in views successfully", context do
+ db_name = context[:db_name]
+ attachment_data = "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+
+ attachment_template_1 = %{
+ "_attachments" => %{
+ "foo.txt" => %{
+ "content_type" => "text/plain",
+ "data" => attachment_data
+ }
+ }
+ }
+
+ attachment_template_2 = %{
+ "_attachments" => %{
+ "foo.txt" => %{
+ "content_type" => "text/plain",
+ "data" => attachment_data
+ },
+ "bar.txt" => %{
+ "content_type" => "text/plain",
+ "data" => attachment_data
+ }
+ }
+ }
+
+ attachment_template_3 = %{
+ "_attachments" => %{
+ "foo.txt" => %{
+ "content_type" => "text/plain",
+ "data" => attachment_data
+ },
+ "bar.txt" => %{
+ "content_type" => "text/plain",
+ "data" => attachment_data
+ },
+ "baz.txt" => %{
+ "content_type" => "text/plain",
+ "data" => attachment_data
+ }
+ }
+ }
+
+ bulk_save(db_name, make_docs(0..9))
+ bulk_save(db_name, make_docs(10..19, attachment_template_1))
+ bulk_save(db_name, make_docs(20..29, attachment_template_2))
+ bulk_save(db_name, make_docs(30..39, attachment_template_3))
+
+ map_function = """
+ function(doc) {
+ var count = 0;
+
+ for(var idx in doc._attachments) {
+ count = count + 1;
+ }
+
+ emit(parseInt(doc._id), count);
+ }
+ """
+
+ reduce_function = """
+ function(key, values) {
+ return sum(values);
+ }
+ """
+
+ result = query(db_name, map_function, reduce_function)
+ assert length(result["rows"]) == 1
+ assert Enum.at(result["rows"], 0)["value"] == 60
+
+ result =
+ query(db_name, map_function, reduce_function, %{
+ startkey: 10,
+ endkey: 19
+ })
+
+ assert length(result["rows"]) == 1
+ assert Enum.at(result["rows"], 0)["value"] == 10
+
+ result = query(db_name, map_function, reduce_function, %{startkey: 20, endkey: 29})
+ assert length(result["rows"]) == 1
+ assert Enum.at(result["rows"], 0)["value"] == 20
+
+ result =
+ query(db_name, map_function, nil, %{
+ startkey: 30,
+ endkey: 39,
+ include_docs: true
+ })
+
+ assert length(result["rows"]) == 10
+ assert Enum.at(result["rows"], 0)["value"] == 3
+ attachment = Enum.at(result["rows"], 0)["doc"]["_attachments"]["baz.txt"]
+ assert attachment["stub"] == true
+ assert Map.has_key?(attachment, "data") == false
+ assert Map.has_key?(attachment, "encoding") == false
+ assert Map.has_key?(attachment, "encoded_length") == false
+
+ result =
+ query(db_name, map_function, nil, %{
+ startkey: 30,
+ endkey: 39,
+ include_docs: true,
+ attachments: true
+ })
+
+ assert length(result["rows"]) == 10
+ assert Enum.at(result["rows"], 0)["value"] == 3
+ attachment = Enum.at(result["rows"], 0)["doc"]["_attachments"]["baz.txt"]
+ assert attachment["data"] == attachment_data
+ assert Map.has_key?(attachment, "stub") == false
+ assert Map.has_key?(attachment, "encoding") == false
+ assert Map.has_key?(attachment, "encoded_length") == false
+
+ result =
+ query(db_name, map_function, nil, %{
+ startkey: 30,
+ endkey: 39,
+ include_docs: true,
+ att_encoding_info: true
+ })
+
+ assert length(result["rows"]) == 10
+ assert Enum.at(result["rows"], 0)["value"] == 3
+ attachment = Enum.at(result["rows"], 0)["doc"]["_attachments"]["baz.txt"]
+ assert attachment["stub"] == true
+ assert attachment["encoding"] == "gzip"
+ assert attachment["encoded_length"] == 47
+ assert Map.has_key?(attachment, "data") == false
+ end
+end
diff --git a/test/elixir/test/attachments_multipart_test.exs b/test/elixir/test/attachments_multipart_test.exs
new file mode 100644
index 000000000..771107c93
--- /dev/null
+++ b/test/elixir/test/attachments_multipart_test.exs
@@ -0,0 +1,409 @@
+defmodule AttachmentMultipartTest do
+ use CouchTestCase
+
+ @moduletag :attachments
+
+ @moduledoc """
+ Test CouchDB attachment multipart requests
+ This is a port of the attachments_multipart.js suite
+ """
+
+ @tag :with_db
+ test "manages attachments multipart requests successfully", context do
+ db_name = context[:db_name]
+
+ document = """
+ {
+ "body": "This is a body.",
+ "_attachments": {
+ "foo.txt": {
+ "follows": true,
+ "content_type": "application/test",
+ "length": 21
+ },
+ "bar.txt": {
+ "follows": true,
+ "content_type": "application/test",
+ "length": 20
+ },
+ "baz.txt": {
+ "follows": true,
+ "content_type": "text/plain",
+ "length": 19
+ }
+ }
+ }
+ """
+
+ multipart_data =
+ "--abc123\r\n" <>
+ "content-type: application/json\r\n" <>
+ "\r\n" <>
+ document <>
+ "\r\n--abc123\r\n" <>
+ "\r\n" <>
+ "this is 21 chars long" <>
+ "\r\n--abc123\r\n" <>
+ "\r\n" <>
+ "this is 20 chars lon" <>
+ "\r\n--abc123\r\n" <> "\r\n" <> "this is 19 chars lo" <> "\r\n--abc123--epilogue"
+
+ resp =
+ Couch.put(
+ "/#{db_name}/multipart",
+ body: multipart_data,
+ headers: ["Content-Type": "multipart/related;boundary=\"abc123\""]
+ )
+
+ assert resp.status_code == 201
+ assert resp.body["ok"] == true
+
+ resp = Couch.get("/#{db_name}/multipart/foo.txt")
+
+ assert resp.body == "this is 21 chars long"
+
+ resp = Couch.get("/#{db_name}/multipart/bar.txt")
+
+ assert resp.body == "this is 20 chars lon"
+
+ resp = Couch.get("/#{db_name}/multipart/baz.txt")
+
+ assert resp.body == "this is 19 chars lo"
+
+ doc = Couch.get("/#{db_name}/multipart", query: %{att_encoding_info: true})
+ first_rev = doc.body["_rev"]
+
+ assert doc.body["_attachments"]["foo.txt"]["stub"] == true
+ assert doc.body["_attachments"]["bar.txt"]["stub"] == true
+ assert doc.body["_attachments"]["baz.txt"]["stub"] == true
+
+ assert Map.has_key?(doc.body["_attachments"]["foo.txt"], "encoding") == false
+ assert Map.has_key?(doc.body["_attachments"]["bar.txt"], "encoding") == false
+ assert doc.body["_attachments"]["baz.txt"]["encoding"] == "gzip"
+
+ document_updated = """
+ {
+ "_rev": "#{first_rev}",
+ "body": "This is a body.",
+ "_attachments": {
+ "foo.txt": {
+ "stub": true,
+ "content_type": "application/test"
+ },
+ "bar.txt": {
+ "follows": true,
+ "content_type": "application/test",
+ "length": 18
+ }
+ }
+ }
+ """
+
+ multipart_data_updated =
+ "--abc123\r\n" <>
+ "content-type: application/json\r\n" <>
+ "\r\n" <>
+ document_updated <>
+ "\r\n--abc123\r\n" <> "\r\n" <> "this is 18 chars l" <> "\r\n--abc123--"
+
+ resp =
+ Couch.put(
+ "/#{db_name}/multipart",
+ body: multipart_data_updated,
+ headers: ["Content-Type": "multipart/related;boundary=\"abc123\""]
+ )
+
+ assert resp.status_code == 201
+
+ resp = Couch.get("/#{db_name}/multipart/bar.txt")
+
+ assert resp.body == "this is 18 chars l"
+
+ resp = Couch.get("/#{db_name}/multipart/baz.txt")
+
+ assert resp.status_code == 404
+
+ resp =
+ Couch.get(
+ "/#{db_name}/multipart",
+ query: %{:attachments => true},
+ headers: [accept: "multipart/related,*/*;"]
+ )
+
+ assert resp.status_code == 200
+ assert resp.headers["Content-length"] == "790"
+ # parse out the multipart
+ sections = parse_multipart(resp)
+
+ assert length(sections) == 3
+ # The first section is the json doc. Check it's content-type.
+ # Each part carries their own meta data.
+
+ assert Enum.at(sections, 0).headers["Content-Type"] == "application/json"
+ assert Enum.at(sections, 1).headers["Content-Type"] == "application/test"
+ assert Enum.at(sections, 2).headers["Content-Type"] == "application/test"
+
+ assert Enum.at(sections, 1).headers["Content-Length"] == "21"
+ assert Enum.at(sections, 2).headers["Content-Length"] == "18"
+
+ assert Enum.at(sections, 1).headers["Content-Disposition"] ==
+ ~s(attachment; filename="foo.txt")
+
+ assert Enum.at(sections, 2).headers["Content-Disposition"] ==
+ ~s(attachment; filename="bar.txt")
+
+ doc = :jiffy.decode(Enum.at(sections, 0).body, [:return_maps])
+
+ assert doc["_attachments"]["foo.txt"]["follows"] == true
+ assert doc["_attachments"]["bar.txt"]["follows"] == true
+
+ assert Enum.at(sections, 1).body == "this is 21 chars long"
+ assert Enum.at(sections, 2).body == "this is 18 chars l"
+
+ # now get attachments incrementally (only the attachments changes since
+ # a certain rev).
+
+ resp =
+ Couch.get(
+ "/#{db_name}/multipart",
+ query: %{:atts_since => ~s(["#{first_rev}"])},
+ headers: [accept: "multipart/related,*/*;"]
+ )
+
+ assert resp.status_code == 200
+
+ sections = parse_multipart(resp)
+ assert length(sections) == 2
+
+ doc = :jiffy.decode(Enum.at(sections, 0).body, [:return_maps])
+
+ assert doc["_attachments"]["foo.txt"]["stub"] == true
+ assert doc["_attachments"]["bar.txt"]["follows"] == true
+ assert Enum.at(sections, 1).body == "this is 18 chars l"
+
+ # try the atts_since parameter together with the open_revs parameter
+ resp =
+ Couch.get(
+ "/#{db_name}/multipart",
+ query: %{
+ :open_revs => ~s(["#{doc["_rev"]}"]),
+ :atts_since => ~s(["#{first_rev}"])
+ },
+ headers: [accept: "multipart/related,*/*;"]
+ )
+
+ assert resp.status_code == 200
+ sections = parse_multipart(resp)
+
+ # 1 section, with a multipart/related Content-Type
+ assert length(sections) == 1
+
+ ctype_value = Enum.at(sections, 0).headers["Content-Type"]
+ assert String.starts_with?(ctype_value, "multipart/related;") == true
+
+ inner_sections = parse_multipart(Enum.at(sections, 0))
+ # 2 inner sections: a document body section plus an attachment data section
+ assert length(inner_sections) == 3
+ assert Enum.at(inner_sections, 0).headers["Content-Type"] == "application/json"
+
+ doc = :jiffy.decode(Enum.at(inner_sections, 0).body, [:return_maps])
+ assert doc["_attachments"]["foo.txt"]["follows"] == true
+ assert doc["_attachments"]["bar.txt"]["follows"] == true
+
+ assert Enum.at(inner_sections, 1).body == "this is 21 chars long"
+ assert Enum.at(inner_sections, 2).body == "this is 18 chars l"
+
+ # try it with a rev that doesn't exist (should get all attachments)
+
+ resp =
+ Couch.get(
+ "/#{db_name}/multipart",
+ query: %{
+ :atts_since => ~s(["1-2897589","#{first_rev}"])
+ },
+ headers: [accept: "multipart/related,*/*;"]
+ )
+
+ assert resp.status_code == 200
+ sections = parse_multipart(resp)
+
+ assert length(sections) == 2
+
+ doc = :jiffy.decode(Enum.at(sections, 0).body, [:return_maps])
+ assert doc["_attachments"]["foo.txt"]["stub"] == true
+ assert doc["_attachments"]["bar.txt"]["follows"] == true
+ assert Enum.at(sections, 1).body == "this is 18 chars l"
+ end
+
+ @tag :with_db
+ test "manages compressed attachments successfully", context do
+ db_name = context[:db_name]
+
+ # check that with the document multipart/mixed API it's possible to receive
+ # attachments in compressed form (if they're stored in compressed form)
+ server_config = [
+ %{
+ :section => "attachments",
+ :key => "compression_level",
+ :value => "8"
+ },
+ %{
+ :section => "attachments",
+ :key => "compressible_types",
+ :value => "text/plain"
+ }
+ ]
+
+ run_on_modified_server(
+ server_config,
+ fn -> test_multipart_att_compression(db_name) end
+ )
+ end
+
+ defp test_multipart_att_compression(dbname) do
+ doc = %{
+ "_id" => "foobar"
+ }
+
+ lorem = Couch.get("/_utils/script/test/lorem.txt").body
+ hello_data = "hello world"
+ {_, resp} = create_doc(dbname, doc)
+ first_rev = resp.body["rev"]
+
+ resp =
+ Couch.put(
+ "/#{dbname}/#{doc["_id"]}/data.bin",
+ query: %{:rev => first_rev},
+ body: hello_data,
+ headers: ["Content-Type": "application/binary"]
+ )
+
+ assert resp.status_code == 201
+ second_rev = resp.body["rev"]
+
+ resp =
+ Couch.put(
+ "/#{dbname}/#{doc["_id"]}/lorem.txt",
+ query: %{:rev => second_rev},
+ body: lorem,
+ headers: ["Content-Type": "text/plain"]
+ )
+
+ assert resp.status_code == 201
+ third_rev = resp.body["rev"]
+
+ resp =
+ Couch.get(
+ "/#{dbname}/#{doc["_id"]}",
+ query: %{:open_revs => ~s(["#{third_rev}"])},
+ headers: [Accept: "multipart/mixed", "X-CouchDB-Send-Encoded-Atts": "true"]
+ )
+
+ assert resp.status_code == 200
+ sections = parse_multipart(resp)
+ # 1 section, with a multipart/related Content-Type
+ assert length(sections) == 1
+ ctype_value = Enum.at(sections, 0).headers["Content-Type"]
+ assert String.starts_with?(ctype_value, "multipart/related;") == true
+
+ inner_sections = parse_multipart(Enum.at(sections, 0))
+ # 3 inner sections: a document body section plus 2 attachment data sections
+ assert length(inner_sections) == 3
+ assert Enum.at(inner_sections, 0).headers["Content-Type"] == "application/json"
+
+ doc = :jiffy.decode(Enum.at(inner_sections, 0).body, [:return_maps])
+ assert doc["_attachments"]["lorem.txt"]["follows"] == true
+ assert doc["_attachments"]["lorem.txt"]["encoding"] == "gzip"
+ assert doc["_attachments"]["data.bin"]["follows"] == true
+ assert doc["_attachments"]["data.bin"]["encoding"] != "gzip"
+
+ if Enum.at(inner_sections, 1).body == hello_data do
+ assert Enum.at(inner_sections, 2).body != lorem
+ else
+ if assert Enum.at(inner_sections, 2).body == hello_data do
+ assert Enum.at(inner_sections, 1).body != lorem
+ else
+ assert false, "Could not found data.bin attachment data"
+ end
+ end
+
+ # now test that it works together with the atts_since parameter
+
+ resp =
+ Couch.get(
+ "/#{dbname}/#{doc["_id"]}",
+ query: %{:open_revs => ~s(["#{third_rev}"]), :atts_since => ~s(["#{second_rev}"])},
+ headers: [Accept: "multipart/mixed", "X-CouchDB-Send-Encoded-Atts": "true"]
+ )
+
+ assert resp.status_code == 200
+ sections = parse_multipart(resp)
+ # 1 section, with a multipart/related Content-Type
+
+ assert length(sections) == 1
+ ctype_value = Enum.at(sections, 0).headers["Content-Type"]
+ assert String.starts_with?(ctype_value, "multipart/related;") == true
+
+ inner_sections = parse_multipart(Enum.at(sections, 0))
+ # 3 inner sections: a document body section plus 2 attachment data sections
+ assert length(inner_sections) == 3
+ assert Enum.at(inner_sections, 0).headers["Content-Type"] == "application/json"
+ doc = :jiffy.decode(Enum.at(inner_sections, 0).body, [:return_maps])
+ assert doc["_attachments"]["lorem.txt"]["follows"] == true
+ assert doc["_attachments"]["lorem.txt"]["encoding"] == "gzip"
+ assert Enum.at(inner_sections, 1).body != lorem
+ end
+
+ def get_boundary(response) do
+ ctype = response.headers["Content-Type"]
+ ctype_args = String.split(ctype, "; ")
+ ctype_args = Enum.slice(ctype_args, 1, length(ctype_args))
+
+ boundary_arg =
+ Enum.find(
+ ctype_args,
+ fn arg -> String.starts_with?(arg, "boundary=") end
+ )
+
+ boundary = Enum.at(String.split(boundary_arg, "="), 1)
+
+ if String.starts_with?(boundary, ~s(")) do
+ :jiffy.decode(boundary)
+ else
+ boundary
+ end
+ end
+
+ def parse_multipart(response) do
+ boundary = get_boundary(response)
+
+ leading = "--#{boundary}\r\n"
+ last = "\r\n--#{boundary}--"
+ body = response.body
+ mimetext = Enum.at(String.split(body, leading, parts: 2), 1)
+ mimetext = Enum.at(String.split(mimetext, last, parts: 2), 0)
+
+ sections = String.split(mimetext, ~s(\r\n--#{boundary}))
+
+ Enum.map(sections, fn section ->
+ section_parts = String.split(section, "\r\n\r\n", parts: 2)
+ raw_headers = String.split(Enum.at(section_parts, 0), "\r\n")
+ body = Enum.at(section_parts, 1)
+
+ headers =
+ Enum.reduce(raw_headers, %{}, fn raw_header, acc ->
+ if raw_header != "" do
+ header_parts = String.split(raw_header, ": ")
+ Map.put(acc, Enum.at(header_parts, 0), Enum.at(header_parts, 1))
+ else
+ acc
+ end
+ end)
+
+ %{
+ :headers => headers,
+ :body => body
+ }
+ end)
+ end
+end
diff --git a/test/elixir/test/attachments.exs b/test/elixir/test/attachments_test.exs
index 7f235213e..7f235213e 100644
--- a/test/elixir/test/attachments.exs
+++ b/test/elixir/test/attachments_test.exs