diff options
-rw-r--r-- | test/elixir/README.md | 12 | ||||
-rw-r--r-- | test/elixir/lib/couch/db_test.ex | 133 | ||||
-rw-r--r-- | test/elixir/test/attachment_names_test.exs | 97 | ||||
-rw-r--r-- | test/elixir/test/attachment_paths_test.exs | 177 | ||||
-rw-r--r-- | test/elixir/test/attachment_ranges_test.exs | 143 | ||||
-rw-r--r-- | test/elixir/test/attachment_views_test.exs | 142 | ||||
-rw-r--r-- | test/elixir/test/attachments_multipart_test.exs | 409 | ||||
-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 |