diff options
Diffstat (limited to 'test/roster_tests.erl')
-rw-r--r-- | test/roster_tests.erl | 527 |
1 files changed, 527 insertions, 0 deletions
diff --git a/test/roster_tests.erl b/test/roster_tests.erl new file mode 100644 index 000000000..2d05709ab --- /dev/null +++ b/test/roster_tests.erl @@ -0,0 +1,527 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 22 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(roster_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, recv_iq/1, send/2, disconnect/1, del_roster/1, + del_roster/2, make_iq_result/1, wait_for_slave/1, + wait_for_master/1, recv_presence/1, self_presence/2, + put_event/2, get_event/1, match_failure/2, get_roster/1, + is_feature_advertised/2]). +-include("suite.hrl"). +-include("mod_roster.hrl"). + +-record(state, {subscription = none :: none | from | to | both, + peer_available = false, + pending_in = false :: boolean(), + pending_out = false :: boolean()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_TestCase, Config) -> + Config. + +stop(_TestCase, Config) -> + Config. + +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {roster_single, [sequence], + [single_test(feature_enabled), + single_test(iq_set_many_items), + single_test(iq_set_duplicated_groups), + single_test(iq_get_item), + single_test(iq_unexpected_element), + single_test(iq_set_ask), + single_test(set_item), + single_test(version)]}. + +feature_enabled(Config) -> + ct:comment("Checking if roster versioning stream feature is set"), + true = ?config(rosterver, Config), + disconnect(Config). + +set_item(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + Item = #roster_item{jid = JID}, + {V1, Item} = set_items(Config, [Item]), + {V1, [Item]} = get_items(Config), + ItemWithGroups = Item#roster_item{groups = [<<"G1">>, <<"G2">>]}, + {V2, ItemWithGroups} = set_items(Config, [ItemWithGroups]), + {V2, [ItemWithGroups]} = get_items(Config), + {V3, Item} = set_items(Config, [Item]), + {V3, [Item]} = get_items(Config), + ItemWithName = Item#roster_item{name = <<"some name">>}, + {V4, ItemWithName} = set_items(Config, [ItemWithName]), + {V4, [ItemWithName]} = get_items(Config), + ItemRemoved = Item#roster_item{subscription = remove}, + {V5, ItemRemoved} = set_items(Config, [ItemRemoved]), + {V5, []} = get_items(Config), + del_roster(disconnect(Config), JID). + +iq_set_many_items(Config) -> + J1 = jid:from_string(<<"nurse1@example.com">>), + J2 = jid:from_string(<<"nurse2@example.com">>), + ct:comment("Trying to send roster-set with many <item/> elements"), + Items = [#roster_item{jid = J1}, #roster_item{jid = J2}], + #stanza_error{reason = 'bad-request'} = set_items(Config, Items), + disconnect(Config). + +iq_set_duplicated_groups(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + G = randoms:get_string(), + ct:comment("Trying to send roster-set with duplicated groups"), + Item = #roster_item{jid = JID, groups = [G, G]}, + #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), + disconnect(Config). + +iq_set_ask(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + ct:comment("Trying to send roster-set with 'ask' included"), + Item = #roster_item{jid = JID, ask = subscribe}, + #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), + disconnect(Config). + +iq_get_item(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + ct:comment("Trying to send roster-get with <item/> element"), + #iq{type = error} = Err3 = + send_recv(Config, #iq{type = get, + sub_els = [#roster_query{ + items = [#roster_item{jid = JID}]}]}), + #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err3), + disconnect(Config). + +iq_unexpected_element(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + ct:comment("Trying to send IQs with unexpected element"), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err4 = + send_recv(Config, #iq{type = Type, + sub_els = [#roster_item{jid = JID}]}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err4) + end, [get, set]), + disconnect(Config). + +version(Config) -> + JID = jid:from_string(<<"nurse@example.com">>), + ct:comment("Requesting roster"), + {InitialVersion, _} = get_items(Config, <<"">>), + ct:comment("Requesting roster with initial version"), + {empty, []} = get_items(Config, InitialVersion), + ct:comment("Adding JID to the roster"), + {NewVersion, _} = set_items(Config, [#roster_item{jid = JID}]), + ct:comment("Requesting roster with initial version"), + {NewVersion, _} = get_items(Config, InitialVersion), + ct:comment("Requesting roster with new version"), + {empty, []} = get_items(Config, NewVersion), + del_roster(disconnect(Config), JID). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {roster_master_slave, [parallel], + [master_slave_test(subscribe)]}. + +subscribe_master(Config) -> + Actions = actions(), + process_subscriptions_master(Config, Actions), + del_roster(disconnect(Config)). + +subscribe_slave(Config) -> + process_subscriptions_slave(Config), + del_roster(disconnect(Config)). + +process_subscriptions_master(Config, Actions) -> + EnumeratedActions = lists:zip(lists:seq(1, length(Actions)), Actions), + self_presence(Config, available), + lists:foldl( + fun({N, {Dir, Type}}, State) -> + if Dir == out -> put_event(Config, {N, in, Type}); + Dir == in -> put_event(Config, {N, out, Type}) + end, + wait_for_slave(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), + pp(get_roster(Config))]), + transition(Config, Dir, Type, State) + end, #state{}, EnumeratedActions), + put_event(Config, done), + wait_for_slave(Config), + Config. + +process_subscriptions_slave(Config) -> + self_presence(Config, available), + process_subscriptions_slave(Config, get_event(Config), #state{}). + +process_subscriptions_slave(Config, done, _State) -> + wait_for_master(Config), + Config; +process_subscriptions_slave(Config, {N, Dir, Type}, State) -> + wait_for_master(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), pp(get_roster(Config))]), + NewState = transition(Config, Dir, Type, State), + process_subscriptions_slave(Config, get_event(Config), NewState). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("roster_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("roster_" ++ atom_to_list(T)), [parallel], + [list_to_atom("roster_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("roster_" ++ atom_to_list(T) ++ "_slave")]}. + +get_items(Config) -> + get_items(Config, <<"">>). + +get_items(Config, Version) -> + case send_recv(Config, #iq{type = get, + sub_els = [#roster_query{ver = Version}]}) of + #iq{type = result, + sub_els = [#roster_query{ver = NewVersion, items = Items}]} -> + {NewVersion, Items}; + #iq{type = result, sub_els = []} -> + {empty, []}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_item(Config, JID) -> + case get_items(Config) of + {_Ver, Items} when is_list(Items) -> + lists:keyfind(JID, #roster_item.jid, Items); + _ -> + false + end. + +set_items(Config, Items) -> + case send_recv(Config, #iq{type = set, + sub_els = [#roster_query{items = Items}]}) of + #iq{type = result, sub_els = []} -> + recv_push(Config); + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +recv_push(Config) -> + ct:comment("Receiving roster push"), + Push = #iq{type = set, + sub_els = [#roster_query{ver = Ver, items = [PushItem]}]} + = recv_iq(Config), + send(Config, make_iq_result(Push)), + {Ver, PushItem}. + +recv_push(Config, Subscription, Ask) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + Match = #roster_item{jid = PeerBareJID, + subscription = Subscription, + ask = Ask, + groups = [], + name = <<"">>}, + ct:comment("Receiving roster push"), + Push = #iq{type = set, sub_els = [#roster_query{items = [Item]}]} = + recv_iq(Config), + case Item of + Match -> send(Config, make_iq_result(Push)); + _ -> match_failure(Item, Match) + end. + +recv_presence(Config, Type) -> + PeerJID = ?config(peer, Config), + case recv_presence(Config) of + #presence{from = PeerJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerJID, type = Type}) + end. + +recv_subscription(Config, Type) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + case recv_presence(Config) of + #presence{from = PeerBareJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerBareJID, type = Type}) + end. + +pp(Term) -> + io_lib_pretty:print(Term, fun pp/2). + +pp(state, N) -> + Fs = record_info(fields, state), + try N = length(Fs), Fs + catch _:_ -> no end; +pp(roster, N) -> + Fs = record_info(fields, roster), + try N = length(Fs), Fs + catch _:_ -> no end; +pp(_, _) -> no. + +%% RFC6121, A.2.1 +transition(Config, out, subscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{to = PeerBareJID, type = subscribe}), + case {Sub, Out, In} of + {none, false, _} -> + recv_push(Config, none, subscribe), + State#state{pending_out = true}; + {none, true, false} -> + %% BUG: we should not receive roster push here + recv_push(Config, none, subscribe), + State; + {from, false, false} -> + recv_push(Config, from, subscribe), + State#state{pending_out = true}; + _ -> + State + end; +%% RFC6121, A.2.2 +transition(Config, out, unsubscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{to = PeerBareJID, type = unsubscribe}), + case {Sub, Out, In} of + {none, true, _} -> + recv_push(Config, none, undefined), + State#state{pending_out = false}; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + State#state{pending_out = false}; + {both, false, false} -> + recv_push(Config, from, undefined), + recv_presence(Config, unavailable), + State#state{subscription = from, peer_available = false}; + _ -> + State + end; +%% RFC6121, A.2.3 +transition(Config, out, subscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{to = PeerBareJID, type = subscribed}), + case {Sub, Out, In} of + {none, false, true} -> + recv_push(Config, from, undefined), + State#state{subscription = from, pending_in = false}; + {none, true, true} -> + recv_push(Config, from, subscribe), + State#state{subscription = from, pending_in = false}; + {to, false, true} -> + recv_push(Config, both, undefined), + State#state{subscription = both, pending_in = false}; + {to, false, _} -> + %% BUG: we should not transition to 'both' state + recv_push(Config, both, undefined), + State#state{subscription = both}; + _ -> + State + end; +%% RFC6121, A.2.4 +transition(Config, out, unsubscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{to = PeerBareJID, type = unsubscribed}), + case {Sub, Out, In} of + {none, false, true} -> + State#state{subscription = none, pending_in = false}; + {none, true, true} -> + recv_push(Config, none, subscribe), + State#state{subscription = none, pending_in = false}; + {to, _, true} -> + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + State#state{subscription = to}; + _ -> + State + end; +%% RFC6121, A.3.1 +transition(Config, in, subscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, false, false} -> + recv_subscription(Config, Type), + State#state{pending_in = true}; + {none, true, false} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{pending_in = true}; + {to, false, false} -> + %% BUG: we should not receive roster push in this state! + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = true}; + _ -> + State + end; +%% RFC6121, A.3.2 +transition(Config, in, unsubscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, _, true} -> + State#state{pending_in = false}; + {to, _, true} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{subscription = to}; + _ -> + State + end; +%% RFC6121, A.3.3 +transition(Config, in, subscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = to, pending_out = false, peer_available = true}; + {from, true, _} -> + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + {from, false, _} -> + %% BUG: we should not transition to 'both' in this state + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + _ -> + State + end; +%% RFC6121, A.3.4 +transition(Config, in, unsubscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, true} -> + %% BUG: we should receive roster push in this state! + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, true, false} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, false, false} -> + State; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + recv_presence(Config, unavailable), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + recv_subscription(Config, Type), + State#state{subscription = from, pending_out = false}; + {both, _, _} -> + recv_push(Config, from, undefined), + recv_subscription(Config, Type), + recv_presence(Config, unavailable), + State#state{subscription = from, peer_available = false}; + _ -> + State + end; +%% Outgoing roster remove +transition(Config, out, remove, + #state{subscription = Sub, pending_in = In, pending_out = Out}) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + Item = #roster_item{jid = PeerBareJID, subscription = remove}, + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, + sub_els = [#roster_query{items = [Item]}]}), + recv_push(Config, remove, undefined), + case {Sub, Out, In} of + {to, _, _} -> + recv_presence(Config, unavailable); + {both, _, _} -> + recv_presence(Config, unavailable); + _ -> + ok + end, + #state{}; +%% Incoming roster remove +transition(Config, in, remove, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, _} -> + ok; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribe); + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, unsubscribe); + {to, false, _} -> + %% BUG: we should receive push here + %% recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, unsubscribed); + {both, _, _} -> + recv_presence(Config, unavailable), + recv_push(Config, to, undefined), + recv_subscription(Config, unsubscribe), + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribed); + _ -> + ok + end, + State#state{subscription = none}. + +actions() -> + States = [{Dir, Type} || Dir <- [out, in], + Type <- [subscribe, subscribed, + unsubscribe, unsubscribed, + remove]], + Actions = lists:flatten([[X, Y] || X <- States, Y <- States]), + remove_dups(Actions, []). + +remove_dups([X|T], [X,X|_] = Acc) -> + remove_dups(T, Acc); +remove_dups([X|T], Acc) -> + remove_dups(T, [X|Acc]); +remove_dups([], Acc) -> + lists:reverse(Acc). |