aboutsummaryrefslogtreecommitdiff
path: root/test/ejabberd_SUITE.erl
diff options
context:
space:
mode:
Diffstat (limited to 'test/ejabberd_SUITE.erl')
-rw-r--r--test/ejabberd_SUITE.erl638
1 files changed, 638 insertions, 0 deletions
diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl
new file mode 100644
index 000000000..0d6659639
--- /dev/null
+++ b/test/ejabberd_SUITE.erl
@@ -0,0 +1,638 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeniy Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2013, Evgeniy Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 2 Jun 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(ejabberd_SUITE).
+
+-compile(export_all).
+
+-include_lib("common_test/include/ct.hrl").
+-include("ns.hrl").
+-include("ejabberd.hrl").
+-include("xmpp_codec.hrl").
+
+-define(STREAM_HEADER,
+ <<"<?xml version='1.0'?><stream:stream "
+ "xmlns:stream='http://etherx.jabber.org/stream"
+ "s' xmlns='jabber:client' to='~s' version='1.0"
+ "'>">>).
+
+-define(STREAM_TRAILER, <<"</stream:stream>">>).
+
+-define(PUBSUB(Node), <<(?NS_PUBSUB)/binary, "#", Node>>).
+
+suite() ->
+ [{timetrap,{seconds,30}}].
+
+init_per_suite(Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ PrivDir = proplists:get_value(priv_dir, Config),
+ ConfigPath = filename:join([DataDir, "ejabberd.cfg"]),
+ LogPath = filename:join([PrivDir, "ejabberd.log"]),
+ SASLPath = filename:join([PrivDir, "sasl.log"]),
+ MnesiaDir = filename:join([PrivDir, "mnesia"]),
+ application:set_env(ejabberd, config, ConfigPath),
+ application:set_env(ejabberd, log_path, LogPath),
+ application:set_env(sasl, sasl_error_logger, {file, SASLPath}),
+ application:set_env(mnesia, dir, MnesiaDir),
+ [{server, <<"localhost">>},
+ {port, 5222},
+ {user, <<"test_suite">>},
+ {password, <<"pass">>}
+ |Config].
+
+end_per_suite(_Config) ->
+ ok.
+
+init_per_group(_GroupName, Config) ->
+ Config.
+
+end_per_group(_GroupName, _Config) ->
+ ok.
+
+init_per_testcase(start_ejabberd, Config) ->
+ Config;
+init_per_testcase(TestCase, OrigConfig) ->
+ Resource = list_to_binary(atom_to_list(TestCase)),
+ Config = [{resource, Resource}|OrigConfig],
+ case TestCase of
+ test_connect ->
+ Config;
+ test_auth ->
+ connect(Config);
+ auth_md5 ->
+ connect(Config);
+ auth_plain ->
+ connect(Config);
+ test_bind ->
+ auth(connect(Config));
+ test_open_session ->
+ bind(auth(connect(Config)));
+ _ ->
+ open_session(bind(auth(connect(Config))))
+ end.
+
+end_per_testcase(_TestCase, _Config) ->
+ ok.
+
+groups() ->
+ [].
+
+%%all() -> [start_ejabberd, pubsub].
+
+all() ->
+ [start_ejabberd,
+ test_connect,
+ auth_plain,
+ auth_md5,
+ test_auth,
+ test_bind,
+ test_open_session,
+ roster_get,
+ presence_broadcast,
+ ping,
+ version,
+ time,
+ stats,
+ disco,
+ last,
+ private,
+ privacy,
+ blocking,
+ vcard,
+ pubsub,
+ stop_ejabberd].
+
+start_ejabberd(Config) ->
+ ok = application:start(ejabberd),
+ ok = re_register(Config),
+ Config.
+
+stop_ejabberd(Config) ->
+ ok = application:stop(ejabberd),
+ #stream_error{reason = 'system-shutdown'} = recv(),
+ {xmlstreamend, <<"stream:stream">>} = recv(),
+ Config.
+
+test_connect(Config) ->
+ disconnect(connect(Config)).
+
+connect(Config) ->
+ {ok, Sock} = ejabberd_socket:connect(
+ binary_to_list(?config(server, Config)),
+ ?config(port, Config),
+ [binary, {packet, 0}, {active, false}]),
+ Config1 = [{socket, Sock}|Config],
+ ok = send_text(Config1, io_lib:format(?STREAM_HEADER,
+ [?config(server, Config1)])),
+ {xmlstreamstart, <<"stream:stream">>, Attrs} = recv(),
+ <<"jabber:client">> = xml:get_attr_s(<<"xmlns">>, Attrs),
+ <<"1.0">> = xml:get_attr_s(<<"version">>, Attrs),
+ #stream_features{sub_els = Fs} = recv(),
+ Mechs = lists:flatmap(
+ fun(#sasl_mechanisms{mechanism = Ms}) ->
+ Ms;
+ (_) ->
+ []
+ end, Fs),
+ [{mechs, Mechs}|Config1].
+
+disconnect(Config) ->
+ Socket = ?config(socket, Config),
+ ok = ejabberd_socket:send(Socket, ?STREAM_TRAILER),
+ {xmlstreamend, <<"stream:stream">>} = recv(),
+ ejabberd_socket:close(Socket),
+ Config.
+
+test_auth(Config) ->
+ disconnect(auth(Config)).
+
+auth(Config) ->
+ Mechs = ?config(mechs, Config),
+ HaveMD5 = lists:member(<<"DIGEST-MD5">>, Mechs),
+ HavePLAIN = lists:member(<<"PLAIN">>, Mechs),
+ if HavePLAIN ->
+ auth_SASL(<<"PLAIN">>, Config);
+ HaveMD5 ->
+ auth_SASL(<<"DIGEST-MD5">>, Config);
+ true ->
+ ct:fail(no_sasl_mechanisms_available)
+ end.
+
+test_bind(Config) ->
+ disconnect(bind(Config)).
+
+bind(Config) ->
+ ID = send(Config,
+ #iq{type = set,
+ sub_els = [#bind{resource = ?config(resource, Config)}]}),
+ #iq{type = result, id = ID, sub_els = [#bind{}]} = recv(),
+ Config.
+
+test_open_session(Config) ->
+ disconnect(open_session(Config)).
+
+open_session(Config) ->
+ ID = send(Config, #iq{type = set, sub_els = [#session{}]}),
+ #iq{type = result, id = ID, sub_els = SubEls} = recv(),
+ case SubEls of
+ [] ->
+ ok;
+ [#session{}] ->
+ %% BUG: we should not receive this!
+ %% TODO: should be fixed in ejabberd
+ ok
+ end,
+ Config.
+
+roster_get(Config) ->
+ ID = send(Config, #iq{type = get, sub_els = [#roster{}]}),
+ #iq{type = result, id = ID,
+ sub_els = [#roster{item = []}]} = recv(),
+ disconnect(Config).
+
+presence_broadcast(Config) ->
+ send(Config, #presence{}),
+ JID = my_jid(Config),
+ #presence{from = JID, to = JID} = recv(),
+ disconnect(Config).
+
+ping(Config) ->
+ true = is_feature_advertised(Config, ?NS_PING),
+ ID = send(Config,
+ #iq{type = get, sub_els = [#ping{}], to = server_jid(Config)}),
+ #iq{type = result, id = ID, sub_els = []} = recv(),
+ disconnect(Config).
+
+version(Config) ->
+ true = is_feature_advertised(Config, ?NS_VERSION),
+ ID = send(Config, #iq{type = get, sub_els = [#version{}],
+ to = server_jid(Config)}),
+ #iq{type = result, id = ID, sub_els = [#version{}]} = recv(),
+ disconnect(Config).
+
+time(Config) ->
+ true = is_feature_advertised(Config, ?NS_TIME),
+ ID = send(Config, #iq{type = get, sub_els = [#time{}],
+ to = server_jid(Config)}),
+ #iq{type = result, id = ID, sub_els = [#time{}]} = recv(),
+ disconnect(Config).
+
+disco(Config) ->
+ true = is_feature_advertised(Config, ?NS_DISCO_INFO),
+ true = is_feature_advertised(Config, ?NS_DISCO_ITEMS),
+ I1 = send(Config, #iq{type = get, sub_els = [#disco_items{}],
+ to = server_jid(Config)}),
+ #iq{type = result, id = I1, sub_els = [#disco_items{items = Items}]} = recv(),
+ lists:foreach(
+ fun(#disco_item{jid = JID, node = Node}) ->
+ I = send(Config,
+ #iq{type = get, to = JID,
+ sub_els = [#disco_info{node = Node}]}),
+ #iq{type = result, id = I, sub_els = _} = recv()
+ end, Items),
+ disconnect(Config).
+
+private(Config) ->
+ I1 = send(Config, #iq{type = get, sub_els = [#private{}],
+ to = server_jid(Config)}),
+ #iq{type = error, id = I1} = recv(),
+ Conference = #bookmark_conference{name = <<"Some name">>,
+ autojoin = true,
+ jid = jlib:make_jid(
+ <<"some">>,
+ <<"some.conference.org">>,
+ <<>>)},
+ Storage = #bookmark_storage{conference = [Conference]},
+ I2 = send(Config, #iq{type = set,
+ sub_els = [#private{sub_els = [Storage]}]}),
+ #iq{type = result, id = I2, sub_els = []} = recv(),
+ I3 = send(Config,
+ #iq{type = get,
+ sub_els = [#private{sub_els = [#bookmark_storage{}]}]}),
+ #iq{type = result, id = I3,
+ sub_els = [#private{sub_els = [Storage]}]} = recv(),
+ disconnect(Config).
+
+last(Config) ->
+ true = is_feature_advertised(Config, ?NS_LAST),
+ ID = send(Config, #iq{type = get, sub_els = [#last{}],
+ to = server_jid(Config)}),
+ #iq{type = result, id = ID, sub_els = [#last{}]} = recv(),
+ disconnect(Config).
+
+privacy(Config) ->
+ %% BUG: the feature MUST be advertised via disco#info:
+ %% http://xmpp.org/extensions/xep-0016.html#disco
+ %% It seems like this bug exists because Privacy Lists
+ %% were implemented according to the old RFC where support
+ %% needn't be advertised via service discovery.
+ %% TODO: fix in ejabberd
+ %% true = is_feature_advertised(Config, ?NS_PRIVACY),
+ I1 = send(Config, #iq{type = get, sub_els = [#privacy{}]}),
+ #iq{type = result, id = I1, sub_els = [#privacy{}]} = recv(),
+ JID = <<"tybalt@example.com">>,
+ I2 = send(Config,
+ #iq{type = set,
+ sub_els = [#privacy{
+ list = [#privacy_list{
+ name = <<"public">>,
+ privacy_item =
+ [#privacy_item{
+ type = jid,
+ order = 3,
+ action = deny,
+ stanza = 'presence-in',
+ value = JID}]}]}]}),
+ #iq{type = result, id = I2, sub_els = []} = recv(),
+ _Push1 = #iq{type = set, id = PushI1,
+ sub_els = [#privacy{
+ list = [#privacy_list{
+ name = <<"public">>}]}]} = recv(),
+ %% BUG: ejabberd replies on this result
+ %% TODO: this should be fixed in ejabberd
+ %% _ = send(Config, Push1#iq{type = result, sub_els = []}),
+ I3 = send(Config, #iq{type = set,
+ sub_els = [#privacy{active = <<"public">>}]}),
+ #iq{type = result, id = I3, sub_els = []} = recv(),
+ I4 = send(Config, #iq{type = set,
+ sub_els = [#privacy{default = <<"public">>}]}),
+ #iq{type = result, id = I4, sub_els = []} = recv(),
+ I5 = send(Config, #iq{type = get, sub_els = [#privacy{}]}),
+ #iq{type = result, id = I5,
+ sub_els = [#privacy{default = <<"public">>,
+ active = <<"public">>,
+ list = [#privacy_list{name = <<"public">>}]}]} = recv(),
+ I6 = send(Config,
+ #iq{type = set, sub_els = [#privacy{default = none}]}),
+ #iq{type = result, id = I6, sub_els = []} = recv(),
+ I7 = send(Config, #iq{type = set, sub_els = [#privacy{active = none}]}),
+ #iq{type = result, id = I7, sub_els = []} = recv(),
+ I8 = send(Config, #iq{type = set,
+ sub_els = [#privacy{
+ list =
+ [#privacy_list{
+ name = <<"public">>}]}]}),
+ #iq{type = result, id = I8, sub_els = []} = recv(),
+ %% BUG: We should receive this:
+ %% _Push2 = #iq{type = set, id = PushI2, sub_els = []} = recv(),
+ %% TODO: this should be fixed in ejabberd
+ _Push2 = #iq{type = set, id = PushI2,
+ sub_els = [#privacy{
+ list = [#privacy_list{
+ name = <<"public">>}]}]} = recv(),
+ disconnect(Config).
+
+blocking(Config) ->
+ true = is_feature_advertised(Config, ?NS_BLOCKING),
+ JID = jlib:make_jid(<<"romeo">>, <<"montague.net">>, <<>>),
+ I1 = send(Config, #iq{type = get, sub_els = [#block_list{}]}),
+ #iq{type = result, id = I1, sub_els = [#block_list{}]} = recv(),
+ I2 = send(Config, #iq{type = set,
+ sub_els = [#block{block_item = [JID]}]}),
+ #iq{type = result, id = I2, sub_els = []} = recv(),
+ #iq{type = set, id = _,
+ sub_els = [#privacy{list = [#privacy_list{}]}]} = recv(),
+ #iq{type = set, id = _,
+ sub_els = [#block{block_item = [JID]}]} = recv(),
+ I3 = send(Config, #iq{type = set,
+ sub_els = [#unblock{block_item = [JID]}]}),
+ #iq{type = result, id = I3, sub_els = []} = recv(),
+ #iq{type = set, id = _,
+ sub_els = [#privacy{list = [#privacy_list{}]}]} = recv(),
+ #iq{type = set, id = _,
+ sub_els = [#unblock{block_item = [JID]}]} = recv(),
+ disconnect(Config).
+
+vcard(Config) ->
+ true = is_feature_advertised(Config, ?NS_VCARD),
+ VCard =
+ #vcard{fn = <<"Peter Saint-Andre">>,
+ n = #vcard_name{family = <<"Saint-Andre">>,
+ given = <<"Peter">>},
+ nickname = <<"stpeter">>,
+ bday = <<"1966-08-06">>,
+ adr = [#vcard_adr{work = true,
+ extadd = <<"Suite 600">>,
+ street = <<"1899 Wynkoop Street">>,
+ locality = <<"Denver">>,
+ region = <<"CO">>,
+ pcode = <<"80202">>,
+ ctry = <<"USA">>},
+ #vcard_adr{home = true,
+ locality = <<"Denver">>,
+ region = <<"CO">>,
+ pcode = <<"80209">>,
+ ctry = <<"USA">>}],
+ tel = [#vcard_tel{work = true,voice = true,
+ number = <<"303-308-3282">>},
+ #vcard_tel{home = true,voice = true,
+ number = <<"303-555-1212">>}],
+ email = [#vcard_email{internet = true,pref = true,
+ userid = <<"stpeter@jabber.org">>}],
+ jabberid = <<"stpeter@jabber.org">>,
+ title = <<"Executive Director">>,role = <<"Patron Saint">>,
+ org = #vcard_org{name = <<"XMPP Standards Foundation">>},
+ url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>,
+ desc = <<"More information about me is located on my "
+ "personal website: http://www.saint-andre.com/">>},
+ I1 = send(Config, #iq{type = set, sub_els = [VCard]}),
+ #iq{type = result, id = I1, sub_els = []} = recv(),
+ I2 = send(Config, #iq{type = get, sub_els = [#vcard{}]}),
+ %% TODO: check if VCard == VCard1.
+ #iq{type = result, id = I2, sub_els = [_VCard1]} = recv(),
+ disconnect(Config).
+
+stats(Config) ->
+ ServerJID = server_jid(Config),
+ ID = send(Config, #iq{type = get, sub_els = [#stats{}],
+ to = server_jid(Config)}),
+ #iq{type = result, id = ID, sub_els = [#stats{stat = Stats}]} = recv(),
+ lists:foreach(
+ fun(#stat{name = Name} = Stat) ->
+ I = send(Config, #iq{type = get,
+ sub_els = [#stats{stat = [Stat]}],
+ to = server_jid(Config)}),
+ #iq{type = result, id = I, sub_els = [_|_]} = recv()
+ end, Stats),
+ disconnect(Config).
+
+pubsub(Config) ->
+ true = is_feature_advertised(Config, ?NS_PUBSUB),
+ %% Get subscriptions
+ %% true = is_feature_advertised(Config, ?PUBSUB("retrieve-subscriptions")),
+ %% I1 = send(Config, #iq{type = get, to = pubsub_jid(Config),
+ %% sub_els = [#pubsub{subscriptions = {none, []}}]}),
+ %% #iq{type = result, id = I1,
+ %% sub_els = [#pubsub{subscriptions = {none, []}}]} = recv(),
+ %% %% Get affiliations
+ %% true = is_feature_advertised(Config, ?PUBSUB("retrieve-affiliations")),
+ %% I2 = send(Config, #iq{type = get, to = pubsub_jid(Config),
+ %% sub_els = [#pubsub{affiliations = []}]}),
+ %% #iq{type = result, id = I2,
+ %% sub_els = [#pubsub{affiliations = []}]} = recv(),
+
+ true = is_feature_advertised(Config, ?NS_PUBSUB),
+ %% Publish <presence/> element within node "presence"
+ ItemID = randoms:get_string(),
+ Node = <<"presence">>,
+ Item = #pubsub_item{id = ItemID, sub_els = [#presence{}]},
+ I1 = send(Config,
+ #iq{type = set, to = pubsub_jid(Config),
+ sub_els = [#pubsub{publish = {Node, [Item]}}]}),
+ #iq{type = result, id = I1,
+ sub_els = [#pubsub{publish = {<<"presence">>,
+ [#pubsub_item{id = ItemID}]}}]} = recv(),
+ %% Subscribe to node "presence"
+ I2 = send(Config,
+ #iq{type = set, to = pubsub_jid(Config),
+ sub_els = [#pubsub{subscribe = {Node, my_jid(Config)}}]}),
+ #message{sub_els = [#pubsub_event{}, #delay{}]} = recv(),
+ #iq{type = result, id = I2} = recv(),
+ disconnect(Config).
+
+auth_md5(Config) ->
+ Mechs = ?config(mechs, Config),
+ case lists:member(<<"DIGEST-MD5">>, Mechs) of
+ true ->
+ disconnect(auth_SASL(<<"DIGEST-MD5">>, Config));
+ false ->
+ disconnect(Config),
+ {skipped, 'DIGEST-MD5_not_available'}
+ end.
+
+auth_plain(Config) ->
+ Mechs = ?config(mechs, Config),
+ case lists:member(<<"PLAIN">>, Mechs) of
+ true ->
+ disconnect(auth_SASL(<<"PLAIN">>, Config));
+ false ->
+ disconnect(Config),
+ {skipped, 'PLAIN_not_available'}
+ end.
+
+auth_SASL(Mech, Config) ->
+ {Response, SASL} = sasl_new(Mech,
+ ?config(user, Config),
+ ?config(server, Config),
+ ?config(password, Config)),
+ send(Config, #sasl_auth{mechanism = Mech, cdata = Response}),
+ wait_auth_SASL_result([{sasl, SASL}|Config]).
+
+wait_auth_SASL_result(Config) ->
+ case recv() of
+ #sasl_success{} ->
+ ejabberd_socket:reset_stream(?config(socket, Config)),
+ send_text(Config,
+ io_lib:format(?STREAM_HEADER,
+ [?config(server, Config)])),
+ {xmlstreamstart, <<"stream:stream">>, Attrs} = recv(),
+ <<"jabber:client">> = xml:get_attr_s(<<"xmlns">>, Attrs),
+ <<"1.0">> = xml:get_attr_s(<<"version">>, Attrs),
+ #stream_features{} = recv(),
+ Config;
+ #sasl_challenge{cdata = ClientIn} ->
+ {Response, SASL} = (?config(sasl, Config))(ClientIn),
+ send(Config, #sasl_response{cdata = Response}),
+ Config1 = proplists:delete(sasl, Config),
+ wait_auth_SASL_result([{sasl, SASL}|Config1]);
+ #sasl_failure{} ->
+ ct:fail(sasl_auth_failed)
+ end.
+
+%%%===================================================================
+%%% Aux functions
+%%%===================================================================
+re_register(Config) ->
+ User = ?config(user, Config),
+ Server = ?config(server, Config),
+ Pass = ?config(password, Config),
+ {atomic, ok} = ejabberd_auth:try_register(User, Server, Pass),
+ ok.
+
+recv() ->
+ receive
+ {'$gen_event', {xmlstreamelement, El}} ->
+ ct:log("recv: ~p", [El]),
+ xmpp_codec:decode(El);
+ {'$gen_event', Event} ->
+ Event
+ end.
+
+send_text(Config, Text) ->
+ ejabberd_socket:send(?config(socket, Config), Text).
+
+send(State, Pkt) ->
+ {NewID, NewPkt} = case Pkt of
+ #message{id = I} ->
+ ID = id(I),
+ {ID, Pkt#message{id = ID}};
+ #presence{id = I} ->
+ ID = id(I),
+ {ID, Pkt#presence{id = ID}};
+ #iq{id = I} ->
+ ID = id(I),
+ {ID, Pkt#iq{id = ID}};
+ _ ->
+ {undefined, Pkt}
+ end,
+ El = xmpp_codec:encode(NewPkt),
+ ct:log("sent: ~p", [El]),
+ ok = send_text(State, xml:element_to_binary(El)),
+ NewID.
+
+sasl_new(<<"PLAIN">>, User, Server, Password) ->
+ {<<User/binary, $@, Server/binary, 0, User/binary, 0, Password/binary>>,
+ fun (_) -> {error, <<"Invalid SASL challenge">>} end};
+sasl_new(<<"DIGEST-MD5">>, User, Server, Password) ->
+ {<<"">>,
+ fun (ServerIn) ->
+ case cyrsasl_digest:parse(ServerIn) of
+ bad -> {error, <<"Invalid SASL challenge">>};
+ KeyVals ->
+ Nonce = xml:get_attr_s(<<"nonce">>, KeyVals),
+ CNonce = id(),
+ DigestURI = <<"xmpp/", Server/binary>>,
+ Realm = Server,
+ NC = <<"00000001">>,
+ QOP = <<"auth">>,
+ AuthzId = <<"">>,
+ MyResponse = response(User, Password, Nonce, AuthzId,
+ Realm, CNonce, DigestURI, NC, QOP,
+ <<"AUTHENTICATE">>),
+ ServerResponse = response(User, Password, Nonce,
+ AuthzId, Realm, CNonce, DigestURI,
+ NC, QOP, <<"">>),
+ Resp = <<"username=\"", User/binary, "\",realm=\"",
+ Realm/binary, "\",nonce=\"", Nonce/binary,
+ "\",cnonce=\"", CNonce/binary, "\",nc=", NC/binary,
+ ",qop=", QOP/binary, ",digest-uri=\"",
+ DigestURI/binary, "\",response=\"",
+ MyResponse/binary, "\"">>,
+ {Resp,
+ fun (ServerIn2) ->
+ case cyrsasl_digest:parse(ServerIn2) of
+ bad -> {error, <<"Invalid SASL challenge">>};
+ KeyVals2 ->
+ RspAuth = xml:get_attr_s(<<"rspauth">>,
+ KeyVals2),
+ if RspAuth == ServerResponse ->
+ {<<"">>,
+ fun (_) ->
+ {error,
+ <<"Invalid SASL challenge">>}
+ end};
+ true ->
+ {error, <<"Invalid SASL challenge">>}
+ end
+ end
+ end}
+ end
+ end}.
+
+hex(S) ->
+ sha:to_hexlist(S).
+
+response(User, Passwd, Nonce, AuthzId, Realm, CNonce,
+ DigestURI, NC, QOP, A2Prefix) ->
+ A1 = case AuthzId of
+ <<"">> ->
+ <<((crypto:md5(<<User/binary, ":", Realm/binary, ":",
+ Passwd/binary>>)))/binary,
+ ":", Nonce/binary, ":", CNonce/binary>>;
+ _ ->
+ <<((crypto:md5(<<User/binary, ":", Realm/binary, ":",
+ Passwd/binary>>)))/binary,
+ ":", Nonce/binary, ":", CNonce/binary, ":",
+ AuthzId/binary>>
+ end,
+ A2 = case QOP of
+ <<"auth">> ->
+ <<A2Prefix/binary, ":", DigestURI/binary>>;
+ _ ->
+ <<A2Prefix/binary, ":", DigestURI/binary,
+ ":00000000000000000000000000000000">>
+ end,
+ T = <<(hex((crypto:md5(A1))))/binary, ":", Nonce/binary,
+ ":", NC/binary, ":", CNonce/binary, ":", QOP/binary,
+ ":", (hex((crypto:md5(A2))))/binary>>,
+ hex((crypto:md5(T))).
+
+my_jid(Config) ->
+ jlib:make_jid(?config(user, Config),
+ ?config(server, Config),
+ ?config(resource, Config)).
+
+server_jid(Config) ->
+ jlib:make_jid(<<>>, ?config(server, Config), <<>>).
+
+pubsub_jid(Config) ->
+ Server = ?config(server, Config),
+ jlib:make_jid(<<>>, <<"pubsub.", Server/binary>>, <<>>).
+
+id() ->
+ id(undefined).
+
+id(undefined) ->
+ randoms:get_string();
+id(ID) ->
+ ID.
+
+is_feature_advertised(Config, Feature) ->
+ ID = send(Config, #iq{type = get, sub_els = [#disco_info{}],
+ to = server_jid(Config)}),
+ #iq{type = result, id = ID,
+ sub_els = [#disco_info{feature = Features}]} = recv(),
+ lists:member(Feature, Features).
+
+bookmark_conference() ->
+ #bookmark_conference{name = <<"Some name">>,
+ autojoin = true,
+ jid = jlib:make_jid(
+ <<"some">>,
+ <<"some.conference.org">>,
+ <<>>)}.