blob: 6560cc9497d1590bdb1235d70af3a1618f87bd46 [file]
% 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_test).
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
-define(USER, "chttpd_db_test_admin").
-define(PASS, "pass").
-define(AUTH, {basic_auth, {?USER, ?PASS}}).
-define(CONTENT_JSON, {"Content-Type", "application/json"}).
-define(DESTHEADER1, {"Destination", "foo%E5%95%8Abar"}).
-define(DESTHEADER2, {"Destination", "foo%2Fbar%23baz%3Fpow%3Afiz"}).
-define(FIXTURE_TXT, ?ABS_PATH(?FILE)).
-define(i2l(I), integer_to_list(I)).
% seconds
-define(TIMEOUT, 60).
setup() ->
Hashed = couch_passwords:hash_admin_password(?PASS),
ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist = false),
Db = ?tempdb(),
create_db(Db),
Db.
teardown(Db) ->
delete_db(Db),
ok = config:delete("admins", ?USER, _Persist = false).
all_test_() ->
{
"chttpd db tests",
{
setup,
fun chttpd_test_util:start_couch/0,
fun chttpd_test_util:stop_couch/1,
{
foreach,
fun setup/0,
fun teardown/1,
[
?TDEF_FE(t_return_ok_true_on_bulk_update, ?TIMEOUT),
?TDEF_FE(t_return_201_new_edits_false_with_revs_on_bulk_update, ?TIMEOUT),
?TDEF_FE(t_return_400_new_edits_false_no_revs_on_bulk_update, ?TIMEOUT),
?TDEF_FE(t_return_201_new_edits_false_with_rev_on_doc_update, ?TIMEOUT),
?TDEF_FE(t_return_201_new_edits_false_with_revisions_on_doc_update, ?TIMEOUT),
?TDEF_FE(t_return_400_new_edits_false_no_revs_on_doc_update, ?TIMEOUT),
?TDEF_FE(t_return_ok_true_on_ensure_full_commit, ?TIMEOUT),
?TDEF_FE(t_return_404_for_ensure_full_commit_on_no_db, ?TIMEOUT),
?TDEF_FE(t_accept_live_as_an_alias_for_continuous, ?TIMEOUT),
?TDEF_FE(t_return_headers_after_starting_continuous, ?TIMEOUT),
?TDEF_FE(t_return_404_for_delete_att_on_notadoc, ?TIMEOUT),
?TDEF_FE(t_return_409_for_del_att_without_rev, ?TIMEOUT),
?TDEF_FE(t_return_200_for_del_att_with_rev, ?TIMEOUT),
?TDEF_FE(t_return_409_for_put_att_nonexistent_rev, ?TIMEOUT),
?TDEF_FE(t_return_update_seq_when_set_on_all_docs, ?TIMEOUT),
?TDEF_FE(t_not_return_update_seq_when_unset_on_all_docs, ?TIMEOUT),
?TDEF_FE(t_return_correct_id_on_doc_copy, ?TIMEOUT),
?TDEF_FE(t_return_only_one_ok_on_doc_copy, ?TIMEOUT),
?TDEF_FE(t_return_400_for_bad_engine, ?TIMEOUT),
?TDEF_FE(t_not_change_db_proper_after_rewriting_shardmap, ?TIMEOUT),
?TDEF_FE(t_succeed_on_all_docs_with_queries_keys, ?TIMEOUT),
?TDEF_FE(t_succeed_on_all_docs_with_queries_limit_skip, ?TIMEOUT),
?TDEF_FE(t_succeed_on_all_docs_with_multiple_queries, ?TIMEOUT),
?TDEF_FE(t_succeed_on_design_docs_with_queries_keys, ?TIMEOUT),
?TDEF_FE(t_succeed_on_design_docs_with_queries_limit_skip, ?TIMEOUT),
?TDEF_FE(t_succeed_on_design_docs_with_multiple_queries, ?TIMEOUT),
?TDEF_FE(t_succeed_on_local_docs_with_queries_keys, ?TIMEOUT),
?TDEF_FE(t_succeed_on_local_docs_with_queries_limit_skip, ?TIMEOUT),
?TDEF_FE(t_succeed_on_local_docs_with_multiple_queries, ?TIMEOUT)
]
}
}
}.
t_return_ok_true_on_bulk_update(Db) ->
{_, #{<<"rev">> := Rev}} = create_doc(Db, "testdoc"),
Body = #{<<"docs">> => [#{<<"_id">> => <<"testdoc">>, <<"_rev">> => Rev}]},
{_, [#{<<"ok">> := Res}]} = req(post, url(Db, "_bulk_docs"), Body),
?assert(Res).
t_return_201_new_edits_false_with_revs_on_bulk_update(Db) ->
{_, #{<<"rev">> := Rev}} = create_doc(Db, "dochasrev"),
Body = #{
<<"docs">> => [#{<<"_id">> => <<"dochasrev">>, <<"_rev">> => Rev}],
<<"new_edits">> => false
},
{Status, Response} = req(post, url(Db, "_bulk_docs"), Body),
?assertEqual(201, Status),
?assertEqual([], Response).
t_return_400_new_edits_false_no_revs_on_bulk_update(Db) ->
create_doc(Db, "docnorev"),
NewDoc = #{<<"docs">> => [#{<<"_id">> => <<"docnorev">>}], <<"new_edits">> => false},
{Status, Response} = req(post, url(Db, "_bulk_docs"), NewDoc),
?assertEqual(400, Status),
?assertMatch(#{<<"error">> := <<"bad_request">>}, Response).
t_return_201_new_edits_false_with_rev_on_doc_update(Db) ->
Body = #{<<"foo">> => <<"bar">>, <<"_rev">> => <<"1-abc">>},
{Status, #{<<"id">> := Id, <<"rev">> := Rev}} =
req(put, url(Db, "docrev2?new_edits=false"), Body),
?assertEqual(201, Status),
?assertEqual(<<"docrev2">>, Id),
?assertEqual(<<"1-abc">>, Rev).
t_return_201_new_edits_false_with_revisions_on_doc_update(Db) ->
Body = #{
<<"foo">> => <<"bar">>,
<<"_revisions">> => #{<<"ids">> => [<<"abc">>, <<"def">>], <<"start">> => 2}
},
{Status, #{<<"id">> := Id, <<"rev">> := Rev}} =
req(put, url(Db, "docrev3?new_edits=false"), Body),
?assertEqual(201, Status),
?assertEqual(<<"docrev3">>, Id),
?assertEqual(<<"2-abc">>, Rev).
t_return_400_new_edits_false_no_revs_on_doc_update(Db) ->
Body = #{<<"foo">> => <<"bar">>},
{Status, Response} = req(put, url(Db, "docrev3?new_edits=false"), Body),
?assertEqual(400, Status),
?assertMatch(#{<<"error">> := <<"bad_request">>}, Response).
t_return_ok_true_on_ensure_full_commit(Db) ->
{Status, #{<<"ok">> := Res}} = req(post, url(Db, "_ensure_full_commit")),
?assertEqual(201, Status),
?assert(Res).
t_return_404_for_ensure_full_commit_on_no_db(Db) ->
{Status, Response} = req(post, url(Db) ++ "-missing-db" ++ "/_ensure_full_commit"),
?assertEqual(404, Status),
?assertMatch(#{<<"error">> := <<"not_found">>}, Response).
t_accept_live_as_an_alias_for_continuous(Db) ->
GetLastSeq =
fun(Chunks) ->
LastSeqBin = lists:last(Chunks),
{Result} =
try ?JSON_DECODE(LastSeqBin) of
Data -> Data
catch
_:_ ->
% should not happen, abort
?assert(false)
end,
couch_util:get_value(<<"last_seq">>, Result, undefined)
end,
LastSeq1 = GetLastSeq(wait_non_empty_chunk(Db)),
create_doc(Db, "testdoc2"),
LastSeq2 = GetLastSeq(wait_non_empty_chunk(Db)),
?assertNotEqual(LastSeq1, LastSeq2).
t_return_headers_after_starting_continuous(Db) ->
{ok, _, _, Bin} = test_request:get(url(Db, "_changes?feed=live&timeout=1"), [?AUTH]),
Parts = binary:split(Bin, <<"\n">>, [global]),
%% we should receive at least one part even when timeout=1
?assertNotEqual([], Parts).
t_return_404_for_delete_att_on_notadoc(Db) ->
{Status, Response} = req(delete, url(Db, "notadoc/att.pdf")),
?assertEqual(404, Status),
?assertMatch(#{<<"error">> := <<"not_found">>, <<"reason">> := <<"missing">>}, Response),
{Status1, Response} = req(get, url(Db, "notadoc")),
?assertEqual(404, Status1).
t_return_409_for_del_att_without_rev(Db) ->
{Status, _} = req(put, url(Db, "testdoc3"), attachment_doc()),
?assertEqual(201, Status),
{Status1, _} = req(delete, url(Db, "testdoc3/file.erl")),
?assertEqual(409, Status1).
t_return_200_for_del_att_with_rev(Db) ->
{Status, #{<<"rev">> := Rev}} = req(put, url(Db, "testdoc4"), attachment_doc()),
?assertEqual(201, Status),
{Status1, _} = req(delete, url(Db, "testdoc4/file.erl?rev=" ++ Rev)),
?assertEqual(200, Status1).
t_return_409_for_put_att_nonexistent_rev(Db) ->
{Status, Response} = req(put, url(Db, "t_return_404/file.erl?rev=1-000"), attachment_doc()),
?assertEqual(409, Status),
?assertMatch(#{<<"error">> := <<"not_found">>, <<"reason">> := <<"missing_rev">>}, Response).
t_return_update_seq_when_set_on_all_docs(Db) ->
[create_doc(Db, "testdoc" ++ ?i2l(I)) || I <- lists:seq(1, 3)],
{Status, #{<<"update_seq">> := UpdateSeq, <<"offset">> := Offset}} =
req(get, url(Db, "_all_docs?update_seq=true&keys=[\"testdoc1\"]")),
?assertEqual(200, Status),
?assertNotEqual(undefined, UpdateSeq),
?assertNotEqual(undefined, Offset).
t_not_return_update_seq_when_unset_on_all_docs(Db) ->
[create_doc(Db, "testdoc" ++ ?i2l(I)) || I <- lists:seq(1, 3)],
{Status, #{<<"offset">> := Offset} = Response} =
req(get, url(Db, "_all_docs?update_seq=false&keys=[\"testdoc1\"]")),
?assertEqual(200, Status),
?assertNot(maps:is_key(<<"update_seq">>, Response)),
?assertNotEqual(undefined, Offset).
t_return_correct_id_on_doc_copy(Db) ->
create_doc(Db, "testdoc"),
{_, #{<<"id">> := Id1}} = req(copy, url(Db, "testdoc"), [?CONTENT_JSON, ?AUTH, ?DESTHEADER1]),
{_, #{<<"id">> := Id2}} = req(copy, url(Db, "testdoc"), [?CONTENT_JSON, ?AUTH, ?DESTHEADER2]),
?assertEqual(<<102, 111, 111, 229, 149, 138, 98, 97, 114>>, Id1),
?assertEqual(<<"foo/bar#baz?pow:fiz">>, Id2).
t_return_only_one_ok_on_doc_copy(Db) ->
create_doc(Db, "testdoc"),
{_, #{<<"ok">> := Res}} = req(copy, url(Db, "testdoc"), [?CONTENT_JSON, ?AUTH, ?DESTHEADER1]),
?assert(Res).
t_return_400_for_bad_engine(Db) ->
{Status, _} = req(put, url(Db) ++ "?engine=cowabunga"),
?assertEqual(400, Status).
t_not_change_db_proper_after_rewriting_shardmap(Db) ->
delete_db(Db),
{201, _} = req(put, url(Db) ++ "?partitioned=true&q=1"),
ShardDbName = ?l2b(config:get("mem3", "shards_db", "_dbs")),
{ok, ShardDb} = mem3_util:ensure_exists(ShardDbName),
{ok, #doc{body = {Props}}} = couch_db:open_doc(
ShardDb, Db, [ejson_body]
),
Shards = mem3_util:build_shards(Db, Props),
{Prop2} = ?JSON_DECODE(?JSON_ENCODE({Props})),
Shards2 = mem3_util:build_shards(Db, Prop2),
?assertEqual(Shards2, Shards).
t_succeed_on_all_docs_with_queries_keys(Db) ->
helper_queries_with_keys(Db, "_all_docs", 3).
t_succeed_on_design_docs_with_queries_keys(Db) ->
helper_queries_with_keys(Db, "_design_docs", 1).
t_succeed_on_local_docs_with_queries_keys(Db) ->
helper_queries_with_keys(Db, "_local_docs", 1).
helper_queries_with_keys(Db, Path, ExpectedRows) ->
[create_doc(Db, "testdoc" ++ ?i2l(I)) || I <- lists:seq(1, 5)],
[create_doc(Db, "_design/ddoc" ++ ?i2l(I)) || I <- lists:seq(1, 5)],
[create_doc(Db, "_local/doc" ++ ?i2l(I)) || I <- lists:seq(1, 5)],
QueryDoc = #{
<<"queries">> => [
#{
<<"keys">> =>
[<<"testdoc1">>, <<"_design/ddoc3">>, <<"_local/doc5">>]
}
]
},
{Status, #{<<"results">> := [#{<<"rows">> := Rows}]}} =
req(post, url(Db, Path ++ "/queries"), QueryDoc),
?assertEqual(200, Status),
?assertEqual(ExpectedRows, length(Rows)).
t_succeed_on_all_docs_with_queries_limit_skip(Db) ->
helper_queries_with_limit_skip(Db, "testdoc", "_all_docs", 2, 5).
t_succeed_on_design_docs_with_queries_limit_skip(Db) ->
helper_queries_with_limit_skip(Db, "_design/ddoc", "_design_docs", 2, 5).
t_succeed_on_local_docs_with_queries_limit_skip(Db) ->
helper_queries_with_limit_skip(Db, "_local/doc", "_local_docs", null, 5).
helper_queries_with_limit_skip(Db, Id, Path, ExpectedOffset, ExpectedRows) ->
[create_doc(Db, Id ++ ?i2l(I)) || I <- lists:seq(1, 10)],
QueryDoc = #{<<"queries">> => [#{<<"limit">> => 5, <<"skip">> => 2}]},
{Status, #{<<"results">> := [#{<<"offset">> := Offset, <<"rows">> := Rows}]}} =
req(post, url(Db, Path ++ "/queries"), QueryDoc),
?assertEqual(200, Status),
?assertEqual(ExpectedOffset, Offset),
?assertEqual(ExpectedRows, length(Rows)).
t_succeed_on_all_docs_with_multiple_queries(Db) ->
helper_queries_with_keys_limit_skip(Db, "_all_docs", 2, 3, 5).
t_succeed_on_design_docs_with_multiple_queries(Db) ->
helper_queries_with_keys_limit_skip(Db, "_design_docs", 2, 1, 5).
t_succeed_on_local_docs_with_multiple_queries(Db) ->
helper_queries_with_keys_limit_skip(Db, "_local_docs", null, 1, 5).
helper_queries_with_keys_limit_skip(Db, Path, ExpectedOffset, ExpectedRows1, ExpectedRows2) ->
[create_doc(Db, "testdoc" ++ ?i2l(I)) || I <- lists:seq(1, 10)],
[create_doc(Db, "_design/ddoc" ++ ?i2l(I)) || I <- lists:seq(1, 10)],
[create_doc(Db, "_local/doc" ++ ?i2l(I)) || I <- lists:seq(1, 10)],
QueryDoc = #{
<<"queries">> => [
#{
<<"keys">> =>
[<<"testdoc1">>, <<"_design/ddoc3">>, <<"_local/doc5">>]
},
#{<<"limit">> => 5, <<"skip">> => 2}
]
},
{Status, #{
<<"results">> := [
#{<<"rows">> := Rows1},
#{<<"offset">> := Offset2, <<"rows">> := Rows2}
]
}} = req(post, url(Db, Path ++ "/queries"), QueryDoc),
?assertEqual(200, Status),
?assertEqual(ExpectedRows1, length(Rows1)),
?assertEqual(ExpectedOffset, Offset2),
?assertEqual(ExpectedRows2, length(Rows2)).
%%%%%%%%%%%%%%%%%%%% Utility Functions %%%%%%%%%%%%%%%%%%%%
url(Db) ->
Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
Port = mochiweb_socket_server:get(chttpd, port),
lists:concat(["http://", Addr, ":", Port, "/", ?b2l(Db)]).
url(Db, Path) ->
url(Db) ++ "/" ++ Path.
create_db(Db) ->
case req(put, url(Db)) of
{201, #{}} -> ok;
Error -> error({failed_to_create_test_db, Db, Error})
end.
delete_db(Db) ->
case req(delete, url(Db)) of
{200, #{}} -> ok;
Error -> error({failed_to_delete_test_db, Db, Error})
end.
create_doc(Db, Id) ->
req(put, url(Db, Id), #{<<"mr">> => <<"rockoartischocko">>}).
req(Method, Url) ->
Headers = [?CONTENT_JSON, ?AUTH],
{ok, Code, _, Res} = test_request:request(Method, Url, Headers),
{Code, jiffy:decode(Res, [return_maps])}.
req(copy, Url, Headers) ->
{ok, Code, _, Res} = test_request:request(copy, Url, Headers),
{Code, jiffy:decode(Res, [return_maps])};
req(Method, Url, #{} = Body) ->
req(Method, Url, jiffy:encode(Body));
req(Method, Url, Body) ->
Headers = [?CONTENT_JSON, ?AUTH],
{ok, Code, _, Res} = test_request:request(Method, Url, Headers, Body),
{Code, jiffy:decode(Res, [return_maps])}.
wait_non_empty_chunk(Db) ->
test_util:wait(
fun() ->
{ok, _, _, Bin} = test_request:get(url(Db, "_changes?feed=live&timeout=1"), [?AUTH]),
Parts = binary:split(Bin, <<"\n">>, [global]),
case [P || P <- Parts, size(P) > 0] of
[] -> wait;
Chunks -> Chunks
end
end
).
attachment_doc() ->
{ok, Data} = file:read_file(?FIXTURE_TXT),
#{
<<"_attachments">> => #{
<<"file.erl">> => #{
<<"content_type">> => <<"text/plain">>,
<<"data">> => base64:encode(Data)
}
}
}.