%% 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_db_bulk_get_test). -include_lib("couch/include/couch_eunit.hrl"). -include_lib("couch/include/couch_db.hrl"). -define(TIMEOUT, 3000). setup() -> mock(config), mock(chttpd), mock(couch_epi), mock(couch_httpd), mock(couch_stats), mock(fabric), mock(mochireq), Pid = spawn_accumulator(), Pid. teardown(Pid) -> ok = stop_accumulator(Pid), meck:unload(config), meck:unload(chttpd), meck:unload(couch_epi), meck:unload(couch_httpd), meck:unload(couch_stats), meck:unload(fabric), meck:unload(mochireq). bulk_get_test_() -> { "/db/_bulk_get tests", { foreach, fun setup/0, fun teardown/1, [ fun should_require_docs_field/1, fun should_not_accept_specific_query_params/1, fun should_return_empty_results_on_no_docs/1, fun should_get_doc_with_all_revs/1, fun should_validate_doc_with_bad_id/1, fun should_validate_doc_with_bad_rev/1, fun should_validate_missing_doc/1, fun should_validate_bad_atts_since/1, fun should_include_attachments_when_atts_since_specified/1 ] } }. should_require_docs_field(_) -> Req = fake_request({[{}]}), ?_assertThrow({bad_request, _}, chttpd_db:db_req(Req, nil)). should_not_accept_specific_query_params(_) -> Req = fake_request({[{<<"docs">>, []}]}), lists:map(fun (Param) -> {Param, ?_assertThrow({bad_request, _}, begin ok = meck:expect(chttpd, qs, fun(_) -> [{Param, ""}] end), chttpd_db:db_req(Req, nil) end)} end, ["rev", "open_revs", "atts_since", "w", "new_edits"]). should_return_empty_results_on_no_docs(Pid) -> Req = fake_request({[{<<"docs">>, []}]}), chttpd_db:db_req(Req, nil), Results = get_results_from_response(Pid), ?_assertEqual([], Results). should_get_doc_with_all_revs(Pid) -> DocId = <<"docudoc">>, Req = fake_request(DocId), RevA = {[{<<"_id">>, DocId}, {<<"_rev">>, <<"1-ABC">>}]}, RevB = {[{<<"_id">>, DocId}, {<<"_rev">>, <<"1-CDE">>}]}, DocRevA = #doc{id = DocId, body = {[{<<"_rev">>, <<"1-ABC">>}]}}, DocRevB = #doc{id = DocId, body = {[{<<"_rev">>, <<"1-CDE">>}]}}, mock_open_revs(all, {ok, [{ok, DocRevA}, {ok, DocRevB}]}), chttpd_db:db_req(Req, test_util:fake_db([{name, <<"foo">>}])), [{Result}] = get_results_from_response(Pid), ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)), Docs = couch_util:get_value(<<"docs">>, Result), ?assertEqual(2, length(Docs)), [{DocA0}, {DocB0}] = Docs, DocA = couch_util:get_value(<<"ok">>, DocA0), DocB = couch_util:get_value(<<"ok">>, DocB0), ?_assertEqual([RevA, RevB], [DocA, DocB]). should_validate_doc_with_bad_id(Pid) -> DocId = <<"_docudoc">>, Req = fake_request(DocId), chttpd_db:db_req(Req, test_util:fake_db([{name, <<"foo">>}])), [{Result}] = get_results_from_response(Pid), ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)), Docs = couch_util:get_value(<<"docs">>, Result), ?assertEqual(1, length(Docs)), [{DocResult}] = Docs, Doc = couch_util:get_value(<<"error">>, DocResult), ?_assertMatch({[{<<"id">>, DocId}, {<<"rev">>, null}, {<<"error">>, <<"illegal_docid">>}, {<<"reason">>, _}]}, Doc). should_validate_doc_with_bad_rev(Pid) -> DocId = <<"docudoc">>, Rev = <<"revorev">>, Req = fake_request(DocId, Rev), chttpd_db:db_req(Req, test_util:fake_db([{name, <<"foo">>}])), [{Result}] = get_results_from_response(Pid), ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)), Docs = couch_util:get_value(<<"docs">>, Result), ?assertEqual(1, length(Docs)), [{DocResult}] = Docs, Doc = couch_util:get_value(<<"error">>, DocResult), ?_assertMatch({[{<<"id">>, DocId}, {<<"rev">>, Rev}, {<<"error">>, <<"bad_request">>}, {<<"reason">>, _}]}, Doc). should_validate_missing_doc(Pid) -> DocId = <<"docudoc">>, Rev = <<"1-revorev">>, Req = fake_request(DocId, Rev), mock_open_revs([{1,<<"revorev">>}], {ok, []}), chttpd_db:db_req(Req, test_util:fake_db([{name, <<"foo">>}])), [{Result}] = get_results_from_response(Pid), ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)), Docs = couch_util:get_value(<<"docs">>, Result), ?assertEqual(1, length(Docs)), [{DocResult}] = Docs, Doc = couch_util:get_value(<<"error">>, DocResult), ?_assertMatch({[{<<"id">>, DocId}, {<<"rev">>, Rev}, {<<"error">>, <<"not_found">>}, {<<"reason">>, _}]}, Doc). should_validate_bad_atts_since(Pid) -> DocId = <<"docudoc">>, Rev = <<"1-revorev">>, Req = fake_request(DocId, Rev, <<"badattsince">>), mock_open_revs([{1,<<"revorev">>}], {ok, []}), chttpd_db:db_req(Req, test_util:fake_db([{name, <<"foo">>}])), [{Result}] = get_results_from_response(Pid), ?assertEqual(DocId, couch_util:get_value(<<"id">>, Result)), Docs = couch_util:get_value(<<"docs">>, Result), ?assertEqual(1, length(Docs)), [{DocResult}] = Docs, Doc = couch_util:get_value(<<"error">>, DocResult), ?_assertMatch({[{<<"id">>, DocId}, {<<"rev">>, <<"badattsince">>}, {<<"error">>, <<"bad_request">>}, {<<"reason">>, _}]}, Doc). should_include_attachments_when_atts_since_specified(_) -> DocId = <<"docudoc">>, Rev = <<"1-revorev">>, Req = fake_request(DocId, Rev, [<<"1-abc">>]), mock_open_revs([{1,<<"revorev">>}], {ok, []}), chttpd_db:db_req(Req, test_util:fake_db([{name, <<"foo">>}])), ?_assert(meck:called(fabric, open_revs, ['_', DocId, [{1, <<"revorev">>}], [{atts_since, [{1, <<"abc">>}]}, attachments, {user_ctx, undefined}]])). %% helpers fake_request(Payload) when is_tuple(Payload) -> #httpd{method='POST', path_parts=[<<"db">>, <<"_bulk_get">>], mochi_req=mochireq, req_body=Payload}; fake_request(DocId) when is_binary(DocId) -> fake_request({[{<<"docs">>, [{[{<<"id">>, DocId}]}]}]}). fake_request(DocId, Rev) -> fake_request({[{<<"docs">>, [{[{<<"id">>, DocId}, {<<"rev">>, Rev}]}]}]}). fake_request(DocId, Rev, AttsSince) -> fake_request({[{<<"docs">>, [{[{<<"id">>, DocId}, {<<"rev">>, Rev}, {<<"atts_since">>, AttsSince}]}]}]}). mock_open_revs(RevsReq0, RevsResp) -> ok = meck:expect(fabric, open_revs, fun(_, _, RevsReq1, _) -> ?assertEqual(RevsReq0, RevsReq1), RevsResp end). mock(mochireq) -> ok = meck:new(mochireq, [non_strict]), ok = meck:expect(mochireq, parse_qs, fun() -> [] end), ok = meck:expect(mochireq, accepts_content_type, fun(_) -> false end), ok; mock(couch_httpd) -> ok = meck:new(couch_httpd, [passthrough]), ok = meck:expect(couch_httpd, validate_ctype, fun(_, _) -> ok end), ok; mock(chttpd) -> ok = meck:new(chttpd, [passthrough]), ok = meck:expect(chttpd, start_json_response, fun(_, _) -> {ok, nil} end), ok = meck:expect(chttpd, end_json_response, fun(_) -> ok end), ok = meck:expect(chttpd, send_chunk, fun send_chunk/2), ok = meck:expect(chttpd, json_body_obj, fun (#httpd{req_body=Body}) -> Body end), ok; mock(couch_epi) -> ok = meck:new(couch_epi, [passthrough]), ok = meck:expect(couch_epi, any, fun(_, _, _, _, _) -> false end), ok; mock(couch_stats) -> ok = meck:new(couch_stats, [passthrough]), ok = meck:expect(couch_stats, increment_counter, fun(_) -> ok end), ok = meck:expect(couch_stats, increment_counter, fun(_, _) -> ok end), ok = meck:expect(couch_stats, decrement_counter, fun(_) -> ok end), ok = meck:expect(couch_stats, decrement_counter, fun(_, _) -> ok end), ok = meck:expect(couch_stats, update_histogram, fun(_, _) -> ok end), ok = meck:expect(couch_stats, update_gauge, fun(_, _) -> ok end), ok; mock(fabric) -> ok = meck:new(fabric, [passthrough]), ok; mock(config) -> ok = meck:new(config, [passthrough]), ok = meck:expect(config, get, fun(_, _, Default) -> Default end), ok. spawn_accumulator() -> Parent = self(), Pid = spawn(fun() -> accumulator_loop(Parent, []) end), erlang:put(chunks_gather, Pid), Pid. accumulator_loop(Parent, Acc) -> receive {stop, Ref} -> Parent ! {ok, Ref}; {get, Ref} -> Parent ! {ok, Ref, Acc}, accumulator_loop(Parent, Acc); {put, Ref, Chunk} -> Parent ! {ok, Ref}, accumulator_loop(Parent, [Chunk|Acc]) end. stop_accumulator(Pid) -> Ref = make_ref(), Pid ! {stop, Ref}, receive {ok, Ref} -> ok after ?TIMEOUT -> throw({timeout, <<"process stop timeout">>}) end. send_chunk(_, []) -> {ok, nil}; send_chunk(_Req, [H|T]=Chunk) when is_list(Chunk) -> send_chunk(_Req, H), send_chunk(_Req, T); send_chunk(_, Chunk) -> Worker = erlang:get(chunks_gather), Ref = make_ref(), Worker ! {put, Ref, Chunk}, receive {ok, Ref} -> {ok, nil} after ?TIMEOUT -> throw({timeout, <<"send chunk timeout">>}) end. get_response(Pid) -> Ref = make_ref(), Pid ! {get, Ref}, receive {ok, Ref, Acc} -> ?JSON_DECODE(iolist_to_binary(lists:reverse(Acc))) after ?TIMEOUT -> throw({timeout, <<"get response timeout">>}) end. get_results_from_response(Pid) -> {Resp} = get_response(Pid), couch_util:get_value(<<"results">>, Resp).