You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ch...@apache.org on 2014/08/26 22:51:43 UTC
[23/42] Move files out of test/couchdb into top level test/ folder
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/e40ca37c/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/e40ca37c/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/e40ca37c/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/e40ca37c/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/e40ca37c/test/couchdb_modules_load_tests.erl
----------------------------------------------------------------------
diff --git a/test/couchdb_modules_load_tests.erl b/test/couchdb_modules_load_tests.erl
new file mode 100644
index 0000000..4eaa42b
--- /dev/null
+++ b/test/couchdb_modules_load_tests.erl
@@ -0,0 +1,68 @@
+% 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_modules_load_tests).
+
+-include("couch_eunit.hrl").
+
+
+modules_load_test_() ->
+ {
+ "Verify that all modules loads",
+ should_load_modules()
+ }.
+
+
+should_load_modules() ->
+ Modules = [
+ couch_auth_cache,
+ couch_btree,
+ couch_changes,
+ couch_compress,
+ couch_config,
+ couch_config_writer,
+ couch_db,
+ couch_db_update_notifier,
+ couch_db_update_notifier_sup,
+ couch_db_updater,
+ couch_doc,
+ % Fails unless couch_config gen_server is started.
+ % couch_ejson_compare,
+ couch_event_sup,
+ couch_external_manager,
+ couch_external_server,
+ couch_file,
+ couch_httpd,
+ couch_httpd_db,
+ couch_httpd_external,
+ couch_httpd_misc_handlers,
+ couch_httpd_rewrite,
+ couch_httpd_stats_handlers,
+ couch_key_tree,
+ couch_log,
+ couch_os_process,
+ couch_query_servers,
+ couch_ref_counter,
+ couch_server,
+ couch_server_sup,
+ couch_stats_aggregator,
+ couch_stats_collector,
+ couch_stream,
+ couch_task_status,
+ couch_util,
+ couch_work_queue,
+ json_stream_parse
+ ],
+ [should_load_module(Mod) || Mod <- Modules].
+
+should_load_module(Mod) ->
+ {atom_to_list(Mod), ?_assertMatch({module, _}, code:load_file(Mod))}.
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/e40ca37c/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/e40ca37c/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.
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/e40ca37c/test/couchdb_update_conflicts_tests.erl
----------------------------------------------------------------------
diff --git a/test/couchdb_update_conflicts_tests.erl b/test/couchdb_update_conflicts_tests.erl
new file mode 100644
index 0000000..7226860
--- /dev/null
+++ b/test/couchdb_update_conflicts_tests.erl
@@ -0,0 +1,243 @@
+% 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_update_conflicts_tests).
+
+-include("couch_eunit.hrl").
+-include_lib("couchdb/couch_db.hrl").
+
+-define(i2l(I), integer_to_list(I)).
+-define(ADMIN_USER, {userctx, #user_ctx{roles=[<<"_admin">>]}}).
+-define(DOC_ID, <<"foobar">>).
+-define(NUM_CLIENTS, [100, 500, 1000, 2000, 5000, 10000]).
+-define(TIMEOUT, 10000).
+
+
+start() ->
+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN),
+ couch_config:set("couchdb", "delayed_commits", "true", 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, overwrite]),
+ Doc = couch_doc:from_json_obj({[{<<"_id">>, ?DOC_ID},
+ {<<"value">>, 0}]}),
+ {ok, Rev} = couch_db:update_doc(Db, Doc, []),
+ ok = couch_db:close(Db),
+ RevStr = couch_doc:rev_to_str(Rev),
+ {DbName, RevStr}.
+setup(_) ->
+ setup().
+
+teardown({DbName, _}) ->
+ ok = couch_server:delete(DbName, []),
+ ok.
+teardown(_, {DbName, _RevStr}) ->
+ teardown({DbName, _RevStr}).
+
+
+view_indexes_cleanup_test_() ->
+ {
+ "Update conflicts",
+ {
+ setup,
+ fun start/0, fun stop/1,
+ [
+ concurrent_updates(),
+ couchdb_188()
+ ]
+ }
+ }.
+
+concurrent_updates()->
+ {
+ "Concurrent updates",
+ {
+ foreachx,
+ fun setup/1, fun teardown/2,
+ [{NumClients, fun should_concurrently_update_doc/2}
+ || NumClients <- ?NUM_CLIENTS]
+ }
+ }.
+
+couchdb_188()->
+ {
+ "COUCHDB-188",
+ {
+ foreach,
+ fun setup/0, fun teardown/1,
+ [fun should_bulk_create_delete_doc/1]
+ }
+ }.
+
+
+should_concurrently_update_doc(NumClients, {DbName, InitRev})->
+ {?i2l(NumClients) ++ " clients",
+ {inorder,
+ [{"update doc",
+ {timeout, ?TIMEOUT div 1000,
+ ?_test(concurrent_doc_update(NumClients, DbName, InitRev))}},
+ {"ensure in single leaf",
+ ?_test(ensure_in_single_revision_leaf(DbName))}]}}.
+
+should_bulk_create_delete_doc({DbName, InitRev})->
+ ?_test(bulk_delete_create(DbName, InitRev)).
+
+
+concurrent_doc_update(NumClients, DbName, InitRev) ->
+ Clients = lists:map(
+ fun(Value) ->
+ ClientDoc = couch_doc:from_json_obj({[
+ {<<"_id">>, ?DOC_ID},
+ {<<"_rev">>, InitRev},
+ {<<"value">>, Value}
+ ]}),
+ Pid = spawn_client(DbName, ClientDoc),
+ {Value, Pid, erlang:monitor(process, Pid)}
+ end,
+ lists:seq(1, NumClients)),
+
+ lists:foreach(fun({_, Pid, _}) -> Pid ! go end, Clients),
+
+ {NumConflicts, SavedValue} = lists:foldl(
+ fun({Value, Pid, MonRef}, {AccConflicts, AccValue}) ->
+ receive
+ {'DOWN', MonRef, process, Pid, {ok, _NewRev}} ->
+ {AccConflicts, Value};
+ {'DOWN', MonRef, process, Pid, conflict} ->
+ {AccConflicts + 1, AccValue};
+ {'DOWN', MonRef, process, Pid, Error} ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, "Client " ++ ?i2l(Value)
+ ++ " got update error: "
+ ++ couch_util:to_list(Error)}]})
+ after ?TIMEOUT div 2 ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, "Timeout waiting for client "
+ ++ ?i2l(Value) ++ " to die"}]})
+ end
+ end, {0, nil}, Clients),
+ ?assertEqual(NumClients - 1, NumConflicts),
+
+ {ok, Db} = couch_db:open_int(DbName, []),
+ {ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []),
+ ok = couch_db:close(Db),
+ ?assertEqual(1, length(Leaves)),
+
+ [{ok, Doc2}] = Leaves,
+ {JsonDoc} = couch_doc:to_json_obj(Doc2, []),
+ ?assertEqual(SavedValue, couch_util:get_value(<<"value">>, JsonDoc)).
+
+ensure_in_single_revision_leaf(DbName) ->
+ {ok, Db} = couch_db:open_int(DbName, []),
+ {ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []),
+ ok = couch_db:close(Db),
+ [{ok, Doc}] = Leaves,
+
+ %% FIXME: server restart won't work from test side
+ %% stop(ok),
+ %% start(),
+
+ {ok, Db2} = couch_db:open_int(DbName, []),
+ {ok, Leaves2} = couch_db:open_doc_revs(Db2, ?DOC_ID, all, []),
+ ok = couch_db:close(Db2),
+ ?assertEqual(1, length(Leaves2)),
+
+ [{ok, Doc2}] = Leaves,
+ ?assertEqual(Doc, Doc2).
+
+bulk_delete_create(DbName, InitRev) ->
+ {ok, Db} = couch_db:open_int(DbName, []),
+
+ DeletedDoc = couch_doc:from_json_obj({[
+ {<<"_id">>, ?DOC_ID},
+ {<<"_rev">>, InitRev},
+ {<<"_deleted">>, true}
+ ]}),
+ NewDoc = couch_doc:from_json_obj({[
+ {<<"_id">>, ?DOC_ID},
+ {<<"value">>, 666}
+ ]}),
+
+ {ok, Results} = couch_db:update_docs(Db, [DeletedDoc, NewDoc], []),
+ ok = couch_db:close(Db),
+
+ ?assertEqual(2, length([ok || {ok, _} <- Results])),
+ [{ok, Rev1}, {ok, Rev2}] = Results,
+
+ {ok, Db2} = couch_db:open_int(DbName, []),
+ {ok, [{ok, Doc1}]} = couch_db:open_doc_revs(
+ Db2, ?DOC_ID, [Rev1], [conflicts, deleted_conflicts]),
+ {ok, [{ok, Doc2}]} = couch_db:open_doc_revs(
+ Db2, ?DOC_ID, [Rev2], [conflicts, deleted_conflicts]),
+ ok = couch_db:close(Db2),
+
+ {Doc1Props} = couch_doc:to_json_obj(Doc1, []),
+ {Doc2Props} = couch_doc:to_json_obj(Doc2, []),
+
+ %% Document was deleted
+ ?assert(couch_util:get_value(<<"_deleted">>, Doc1Props)),
+ %% New document not flagged as deleted
+ ?assertEqual(undefined, couch_util:get_value(<<"_deleted">>,
+ Doc2Props)),
+ %% New leaf revision has the right value
+ ?assertEqual(666, couch_util:get_value(<<"value">>,
+ Doc2Props)),
+ %% Deleted document has no conflicts
+ ?assertEqual(undefined, couch_util:get_value(<<"_conflicts">>,
+ Doc1Props)),
+ %% Deleted document has no deleted conflicts
+ ?assertEqual(undefined, couch_util:get_value(<<"_deleted_conflicts">>,
+ Doc1Props)),
+ %% New leaf revision doesn't have conflicts
+ ?assertEqual(undefined, couch_util:get_value(<<"_conflicts">>,
+ Doc1Props)),
+ %% New leaf revision doesn't have deleted conflicts
+ ?assertEqual(undefined, couch_util:get_value(<<"_deleted_conflicts">>,
+ Doc1Props)),
+
+ %% Deleted revision has position 2
+ ?assertEqual(2, element(1, Rev1)),
+ %% New leaf revision has position 1
+ ?assertEqual(1, element(1, Rev2)).
+
+
+spawn_client(DbName, Doc) ->
+ spawn(fun() ->
+ {ok, Db} = couch_db:open_int(DbName, []),
+ receive
+ go -> ok
+ end,
+ erlang:yield(),
+ Result = try
+ couch_db:update_doc(Db, Doc, [])
+ catch _:Error ->
+ Error
+ end,
+ ok = couch_db:close(Db),
+ exit(Result)
+ end).
http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/e40ca37c/test/couchdb_vhosts_tests.erl
----------------------------------------------------------------------
diff --git a/test/couchdb_vhosts_tests.erl b/test/couchdb_vhosts_tests.erl
new file mode 100644
index 0000000..94b1957
--- /dev/null
+++ b/test/couchdb_vhosts_tests.erl
@@ -0,0 +1,441 @@
+% 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_vhosts_tests).
+
+-include("couch_eunit.hrl").
+-include_lib("couchdb/couch_db.hrl").
+
+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}).
+-define(TIMEOUT, 1000).
+-define(iofmt(S, A), lists:flatten(io_lib:format(S, A))).
+
+
+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() ->
+ DbName = ?tempdb(),
+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]),
+ Doc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc1">>},
+ {<<"value">>, 666}
+ ]}),
+
+ Doc1 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/doc1">>},
+ {<<"shows">>, {[
+ {<<"test">>, <<"function(doc, req) {
+ return { json: {
+ requested_path: '/' + req.requested_path.join('/'),
+ path: '/' + req.path.join('/')}};}">>}
+ ]}},
+ {<<"rewrites">>, [
+ {[
+ {<<"from">>, <<"/">>},
+ {<<"to">>, <<"_show/test">>}
+ ]}
+ ]}
+ ]}),
+ {ok, _} = couch_db:update_docs(Db, [Doc, Doc1]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db),
+
+ Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"),
+ Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)),
+ Url = "http://" ++ Addr ++ ":" ++ Port,
+ {Url, ?b2l(DbName)}.
+
+setup_oauth() ->
+ DbName = ?tempdb(),
+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]),
+
+ couch_config:set("couch_httpd_auth", "authentication_db",
+ ?b2l(?tempdb()), false),
+ couch_config:set("oauth_token_users", "otoksec1", "joe", false),
+ couch_config:set("oauth_consumer_secrets", "consec1", "foo", false),
+ couch_config:set("oauth_token_secrets", "otoksec1", "foobar", false),
+ couch_config:set("couch_httpd_auth", "require_valid_user", "true", false),
+
+ ok = couch_config:set(
+ "vhosts", "oauth-example.com",
+ "/" ++ ?b2l(DbName) ++ "/_design/test/_rewrite/foobar", false),
+
+ DDoc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/test">>},
+ {<<"language">>, <<"javascript">>},
+ {<<"rewrites">>, [
+ {[
+ {<<"from">>, <<"foobar">>},
+ {<<"to">>, <<"_info">>}
+ ]}
+ ]}
+ ]}),
+ {ok, _} = couch_db:update_doc(Db, DDoc, []),
+
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db),
+
+ Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"),
+ Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)),
+ Url = "http://" ++ Addr ++ ":" ++ Port,
+ {Url, ?b2l(DbName)}.
+
+teardown({_, DbName}) ->
+ ok = couch_server:delete(?l2b(DbName), []),
+ ok.
+
+
+vhosts_test_() ->
+ {
+ "Virtual Hosts rewrite tests",
+ {
+ setup,
+ fun start/0, fun stop/1,
+ {
+ foreach,
+ fun setup/0, fun teardown/1,
+ [
+ fun should_return_database_info/1,
+ fun should_return_revs_info/1,
+ fun should_serve_utils_for_vhost/1,
+ fun should_return_virtual_request_path_field_in_request/1,
+ fun should_return_real_request_path_field_in_request/1,
+ fun should_match_wildcard_vhost/1,
+ fun should_return_db_info_for_wildcard_vhost_for_custom_db/1,
+ fun should_replace_rewrite_variables_for_db_and_doc/1,
+ fun should_return_db_info_for_vhost_with_resource/1,
+ fun should_return_revs_info_for_vhost_with_resource/1,
+ fun should_return_db_info_for_vhost_with_wildcard_resource/1,
+ fun should_return_path_for_vhost_with_wildcard_host/1
+ ]
+ }
+ }
+ }.
+
+oauth_test_() ->
+ {
+ "Virtual Hosts OAuth tests",
+ {
+ setup,
+ fun start/0, fun stop/1,
+ {
+ foreach,
+ fun setup_oauth/0, fun teardown/1,
+ [
+ fun should_require_auth/1,
+ fun should_succeed_oauth/1,
+ fun should_fail_oauth_with_wrong_credentials/1
+ ]
+ }
+ }
+ }.
+
+
+should_return_database_info({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false),
+ case test_request:get(Url, [], [{host_header, "example.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = ejson:decode(Body),
+ ?assert(proplists:is_defined(<<"db_name">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_return_revs_info({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false),
+ case test_request:get(Url ++ "/doc1?revs_info=true", [],
+ [{host_header, "example.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = ejson:decode(Body),
+ ?assert(proplists:is_defined(<<"_revs_info">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_serve_utils_for_vhost({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false),
+ case test_request:get(Url ++ "/_utils/index.html", [],
+ [{host_header, "example.com"}]) of
+ {ok, _, _, Body} ->
+ ?assertMatch(<<"<!DOCTYPE html>", _/binary>>, Body);
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_return_virtual_request_path_field_in_request({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", "example1.com",
+ "/" ++ DbName ++ "/_design/doc1/_rewrite/",
+ false),
+ case test_request:get(Url, [], [{host_header, "example1.com"}]) of
+ {ok, _, _, Body} ->
+ {Json} = ejson:decode(Body),
+ ?assertEqual(<<"/">>,
+ proplists:get_value(<<"requested_path">>, Json));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_return_real_request_path_field_in_request({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", "example1.com",
+ "/" ++ DbName ++ "/_design/doc1/_rewrite/",
+ false),
+ case test_request:get(Url, [], [{host_header, "example1.com"}]) of
+ {ok, _, _, Body} ->
+ {Json} = ejson:decode(Body),
+ Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"),
+ ?assertEqual(Path, proplists:get_value(<<"path">>, Json));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_match_wildcard_vhost({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", "*.example.com",
+ "/" ++ DbName ++ "/_design/doc1/_rewrite", false),
+ case test_request:get(Url, [], [{host_header, "test.example.com"}]) of
+ {ok, _, _, Body} ->
+ {Json} = ejson:decode(Body),
+ Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"),
+ ?assertEqual(Path, proplists:get_value(<<"path">>, Json));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_return_db_info_for_wildcard_vhost_for_custom_db({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", ":dbname.example1.com",
+ "/:dbname", false),
+ Host = DbName ++ ".example1.com",
+ case test_request:get(Url, [], [{host_header, Host}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = ejson:decode(Body),
+ ?assert(proplists:is_defined(<<"db_name">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_replace_rewrite_variables_for_db_and_doc({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts",":appname.:dbname.example1.com",
+ "/:dbname/_design/:appname/_rewrite/", false),
+ Host = "doc1." ++ DbName ++ ".example1.com",
+ case test_request:get(Url, [], [{host_header, Host}]) of
+ {ok, _, _, Body} ->
+ {Json} = ejson:decode(Body),
+ Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"),
+ ?assertEqual(Path, proplists:get_value(<<"path">>, Json));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_return_db_info_for_vhost_with_resource({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts",
+ "example.com/test", "/" ++ DbName, false),
+ ReqUrl = Url ++ "/test",
+ case test_request:get(ReqUrl, [], [{host_header, "example.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = ejson:decode(Body),
+ ?assert(proplists:is_defined(<<"db_name">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+
+should_return_revs_info_for_vhost_with_resource({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts",
+ "example.com/test", "/" ++ DbName, false),
+ ReqUrl = Url ++ "/test/doc1?revs_info=true",
+ case test_request:get(ReqUrl, [], [{host_header, "example.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = ejson:decode(Body),
+ ?assert(proplists:is_defined(<<"_revs_info">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_return_db_info_for_vhost_with_wildcard_resource({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", "*.example2.com/test", "/*", false),
+ ReqUrl = Url ++ "/test",
+ Host = DbName ++ ".example2.com",
+ case test_request:get(ReqUrl, [], [{host_header, Host}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = ejson:decode(Body),
+ ?assert(proplists:is_defined(<<"db_name">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_return_path_for_vhost_with_wildcard_host({Url, DbName}) ->
+ ?_test(begin
+ ok = couch_config:set("vhosts", "*/test1",
+ "/" ++ DbName ++ "/_design/doc1/_show/test",
+ false),
+ case test_request:get(Url ++ "/test1") of
+ {ok, _, _, Body} ->
+ {Json} = ejson:decode(Body),
+ Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"),
+ ?assertEqual(Path, proplists:get_value(<<"path">>, Json));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_require_auth({Url, _}) ->
+ ?_test(begin
+ case test_request:get(Url, [], [{host_header, "oauth-example.com"}]) of
+ {ok, Code, _, Body} ->
+ ?assertEqual(401, Code),
+ {JsonBody} = ejson:decode(Body),
+ ?assertEqual(<<"unauthorized">>,
+ couch_util:get_value(<<"error">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_succeed_oauth({Url, _}) ->
+ ?_test(begin
+ AuthDbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+ JoeDoc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"org.couchdb.user:joe">>},
+ {<<"type">>, <<"user">>},
+ {<<"name">>, <<"joe">>},
+ {<<"roles">>, []},
+ {<<"password_sha">>, <<"fe95df1ca59a9b567bdca5cbaf8412abd6e06121">>},
+ {<<"salt">>, <<"4e170ffeb6f34daecfd814dfb4001a73">>}
+ ]}),
+ {ok, AuthDb} = couch_db:open_int(?l2b(AuthDbName), [?ADMIN_USER]),
+ {ok, _} = couch_db:update_doc(AuthDb, JoeDoc, [?ADMIN_USER]),
+
+ Host = "oauth-example.com",
+ Consumer = {"consec1", "foo", hmac_sha1},
+ SignedParams = oauth:sign(
+ "GET", "http://" ++ Host ++ "/", [], Consumer, "otoksec1", "foobar"),
+ OAuthUrl = oauth:uri(Url, SignedParams),
+
+ case test_request:get(OAuthUrl, [], [{host_header, Host}]) of
+ {ok, Code, _, Body} ->
+ ?assertEqual(200, Code),
+ {JsonBody} = ejson:decode(Body),
+ ?assertEqual(<<"test">>,
+ couch_util:get_value(<<"name">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).
+
+should_fail_oauth_with_wrong_credentials({Url, _}) ->
+ ?_test(begin
+ AuthDbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+ JoeDoc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"org.couchdb.user:joe">>},
+ {<<"type">>, <<"user">>},
+ {<<"name">>, <<"joe">>},
+ {<<"roles">>, []},
+ {<<"password_sha">>, <<"fe95df1ca59a9b567bdca5cbaf8412abd6e06121">>},
+ {<<"salt">>, <<"4e170ffeb6f34daecfd814dfb4001a73">>}
+ ]}),
+ {ok, AuthDb} = couch_db:open_int(?l2b(AuthDbName), [?ADMIN_USER]),
+ {ok, _} = couch_db:update_doc(AuthDb, JoeDoc, [?ADMIN_USER]),
+
+ Host = "oauth-example.com",
+ Consumer = {"consec1", "bad_secret", hmac_sha1},
+ SignedParams = oauth:sign(
+ "GET", "http://" ++ Host ++ "/", [], Consumer, "otoksec1", "foobar"),
+ OAuthUrl = oauth:uri(Url, SignedParams),
+
+ case test_request:get(OAuthUrl, [], [{host_header, Host}]) of
+ {ok, Code, _, Body} ->
+ ?assertEqual(401, Code),
+ {JsonBody} = ejson:decode(Body),
+ ?assertEqual(<<"unauthorized">>,
+ couch_util:get_value(<<"error">>, JsonBody));
+ Else ->
+ erlang:error({assertion_failed,
+ [{module, ?MODULE},
+ {line, ?LINE},
+ {reason, ?iofmt("Request failed: ~p", [Else])}]})
+ end
+ end).