summaryrefslogtreecommitdiff
path: root/src/couch_replicator/test/eunit/couch_replicator_filtered_tests.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/couch_replicator/test/eunit/couch_replicator_filtered_tests.erl')
-rw-r--r--src/couch_replicator/test/eunit/couch_replicator_filtered_tests.erl240
1 files changed, 240 insertions, 0 deletions
diff --git a/src/couch_replicator/test/eunit/couch_replicator_filtered_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_filtered_tests.erl
new file mode 100644
index 000000000..70b25a31b
--- /dev/null
+++ b/src/couch_replicator/test/eunit/couch_replicator_filtered_tests.erl
@@ -0,0 +1,240 @@
+% 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(couch_replicator_filtered_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch_replicator/src/couch_replicator.hrl").
+
+-define(DDOC, {[
+ {<<"_id">>, <<"_design/filter_ddoc">>},
+ {<<"filters">>, {[
+ {<<"testfilter">>, <<"
+ function(doc, req){if (doc.class == 'mammal') return true;}
+ ">>},
+ {<<"queryfilter">>, <<"
+ function(doc, req) {
+ if (doc.class && req.query.starts) {
+ return doc.class.indexOf(req.query.starts) === 0;
+ }
+ else {
+ return false;
+ }
+ }
+ ">>}
+ ]}},
+ {<<"views">>, {[
+ {<<"mammals">>, {[
+ {<<"map">>, <<"
+ function(doc) {
+ if (doc.class == 'mammal') {
+ emit(doc._id, null);
+ }
+ }
+ ">>}
+ ]}}
+ ]}}
+]}).
+
+setup(_) ->
+ Ctx = test_util:start_couch([couch_replicator]),
+ Source = create_db(),
+ create_docs(Source),
+ Target = create_db(),
+ {Ctx, {Source, Target}}.
+
+teardown(_, {Ctx, {Source, Target}}) ->
+ delete_db(Source),
+ delete_db(Target),
+ ok = application:stop(couch_replicator),
+ ok = test_util:stop_couch(Ctx).
+
+filtered_replication_test_() ->
+ Pairs = [{remote, remote}],
+ {
+ "Filtered replication tests",
+ {
+ foreachx,
+ fun setup/1, fun teardown/2,
+ [{Pair, fun should_succeed/2} || Pair <- Pairs]
+ }
+ }.
+
+query_filtered_replication_test_() ->
+ Pairs = [{remote, remote}],
+ {
+ "Filtered with query replication tests",
+ {
+ foreachx,
+ fun setup/1, fun teardown/2,
+ [{Pair, fun should_succeed_with_query/2} || Pair <- Pairs]
+ }
+ }.
+
+view_filtered_replication_test_() ->
+ Pairs = [{remote, remote}],
+ {
+ "Filtered with a view replication tests",
+ {
+ foreachx,
+ fun setup/1, fun teardown/2,
+ [{Pair, fun should_succeed_with_view/2} || Pair <- Pairs]
+ }
+ }.
+
+should_succeed({From, To}, {_Ctx, {Source, Target}}) ->
+ RepObject = {[
+ {<<"source">>, db_url(From, Source)},
+ {<<"target">>, db_url(To, Target)},
+ {<<"filter">>, <<"filter_ddoc/testfilter">>}
+ ]},
+ {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER),
+ %% FilteredFun is an Erlang version of following JS function
+ %% function(doc, req){if (doc.class == 'mammal') return true;}
+ FilterFun = fun(_DocId, {Props}) ->
+ couch_util:get_value(<<"class">>, Props) == <<"mammal">>
+ end,
+ {ok, TargetDbInfo, AllReplies} = compare_dbs(Source, Target, FilterFun),
+ {lists:flatten(io_lib:format("~p -> ~p", [From, To])), [
+ {"Target DB has proper number of docs",
+ ?_assertEqual(1, proplists:get_value(doc_count, TargetDbInfo))},
+ {"Target DB doesn't have deleted docs",
+ ?_assertEqual(0, proplists:get_value(doc_del_count, TargetDbInfo))},
+ {"All the docs filtered as expected",
+ ?_assert(lists:all(fun(Valid) -> Valid end, AllReplies))}
+ ]}.
+
+should_succeed_with_query({From, To}, {_Ctx, {Source, Target}}) ->
+ RepObject = {[
+ {<<"source">>, db_url(From, Source)},
+ {<<"target">>, db_url(To, Target)},
+ {<<"filter">>, <<"filter_ddoc/queryfilter">>},
+ {<<"query_params">>, {[
+ {<<"starts">>, <<"a">>}
+ ]}}
+ ]},
+ {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER),
+ FilterFun = fun(_DocId, {Props}) ->
+ case couch_util:get_value(<<"class">>, Props) of
+ <<"a", _/binary>> -> true;
+ _ -> false
+ end
+ end,
+ {ok, TargetDbInfo, AllReplies} = compare_dbs(Source, Target, FilterFun),
+ {lists:flatten(io_lib:format("~p -> ~p", [From, To])), [
+ {"Target DB has proper number of docs",
+ ?_assertEqual(2, proplists:get_value(doc_count, TargetDbInfo))},
+ {"Target DB doesn't have deleted docs",
+ ?_assertEqual(0, proplists:get_value(doc_del_count, TargetDbInfo))},
+ {"All the docs filtered as expected",
+ ?_assert(lists:all(fun(Valid) -> Valid end, AllReplies))}
+ ]}.
+
+should_succeed_with_view({From, To}, {_Ctx, {Source, Target}}) ->
+ RepObject = {[
+ {<<"source">>, db_url(From, Source)},
+ {<<"target">>, db_url(To, Target)},
+ {<<"filter">>, <<"_view">>},
+ {<<"query_params">>, {[
+ {<<"view">>, <<"filter_ddoc/mammals">>}
+ ]}}
+ ]},
+ {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER),
+ FilterFun = fun(_DocId, {Props}) ->
+ couch_util:get_value(<<"class">>, Props) == <<"mammal">>
+ end,
+ {ok, TargetDbInfo, AllReplies} = compare_dbs(Source, Target, FilterFun),
+ {lists:flatten(io_lib:format("~p -> ~p", [From, To])), [
+ {"Target DB has proper number of docs",
+ ?_assertEqual(1, proplists:get_value(doc_count, TargetDbInfo))},
+ {"Target DB doesn't have deleted docs",
+ ?_assertEqual(0, proplists:get_value(doc_del_count, TargetDbInfo))},
+ {"All the docs filtered as expected",
+ ?_assert(lists:all(fun(Valid) -> Valid end, AllReplies))}
+ ]}.
+
+compare_dbs(Source, Target, FilterFun) ->
+ {ok, SourceDb} = couch_db:open_int(Source, []),
+ {ok, TargetDb} = couch_db:open_int(Target, []),
+ {ok, TargetDbInfo} = couch_db:get_db_info(TargetDb),
+ Fun = fun(FullDocInfo, Acc) ->
+ {ok, DocId, SourceDoc} = read_doc(SourceDb, FullDocInfo),
+ TargetReply = read_doc(TargetDb, DocId),
+ case FilterFun(DocId, SourceDoc) of
+ true ->
+ ValidReply = {ok, DocId, SourceDoc} == TargetReply,
+ {ok, [ValidReply|Acc]};
+ false ->
+ ValidReply = {not_found, missing} == TargetReply,
+ {ok, [ValidReply|Acc]}
+ end
+ end,
+ {ok, AllReplies} = couch_db:fold_docs(SourceDb, Fun, [], []),
+ ok = couch_db:close(SourceDb),
+ ok = couch_db:close(TargetDb),
+ {ok, TargetDbInfo, AllReplies}.
+
+read_doc(Db, DocIdOrInfo) ->
+ case couch_db:open_doc(Db, DocIdOrInfo) of
+ {ok, Doc} ->
+ {Props} = couch_doc:to_json_obj(Doc, [attachments]),
+ DocId = couch_util:get_value(<<"_id">>, Props),
+ {ok, DocId, {Props}};
+ Error ->
+ Error
+ end.
+
+create_db() ->
+ DbName = ?tempdb(),
+ {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]),
+ ok = couch_db:close(Db),
+ DbName.
+
+create_docs(DbName) ->
+ {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]),
+ DDoc = couch_doc:from_json_obj(?DDOC),
+ Doc1 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc1">>},
+ {<<"class">>, <<"mammal">>},
+ {<<"value">>, 1}
+
+ ]}),
+ Doc2 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc2">>},
+ {<<"class">>, <<"amphibians">>},
+ {<<"value">>, 2}
+
+ ]}),
+ Doc3 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc3">>},
+ {<<"class">>, <<"reptiles">>},
+ {<<"value">>, 3}
+
+ ]}),
+ Doc4 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc4">>},
+ {<<"class">>, <<"arthropods">>},
+ {<<"value">>, 2}
+
+ ]}),
+ {ok, _} = couch_db:update_docs(Db, [DDoc, Doc1, Doc2, Doc3, Doc4]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db).
+
+delete_db(DbName) ->
+ ok = couch_server:delete(DbName, [?ADMIN_CTX]).
+
+db_url(remote, DbName) ->
+ Addr = config:get("httpd", "bind_address", "127.0.0.1"),
+ Port = mochiweb_socket_server:get(couch_httpd, port),
+ ?l2b(io_lib:format("http://~s:~b/~s", [Addr, Port, DbName])).