blob: b3a01bc3002058546448552f2d76431697e1f1a7 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(chttpd_bulk_get_test).
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch/include/couch_eunit.hrl").
-define(USER, "chttpd_bulk_get_test_admin").
-define(PASS, "pass").
-define(AUTH, {basic_auth, {?USER, ?PASS}}).
-define(JSON, {"Content-Type", "application/json"}).
-define(MP_MIXED, {"Accept", "multipart/mixed"}).
-define(MP_RELATED, {"Accept", "multipart/related"}).
-define(DOC, <<"doc">>).
-define(REVA, <<"reva">>).
-define(REVB, <<"revb">>).
-define(REVC, <<"revc">>).
-define(ATT, <<"att">>).
-define(ATT_DATA, <<"dGhlZGF0YQ==">>).
-define(DOC_COUNT, 2000).
-define(BULK_GET_TESTS, [
?TDEF(t_invalid_method),
?TDEF(t_empty_request),
?TDEF(t_no_docs),
?TDEF(t_invalid_query_params),
?TDEF(t_invalid_doc),
?TDEF(t_doc_no_id),
?TDEF(t_invalid_doc_id),
?TDEF(t_missing_doc),
?TDEF(t_invalid_rev),
?TDEF(t_missing_rev),
?TDEF(t_doc_all_revs),
?TDEF(t_specific_rev),
?TDEF(t_specific_rev_latest),
?TDEF(t_ancestor_rev_latest),
?TDEF(t_revs_true),
?TDEF(t_attachments_true),
?TDEF(t_atts_since),
?TDEF(t_invalid_atts_since),
?TDEF(t_invalid_atts_since_invalid_rev),
?TDEF(t_atts_since_returns_attachment),
?TDEF(t_atts_since_overrides_attachments_true),
?TDEF(t_atts_since_multiple),
?TDEF(t_atts_since_multiple_attachments_true),
?TDEF(t_missing_rev_latest),
?TDEF(t_fabric_worker_error)
]).
-define(BULK_GET_MULTIPART_TESTS, [
?TDEF(t_mp_empty_request),
?TDEF(t_mp_no_docs),
?TDEF(t_mp_invalid_doc),
?TDEF(t_mp_doc_no_id),
?TDEF(t_mp_invalid_doc_id),
?TDEF(t_mp_missing_doc),
?TDEF(t_mp_invalid_rev),
?TDEF(t_mp_missing_rev),
?TDEF(t_mp_doc_all_revs),
?TDEF(t_mp_specific_rev),
?TDEF(t_mp_specific_rev_multipart_related),
?TDEF(t_mp_revs_true),
?TDEF(t_mp_atts_since),
?TDEF(t_mp_atts_since_returns_attachment),
?TDEF(t_mp_atts_since_overrides_attachments_true)
]).
test_docs_revs() ->
[
{?DOC, [?REVA]},
{?DOC, [?REVB, ?REVA]},
{?DOC, [?REVC, ?REVA]}
].
bulk_get_test_() ->
{
setup,
fun setup_basic/0,
fun teardown/1,
with(?BULK_GET_TESTS)
}.
bulk_get_multipart_test_() ->
{
setup,
fun setup_basic/0,
fun teardown/1,
with(?BULK_GET_MULTIPART_TESTS)
}.
bulk_get_test_no_batches_test_() ->
{
setup,
fun setup_no_batches/0,
fun teardown_no_batches/1,
with(?BULK_GET_TESTS)
}.
bulk_get_multipart_no_batches_test_() ->
{
setup,
fun setup_no_batches/0,
fun teardown_no_batches/1,
with(?BULK_GET_MULTIPART_TESTS)
}.
bulk_get_multiple_docs_test_() ->
{
foreach,
fun setup_multiple/0,
fun teardown/1,
[
?TDEF_FE(t_multiple_docs, 10),
?TDEF_FE(t_mp_multiple_docs, 10)
]
}.
bulk_get_multiple_docs_no_batches_test_() ->
{
foreach,
fun setup_multiple_no_batches/0,
fun teardown/1,
[
?TDEF_FE(t_multiple_docs, 10),
?TDEF_FE(t_mp_multiple_docs, 10)
]
}.
t_invalid_method({_, DbUrl}) ->
?assertMatch({405, _}, req(put, DbUrl ++ "/_bulk_get", #{})).
t_empty_request({_, DbUrl}) ->
{Code, Res} = bulk_get(DbUrl, []),
?assertEqual(200, Code),
?assertEqual([], Res).
t_mp_empty_request({_, DbUrl}) ->
{Code, Res} = bulk_get_mp(DbUrl, []),
?assertEqual(200, Code),
?assertEqual([], Res).
t_no_docs({_, DbUrl}) ->
{Code, Res} = req(post, DbUrl ++ "/_bulk_get", #{}),
?assertEqual(400, Code),
?assertMatch(#{<<"error">> := <<"bad_request">>}, Res).
t_mp_no_docs({_, DbUrl}) ->
{Code, Res} = req_mp(post, DbUrl ++ "/_bulk_get", #{}, ?MP_MIXED),
?assertEqual(400, Code),
?assertMatch(#{<<"error">> := <<"bad_request">>}, Res).
t_invalid_query_params({_, DbUrl}) ->
Docs = #{<<"docs">> => [#{<<"id">> => ?DOC, <<"rev">> => <<"2-revb">>}]},
Params = ["rev", "open_revs", "atts_since", "w", "new_edits"],
lists:foreach(
fun(Param) ->
{Code, Res} = req(post, DbUrl ++ "/_bulk_get?" ++ Param, Docs),
?assertEqual(400, Code),
?assertMatch(#{<<"error">> := <<"bad_request">>}, Res)
end,
Params
).
t_invalid_doc({_, DbUrl}) ->
{Code, Res} = bulk_get(DbUrl, [<<"foo">>]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"id">> := null,
<<"rev">> := null,
<<"error">> := <<"bad_request">>
}
}
],
<<"id">> := null
}
],
Res
).
t_mp_invalid_doc({_, DbUrl}) ->
{Code, Res} = bulk_get_mp(DbUrl, [<<"foo">>]),
?assertEqual(200, Code),
?assertMatch([{#{}, #{}}], Res),
[{ChunkHeaders, Error}] = Res,
?assertMatch(
#{
<<"content-type">> := <<"application/json; error=\"true\"">>
},
ChunkHeaders
),
?assertMatch(
#{
<<"error">> := <<"bad_request">>,
<<"id">> := null,
<<"rev">> := null
},
Error
).
t_doc_no_id({_, DbUrl}) ->
{Code, Res} = bulk_get(DbUrl, [#{<<"rev">> => <<"1-foo">>}]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"id">> := null,
<<"rev">> := null,
<<"error">> := <<"bad_request">>
}
}
],
<<"id">> := null
}
],
Res
).
t_mp_doc_no_id({_, DbUrl}) ->
{Code, Res} = bulk_get_mp(DbUrl, [#{<<"rev">> => <<"1-foo">>}]),
?assertEqual(200, Code),
?assertMatch([{#{}, #{}}], Res),
[{ChunkHeaders, Error}] = Res,
?assertMatch(
#{
<<"content-type">> := <<"application/json; error=\"true\"">>
},
ChunkHeaders
),
?assertMatch(
#{
<<"error">> := <<"bad_request">>,
<<"id">> := null,
<<"rev">> := null
},
Error
).
t_invalid_doc_id({_, DbUrl}) ->
{Code, Res} = bulk_get(DbUrl, [#{<<"id">> => <<>>, <<"rev">> => <<"1-foo">>}]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"id">> := <<>>,
<<"rev">> := null,
<<"error">> := <<"illegal_docid">>
}
}
],
<<"id">> := <<>>
}
],
Res
).
t_mp_invalid_doc_id({_, DbUrl}) ->
{Code, Res} = bulk_get_mp(DbUrl, [#{<<"id">> => <<>>, <<"rev">> => <<"1-foo">>}]),
?assertEqual(200, Code),
?assertMatch([{#{}, #{}}], Res),
[{ChunkHeaders, Error}] = Res,
?assertMatch(
#{
<<"content-type">> := <<"application/json; error=\"true\"">>
},
ChunkHeaders
),
?assertMatch(
#{
<<"id">> := <<>>,
<<"rev">> := null,
<<"error">> := <<"illegal_docid">>
},
Error
).
t_missing_doc({_, DbUrl}) ->
{Code, Res} = bulk_get(DbUrl, [#{<<"id">> => <<"missing">>}]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"error">> := <<"not_found">>,
<<"id">> := <<"missing">>,
<<"rev">> := <<"undefined">>
}
}
],
<<"id">> := <<"missing">>
}
],
Res
).
t_mp_missing_doc({_, DbUrl}) ->
{Code, Res} = bulk_get_mp(DbUrl, [#{<<"id">> => <<"missing">>}]),
?assertEqual(200, Code),
?assertMatch([{#{}, #{}}], Res),
[{ChunkHeaders, Error}] = Res,
?assertMatch(
#{
<<"content-type">> := <<"application/json; error=\"true\"">>
},
ChunkHeaders
),
?assertMatch(
#{
<<"error">> := <<"not_found">>,
<<"id">> := <<"missing">>,
<<"rev">> := <<"undefined">>
},
Error
).
t_invalid_rev({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => 42},
{Code, Res} = bulk_get(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"error">> := <<"bad_request">>,
<<"id">> := ?DOC,
<<"rev">> := 42
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_mp_invalid_rev({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => 42},
{Code, Res} = bulk_get_mp(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch([{#{}, #{}}], Res),
[{ChunkHeaders, Error}] = Res,
?assertMatch(
#{
<<"content-type">> := <<"application/json; error=\"true\"">>
},
ChunkHeaders
),
?assertMatch(
#{
<<"error">> := <<"bad_request">>,
<<"id">> := ?DOC,
<<"rev">> := 42
},
Error
).
t_missing_rev({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"1-x">>},
{Code, Res} = bulk_get(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"error">> := <<"not_found">>,
<<"id">> := ?DOC,
<<"rev">> := <<"1-x">>
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_mp_missing_rev({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"1-x">>},
{Code, Res} = bulk_get_mp(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch([{#{}, #{}}], Res),
[{ChunkHeaders, Error}] = Res,
?assertMatch(
#{
<<"content-type">> := <<"application/json; error=\"true\"">>
},
ChunkHeaders
),
?assertMatch(
#{
<<"error">> := <<"not_found">>,
<<"reason">> := <<"missing">>,
<<"rev">> := <<"1-x">>
},
Error
).
t_doc_all_revs({_, DbUrl}) ->
{Code, Res} = bulk_get(DbUrl, [#{<<"id">> => ?DOC}]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
},
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revc">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_mp_doc_all_revs({_, DbUrl}) ->
{Code, Res0} = bulk_get_mp(DbUrl, [#{<<"id">> => ?DOC}]),
?assertEqual(200, Code),
?assertMatch([{#{}, [_, _]}, {#{}, [_, _]}], Res0),
% Sort to ensure we get a determinstic order for results
CmpFun = fun({#{<<"x-rev-id">> := A}, _}, {#{<<"x-rev-id">> := B}, _}) ->
A =< B
end,
Res = lists:sort(CmpFun, Res0),
[
{ChunkHeaders1, [Doc1, AttA]},
{ChunkHeaders2, [Doc2, AttB]}
] = Res,
% Chunk headers
?assertMatch(
#{
<<"x-doc-id">> := ?DOC,
<<"x-rev-id">> := <<"2-revb">>
},
ChunkHeaders1
),
?assertMatch(
#{
<<"x-doc-id">> := ?DOC,
<<"x-rev-id">> := <<"2-revc">>
},
ChunkHeaders2
),
% Doc bodies
?assertMatch({#{}, #{}}, Doc1),
{DocHeaders1, DocBody1} = Doc1,
?assertMatch(
#{
<<"content-type">> := <<"application/json">>
},
DocHeaders1
),
?assertMatch(
#{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"follows">> := true}
}
},
DocBody1
),
?assertMatch({#{}, #{}}, Doc2),
{DocHeaders2, DocBody2} = Doc2,
?assertMatch(
#{
<<"content-type">> := <<"application/json">>
},
DocHeaders2
),
?assertMatch(
#{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revc">>,
<<"_attachments">> := #{
?ATT := #{<<"follows">> := true}
}
},
DocBody2
),
% 2-revb attachments
?assertMatch({#{}, <<_/binary>>}, AttA),
{AttAHeaders, AttAData} = AttA,
?assertMatch(
#{
<<"content-disposition">> := <<"attachment; filename=\"att\"">>
},
AttAHeaders
),
?assertEqual(<<"thedata">>, AttAData),
% 2-revc attachments
?assertMatch({#{}, <<_/binary>>}, AttB),
{AttBHeaders, AttBData} = AttB,
?assertMatch(
#{
<<"content-disposition">> := <<"attachment; filename=\"att\"">>
},
AttBHeaders
),
?assertEqual(<<"thedata">>, AttBData).
t_specific_rev({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"2-revb">>},
{Code, Res} = bulk_get(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_mp_specific_rev({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"2-revb">>},
{Code, Res} = bulk_get_mp(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch([{#{}, [_, _]}], Res),
[{ChunkHeaders, [Doc1, Att]}] = Res,
% Whole doc + att chunk headers
?assertMatch(
#{
<<"x-doc-id">> := ?DOC,
<<"x-rev-id">> := <<"2-revb">>
},
ChunkHeaders
),
% Doc body
?assertMatch({#{}, #{}}, Doc1),
{DocHeaders, DocBody} = Doc1,
?assertMatch(
#{
<<"content-type">> := <<"application/json">>
},
DocHeaders
),
?assertMatch(
#{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"follows">> := true}
}
},
DocBody
),
% Att
?assertMatch({#{}, <<_/binary>>}, Att),
{AttHeaders, AttData} = Att,
?assertMatch(
#{
<<"content-disposition">> := <<"attachment; filename=\"att\"">>
},
AttHeaders
),
?assertEqual(<<"thedata">>, AttData).
t_mp_specific_rev_multipart_related({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"2-revb">>},
{Code, Res} = bulk_get_mp(DbUrl, [Doc], "", ?MP_RELATED),
?assertEqual(200, Code),
?assertMatch([{#{}, [_, _]}], Res),
[{ChunkHeaders, [Doc1, Att]}] = Res,
% Whole doc + att chunk headers
?assertMatch(
#{
<<"x-doc-id">> := ?DOC,
<<"x-rev-id">> := <<"2-revb">>
},
ChunkHeaders
),
% Doc body
?assertMatch({#{}, #{}}, Doc1),
{DocHeaders, DocBody} = Doc1,
?assertMatch(
#{
<<"content-type">> := <<"application/json">>
},
DocHeaders
),
?assertMatch(
#{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"follows">> := true}
}
},
DocBody
),
% Att
?assertMatch({#{}, <<_/binary>>}, Att),
{AttHeaders, AttData} = Att,
?assertMatch(
#{
<<"content-disposition">> := <<"attachment; filename=\"att\"">>
},
AttHeaders
),
?assertEqual(<<"thedata">>, AttData).
t_specific_rev_latest({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"2-revb">>},
{Code, Res} = bulk_get(DbUrl, [Doc], "?latest=true"),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_ancestor_rev_latest({_, DbUrl}) ->
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"1-reva">>},
{Code, Res} = bulk_get(DbUrl, [Doc], "?latest=true"),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
},
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revc">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_revs_true({_, DbUrl}) ->
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"1-reva">>
},
{Code, Res} = bulk_get(DbUrl, [Doc], "?revs=true"),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"1-reva">>,
<<"_revisions">> :=
#{<<"ids">> := [<<"reva">>], <<"start">> := 1},
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_mp_revs_true({_, DbUrl}) ->
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"1-reva">>
},
{Code, Res} = bulk_get_mp(DbUrl, [Doc], "?revs=true"),
?assertEqual(200, Code),
?assertMatch([{#{}, [_, _]}], Res),
[{ChunkHeaders, [Doc1, Att]}] = Res,
% Whole doc + att chunk headers
?assertMatch(
#{
<<"x-doc-id">> := ?DOC,
<<"x-rev-id">> := <<"1-reva">>
},
ChunkHeaders
),
% Doc body
?assertMatch({#{}, #{}}, Doc1),
{DocHeaders, DocBody} = Doc1,
?assertMatch(
#{
<<"content-type">> := <<"application/json">>
},
DocHeaders
),
?assertMatch(
#{
<<"_id">> := ?DOC,
<<"_rev">> := <<"1-reva">>,
<<"_revisions">> :=
#{<<"ids">> := [<<"reva">>], <<"start">> := 1},
<<"_attachments">> := #{
?ATT := #{<<"follows">> := true}
}
},
DocBody
),
% Att
?assertMatch({#{}, <<_/binary>>}, Att),
{AttHeaders, AttData} = Att,
?assertMatch(
#{
<<"content-disposition">> := <<"attachment; filename=\"att\"">>
},
AttHeaders
),
?assertEqual(<<"thedata">>, AttData).
t_attachments_true({_, DbUrl}) ->
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"1-reva">>
},
{Code, Res} = bulk_get(DbUrl, [Doc], "?attachments=true"),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"1-reva">>,
<<"_attachments">> := #{
?ATT := #{<<"data">> := ?ATT_DATA}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_atts_since({_, DbUrl}) ->
% Attachment should not be returned as 2 from 2-revb is not stricly greater
% than 1 from our attachment's revpos
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"2-revb">>]
},
{Code, Res} = bulk_get(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_invalid_atts_since({_, DbUrl}) ->
% atts_since is not a list even
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => <<"badsince">>
},
{Code, Res} = bulk_get(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"id">> := ?DOC,
<<"error">> := <<"bad_request">>,
<<"rev">> := <<"badsince">>
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_invalid_atts_since_invalid_rev({_, DbUrl}) ->
% atts_since is list but the revision is bad
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"badsince">>]
},
{Code, Res} = bulk_get(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"id">> := ?DOC,
<<"error">> := <<"bad_request">>,
<<"rev">> := <<"badsince">>
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_mp_atts_since({_, DbUrl}) ->
% Attachments should not be returned as 2 from 2-revb is not stricly
% greater than 1 from our attachment's revpos. As far as multpart encoding
% goes, this is an odd corner case: when all attachments are stubs it seems
% the doc body is encoded directly as the multipart/* top level part
% instead of having another nested application/json doc.
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"2-revb">>]
},
{Code, Res} = bulk_get_mp(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch([{#{}, #{}}], Res),
[{ChunkHeaders, DocBody}] = Res,
?assertMatch(
#{
<<"content-type">> := <<"application/json">>
},
ChunkHeaders
),
?assertMatch(
#{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
},
DocBody
).
t_atts_since_returns_attachment({_, DbUrl}) ->
% 0-baz revpos 0 is less than revpos 1 of our attachment
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"0-baz">>]
},
{Code, Res} = bulk_get(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"data">> := ?ATT_DATA}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_mp_atts_since_returns_attachment({_, DbUrl}) ->
% 0-baz revpos 0 is less than revpos 1 of our attachment
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"0-baz">>]
},
{Code, Res} = bulk_get_mp(DbUrl, [Doc]),
?assertEqual(200, Code),
?assertMatch([{#{}, [_, _]}], Res),
[{ChunkHeaders, [Doc1, Att]}] = Res,
% Whole doc + att chunk headers
?assertMatch(
#{
<<"x-doc-id">> := ?DOC,
<<"x-rev-id">> := <<"2-revb">>
},
ChunkHeaders
),
% Doc body
?assertMatch({#{}, #{}}, Doc1),
{DocHeaders, DocBody} = Doc1,
?assertMatch(
#{
<<"content-type">> := <<"application/json">>
},
DocHeaders
),
?assertMatch(
#{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"follows">> := true}
}
},
DocBody
),
% Att
?assertMatch({#{}, <<_/binary>>}, Att),
{AttHeaders, AttData} = Att,
?assertMatch(
#{
<<"content-disposition">> := <<"attachment; filename=\"att\"">>
},
AttHeaders
),
?assertEqual(<<"thedata">>, AttData).
t_atts_since_overrides_attachments_true({_, DbUrl}) ->
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"2-revb">>]
},
{Code, Res} = bulk_get(DbUrl, [Doc], "?attachments=true"),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_mp_atts_since_overrides_attachments_true({_, DbUrl}) ->
Doc = #{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"2-revb">>]
},
{Code, Res} = bulk_get_mp(DbUrl, [Doc], "?attachments=true"),
?assertEqual(200, Code),
?assertMatch([{#{}, #{}}], Res),
[{ChunkHeaders, DocBody}] = Res,
?assertMatch(
#{
<<"content-type">> := <<"application/json">>
},
ChunkHeaders
),
?assertMatch(
#{
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>,
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
}
},
DocBody
).
t_atts_since_multiple({_, DbUrl}) ->
% Attachment revpos is 1 so we do not expect this attachment body
Docs = [
#{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"2-revb">>]
},
% Should get the attachment as revpos=1 is greater than 0-foo
#{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"0-foo">>]
},
% Empty atts_since. Do not expect to get the attachment
#{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => []
},
% Include a document without atts_since to ensure atts_since applies only to
% individual requests
#{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>
}
],
{Code, Res} = bulk_get(DbUrl, Docs),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
},
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>
}
}
],
<<"id">> := ?DOC
},
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_attachments">> := #{
?ATT := #{<<"data">> := ?ATT_DATA}
},
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>
}
}
],
<<"id">> := ?DOC
},
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
},
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>
}
}
],
<<"id">> := ?DOC
},
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
},
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_atts_since_multiple_attachments_true({_, DbUrl}) ->
% Since attachments=true, expect to always get the attachments, unless
% there is an atts_since present and atts_since would prevent the
% attachment from being returned.
Docs = [
% Attachment revpos is 1 so we do not expect this attachment body
#{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"2-revb">>]
},
% Should get the attachment as revpos=1 is greater than 0-foo
#{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => [<<"0-foo">>]
},
% Should get the attachment as it is set as a default option
#{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>,
<<"atts_since">> => []
},
% Check a doc without atts_since to ensure atts_since applies only to
% individual requests, otherwise default options apply
#{
<<"id">> => ?DOC,
<<"rev">> => <<"2-revb">>
}
],
{Code, Res} = bulk_get(DbUrl, Docs, "?attachments=true"),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_attachments">> := #{
?ATT := #{<<"stub">> := true}
},
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>
}
}
],
<<"id">> := ?DOC
},
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_attachments">> := #{
?ATT := #{<<"data">> := ?ATT_DATA}
},
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>
}
}
],
<<"id">> := ?DOC
},
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_attachments">> := #{
?ATT := #{<<"data">> := ?ATT_DATA}
},
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>
}
}
],
<<"id">> := ?DOC
},
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_attachments">> := #{
?ATT := #{<<"data">> := ?ATT_DATA}
},
<<"_id">> := ?DOC,
<<"_rev">> := <<"2-revb">>
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_missing_rev_latest({_, DbUrl}) ->
% Check the case of latest and a missing rev
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"1-x">>},
{Code, Res} = bulk_get(DbUrl, [Doc], "?latest=true"),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"error">> := <<"not_found">>,
<<"id">> := ?DOC,
<<"rev">> := <<"1-x">>
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_fabric_worker_error({_, DbUrl}) ->
% Check the case handling errors returned by fabric:open_revs/3,4
Doc = #{<<"id">> => ?DOC, <<"rev">> => <<"1-reva">>},
meck:expect(fabric, open_revs, 3, meck:val({error, fabric_error_foo})),
meck:expect(fabric, open_revs, 4, meck:val({error, fabric_error_foo})),
{Code, Res} = bulk_get(DbUrl, [Doc], "?latest=true"),
meck:expect(fabric, open_revs, 3, meck:passthrough()),
meck:expect(fabric, open_revs, 4, meck:passthrough()),
?assertEqual(200, Code),
?assertMatch(
[
#{
<<"docs">> := [
#{
<<"error">> := #{
<<"error">> := <<"internal_fabric_error">>,
<<"id">> := ?DOC,
<<"rev">> := <<"1-reva">>,
<<"reason">> := <<"fabric_error_foo">>
}
}
],
<<"id">> := ?DOC
}
],
Res
).
t_multiple_docs({_, DbUrl}) ->
Reqs = [#{<<"id">> => integer_to_binary(I)} || I <- lists:seq(1, ?DOC_COUNT)],
{Code, Res} = bulk_get(DbUrl, Reqs),
?assertEqual(200, Code),
?assertMatch([#{<<"docs">> := _} | _], Res),
?assertEqual(?DOC_COUNT, length(Res)),
lists:foreach(
fun({I, Docs}) ->
Id = integer_to_binary(I),
?assertMatch(
#{
<<"docs">> := [
#{
<<"ok">> := #{
<<"_id">> := Id,
<<"_rev">> := <<"1-reva">>
}
}
],
<<"id">> := Id
},
Docs
)
end,
lists:zip(lists:seq(1, ?DOC_COUNT), Res)
).
t_mp_multiple_docs({_, DbUrl}) ->
Reqs = [#{<<"id">> => integer_to_binary(I)} || I <- lists:seq(1, ?DOC_COUNT)],
{Code, Res} = bulk_get_mp(DbUrl, Reqs),
?assertEqual(200, Code),
?assertEqual(?DOC_COUNT, length(Res)),
lists:foreach(
fun({I, Docs}) ->
Id = integer_to_binary(I),
?assertMatch(
{
#{<<"content-type">> := <<"application/json">>},
#{<<"_id">> := Id, <<"_rev">> := <<"1-reva">>}
},
Docs
)
end,
lists:zip(lists:seq(1, ?DOC_COUNT), Res)
).
% Utility functions
setup_ctx() ->
Ctx = test_util:start_couch([chttpd]),
Hashed = couch_passwords:hash_admin_password(?PASS),
ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist = false),
Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
Db = ?b2l(?tempdb()),
Port = mochiweb_socket_server:get(chttpd, port),
Url = lists:concat(["http://", Addr, ":", Port, "/"]),
ok = create_db(Url, Db),
{Ctx, Url, Db}.
teardown({Ctx, DbUrl}) ->
meck:unload(),
delete_db(DbUrl),
Persist = false,
ok = config:delete("admins", ?USER, Persist),
ok = config:delete("chttpd", "use_batches", Persist),
test_util:stop_couch(Ctx).
teardown_no_batches({Ctx, DbUrl}) ->
% Verify that the non-batched open_revs/4 was called
?assert(meck:num_calls(fabric, open_revs, 4) >= 1),
% Verify that no calls to the batched open_revs/3 were made
?assertEqual(0, meck:num_calls(fabric, open_revs, 3)),
teardown({Ctx, DbUrl}).
setup_basic() ->
{Ctx, Url, Db} = setup_ctx(),
DbUrl = Url ++ Db,
ok = create_docs(DbUrl, test_docs_revs()),
meck:new(fabric, [passthrough]),
{Ctx, DbUrl}.
setup_no_batches() ->
{Ctx, DbUrl} = setup_basic(),
config:set("chttpd", "bulk_get_use_batches", "false", _Persist = false),
{Ctx, DbUrl}.
setup_multiple() ->
{Ctx, Url, Db} = setup_ctx(),
DbUrl = Url ++ Db,
Docs = [{integer_to_binary(I), [?REVA]} || I <- lists:seq(1, ?DOC_COUNT)],
ok = create_docs(DbUrl, Docs, _WithAtts = false),
{Ctx, DbUrl}.
setup_multiple_no_batches() ->
{Ctx, DbUrl} = setup_multiple(),
config:set("chttpd", "bulk_get_use_batches", "false", _Persist = false),
{Ctx, DbUrl}.
create_db(Top, Db) ->
case req(put, Top ++ Db) of
{201, #{}} ->
ok;
Error ->
error({failed_to_create_test_db, Db, Error})
end.
delete_db(DbUrl) ->
case req(delete, DbUrl) of
{200, #{}} ->
ok;
Error ->
error({failed_to_delete_test_db, DbUrl, Error})
end.
create_docs(DbUrl, DocRevs) ->
create_docs(DbUrl, DocRevs, true).
create_docs(DbUrl, DocRevs, WithAtts) ->
Docs = lists:map(
fun({Id, Revs}) ->
Doc = #{
<<"_id">> => Id,
<<"_revisions">> => #{
<<"ids">> => Revs,
<<"start">> => length(Revs)
}
},
case WithAtts of
true -> add_att(Doc);
false -> Doc
end
end,
DocRevs
),
Body = #{
<<"docs">> => Docs,
<<"new_edits">> => false
},
{Code, Res} = req(post, DbUrl ++ "/_bulk_docs", Body),
?assertEqual(201, Code),
?assertEqual([], Res),
ok.
add_att(#{} = Doc) ->
Doc#{
<<"_attachments">> => #{
?ATT => #{
<<"revpos">> => 1,
<<"content_type">> => <<"application/octet-stream">>,
<<"data">> => ?ATT_DATA
}
}
}.
bulk_get(DbUrl, Docs) ->
bulk_get(DbUrl, Docs, "").
bulk_get(DbUrl, Docs, Params) ->
Url = DbUrl ++ "/_bulk_get" ++ Params,
{Code, Res} = req(post, Url, #{<<"docs">> => Docs}),
#{<<"results">> := DocResults} = Res,
{Code, DocResults}.
req(Method, Url) ->
Headers = [?JSON, ?AUTH],
{ok, Code, _, Res} = test_request:request(Method, Url, Headers),
{Code, json_decode(Res)}.
req(Method, Url, #{} = Body) ->
req(Method, Url, jiffy:encode(Body));
req(Method, Url, Body) ->
Headers = [?JSON, ?AUTH],
{ok, Code, _, Res} = test_request:request(Method, Url, Headers, Body),
{Code, json_decode(Res)}.
% Handle multipart _bulk_get requests
bulk_get_mp(DbUrl, Docs) ->
bulk_get_mp(DbUrl, Docs, "").
bulk_get_mp(DbUrl, Doc, Params) ->
bulk_get_mp(DbUrl, Doc, Params, ?MP_MIXED).
bulk_get_mp(DbUrl, Docs, Params, MpType) ->
Url = DbUrl ++ "/_bulk_get" ++ Params,
{Code, Res} = req_mp(post, Url, #{<<"docs">> => Docs}, MpType),
{Code, Res}.
req_mp(Method, Url, #{} = Body, MpType) ->
req_mp(Method, Url, jiffy:encode(Body), MpType);
req_mp(Method, Url, Body, MpType) ->
Headers = [?JSON, ?AUTH, MpType],
{ok, Code, ResHeaders, Res} = test_request:request(Method, Url, Headers, Body),
CType = header_value("Content-Type", ResHeaders),
case CType of
"application/json" ->
{Code, json_decode(Res)};
"multipart/" ++ _ ->
Chunks = split(Res, CType),
{Code, lists:map(fun chunk_parse_fun/1, Chunks)}
end.
% In a multipart response, each chunk would have its own headers, content type,
% and potentially nested parts with their own multipart encoding
%
chunk_parse_fun(Chunk) when is_binary(Chunk) ->
{Headers, Body} = parse_headers_and_body(Chunk),
#{<<"content-type">> := CType} = Headers,
case CType of
<<"application/json", _/binary>> ->
{Headers, json_decode(Body)};
<<"multipart/", _/binary>> ->
{Headers, lists:map(fun chunk_parse_fun/1, split(Body, CType))};
_ ->
{Headers, Body}
end.
% Split the binary into parts based on the provided boundary. The splitting is
% naive, after a basic binary:split/3 we have to do some cleanups and remove a
% few trailing bits off the start and end.
%
split(Chunk, CType) ->
Boundary = get_boundary(CType),
Parts = binary:split(Chunk, <<"--", Boundary/binary>>, [global]),
Parts1 = [string:trim(P) || P <- Parts],
[P || P <- Parts1, P =/= <<>> andalso P =/= <<"--">>].
% Parse the headers and body from a binary chunk. This does just enough to
% parse things out for the test and is not a full featured multipart parser
%
parse_headers_and_body(Bin) ->
[HeadersBin, BodyBin] = binary:split(Bin, <<"\r\n\r\n">>),
HeaderLines = binary:split(HeadersBin, <<"\r\n">>, [global, trim_all]),
MapFun = fun(Header) ->
[Name, Val] = binary:split(Header, <<":">>),
{string:lowercase(string:trim(Name)), string:trim(Val)}
end,
{maps:from_list(lists:map(MapFun, HeaderLines)), BodyBin}.
header_value(Key, Headers) ->
header_value(Key, Headers, undefined).
header_value(Key, Headers, Default) ->
Headers1 = [{string:to_lower(K), V} || {K, V} <- Headers],
case lists:keyfind(string:to_lower(Key), 1, Headers1) of
{_, Value} -> Value;
_ -> Default
end.
get_boundary(CType) when is_binary(CType) ->
get_boundary(binary_to_list(CType));
get_boundary(CType) when is_list(CType) ->
case mochiweb_util:parse_header(CType) of
{"multipart/" ++ _, HeaderOpts} ->
case couch_util:get_value("boundary", HeaderOpts) of
undefined -> undefined;
B when is_list(B) -> iolist_to_binary(B)
end;
_ ->
undefined
end.
json_decode(Bin) when is_binary(Bin) ->
jiffy:decode(Bin, [return_maps]).