http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_attachments_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_attachments_tests.erl b/test/couchdb_attachments_tests.erl new file mode 100644 index 0000000..cf59785 --- /dev/null +++ b/test/couchdb_attachments_tests.erl @@ -0,0 +1,638 @@ +% 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(couchdb_attachments_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(COMPRESSION_LEVEL, 8). +-define(ATT_BIN_NAME, <<"logo.png">>). +-define(ATT_TXT_NAME, <<"file.erl">>). +-define(FIXTURE_PNG, filename:join([?FIXTURESDIR, "logo.png"])). +-define(FIXTURE_TXT, ?FILE). +-define(TIMEOUT, 1000). +-define(TIMEOUT_EUNIT, 10). +-define(TIMEWAIT, 100). +-define(i2l(I), integer_to_list(I)). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + % ensure in default compression settings for attachments_compression_tests + couch_config:set("attachments", "compression_level", + ?i2l(?COMPRESSION_LEVEL), false), + couch_config:set("attachments", "compressible_types", "text/*", false), + Pid. + +stop(Pid) -> + erlang:monitor(process, Pid), + couch_server_sup:stop(), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, server_stop}) + end. + +setup() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, []), + ok = couch_db:close(Db), + Addr = couch_config:get("httpd", "bind_address", any), + Port = mochiweb_socket_server:get(couch_httpd, port), + Host = Addr ++ ":" ++ ?i2l(Port), + {Host, ?b2l(DbName)}. + +setup({binary, standalone}) -> + {Host, DbName} = setup(), + setup_att(fun create_standalone_png_att/2, Host, DbName, ?FIXTURE_PNG); +setup({text, standalone}) -> + {Host, DbName} = setup(), + setup_att(fun create_standalone_text_att/2, Host, DbName, ?FIXTURE_TXT); +setup({binary, inline}) -> + {Host, DbName} = setup(), + setup_att(fun create_inline_png_att/2, Host, DbName, ?FIXTURE_PNG); +setup({text, inline}) -> + {Host, DbName} = setup(), + setup_att(fun create_inline_text_att/2, Host, DbName, ?FIXTURE_TXT); +setup(compressed) -> + {Host, DbName} = setup(), + setup_att(fun create_already_compressed_att/2, Host, DbName, ?FIXTURE_TXT). +setup_att(Fun, Host, DbName, File) -> + HttpHost = "http://" ++ Host, + AttUrl = Fun(HttpHost, DbName), + {ok, Data} = file:read_file(File), + DocUrl = string:join([HttpHost, DbName, "doc"], "/"), + Helpers = {DbName, DocUrl, AttUrl}, + {Data, Helpers}. + +teardown(_, {_, {DbName, _, _}}) -> + teardown(DbName). + +teardown({_, DbName}) -> + teardown(DbName); +teardown(DbName) -> + ok = couch_server:delete(?l2b(DbName), []), + ok. + + +attachments_test_() -> + { + "Attachments tests", + { + setup, + fun start/0, fun stop/1, + [ + attachments_md5_tests(), + attachments_compression_tests() + ] + } + }. + +attachments_md5_tests() -> + { + "Attachments MD5 tests", + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_upload_attachment_without_md5/1, + fun should_upload_attachment_by_chunks_without_md5/1, + fun should_upload_attachment_with_valid_md5_header/1, + fun should_upload_attachment_by_chunks_with_valid_md5_header/1, + fun should_upload_attachment_by_chunks_with_valid_md5_trailer/1, + fun should_reject_attachment_with_invalid_md5/1, + fun should_reject_chunked_attachment_with_invalid_md5/1, + fun should_reject_chunked_attachment_with_invalid_md5_trailer/1 + ] + } + }. + +attachments_compression_tests() -> + Funs = [ + fun should_get_att_without_accept_gzip_encoding/2, + fun should_get_att_with_accept_gzip_encoding/2, + fun should_get_att_with_accept_deflate_encoding/2, + fun should_return_406_response_on_unsupported_encoding/2, + fun should_get_doc_with_att_data/2, + fun should_get_doc_with_att_data_stub/2 + ], + { + "Attachments compression tests", + [ + { + "Created via Attachments API", + created_attachments_compression_tests(standalone, Funs) + }, + { + "Created inline via Document API", + created_attachments_compression_tests(inline, Funs) + }, + { + "Created already been compressed via Attachments API", + { + foreachx, + fun setup/1, fun teardown/2, + [{compressed, Fun} || Fun <- Funs] + } + }, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_not_create_compressed_att_with_deflate_encoding/1, + fun should_not_create_compressed_att_with_compress_encoding/1, + fun should_create_compressible_att_with_ctype_params/1 + ] + } + ] + }. + +created_attachments_compression_tests(Mod, Funs) -> + [ + { + "Compressiable attachments", + { + foreachx, + fun setup/1, fun teardown/2, + [{{text, Mod}, Fun} || Fun <- Funs] + } + }, + { + "Uncompressiable attachments", + { + foreachx, + fun setup/1, fun teardown/2, + [{{binary, Mod}, Fun} || Fun <- Funs] + } + } + ]. + + + +should_upload_attachment_without_md5({Host, DbName}) -> + ?_test(begin + AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), + Body = "We all live in a yellow submarine!", + Headers = [ + {"Content-Length", "34"}, + {"Content-Type", "text/plain"}, + {"Host", Host} + ], + {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), + ?assertEqual(201, Code), + ?assertEqual(true, get_json(Json, [<<"ok">>])) + end). + +should_upload_attachment_by_chunks_without_md5({Host, DbName}) -> + ?_test(begin + AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), + AttData = <<"We all live in a yellow submarine!">>, + <<Part1:21/binary, Part2:13/binary>> = AttData, + Body = chunked_body([Part1, Part2]), + Headers = [ + {"Content-Type", "text/plain"}, + {"Transfer-Encoding", "chunked"}, + {"Host", Host} + ], + {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), + ?assertEqual(201, Code), + ?assertEqual(true, get_json(Json, [<<"ok">>])) + end). + +should_upload_attachment_with_valid_md5_header({Host, DbName}) -> + ?_test(begin + AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), + Body = "We all live in a yellow submarine!", + Headers = [ + {"Content-Length", "34"}, + {"Content-Type", "text/plain"}, + {"Content-MD5", ?b2l(base64:encode(couch_util:md5(Body)))}, + {"Host", Host} + ], + {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), + ?assertEqual(201, Code), + ?assertEqual(true, get_json(Json, [<<"ok">>])) + end). + +should_upload_attachment_by_chunks_with_valid_md5_header({Host, DbName}) -> + ?_test(begin + AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), + AttData = <<"We all live in a yellow submarine!">>, + <<Part1:21/binary, Part2:13/binary>> = AttData, + Body = chunked_body([Part1, Part2]), + Headers = [ + {"Content-Type", "text/plain"}, + {"Content-MD5", ?b2l(base64:encode(couch_util:md5(AttData)))}, + {"Host", Host}, + {"Transfer-Encoding", "chunked"} + ], + {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), + ?assertEqual(201, Code), + ?assertEqual(true, get_json(Json, [<<"ok">>])) + end). + +should_upload_attachment_by_chunks_with_valid_md5_trailer({Host, DbName}) -> + ?_test(begin + AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), + AttData = <<"We all live in a yellow submarine!">>, + <<Part1:21/binary, Part2:13/binary>> = AttData, + Body = [chunked_body([Part1, Part2]), + "Content-MD5: ", base64:encode(couch_util:md5(AttData)), + "\r\n"], + Headers = [ + {"Content-Type", "text/plain"}, + {"Host", Host}, + {"Trailer", "Content-MD5"}, + {"Transfer-Encoding", "chunked"} + ], + {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), + ?assertEqual(201, Code), + ?assertEqual(true, get_json(Json, [<<"ok">>])) + end). + +should_reject_attachment_with_invalid_md5({Host, DbName}) -> + ?_test(begin + AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), + Body = "We all live in a yellow submarine!", + Headers = [ + {"Content-Length", "34"}, + {"Content-Type", "text/plain"}, + {"Content-MD5", ?b2l(base64:encode(<<"foobar!">>))}, + {"Host", Host} + ], + {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), + ?assertEqual(400, Code), + ?assertEqual(<<"content_md5_mismatch">>, + get_json(Json, [<<"error">>])) + end). + + +should_reject_chunked_attachment_with_invalid_md5({Host, DbName}) -> + ?_test(begin + AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), + AttData = <<"We all live in a yellow submarine!">>, + <<Part1:21/binary, Part2:13/binary>> = AttData, + Body = chunked_body([Part1, Part2]), + Headers = [ + {"Content-Type", "text/plain"}, + {"Content-MD5", ?b2l(base64:encode(<<"foobar!">>))}, + {"Host", Host}, + {"Transfer-Encoding", "chunked"} + ], + {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), + ?assertEqual(400, Code), + ?assertEqual(<<"content_md5_mismatch">>, + get_json(Json, [<<"error">>])) + end). + +should_reject_chunked_attachment_with_invalid_md5_trailer({Host, DbName}) -> + ?_test(begin + AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), + AttData = <<"We all live in a yellow submarine!">>, + <<Part1:21/binary, Part2:13/binary>> = AttData, + Body = [chunked_body([Part1, Part2]), + "Content-MD5: ", base64:encode(<<"foobar!">>), + "\r\n"], + Headers = [ + {"Content-Type", "text/plain"}, + {"Host", Host}, + {"Trailer", "Content-MD5"}, + {"Transfer-Encoding", "chunked"} + ], + {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), + ?assertEqual(400, Code), + ?assertEqual(<<"content_md5_mismatch">>, get_json(Json, [<<"error">>])) + end). + +should_get_att_without_accept_gzip_encoding(_, {Data, {_, _, AttUrl}}) -> + ?_test(begin + {ok, Code, Headers, Body} = test_request:get(AttUrl), + ?assertEqual(200, Code), + ?assertNot(lists:member({"Content-Encoding", "gzip"}, Headers)), + ?assertEqual(Data, iolist_to_binary(Body)) + end). + +should_get_att_with_accept_gzip_encoding(compressed, {Data, {_, _, AttUrl}}) -> + ?_test(begin + {ok, Code, Headers, Body} = test_request:get( + AttUrl, [{"Accept-Encoding", "gzip"}]), + ?assertEqual(200, Code), + ?assert(lists:member({"Content-Encoding", "gzip"}, Headers)), + ?assertEqual(Data, zlib:gunzip(iolist_to_binary(Body))) + end); +should_get_att_with_accept_gzip_encoding({text, _}, {Data, {_, _, AttUrl}}) -> + ?_test(begin + {ok, Code, Headers, Body} = test_request:get( + AttUrl, [{"Accept-Encoding", "gzip"}]), + ?assertEqual(200, Code), + ?assert(lists:member({"Content-Encoding", "gzip"}, Headers)), + ?assertEqual(Data, zlib:gunzip(iolist_to_binary(Body))) + end); +should_get_att_with_accept_gzip_encoding({binary, _}, {Data, {_, _, AttUrl}}) -> + ?_test(begin + {ok, Code, Headers, Body} = test_request:get( + AttUrl, [{"Accept-Encoding", "gzip"}]), + ?assertEqual(200, Code), + ?assertEqual(undefined, + couch_util:get_value("Content-Encoding", Headers)), + ?assertEqual(Data, iolist_to_binary(Body)) + end). + +should_get_att_with_accept_deflate_encoding(_, {Data, {_, _, AttUrl}}) -> + ?_test(begin + {ok, Code, Headers, Body} = test_request:get( + AttUrl, [{"Accept-Encoding", "deflate"}]), + ?assertEqual(200, Code), + ?assertEqual(undefined, + couch_util:get_value("Content-Encoding", Headers)), + ?assertEqual(Data, iolist_to_binary(Body)) + end). + +should_return_406_response_on_unsupported_encoding(_, {_, {_, _, AttUrl}}) -> + ?_assertEqual(406, + begin + {ok, Code, _, _} = test_request:get( + AttUrl, [{"Accept-Encoding", "deflate, *;q=0"}]), + Code + end). + +should_get_doc_with_att_data(compressed, {Data, {_, DocUrl, _}}) -> + ?_test(begin + Url = DocUrl ++ "?attachments=true", + {ok, Code, _, Body} = test_request:get( + Url, [{"Accept", "application/json"}]), + ?assertEqual(200, Code), + Json = ejson:decode(Body), + AttJson = couch_util:get_nested_json_value( + Json, [<<"_attachments">>, ?ATT_TXT_NAME]), + AttData = couch_util:get_nested_json_value( + AttJson, [<<"data">>]), + ?assertEqual( + <<"text/plain">>, + couch_util:get_nested_json_value(AttJson,[<<"content_type">>])), + ?assertEqual(Data, base64:decode(AttData)) + end); +should_get_doc_with_att_data({text, _}, {Data, {_, DocUrl, _}}) -> + ?_test(begin + Url = DocUrl ++ "?attachments=true", + {ok, Code, _, Body} = test_request:get( + Url, [{"Accept", "application/json"}]), + ?assertEqual(200, Code), + Json = ejson:decode(Body), + AttJson = couch_util:get_nested_json_value( + Json, [<<"_attachments">>, ?ATT_TXT_NAME]), + AttData = couch_util:get_nested_json_value( + AttJson, [<<"data">>]), + ?assertEqual( + <<"text/plain">>, + couch_util:get_nested_json_value(AttJson,[<<"content_type">>])), + ?assertEqual(Data, base64:decode(AttData)) + end); +should_get_doc_with_att_data({binary, _}, {Data, {_, DocUrl, _}}) -> + ?_test(begin + Url = DocUrl ++ "?attachments=true", + {ok, Code, _, Body} = test_request:get( + Url, [{"Accept", "application/json"}]), + ?assertEqual(200, Code), + Json = ejson:decode(Body), + AttJson = couch_util:get_nested_json_value( + Json, [<<"_attachments">>, ?ATT_BIN_NAME]), + AttData = couch_util:get_nested_json_value( + AttJson, [<<"data">>]), + ?assertEqual( + <<"image/png">>, + couch_util:get_nested_json_value(AttJson,[<<"content_type">>])), + ?assertEqual(Data, base64:decode(AttData)) + end). + +should_get_doc_with_att_data_stub(compressed, {Data, {_, DocUrl, _}}) -> + ?_test(begin + Url = DocUrl ++ "?att_encoding_info=true", + {ok, Code, _, Body} = test_request:get( + Url, [{"Accept", "application/json"}]), + ?assertEqual(200, Code), + Json = ejson:decode(Body), + {AttJson} = couch_util:get_nested_json_value( + Json, [<<"_attachments">>, ?ATT_TXT_NAME]), + ?assertEqual(<<"gzip">>, + couch_util:get_value(<<"encoding">>, AttJson)), + AttLength = couch_util:get_value(<<"length">>, AttJson), + EncLength = couch_util:get_value(<<"encoded_length">>, AttJson), + ?assertEqual(AttLength, EncLength), + ?assertEqual(iolist_size(zlib:gzip(Data)), AttLength) + end); +should_get_doc_with_att_data_stub({text, _}, {Data, {_, DocUrl, _}}) -> + ?_test(begin + Url = DocUrl ++ "?att_encoding_info=true", + {ok, Code, _, Body} = test_request:get( + Url, [{"Accept", "application/json"}]), + ?assertEqual(200, Code), + Json = ejson:decode(Body), + {AttJson} = couch_util:get_nested_json_value( + Json, [<<"_attachments">>, ?ATT_TXT_NAME]), + ?assertEqual(<<"gzip">>, + couch_util:get_value(<<"encoding">>, AttJson)), + AttEncLength = iolist_size(gzip(Data)), + ?assertEqual(AttEncLength, + couch_util:get_value(<<"encoded_length">>, AttJson)), + ?assertEqual(byte_size(Data), + couch_util:get_value(<<"length">>, AttJson)) + end); +should_get_doc_with_att_data_stub({binary, _}, {Data, {_, DocUrl, _}}) -> + ?_test(begin + Url = DocUrl ++ "?att_encoding_info=true", + {ok, Code, _, Body} = test_request:get( + Url, [{"Accept", "application/json"}]), + ?assertEqual(200, Code), + Json = ejson:decode(Body), + {AttJson} = couch_util:get_nested_json_value( + Json, [<<"_attachments">>, ?ATT_BIN_NAME]), + ?assertEqual(undefined, + couch_util:get_value(<<"encoding">>, AttJson)), + ?assertEqual(undefined, + couch_util:get_value(<<"encoded_length">>, AttJson)), + ?assertEqual(byte_size(Data), + couch_util:get_value(<<"length">>, AttJson)) + end). + +should_not_create_compressed_att_with_deflate_encoding({Host, DbName}) -> + ?_assertEqual(415, + begin + HttpHost = "http://" ++ Host, + AttUrl = string:join([HttpHost, DbName, ?docid(), "file.txt"], "/"), + {ok, Data} = file:read_file(?FIXTURE_TXT), + Body = zlib:compress(Data), + Headers = [ + {"Content-Encoding", "deflate"}, + {"Content-Type", "text/plain"} + ], + {ok, Code, _, _} = test_request:put(AttUrl, Headers, Body), + Code + end). + +should_not_create_compressed_att_with_compress_encoding({Host, DbName}) -> + % Note: As of OTP R13B04, it seems there's no LZW compression + % (i.e. UNIX compress utility implementation) lib in OTP. + % However there's a simple working Erlang implementation at: + % http://scienceblogs.com/goodmath/2008/01/simple_lempelziv_compression_i.php + ?_assertEqual(415, + begin + HttpHost = "http://" ++ Host, + AttUrl = string:join([HttpHost, DbName, ?docid(), "file.txt"], "/"), + {ok, Data} = file:read_file(?FIXTURE_TXT), + Headers = [ + {"Content-Encoding", "compress"}, + {"Content-Type", "text/plain"} + ], + {ok, Code, _, _} = test_request:put(AttUrl, Headers, Data), + Code + end). + +should_create_compressible_att_with_ctype_params({Host, DbName}) -> + {timeout, ?TIMEOUT_EUNIT, ?_test(begin + HttpHost = "http://" ++ Host, + DocUrl = string:join([HttpHost, DbName, ?docid()], "/"), + AttUrl = string:join([DocUrl, ?b2l(?ATT_TXT_NAME)], "/"), + {ok, Data} = file:read_file(?FIXTURE_TXT), + Headers = [{"Content-Type", "text/plain; charset=UTF-8"}], + {ok, Code0, _, _} = test_request:put(AttUrl, Headers, Data), + ?assertEqual(201, Code0), + + {ok, Code1, _, Body} = test_request:get( + DocUrl ++ "?att_encoding_info=true"), + ?assertEqual(200, Code1), + Json = ejson:decode(Body), + {AttJson} = couch_util:get_nested_json_value( + Json, [<<"_attachments">>, ?ATT_TXT_NAME]), + ?assertEqual(<<"gzip">>, + couch_util:get_value(<<"encoding">>, AttJson)), + AttEncLength = iolist_size(gzip(Data)), + ?assertEqual(AttEncLength, + couch_util:get_value(<<"encoded_length">>, AttJson)), + ?assertEqual(byte_size(Data), + couch_util:get_value(<<"length">>, AttJson)) + end)}. + + +get_json(Json, Path) -> + couch_util:get_nested_json_value(Json, Path). + +to_hex(Val) -> + to_hex(Val, []). + +to_hex(0, Acc) -> + Acc; +to_hex(Val, Acc) -> + to_hex(Val div 16, [hex_char(Val rem 16) | Acc]). + +hex_char(V) when V < 10 -> $0 + V; +hex_char(V) -> $A + V - 10. + +chunked_body(Chunks) -> + chunked_body(Chunks, []). + +chunked_body([], Acc) -> + iolist_to_binary(lists:reverse(Acc, "0\r\n")); +chunked_body([Chunk | Rest], Acc) -> + Size = to_hex(size(Chunk)), + chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]). + +get_socket() -> + Options = [binary, {packet, 0}, {active, false}], + Addr = couch_config:get("httpd", "bind_address", any), + Port = mochiweb_socket_server:get(couch_httpd, port), + {ok, Sock} = gen_tcp:connect(Addr, Port, Options), + Sock. + +request(Method, Url, Headers, Body) -> + RequestHead = [Method, " ", Url, " HTTP/1.1"], + RequestHeaders = [[string:join([Key, Value], ": "), "\r\n"] + || {Key, Value} <- Headers], + Request = [RequestHead, "\r\n", RequestHeaders, "\r\n", Body, "\r\n"], + Sock = get_socket(), + gen_tcp:send(Sock, list_to_binary(lists:flatten(Request))), + timer:sleep(?TIMEWAIT), % must wait to receive complete response + {ok, R} = gen_tcp:recv(Sock, 0), + gen_tcp:close(Sock), + [Header, Body1] = re:split(R, "\r\n\r\n", [{return, binary}]), + {ok, {http_response, _, Code, _}, _} = + erlang:decode_packet(http, Header, []), + Json = ejson:decode(Body1), + {ok, Code, Json}. + +create_standalone_text_att(Host, DbName) -> + {ok, Data} = file:read_file(?FIXTURE_TXT), + Url = string:join([Host, DbName, "doc", ?b2l(?ATT_TXT_NAME)], "/"), + {ok, Code, _Headers, _Body} = test_request:put( + Url, [{"Content-Type", "text/plain"}], Data), + ?assertEqual(201, Code), + Url. + +create_standalone_png_att(Host, DbName) -> + {ok, Data} = file:read_file(?FIXTURE_PNG), + Url = string:join([Host, DbName, "doc", ?b2l(?ATT_BIN_NAME)], "/"), + {ok, Code, _Headers, _Body} = test_request:put( + Url, [{"Content-Type", "image/png"}], Data), + ?assertEqual(201, Code), + Url. + +create_inline_text_att(Host, DbName) -> + {ok, Data} = file:read_file(?FIXTURE_TXT), + Url = string:join([Host, DbName, "doc"], "/"), + Doc = {[ + {<<"_attachments">>, {[ + {?ATT_TXT_NAME, {[ + {<<"content_type">>, <<"text/plain">>}, + {<<"data">>, base64:encode(Data)} + ]} + }]}} + ]}, + {ok, Code, _Headers, _Body} = test_request:put( + Url, [{"Content-Type", "application/json"}], ejson:encode(Doc)), + ?assertEqual(201, Code), + string:join([Url, ?b2l(?ATT_TXT_NAME)], "/"). + +create_inline_png_att(Host, DbName) -> + {ok, Data} = file:read_file(?FIXTURE_PNG), + Url = string:join([Host, DbName, "doc"], "/"), + Doc = {[ + {<<"_attachments">>, {[ + {?ATT_BIN_NAME, {[ + {<<"content_type">>, <<"image/png">>}, + {<<"data">>, base64:encode(Data)} + ]} + }]}} + ]}, + {ok, Code, _Headers, _Body} = test_request:put( + Url, [{"Content-Type", "application/json"}], ejson:encode(Doc)), + ?assertEqual(201, Code), + string:join([Url, ?b2l(?ATT_BIN_NAME)], "/"). + +create_already_compressed_att(Host, DbName) -> + {ok, Data} = file:read_file(?FIXTURE_TXT), + Url = string:join([Host, DbName, "doc", ?b2l(?ATT_TXT_NAME)], "/"), + {ok, Code, _Headers, _Body} = test_request:put( + Url, [{"Content-Type", "text/plain"}, {"Content-Encoding", "gzip"}], + zlib:gzip(Data)), + ?assertEqual(201, Code), + Url. + +gzip(Data) -> + Z = zlib:open(), + ok = zlib:deflateInit(Z, ?COMPRESSION_LEVEL, deflated, 16 + 15, 8, default), + zlib:deflate(Z, Data), + Last = zlib:deflate(Z, [], finish), + ok = zlib:deflateEnd(Z), + ok = zlib:close(Z), + Last.
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_compaction_daemon.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_compaction_daemon.erl b/test/couchdb_compaction_daemon.erl new file mode 100644 index 0000000..725a97b --- /dev/null +++ b/test/couchdb_compaction_daemon.erl @@ -0,0 +1,231 @@ +% 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(couchdb_compaction_daemon). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). +-define(DELAY, 100). +-define(TIMEOUT, 30000). +-define(TIMEOUT_S, ?TIMEOUT div 1000). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + couch_config:set("compaction_daemon", "check_interval", "3", false), + couch_config:set("compaction_daemon", "min_file_size", "100000", false), + Pid. + +stop(Pid) -> + erlang:monitor(process, Pid), + couch_server_sup:stop(), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, server_stop}) + end. + +setup() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), + create_design_doc(Db), + ok = couch_db:close(Db), + DbName. + +teardown(DbName) -> + Configs = couch_config:get("compactions"), + lists:foreach( + fun({Key, _}) -> + ok = couch_config:delete("compactions", Key, false) + end, + Configs), + couch_server:delete(DbName, [?ADMIN_USER]), + ok. + + +compaction_daemon_test_() -> + { + "Compaction daemon tests", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_compact_by_default_rule/1, + fun should_compact_by_dbname_rule/1 + ] + } + } + }. + + +should_compact_by_default_rule(DbName) -> + {timeout, ?TIMEOUT_S, ?_test(begin + {ok, Db} = couch_db:open_int(DbName, []), + populate(DbName, 70, 70, 200 * 1024), + + {_, DbFileSize} = get_db_frag(DbName), + {_, ViewFileSize} = get_view_frag(DbName), + + ok = couch_config:set("compactions", "_default", + "[{db_fragmentation, \"70%\"}, {view_fragmentation, \"70%\"}]", + false), + + ok = timer:sleep(4000), % something >= check_interval + wait_compaction_finished(DbName), + ok = couch_config:delete("compactions", "_default", false), + + {DbFrag2, DbFileSize2} = get_db_frag(DbName), + {ViewFrag2, ViewFileSize2} = get_view_frag(DbName), + + ?assert(DbFrag2 < 70), + ?assert(ViewFrag2 < 70), + + ?assert(DbFileSize > DbFileSize2), + ?assert(ViewFileSize > ViewFileSize2), + + ?assert(couch_db:is_idle(Db)), + ok = couch_db:close(Db) + end)}. + +should_compact_by_dbname_rule(DbName) -> + {timeout, ?TIMEOUT_S, ?_test(begin + {ok, Db} = couch_db:open_int(DbName, []), + populate(DbName, 70, 70, 200 * 1024), + + {_, DbFileSize} = get_db_frag(DbName), + {_, ViewFileSize} = get_view_frag(DbName), + + ok = couch_config:set("compactions", ?b2l(DbName), + "[{db_fragmentation, \"70%\"}, {view_fragmentation, \"70%\"}]", + false), + + ok = timer:sleep(4000), % something >= check_interval + wait_compaction_finished(DbName), + ok = couch_config:delete("compactions", ?b2l(DbName), false), + + {DbFrag2, DbFileSize2} = get_db_frag(DbName), + {ViewFrag2, ViewFileSize2} = get_view_frag(DbName), + + ?assert(DbFrag2 < 70), + ?assert(ViewFrag2 < 70), + + ?assert(DbFileSize > DbFileSize2), + ?assert(ViewFileSize > ViewFileSize2), + + ?assert(couch_db:is_idle(Db)), + ok = couch_db:close(Db) + end)}. + + +create_design_doc(Db) -> + DDoc = couch_doc:from_json_obj({[ + {<<"_id">>, <<"_design/foo">>}, + {<<"language">>, <<"javascript">>}, + {<<"views">>, {[ + {<<"foo">>, {[ + {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>} + ]}}, + {<<"foo2">>, {[ + {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>} + ]}}, + {<<"foo3">>, {[ + {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>} + ]}} + ]}} + ]}), + {ok, _} = couch_db:update_docs(Db, [DDoc]), + {ok, _} = couch_db:ensure_full_commit(Db), + ok. + +populate(DbName, DbFrag, ViewFrag, MinFileSize) -> + {CurDbFrag, DbFileSize} = get_db_frag(DbName), + {CurViewFrag, ViewFileSize} = get_view_frag(DbName), + populate(DbName, DbFrag, ViewFrag, MinFileSize, CurDbFrag, CurViewFrag, + lists:min([DbFileSize, ViewFileSize])). + +populate(_Db, DbFrag, ViewFrag, MinFileSize, CurDbFrag, CurViewFrag, FileSize) + when CurDbFrag >= DbFrag, CurViewFrag >= ViewFrag, FileSize >= MinFileSize -> + ok; +populate(DbName, DbFrag, ViewFrag, MinFileSize, _, _, _) -> + update(DbName), + {CurDbFrag, DbFileSize} = get_db_frag(DbName), + {CurViewFrag, ViewFileSize} = get_view_frag(DbName), + populate(DbName, DbFrag, ViewFrag, MinFileSize, CurDbFrag, CurViewFrag, + lists:min([DbFileSize, ViewFileSize])). + +update(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + lists:foreach(fun(_) -> + Doc = couch_doc:from_json_obj({[{<<"_id">>, couch_uuids:new()}]}), + {ok, _} = couch_db:update_docs(Db, [Doc]), + query_view(Db#db.name) + end, lists:seq(1, 200)), + couch_db:close(Db). + +db_url(DbName) -> + Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(DbName). + +query_view(DbName) -> + {ok, Code, _Headers, _Body} = test_request:get( + db_url(DbName) ++ "/_design/foo/_view/foo"), + ?assertEqual(200, Code). + +get_db_frag(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, Info} = couch_db:get_db_info(Db), + couch_db:close(Db), + FileSize = couch_util:get_value(disk_size, Info), + DataSize = couch_util:get_value(data_size, Info), + {round((FileSize - DataSize) / FileSize * 100), FileSize}. + +get_view_frag(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, Info} = couch_mrview:get_info(Db, <<"_design/foo">>), + couch_db:close(Db), + FileSize = couch_util:get_value(disk_size, Info), + DataSize = couch_util:get_value(data_size, Info), + {round((FileSize - DataSize) / FileSize * 100), FileSize}. + +wait_compaction_finished(DbName) -> + Parent = self(), + Loop = spawn_link(fun() -> wait_loop(DbName, Parent) end), + receive + {done, Loop} -> + ok + after ?TIMEOUT -> + erlang:error( + {assertion_failed, + [{module, ?MODULE}, {line, ?LINE}, + {reason, "Compaction timeout"}]}) + end. + +wait_loop(DbName, Parent) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, DbInfo} = couch_db:get_db_info(Db), + {ok, ViewInfo} = couch_mrview:get_info(Db, <<"_design/foo">>), + couch_db:close(Db), + case (couch_util:get_value(compact_running, ViewInfo) =:= true) orelse + (couch_util:get_value(compact_running, DbInfo) =:= true) of + false -> + Parent ! {done, self()}; + true -> + ok = timer:sleep(?DELAY), + wait_loop(DbName, Parent) + end. http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_cors_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_cors_tests.erl b/test/couchdb_cors_tests.erl new file mode 100644 index 0000000..4e88ae7 --- /dev/null +++ b/test/couchdb_cors_tests.erl @@ -0,0 +1,344 @@ +% 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(couchdb_cors_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + + +-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). +-define(SUPPORTED_METHODS, + "GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, COPY, OPTIONS"). +-define(TIMEOUT, 1000). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + ok = couch_config:set("httpd", "enable_cors", "true", false), + ok = couch_config:set("vhosts", "example.com", "/", false), + Pid. + +stop(Pid) -> + couch_server_sup:stop(), + erlang:monitor(process, Pid), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, server_stop}) + end. + +setup() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), + couch_db:close(Db), + + couch_config:set("cors", "credentials", "false", false), + couch_config:set("cors", "origins", "http://example.com", false), + + Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + Host = "http://" ++ Addr ++ ":" ++ Port, + {Host, ?b2l(DbName)}. + +setup({Mod, VHost}) -> + {Host, DbName} = setup(), + Url = case Mod of + server -> + Host; + db -> + Host ++ "/" ++ DbName + end, + DefaultHeaders = [{"Origin", "http://example.com"}] + ++ maybe_append_vhost(VHost), + {Host, DbName, Url, DefaultHeaders}. + +teardown(DbName) when is_list(DbName) -> + ok = couch_server:delete(?l2b(DbName), [?ADMIN_USER]), + ok; +teardown({_, DbName}) -> + teardown(DbName). + +teardown(_, {_, DbName, _, _}) -> + teardown(DbName). + + +cors_test_() -> + Funs = [ + fun should_not_allow_origin/2, + fun should_not_allow_origin_with_port_mismatch/2, + fun should_not_allow_origin_with_scheme_mismatch/2, + fun should_not_all_origin_due_case_mismatch/2, + fun should_make_simple_request/2, + fun should_make_preflight_request/2, + fun should_make_prefligh_request_with_port/2, + fun should_make_prefligh_request_with_scheme/2, + fun should_make_prefligh_request_with_wildcard_origin/2, + fun should_make_request_with_credentials/2, + fun should_make_origin_request_with_auth/2, + fun should_make_preflight_request_with_auth/2 + ], + { + "CORS (COUCHDB-431)", + { + setup, + fun start/0, fun stop/1, + [ + cors_tests(Funs), + vhost_cors_tests(Funs), + headers_tests() + ] + } + }. + +headers_tests() -> + { + "Various headers tests", + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_not_return_cors_headers_for_invalid_origin/1, + fun should_not_return_cors_headers_for_invalid_origin_preflight/1, + fun should_make_request_against_attachment/1, + fun should_make_range_request_against_attachment/1, + fun should_make_request_with_if_none_match_header/1 + ] + } + }. + +cors_tests(Funs) -> + { + "CORS tests", + [ + make_test_case(server, false, Funs), + make_test_case(db, false, Funs) + ] + }. + +vhost_cors_tests(Funs) -> + { + "Virtual Host CORS", + [ + make_test_case(server, true, Funs), + make_test_case(db, true, Funs) + ] + }. + +make_test_case(Mod, UseVhost, Funs) -> + { + case Mod of server -> "Server"; db -> "Database" end, + {foreachx, fun setup/1, fun teardown/2, [{{Mod, UseVhost}, Fun} + || Fun <- Funs]} + }. + + +should_not_allow_origin(_, {_, _, Url, Headers0}) -> + ?_assertEqual(undefined, + begin + couch_config:delete("cors", "origins", false), + Headers1 = proplists:delete("Origin", Headers0), + Headers = [{"Origin", "http://127.0.0.1"}] + ++ Headers1, + {ok, _, Resp, _} = test_request:get(Url, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_not_allow_origin_with_port_mismatch({_, VHost}, {_, _, Url, _}) -> + ?_assertEqual(undefined, + begin + Headers = [{"Origin", "http://example.com:5984"}, + {"Access-Control-Request-Method", "GET"}] + ++ maybe_append_vhost(VHost), + {ok, _, Resp, _} = test_request:options(Url, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_not_allow_origin_with_scheme_mismatch({_, VHost}, {_, _, Url, _}) -> + ?_assertEqual(undefined, + begin + Headers = [{"Origin", "http://example.com:5984"}, + {"Access-Control-Request-Method", "GET"}] + ++ maybe_append_vhost(VHost), + {ok, _, Resp, _} = test_request:options(Url, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_not_all_origin_due_case_mismatch({_, VHost}, {_, _, Url, _}) -> + ?_assertEqual(undefined, + begin + Headers = [{"Origin", "http://ExAmPlE.CoM"}, + {"Access-Control-Request-Method", "GET"}] + ++ maybe_append_vhost(VHost), + {ok, _, Resp, _} = test_request:options(Url, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_make_simple_request(_, {_, _, Url, DefaultHeaders}) -> + ?_test(begin + {ok, _, Resp, _} = test_request:get(Url, DefaultHeaders), + ?assertEqual( + undefined, + proplists:get_value("Access-Control-Allow-Credentials", Resp)), + ?assertEqual( + "http://example.com", + proplists:get_value("Access-Control-Allow-Origin", Resp)), + ?assertEqual( + "Cache-Control, Content-Type, Server", + proplists:get_value("Access-Control-Expose-Headers", Resp)) + end). + +should_make_preflight_request(_, {_, _, Url, DefaultHeaders}) -> + ?_assertEqual(?SUPPORTED_METHODS, + begin + Headers = DefaultHeaders + ++ [{"Access-Control-Request-Method", "GET"}], + {ok, _, Resp, _} = test_request:options(Url, Headers), + proplists:get_value("Access-Control-Allow-Methods", Resp) + end). + +should_make_prefligh_request_with_port({_, VHost}, {_, _, Url, _}) -> + ?_assertEqual("http://example.com:5984", + begin + couch_config:set("cors", "origins", "http://example.com:5984", + false), + Headers = [{"Origin", "http://example.com:5984"}, + {"Access-Control-Request-Method", "GET"}] + ++ maybe_append_vhost(VHost), + {ok, _, Resp, _} = test_request:options(Url, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_make_prefligh_request_with_scheme({_, VHost}, {_, _, Url, _}) -> + ?_assertEqual("https://example.com:5984", + begin + couch_config:set("cors", "origins", "https://example.com:5984", + false), + Headers = [{"Origin", "https://example.com:5984"}, + {"Access-Control-Request-Method", "GET"}] + ++ maybe_append_vhost(VHost), + {ok, _, Resp, _} = test_request:options(Url, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_make_prefligh_request_with_wildcard_origin({_, VHost}, {_, _, Url, _}) -> + ?_assertEqual("https://example.com:5984", + begin + couch_config:set("cors", "origins", "*", false), + Headers = [{"Origin", "https://example.com:5984"}, + {"Access-Control-Request-Method", "GET"}] + ++ maybe_append_vhost(VHost), + {ok, _, Resp, _} = test_request:options(Url, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_make_request_with_credentials(_, {_, _, Url, DefaultHeaders}) -> + ?_assertEqual("true", + begin + ok = couch_config:set("cors", "credentials", "true", false), + {ok, _, Resp, _} = test_request:options(Url, DefaultHeaders), + proplists:get_value("Access-Control-Allow-Credentials", Resp) + end). + +should_make_origin_request_with_auth(_, {_, _, Url, DefaultHeaders}) -> + ?_assertEqual("http://example.com", + begin + Hashed = couch_passwords:hash_admin_password(<<"test">>), + couch_config:set("admins", "test", Hashed, false), + {ok, _, Resp, _} = test_request:get( + Url, DefaultHeaders, [{basic_auth, {"test", "test"}}]), + couch_config:delete("admins", "test", false), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_make_preflight_request_with_auth(_, {_, _, Url, DefaultHeaders}) -> + ?_assertEqual(?SUPPORTED_METHODS, + begin + Hashed = couch_passwords:hash_admin_password(<<"test">>), + couch_config:set("admins", "test", Hashed, false), + Headers = DefaultHeaders + ++ [{"Access-Control-Request-Method", "GET"}], + {ok, _, Resp, _} = test_request:options( + Url, Headers, [{basic_auth, {"test", "test"}}]), + couch_config:delete("admins", "test", false), + proplists:get_value("Access-Control-Allow-Methods", Resp) + end). + +should_not_return_cors_headers_for_invalid_origin({Host, _}) -> + ?_assertEqual(undefined, + begin + Headers = [{"Origin", "http://127.0.0.1"}], + {ok, _, Resp, _} = test_request:get(Host, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_not_return_cors_headers_for_invalid_origin_preflight({Host, _}) -> + ?_assertEqual(undefined, + begin + Headers = [{"Origin", "http://127.0.0.1"}, + {"Access-Control-Request-Method", "GET"}], + {ok, _, Resp, _} = test_request:options(Host, Headers), + proplists:get_value("Access-Control-Allow-Origin", Resp) + end). + +should_make_request_against_attachment({Host, DbName}) -> + {"COUCHDB-1689", + ?_assertEqual(200, + begin + Url = Host ++ "/" ++ DbName, + {ok, Code0, _, _} = test_request:put( + Url ++ "/doc/file.txt", [{"Content-Type", "text/plain"}], + "hello, couch!"), + ?assert(Code0 =:= 201), + {ok, Code, _, _} = test_request:get( + Url ++ "/doc?attachments=true", + [{"Origin", "http://example.com"}]), + Code + end)}. + +should_make_range_request_against_attachment({Host, DbName}) -> + {"COUCHDB-1689", + ?_assertEqual(206, + begin + Url = Host ++ "/" ++ DbName, + {ok, Code0, _, _} = test_request:put( + Url ++ "/doc/file.txt", + [{"Content-Type", "application/octet-stream"}], + "hello, couch!"), + ?assert(Code0 =:= 201), + {ok, Code, _, _} = test_request:get( + Url ++ "/doc/file.txt", [{"Origin", "http://example.com"}, + {"Range", "bytes=0-6"}]), + Code + end)}. + +should_make_request_with_if_none_match_header({Host, DbName}) -> + {"COUCHDB-1697", + ?_assertEqual(304, + begin + Url = Host ++ "/" ++ DbName, + {ok, Code0, Headers0, _} = test_request:put( + Url ++ "/doc", [{"Content-Type", "application/json"}], "{}"), + ?assert(Code0 =:= 201), + ETag = proplists:get_value("ETag", Headers0), + {ok, Code, _, _} = test_request:get( + Url ++ "/doc", [{"Origin", "http://example.com"}, + {"If-None-Match", ETag}]), + Code + end)}. + + +maybe_append_vhost(true) -> + [{"Host", "http://example.com"}]; +maybe_append_vhost(false) -> + []. http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_csp_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_csp_tests.erl b/test/couchdb_csp_tests.erl new file mode 100644 index 0000000..adb0e6d --- /dev/null +++ b/test/couchdb_csp_tests.erl @@ -0,0 +1,96 @@ +% 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(couchdb_csp_tests). + +-include("couch_eunit.hrl"). + +-define(TIMEOUT, 1000). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + Pid. + +stop(Pid) -> + couch_server_sup:stop(), + erlang:monitor(process, Pid), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, server_stop}) + end. + +setup() -> + ok = couch_config:set("csp", "enable", "true", false), + Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + lists:concat(["http://", Addr, ":", Port, "/_utils/"]). + +teardown(_) -> + ok. + + +csp_test_() -> + { + "Content Security Policy tests", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_not_return_any_csp_headers_when_disabled/1, + fun should_apply_default_policy/1, + fun should_return_custom_policy/1, + fun should_only_enable_csp_when_true/1 + ] + } + } + }. + + +should_not_return_any_csp_headers_when_disabled(Url) -> + ?_assertEqual(undefined, + begin + ok = couch_config:set("csp", "enable", "false", false), + {ok, _, Headers, _} = test_request:get(Url), + proplists:get_value("Content-Security-Policy", Headers) + end). + +should_apply_default_policy(Url) -> + ?_assertEqual( + "default-src 'self'; img-src 'self'; font-src 'self'; " + "script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';", + begin + {ok, _, Headers, _} = test_request:get(Url), + proplists:get_value("Content-Security-Policy", Headers) + end). + +should_return_custom_policy(Url) -> + ?_assertEqual("default-src 'http://example.com';", + begin + ok = couch_config:set("csp", "header_value", + "default-src 'http://example.com';", false), + {ok, _, Headers, _} = test_request:get(Url), + proplists:get_value("Content-Security-Policy", Headers) + end). + +should_only_enable_csp_when_true(Url) -> + ?_assertEqual(undefined, + begin + ok = couch_config:set("csp", "enable", "tru", false), + {ok, _, Headers, _} = test_request:get(Url), + proplists:get_value("Content-Security-Policy", Headers) + end). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_file_compression_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_file_compression_tests.erl b/test/couchdb_file_compression_tests.erl new file mode 100644 index 0000000..fd3f513 --- /dev/null +++ b/test/couchdb_file_compression_tests.erl @@ -0,0 +1,239 @@ +% 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(couchdb_file_compression_tests). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). +-define(DDOC_ID, <<"_design/test">>). +-define(DOCS_COUNT, 5000). +-define(TIMEOUT, 30000). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + Pid. + +stop(Pid) -> + erlang:monitor(process, Pid), + couch_server_sup:stop(), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, server_stop}) + end. + +setup() -> + couch_config:set("couchdb", "file_compression", "none", false), + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), + ok = populate_db(Db, ?DOCS_COUNT), + DDoc = couch_doc:from_json_obj({[ + {<<"_id">>, ?DDOC_ID}, + {<<"language">>, <<"javascript">>}, + {<<"views">>, {[ + {<<"by_id">>, {[ + {<<"map">>, <<"function(doc){emit(doc._id, doc.string);}">>} + ]}} + ]} + } + ]}), + {ok, _} = couch_db:update_doc(Db, DDoc, []), + refresh_index(DbName), + ok = couch_db:close(Db), + DbName. + +teardown(DbName) -> + ok = couch_server:delete(DbName, [?ADMIN_USER]), + ok. + + +couch_auth_cache_test_() -> + { + "CouchDB file compression tests", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_use_none/1, + fun should_use_deflate_1/1, + fun should_use_deflate_9/1, + fun should_use_snappy/1, + fun should_compare_compression_methods/1 + ] + } + } + }. + + +should_use_none(DbName) -> + couch_config:set("couchdb", "file_compression", "none", false), + { + "Use no compression", + [ + {"compact database", ?_test(compact_db(DbName))}, + {"compact view", ?_test(compact_view(DbName))} + ] + }. + +should_use_deflate_1(DbName) -> + couch_config:set("couchdb", "file_compression", "deflate_1", false), + { + "Use deflate compression at level 1", + [ + {"compact database", ?_test(compact_db(DbName))}, + {"compact view", ?_test(compact_view(DbName))} + ] + }. + +should_use_deflate_9(DbName) -> + couch_config:set("couchdb", "file_compression", "deflate_9", false), + { + "Use deflate compression at level 9", + [ + {"compact database", ?_test(compact_db(DbName))}, + {"compact view", ?_test(compact_view(DbName))} + ] + }. + +should_use_snappy(DbName) -> + couch_config:set("couchdb", "file_compression", "snappy", false), + { + "Use snappy compression", + [ + {"compact database", ?_test(compact_db(DbName))}, + {"compact view", ?_test(compact_view(DbName))} + ] + }. + +should_compare_compression_methods(DbName) -> + {"none > snappy > deflate_1 > deflate_9", + {timeout, ?TIMEOUT div 1000, ?_test(compare_compression_methods(DbName))}}. + +compare_compression_methods(DbName) -> + couch_config:set("couchdb", "file_compression", "none", false), + compact_db(DbName), + compact_view(DbName), + DbSizeNone = db_disk_size(DbName), + ViewSizeNone = view_disk_size(DbName), + + couch_config:set("couchdb", "file_compression", "snappy", false), + compact_db(DbName), + compact_view(DbName), + DbSizeSnappy = db_disk_size(DbName), + ViewSizeSnappy = view_disk_size(DbName), + + ?assert(DbSizeNone > DbSizeSnappy), + ?assert(ViewSizeNone > ViewSizeSnappy), + + couch_config:set("couchdb", "file_compression", "deflate_1", false), + compact_db(DbName), + compact_view(DbName), + DbSizeDeflate1 = db_disk_size(DbName), + ViewSizeDeflate1 = view_disk_size(DbName), + + ?assert(DbSizeSnappy > DbSizeDeflate1), + ?assert(ViewSizeSnappy > ViewSizeDeflate1), + + couch_config:set("couchdb", "file_compression", "deflate_9", false), + compact_db(DbName), + compact_view(DbName), + DbSizeDeflate9 = db_disk_size(DbName), + ViewSizeDeflate9 = view_disk_size(DbName), + + ?assert(DbSizeDeflate1 > DbSizeDeflate9), + ?assert(ViewSizeDeflate1 > ViewSizeDeflate9). + + +populate_db(_Db, NumDocs) when NumDocs =< 0 -> + ok; +populate_db(Db, NumDocs) -> + Docs = lists:map( + fun(_) -> + couch_doc:from_json_obj({[ + {<<"_id">>, couch_uuids:random()}, + {<<"string">>, ?l2b(lists:duplicate(1000, $X))} + ]}) + end, + lists:seq(1, 500)), + {ok, _} = couch_db:update_docs(Db, Docs, []), + populate_db(Db, NumDocs - 500). + +refresh_index(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, DDoc} = couch_db:open_doc(Db, ?DDOC_ID, [ejson_body]), + couch_mrview:query_view(Db, DDoc, <<"by_id">>, [{stale, false}]), + ok = couch_db:close(Db). + +compact_db(DbName) -> + DiskSizeBefore = db_disk_size(DbName), + {ok, Db} = couch_db:open_int(DbName, []), + {ok, CompactPid} = couch_db:start_compact(Db), + MonRef = erlang:monitor(process, CompactPid), + receive + {'DOWN', MonRef, process, CompactPid, normal} -> + ok; + {'DOWN', MonRef, process, CompactPid, Reason} -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "Error compacting database: " + ++ couch_util:to_list(Reason)}]}) + after ?TIMEOUT -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "Timeout waiting for database compaction"}]}) + end, + ok = couch_db:close(Db), + DiskSizeAfter = db_disk_size(DbName), + ?assert(DiskSizeBefore > DiskSizeAfter). + +compact_view(DbName) -> + DiskSizeBefore = view_disk_size(DbName), + {ok, MonRef} = couch_mrview:compact(DbName, ?DDOC_ID, [monitor]), + receive + {'DOWN', MonRef, process, _CompactPid, normal} -> + ok; + {'DOWN', MonRef, process, _CompactPid, Reason} -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "Error compacting view group: " + ++ couch_util:to_list(Reason)}]}) + after ?TIMEOUT -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "Timeout waiting for view group compaction"}]}) + end, + DiskSizeAfter = view_disk_size(DbName), + ?assert(DiskSizeBefore > DiskSizeAfter). + +db_disk_size(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, Info} = couch_db:get_db_info(Db), + ok = couch_db:close(Db), + couch_util:get_value(disk_size, Info). + +view_disk_size(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, DDoc} = couch_db:open_doc(Db, ?DDOC_ID, [ejson_body]), + {ok, Info} = couch_mrview:get_info(Db, DDoc), + ok = couch_db:close(Db), + couch_util:get_value(disk_size, Info). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_http_proxy_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_http_proxy_tests.erl b/test/couchdb_http_proxy_tests.erl new file mode 100644 index 0000000..acb1974 --- /dev/null +++ b/test/couchdb_http_proxy_tests.erl @@ -0,0 +1,462 @@ +% 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(couchdb_http_proxy_tests). + +-include("couch_eunit.hrl"). + +-record(req, {method=get, path="", headers=[], body="", opts=[]}). + +-define(CONFIG_FIXTURE_TEMP, + begin + FileName = filename:join([?TEMPDIR, ?tempfile() ++ ".ini"]), + {ok, Fd} = file:open(FileName, write), + ok = file:truncate(Fd), + ok = file:close(Fd), + FileName + end). +-define(TIMEOUT, 5000). + + +start() -> + % we have to write any config changes to temp ini file to not loose them + % when supervisor will kill all children due to reaching restart threshold + % (each httpd_global_handlers changes causes couch_httpd restart) + couch_server_sup:start_link(?CONFIG_CHAIN ++ [?CONFIG_FIXTURE_TEMP]), + % 49151 is IANA Reserved, let's assume no one is listening there + couch_config:set("httpd_global_handlers", "_error", + "{couch_httpd_proxy, handle_proxy_req, <<\"http://127.0.0.1:49151/\">>}" + ), + ok. + +stop(_) -> + couch_server_sup:stop(), + ok. + +setup() -> + {ok, Pid} = test_web:start_link(), + Value = lists:flatten(io_lib:format( + "{couch_httpd_proxy, handle_proxy_req, ~p}", + [list_to_binary(proxy_url())])), + couch_config:set("httpd_global_handlers", "_test", Value), + % let couch_httpd restart + timer:sleep(100), + Pid. + +teardown(Pid) -> + erlang:monitor(process, Pid), + test_web:stop(), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, test_web_stop}) + end. + + +http_proxy_test_() -> + { + "HTTP Proxy handler tests", + { + setup, + fun start/0, fun stop/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_proxy_basic_request/1, + fun should_return_alternative_status/1, + fun should_respect_trailing_slash/1, + fun should_proxy_headers/1, + fun should_proxy_host_header/1, + fun should_pass_headers_back/1, + fun should_use_same_protocol_version/1, + fun should_proxy_body/1, + fun should_proxy_body_back/1, + fun should_proxy_chunked_body/1, + fun should_proxy_chunked_body_back/1, + fun should_rewrite_location_header/1, + fun should_not_rewrite_external_locations/1, + fun should_rewrite_relative_location/1, + fun should_refuse_connection_to_backend/1 + ] + } + + } + }. + + +should_proxy_basic_request(_) -> + Remote = fun(Req) -> + 'GET' = Req:get(method), + "/" = Req:get(path), + 0 = Req:get(body_length), + <<>> = Req:recv_body(), + {ok, {200, [{"Content-Type", "text/plain"}], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + ?_test(check_request(#req{}, Remote, Local)). + +should_return_alternative_status(_) -> + Remote = fun(Req) -> + "/alternate_status" = Req:get(path), + {ok, {201, [], "ok"}} + end, + Local = fun + ({ok, "201", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{path = "/alternate_status"}, + ?_test(check_request(Req, Remote, Local)). + +should_respect_trailing_slash(_) -> + Remote = fun(Req) -> + "/trailing_slash/" = Req:get(path), + {ok, {200, [], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{path="/trailing_slash/"}, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_headers(_) -> + Remote = fun(Req) -> + "/passes_header" = Req:get(path), + "plankton" = Req:get_header_value("X-CouchDB-Ralph"), + {ok, {200, [], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + path="/passes_header", + headers=[{"X-CouchDB-Ralph", "plankton"}] + }, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_host_header(_) -> + Remote = fun(Req) -> + "/passes_host_header" = Req:get(path), + "www.google.com" = Req:get_header_value("Host"), + {ok, {200, [], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + path="/passes_host_header", + headers=[{"Host", "www.google.com"}] + }, + ?_test(check_request(Req, Remote, Local)). + +should_pass_headers_back(_) -> + Remote = fun(Req) -> + "/passes_header_back" = Req:get(path), + {ok, {200, [{"X-CouchDB-Plankton", "ralph"}], "ok"}} + end, + Local = fun + ({ok, "200", Headers, "ok"}) -> + lists:member({"X-CouchDB-Plankton", "ralph"}, Headers); + (_) -> + false + end, + Req = #req{path="/passes_header_back"}, + ?_test(check_request(Req, Remote, Local)). + +should_use_same_protocol_version(_) -> + Remote = fun(Req) -> + "/uses_same_version" = Req:get(path), + {1, 0} = Req:get(version), + {ok, {200, [], "ok"}} + end, + Local = fun + ({ok, "200", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + path="/uses_same_version", + opts=[{http_vsn, {1, 0}}] + }, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_body(_) -> + Remote = fun(Req) -> + 'PUT' = Req:get(method), + "/passes_body" = Req:get(path), + <<"Hooray!">> = Req:recv_body(), + {ok, {201, [], "ok"}} + end, + Local = fun + ({ok, "201", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + method=put, + path="/passes_body", + body="Hooray!" + }, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_body_back(_) -> + BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], + Remote = fun(Req) -> + 'GET' = Req:get(method), + "/passes_eof_body" = Req:get(path), + {raw, {200, [{"Connection", "close"}], BodyChunks}} + end, + Local = fun + ({ok, "200", _, "foobarbazinga"}) -> + true; + (_) -> + false + end, + Req = #req{path="/passes_eof_body"}, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_chunked_body(_) -> + BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], + Remote = fun(Req) -> + 'POST' = Req:get(method), + "/passes_chunked_body" = Req:get(path), + RecvBody = fun + ({Length, Chunk}, [Chunk | Rest]) -> + Length = size(Chunk), + Rest; + ({0, []}, []) -> + ok + end, + ok = Req:stream_body(1024 * 1024, RecvBody, BodyChunks), + {ok, {201, [], "ok"}} + end, + Local = fun + ({ok, "201", _, "ok"}) -> + true; + (_) -> + false + end, + Req = #req{ + method=post, + path="/passes_chunked_body", + headers=[{"Transfer-Encoding", "chunked"}], + body=chunked_body(BodyChunks) + }, + ?_test(check_request(Req, Remote, Local)). + +should_proxy_chunked_body_back(_) -> + ?_test(begin + Remote = fun(Req) -> + 'GET' = Req:get(method), + "/passes_chunked_body_back" = Req:get(path), + BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>], + {chunked, {200, [{"Transfer-Encoding", "chunked"}], BodyChunks}} + end, + Req = #req{ + path="/passes_chunked_body_back", + opts=[{stream_to, self()}] + }, + + Resp = check_request(Req, Remote, no_local), + ?assertMatch({ibrowse_req_id, _}, Resp), + {_, ReqId} = Resp, + + % Grab headers from response + receive + {ibrowse_async_headers, ReqId, "200", Headers} -> + ?assertEqual("chunked", + proplists:get_value("Transfer-Encoding", Headers)), + ibrowse:stream_next(ReqId) + after 1000 -> + throw({error, timeout}) + end, + + ?assertEqual(<<"foobarbazinga">>, recv_body(ReqId, [])), + ?assertEqual(was_ok, test_web:check_last()) + end). + +should_refuse_connection_to_backend(_) -> + Local = fun + ({ok, "500", _, _}) -> + true; + (_) -> + false + end, + Req = #req{opts=[{url, server_url("/_error")}]}, + ?_test(check_request(Req, no_remote, Local)). + +should_rewrite_location_header(_) -> + { + "Testing location header rewrites", + do_rewrite_tests([ + {"Location", proxy_url() ++ "/foo/bar", + server_url() ++ "/foo/bar"}, + {"Content-Location", proxy_url() ++ "/bing?q=2", + server_url() ++ "/bing?q=2"}, + {"Uri", proxy_url() ++ "/zip#frag", + server_url() ++ "/zip#frag"}, + {"Destination", proxy_url(), + server_url() ++ "/"} + ]) + }. + +should_not_rewrite_external_locations(_) -> + { + "Testing no rewrite of external locations", + do_rewrite_tests([ + {"Location", external_url() ++ "/search", + external_url() ++ "/search"}, + {"Content-Location", external_url() ++ "/s?q=2", + external_url() ++ "/s?q=2"}, + {"Uri", external_url() ++ "/f#f", + external_url() ++ "/f#f"}, + {"Destination", external_url() ++ "/f?q=2#f", + external_url() ++ "/f?q=2#f"} + ]) + }. + +should_rewrite_relative_location(_) -> + { + "Testing relative rewrites", + do_rewrite_tests([ + {"Location", "/foo", + server_url() ++ "/foo"}, + {"Content-Location", "bar", + server_url() ++ "/bar"}, + {"Uri", "/zing?q=3", + server_url() ++ "/zing?q=3"}, + {"Destination", "bing?q=stuff#yay", + server_url() ++ "/bing?q=stuff#yay"} + ]) + }. + + +do_rewrite_tests(Tests) -> + lists:map(fun({Header, Location, Url}) -> + should_rewrite_header(Header, Location, Url) + end, Tests). + +should_rewrite_header(Header, Location, Url) -> + Remote = fun(Req) -> + "/rewrite_test" = Req:get(path), + {ok, {302, [{Header, Location}], "ok"}} + end, + Local = fun + ({ok, "302", Headers, "ok"}) -> + ?assertEqual(Url, couch_util:get_value(Header, Headers)), + true; + (E) -> + ?debugFmt("~p", [E]), + false + end, + Req = #req{path="/rewrite_test"}, + {Header, ?_test(check_request(Req, Remote, Local))}. + + +server_url() -> + server_url("/_test"). + +server_url(Resource) -> + Addr = couch_config:get("httpd", "bind_address"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + lists:concat(["http://", Addr, ":", Port, Resource]). + +proxy_url() -> + "http://127.0.0.1:" ++ integer_to_list(test_web:get_port()). + +external_url() -> + "https://google.com". + +check_request(Req, Remote, Local) -> + case Remote of + no_remote -> + ok; + _ -> + test_web:set_assert(Remote) + end, + Url = case proplists:lookup(url, Req#req.opts) of + none -> + server_url() ++ Req#req.path; + {url, DestUrl} -> + DestUrl + end, + Opts = [{headers_as_is, true} | Req#req.opts], + Resp =ibrowse:send_req( + Url, Req#req.headers, Req#req.method, Req#req.body, Opts + ), + %?debugFmt("ibrowse response: ~p", [Resp]), + case Local of + no_local -> + ok; + _ -> + ?assert(Local(Resp)) + end, + case {Remote, Local} of + {no_remote, _} -> + ok; + {_, no_local} -> + ok; + _ -> + ?assertEqual(was_ok, test_web:check_last()) + end, + Resp. + +chunked_body(Chunks) -> + chunked_body(Chunks, []). + +chunked_body([], Acc) -> + iolist_to_binary(lists:reverse(Acc, "0\r\n\r\n")); +chunked_body([Chunk | Rest], Acc) -> + Size = to_hex(size(Chunk)), + chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]). + +to_hex(Val) -> + to_hex(Val, []). + +to_hex(0, Acc) -> + Acc; +to_hex(Val, Acc) -> + to_hex(Val div 16, [hex_char(Val rem 16) | Acc]). + +hex_char(V) when V < 10 -> $0 + V; +hex_char(V) -> $A + V - 10. + +recv_body(ReqId, Acc) -> + receive + {ibrowse_async_response, ReqId, Data} -> + recv_body(ReqId, [Data | Acc]); + {ibrowse_async_response_end, ReqId} -> + iolist_to_binary(lists:reverse(Acc)); + Else -> + throw({error, unexpected_mesg, Else}) + after ?TIMEOUT -> + throw({error, timeout}) + end. http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_os_daemons_tests.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_os_daemons_tests.erl b/test/couchdb_os_daemons_tests.erl new file mode 100644 index 0000000..aa949c9 --- /dev/null +++ b/test/couchdb_os_daemons_tests.erl @@ -0,0 +1,228 @@ +% 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(couchdb_os_daemons_tests). + +-include("couch_eunit.hrl"). + +%% keep in sync with couchdb/couch_os_daemons.erl +-record(daemon, { + port, + name, + cmd, + kill, + status=running, + cfg_patterns=[], + errors=[], + buf=[] +}). + +-define(DAEMON_CONFIGER, "os_daemon_configer.escript"). +-define(DAEMON_LOOPER, "os_daemon_looper.escript"). +-define(DAEMON_BAD_PERM, "os_daemon_bad_perm.sh"). +-define(DAEMON_CAN_REBOOT, "os_daemon_can_reboot.sh"). +-define(DAEMON_DIE_ON_BOOT, "os_daemon_die_on_boot.sh"). +-define(DAEMON_DIE_QUICKLY, "os_daemon_die_quickly.sh"). +-define(DELAY, 100). +-define(TIMEOUT, 1000). + + +setup(DName) -> + {ok, CfgPid} = couch_config:start_link(?CONFIG_CHAIN), + {ok, OsDPid} = couch_os_daemons:start_link(), + couch_config:set("os_daemons", DName, + filename:join([?FIXTURESDIR, DName]), false), + timer:sleep(?DELAY), % sleep a bit to let daemon set kill flag + {CfgPid, OsDPid}. + +teardown(_, {CfgPid, OsDPid}) -> + erlang:monitor(process, CfgPid), + couch_config:stop(), + receive + {'DOWN', _, _, CfgPid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, config_stop}) + end, + + erlang:monitor(process, OsDPid), + exit(OsDPid, normal), + receive + {'DOWN', _, _, OsDPid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, os_daemon_stop}) + end. + + +os_daemons_test_() -> + { + "OS Daemons tests", + { + foreachx, + fun setup/1, fun teardown/2, + [{?DAEMON_LOOPER, Fun} || Fun <- [ + fun should_check_daemon/2, + fun should_check_daemon_table_form/2, + fun should_clean_tables_on_daemon_remove/2, + fun should_spawn_multiple_daemons/2, + fun should_keep_alive_one_daemon_on_killing_other/2 + ]] + } + }. + +configuration_reader_test_() -> + { + "OS Daemon requests CouchDB configuration", + { + foreachx, + fun setup/1, fun teardown/2, + [{?DAEMON_CONFIGER, + fun should_read_write_config_settings_by_daemon/2}] + + } + }. + +error_test_() -> + { + "OS Daemon process error tests", + { + foreachx, + fun setup/1, fun teardown/2, + [{?DAEMON_BAD_PERM, fun should_fail_due_to_lack_of_permissions/2}, + {?DAEMON_DIE_ON_BOOT, fun should_die_on_boot/2}, + {?DAEMON_DIE_QUICKLY, fun should_die_quickly/2}, + {?DAEMON_CAN_REBOOT, fun should_not_being_halted/2}] + } + }. + + +should_check_daemon(DName, _) -> + ?_test(begin + {ok, [D]} = couch_os_daemons:info([table]), + check_daemon(D, DName) + end). + +should_check_daemon_table_form(DName, _) -> + ?_test(begin + {ok, Tab} = couch_os_daemons:info(), + [D] = ets:tab2list(Tab), + check_daemon(D, DName) + end). + +should_clean_tables_on_daemon_remove(DName, _) -> + ?_test(begin + couch_config:delete("os_daemons", DName, false), + {ok, Tab2} = couch_os_daemons:info(), + ?_assertEqual([], ets:tab2list(Tab2)) + end). + +should_spawn_multiple_daemons(DName, _) -> + ?_test(begin + couch_config:set("os_daemons", "bar", + filename:join([?FIXTURESDIR, DName]), false), + couch_config:set("os_daemons", "baz", + filename:join([?FIXTURESDIR, DName]), false), + timer:sleep(?DELAY), + {ok, Daemons} = couch_os_daemons:info([table]), + lists:foreach(fun(D) -> + check_daemon(D) + end, Daemons), + {ok, Tab} = couch_os_daemons:info(), + lists:foreach(fun(D) -> + check_daemon(D) + end, ets:tab2list(Tab)) + end). + +should_keep_alive_one_daemon_on_killing_other(DName, _) -> + ?_test(begin + couch_config:set("os_daemons", "bar", + filename:join([?FIXTURESDIR, DName]), false), + timer:sleep(?DELAY), + {ok, Daemons} = couch_os_daemons:info([table]), + lists:foreach(fun(D) -> + check_daemon(D) + end, Daemons), + + couch_config:delete("os_daemons", "bar", false), + timer:sleep(?DELAY), + {ok, [D2]} = couch_os_daemons:info([table]), + check_daemon(D2, DName), + + {ok, Tab} = couch_os_daemons:info(), + [T] = ets:tab2list(Tab), + check_daemon(T, DName) + end). + +should_read_write_config_settings_by_daemon(DName, _) -> + ?_test(begin + % have to wait till daemon run all his tests + % see daemon's script for more info + timer:sleep(?TIMEOUT), + {ok, [D]} = couch_os_daemons:info([table]), + check_daemon(D, DName) + end). + +should_fail_due_to_lack_of_permissions(DName, _) -> + ?_test(should_halts(DName, 1000)). + +should_die_on_boot(DName, _) -> + ?_test(should_halts(DName, 1000)). + +should_die_quickly(DName, _) -> + ?_test(should_halts(DName, 4000)). + +should_not_being_halted(DName, _) -> + ?_test(begin + timer:sleep(1000), + {ok, [D1]} = couch_os_daemons:info([table]), + check_daemon(D1, DName, 0), + + % Should reboot every two seconds. We're at 1s, so wait + % until 3s to be in the middle of the next invocation's + % life span. + + timer:sleep(2000), + {ok, [D2]} = couch_os_daemons:info([table]), + check_daemon(D2, DName, 1), + + % If the kill command changed, that means we rebooted the process. + ?assertNotEqual(D1#daemon.kill, D2#daemon.kill) + end). + +should_halts(DName, Time) -> + timer:sleep(Time), + {ok, [D]} = couch_os_daemons:info([table]), + check_dead(D, DName), + couch_config:delete("os_daemons", DName, false). + +check_daemon(D) -> + check_daemon(D, D#daemon.name). + +check_daemon(D, Name) -> + check_daemon(D, Name, 0). + +check_daemon(D, Name, Errs) -> + ?assert(is_port(D#daemon.port)), + ?assertEqual(Name, D#daemon.name), + ?assertNotEqual(undefined, D#daemon.kill), + ?assertEqual(running, D#daemon.status), + ?assertEqual(Errs, length(D#daemon.errors)), + ?assertEqual([], D#daemon.buf). + +check_dead(D, Name) -> + ?assert(is_port(D#daemon.port)), + ?assertEqual(Name, D#daemon.name), + ?assertNotEqual(undefined, D#daemon.kill), + ?assertEqual(halted, D#daemon.status), + ?assertEqual(nil, D#daemon.errors), + ?assertEqual(nil, D#daemon.buf). http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/68453039/test/couchdb_os_proc_pool.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_os_proc_pool.erl b/test/couchdb_os_proc_pool.erl new file mode 100644 index 0000000..1bb266e --- /dev/null +++ b/test/couchdb_os_proc_pool.erl @@ -0,0 +1,179 @@ +% 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(couchdb_os_proc_pool). + +-include("couch_eunit.hrl"). +-include_lib("couchdb/couch_db.hrl"). + +-define(TIMEOUT, 3000). + + +start() -> + {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), + couch_config:set("query_server_config", "os_process_limit", "3", false), + Pid. + +stop(Pid) -> + couch_server_sup:stop(), + erlang:monitor(process, Pid), + receive + {'DOWN', _, _, Pid, _} -> + ok + after ?TIMEOUT -> + throw({timeout, server_stop}) + end. + + +os_proc_pool_test_() -> + { + "OS processes pool tests", + { + setup, + fun start/0, fun stop/1, + [ + should_block_new_proc_on_full_pool(), + should_free_slot_on_proc_unexpected_exit() + ] + } + }. + + +should_block_new_proc_on_full_pool() -> + ?_test(begin + Client1 = spawn_client(), + Client2 = spawn_client(), + Client3 = spawn_client(), + + ?assertEqual(ok, ping_client(Client1)), + ?assertEqual(ok, ping_client(Client2)), + ?assertEqual(ok, ping_client(Client3)), + + Proc1 = get_client_proc(Client1, "1"), + Proc2 = get_client_proc(Client2, "2"), + Proc3 = get_client_proc(Client3, "3"), + + ?assertNotEqual(Proc1, Proc2), + ?assertNotEqual(Proc2, Proc3), + ?assertNotEqual(Proc3, Proc1), + + Client4 = spawn_client(), + ?assertEqual(timeout, ping_client(Client4)), + + ?assertEqual(ok, stop_client(Client1)), + ?assertEqual(ok, ping_client(Client4)), + + Proc4 = get_client_proc(Client4, "4"), + ?assertEqual(Proc1, Proc4), + + lists:map(fun(C) -> + ?assertEqual(ok, stop_client(C)) + end, [Client2, Client3, Client4]) + end). + +should_free_slot_on_proc_unexpected_exit() -> + ?_test(begin + Client1 = spawn_client(), + Client2 = spawn_client(), + Client3 = spawn_client(), + + ?assertEqual(ok, ping_client(Client1)), + ?assertEqual(ok, ping_client(Client2)), + ?assertEqual(ok, ping_client(Client3)), + + Proc1 = get_client_proc(Client1, "1"), + Proc2 = get_client_proc(Client2, "2"), + Proc3 = get_client_proc(Client3, "3"), + + ?assertNotEqual(Proc1, Proc2), + ?assertNotEqual(Proc2, Proc3), + ?assertNotEqual(Proc3, Proc1), + + ?assertEqual(ok, kill_client(Client1)), + + Client4 = spawn_client(), + ?assertEqual(ok, ping_client(Client4)), + + Proc4 = get_client_proc(Client4, "4"), + ?assertNotEqual(Proc4, Proc1), + ?assertNotEqual(Proc2, Proc4), + ?assertNotEqual(Proc3, Proc4), + + lists:map(fun(C) -> + ?assertEqual(ok, stop_client(C)) + end, [Client2, Client3, Client4]) + end). + + +spawn_client() -> + Parent = self(), + Ref = make_ref(), + Pid = spawn(fun() -> + Proc = couch_query_servers:get_os_process(<<"javascript">>), + loop(Parent, Ref, Proc) + end), + {Pid, Ref}. + +ping_client({Pid, Ref}) -> + Pid ! ping, + receive + {pong, Ref} -> + ok + after ?TIMEOUT -> + timeout + end. + +get_client_proc({Pid, Ref}, ClientName) -> + Pid ! get_proc, + receive + {proc, Ref, Proc} -> Proc + after ?TIMEOUT -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, ?LINE}, + {reason, "Timeout getting client " + ++ ClientName ++ " proc"}]}) + end. + +stop_client({Pid, Ref}) -> + Pid ! stop, + receive + {stop, Ref} -> + ok + after ?TIMEOUT -> + timeout + end. + +kill_client({Pid, Ref}) -> + Pid ! die, + receive + {die, Ref} -> + ok + after ?TIMEOUT -> + timeout + end. + +loop(Parent, Ref, Proc) -> + receive + ping -> + Parent ! {pong, Ref}, + loop(Parent, Ref, Proc); + get_proc -> + Parent ! {proc, Ref, Proc}, + loop(Parent, Ref, Proc); + stop -> + couch_query_servers:ret_os_process(Proc), + Parent ! {stop, Ref}; + die -> + Parent ! {die, Ref}, + exit(some_error) + end.
