aboutsummaryrefslogblamecommitdiff
path: root/test/pubsub_tests.erl
blob: 2bca35173d9d6f12bd0aaebbf709e61610b0e663 (plain) (tree)
1
2
3
4
5
6
                                                                      
                                                           
                                                                          

   
                                                  
















                                                                           








                                                                              
                                          









                                                                      
                              













                                          
                                             



























                                                             

                             
                                                              

                                                          


                                                                                

















                                                   
                                                                












                                                        
                                                             









                                     
                                




























                                                                       









                                                                       











                                                                            

                                                                         






                                                                            
                                                                                     































                                                                      
                              







                                                   
                             



























































                                                                           
                                                           





















































































































































































































































































































                                                                                    
                                  
                                                                                  





















































































































































                                                                                   
%%%-------------------------------------------------------------------
%%% Author  : Evgeny Khramtsov <ekhramtsov@process-one.net>
%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2021   ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------

-module(pubsub_tests).

%% API
-compile(export_all).
-import(suite, [pubsub_jid/1, send_recv/2, get_features/2, disconnect/1,
		put_event/2, get_event/1, wait_for_master/1, wait_for_slave/1,
		recv_message/1, my_jid/1, send/2, recv_presence/1, recv/1]).

-include("suite.hrl").
-include_lib("stdlib/include/assert.hrl").

%%%===================================================================
%%% API
%%%===================================================================
%%%===================================================================
%%% Single user tests
%%%===================================================================
single_cases() ->
    {pubsub_single, [sequence],
     [single_test(test_features),
      single_test(test_vcard),
      single_test(test_create),
      single_test(test_configure),
      single_test(test_delete),
      single_test(test_get_affiliations),
      single_test(test_get_subscriptions),
      single_test(test_create_instant),
      single_test(test_default),
      single_test(test_create_configure),
      single_test(test_publish),
      single_test(test_auto_create),
      single_test(test_get_items),
      single_test(test_delete_item),
      single_test(test_purge),
      single_test(test_subscribe),
      single_test(test_subscribe_max_item_1),
      single_test(test_unsubscribe)]}.

test_features(Config) ->
    PJID = pubsub_jid(Config),
    AllFeatures = sets:from_list(get_features(Config, PJID)),
    NeededFeatures = sets:from_list(
		       [?NS_PUBSUB,
			?PUBSUB("access-open"),
			?PUBSUB("access-authorize"),
			?PUBSUB("create-nodes"),
			?PUBSUB("instant-nodes"),
			?PUBSUB("config-node"),
			?PUBSUB("retrieve-default"),
			?PUBSUB("create-and-configure"),
			?PUBSUB("publish"),
			?PUBSUB("auto-create"),
			?PUBSUB("retrieve-items"),
			?PUBSUB("delete-items"),
			?PUBSUB("subscribe"),
			?PUBSUB("retrieve-affiliations"),
			?PUBSUB("modify-affiliations"),
			?PUBSUB("retrieve-subscriptions"),
			?PUBSUB("manage-subscriptions"),
			?PUBSUB("purge-nodes"),
			?PUBSUB("delete-nodes")]),
    true = sets:is_subset(NeededFeatures, AllFeatures),
    disconnect(Config).

test_vcard(Config) ->
    JID = pubsub_jid(Config),
    ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]),
    VCard = mod_pubsub_opt:vcard(?config(server, Config)),
    #iq{type = result, sub_els = [VCard]} =
	send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}),
    disconnect(Config).

test_create(Config) ->
    Node = ?config(pubsub_node, Config),
    Node = create_node(Config, Node),
    disconnect(Config).

test_create_instant(Config) ->
    Node = create_node(Config, <<>>),
    delete_node(Config, Node),
    disconnect(Config).

test_configure(Config) ->
    Node = ?config(pubsub_node, Config),
    NodeTitle = ?config(pubsub_node_title, Config),
    NodeConfig = get_node_config(Config, Node),
    MyNodeConfig = set_opts(NodeConfig,
			    [{title, NodeTitle}]),
    set_node_config(Config, Node, MyNodeConfig),
    NewNodeConfig = get_node_config(Config, Node),
    NodeTitle = proplists:get_value(title, NewNodeConfig, <<>>),
    disconnect(Config).

test_default(Config) ->
    get_default_node_config(Config),
    disconnect(Config).

test_create_configure(Config) ->
    NodeTitle = ?config(pubsub_node_title, Config),
    DefaultNodeConfig = get_default_node_config(Config),
    CustomNodeConfig = set_opts(DefaultNodeConfig,
				[{title, NodeTitle}]),
    Node = create_node(Config, <<>>, CustomNodeConfig),
    NodeConfig = get_node_config(Config, Node),
    NodeTitle = proplists:get_value(title, NodeConfig, <<>>),
    delete_node(Config, Node),
    disconnect(Config).

test_publish(Config) ->
    Node = create_node(Config, <<>>),
    publish_item(Config, Node),
    delete_node(Config, Node),
    disconnect(Config).

test_auto_create(Config) ->
    Node = p1_rand:get_string(),
    publish_item(Config, Node),
    delete_node(Config, Node),
    disconnect(Config).

test_get_items(Config) ->
    Node = create_node(Config, <<>>),
    ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)],
    ItemsOut = get_items(Config, Node),
    true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)]
	== [I || #ps_item{id = I} <- lists:sort(ItemsOut)],
    delete_node(Config, Node),
    disconnect(Config).

test_delete_item(Config) ->
    Node = create_node(Config, <<>>),
    #ps_item{id = I} = publish_item(Config, Node),
    [#ps_item{id = I}] = get_items(Config, Node),
    delete_item(Config, Node, I),
    [] = get_items(Config, Node),
    delete_node(Config, Node),
    disconnect(Config).

test_subscribe(Config) ->
    Node = create_node(Config, <<>>),
    #ps_subscription{type = subscribed} = subscribe_node(Config, Node),
    [#ps_subscription{node = Node}] = get_subscriptions(Config),
    delete_node(Config, Node),
    disconnect(Config).

test_subscribe_max_item_1(Config) ->
    DefaultNodeConfig = get_default_node_config(Config),
    CustomNodeConfig = set_opts(DefaultNodeConfig,
				[{max_items, 1}]),
    Node = create_node(Config, <<>>, CustomNodeConfig),
    #ps_subscription{type = subscribed} = subscribe_node(Config, Node),
    [#ps_subscription{node = Node}] = get_subscriptions(Config),
    delete_node(Config, Node),
    disconnect(Config).

test_unsubscribe(Config) ->
    Node = create_node(Config, <<>>),
    subscribe_node(Config, Node),
    [#ps_subscription{node = Node}] = get_subscriptions(Config),
    unsubscribe_node(Config, Node),
    [] = get_subscriptions(Config),
    delete_node(Config, Node),
    disconnect(Config).

test_get_affiliations(Config) ->
    Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]),
    Affs = get_affiliations(Config),
    ?assertEqual(Nodes, lists:sort([Node || #ps_affiliation{node = Node,
						type = owner} <- Affs])),
    [delete_node(Config, Node) || Node <- Nodes],
    disconnect(Config).

test_get_subscriptions(Config) ->
    Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]),
    [subscribe_node(Config, Node) || Node <- Nodes],
    Subs = get_subscriptions(Config),
    ?assertEqual(Nodes, lists:sort([Node || #ps_subscription{node = Node} <- Subs])),
    [delete_node(Config, Node) || Node <- Nodes],
    disconnect(Config).

test_purge(Config) ->
    Node = create_node(Config, <<>>),
    ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)],
    ItemsOut = get_items(Config, Node),
    true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)]
	== [I || #ps_item{id = I} <- lists:sort(ItemsOut)],
    purge_node(Config, Node),
    [] = get_items(Config, Node),
    delete_node(Config, Node),
    disconnect(Config).

test_delete(Config) ->
    Node = ?config(pubsub_node, Config),
    delete_node(Config, Node),
    disconnect(Config).

%%%===================================================================
%%% Master-slave tests
%%%===================================================================
master_slave_cases() ->
    {pubsub_master_slave, [sequence],
     [master_slave_test(publish),
      master_slave_test(subscriptions),
      master_slave_test(affiliations),
      master_slave_test(authorize)]}.

publish_master(Config) ->
    Node = create_node(Config, <<>>),
    put_event(Config, Node),
    ready = get_event(Config),
    #ps_item{id = ID} = publish_item(Config, Node),
    #ps_item{id = ID} = get_event(Config),
    delete_node(Config, Node),
    disconnect(Config).

publish_slave(Config) ->
    Node = get_event(Config),
    subscribe_node(Config, Node),
    put_event(Config, ready),
    #message{
       sub_els =
	   [#ps_event{
	       items = #ps_items{node = Node,
				 items = [Item]}}]} = recv_message(Config),
    put_event(Config, Item),
    disconnect(Config).

subscriptions_master(Config) ->
    Peer = ?config(slave, Config),
    Node = ?config(pubsub_node, Config),
    Node = create_node(Config, Node),
    [] = get_subscriptions(Config, Node),
    wait_for_slave(Config),
    lists:foreach(
      fun(Type) ->
	      ok = set_subscriptions(Config, Node, [{Peer, Type}]),
	      #ps_item{} = publish_item(Config, Node),
	      case get_subscriptions(Config, Node) of
		  [] when Type == none; Type == pending ->
		      ok;
		  [#ps_subscription{jid = Peer, type = Type}] ->
		      ok
	      end
      end, [subscribed, unconfigured, pending, none]),
    delete_node(Config, Node),
    disconnect(Config).

subscriptions_slave(Config) ->
    wait_for_master(Config),
    MyJID = my_jid(Config),
    Node = ?config(pubsub_node, Config),
    lists:foreach(
      fun(subscribed = Type) ->
	      ?recv2(#message{
			sub_els =
			    [#ps_event{
				subscription = #ps_subscription{
						  node = Node,
						  jid = MyJID,
						  type = Type}}]},
		     #message{sub_els = [#ps_event{}]});
	 (Type) ->
	      #message{
		 sub_els =
		     [#ps_event{
			 subscription = #ps_subscription{
					   node = Node,
					   jid = MyJID,
					   type = Type}}]} =
		  recv_message(Config)
      end, [subscribed, unconfigured, pending, none]),
    disconnect(Config).

affiliations_master(Config) ->
    Peer = ?config(slave, Config),
    BarePeer = jid:remove_resource(Peer),
    lists:foreach(
      fun(Aff) ->
	      Node = <<(atom_to_binary(Aff, utf8))/binary,
		       $-, (p1_rand:get_string())/binary>>,
	      create_node(Config, Node, default_node_config(Config)),
	      #ps_item{id = I} = publish_item(Config, Node),
	      ok = set_affiliations(Config, Node, [{Peer, Aff}]),
	      Affs = get_affiliations(Config, Node),
	      case lists:keyfind(BarePeer, #ps_affiliation.jid, Affs) of
		  false when Aff == none ->
		      ok;
		  #ps_affiliation{type = Aff} ->
		      ok
	      end,
	      put_event(Config, {Aff, Node, I}),
	      wait_for_slave(Config),
	      delete_node(Config, Node)
      end, [outcast, none, member, publish_only, publisher, owner]),
    put_event(Config, disconnect),
    disconnect(Config).

affiliations_slave(Config) ->
    affiliations_slave(Config, get_event(Config)).

affiliations_slave(Config, {outcast, Node, ItemID}) ->
    #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node),
    #stanza_error{} = unsubscribe_node(Config, Node),
    #stanza_error{reason = 'forbidden'} = get_items(Config, Node),
    #stanza_error{reason = 'forbidden'} = publish_item(Config, Node),
    #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID),
    #stanza_error{reason = 'forbidden'} = purge_node(Config, Node),
    #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_node_config(Config, Node, default_node_config(Config)),
    #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]),
    #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_affiliations(Config, Node, [{?config(master, Config), outcast},
					{my_jid(Config), owner}]),
    #stanza_error{reason = 'forbidden'} = delete_node(Config, Node),
    wait_for_master(Config),
    affiliations_slave(Config, get_event(Config));
affiliations_slave(Config, {none, Node, ItemID}) ->
    #ps_subscription{type = subscribed} = subscribe_node(Config, Node),
    ok = unsubscribe_node(Config, Node),
    %% This violates the affiliation char from section 4.1
    [_|_] = get_items(Config, Node),
    #stanza_error{reason = 'forbidden'} = publish_item(Config, Node),
    #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID),
    #stanza_error{reason = 'forbidden'} = purge_node(Config, Node),
    #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_node_config(Config, Node, default_node_config(Config)),
    #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]),
    #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_affiliations(Config, Node, [{?config(master, Config), outcast},
					{my_jid(Config), owner}]),
    #stanza_error{reason = 'forbidden'} = delete_node(Config, Node),
    wait_for_master(Config),
    affiliations_slave(Config, get_event(Config));
affiliations_slave(Config, {member, Node, ItemID}) ->
    #ps_subscription{type = subscribed} = subscribe_node(Config, Node),
    ok = unsubscribe_node(Config, Node),
    [_|_] = get_items(Config, Node),
    #stanza_error{reason = 'forbidden'} = publish_item(Config, Node),
    #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID),
    #stanza_error{reason = 'forbidden'} = purge_node(Config, Node),
    #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_node_config(Config, Node, default_node_config(Config)),
    #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]),
    #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_affiliations(Config, Node, [{?config(master, Config), outcast},
					{my_jid(Config), owner}]),
    #stanza_error{reason = 'forbidden'} = delete_node(Config, Node),
    wait_for_master(Config),
    affiliations_slave(Config, get_event(Config));
affiliations_slave(Config, {publish_only, Node, ItemID}) ->
    #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node),
    #stanza_error{} = unsubscribe_node(Config, Node),
    #stanza_error{reason = 'forbidden'} = get_items(Config, Node),
    #ps_item{id = _MyItemID} = publish_item(Config, Node),
    %% BUG: This should be fixed
    %% ?match(ok, delete_item(Config, Node, MyItemID)),
    #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID),
    #stanza_error{reason = 'forbidden'} = purge_node(Config, Node),
    #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_node_config(Config, Node, default_node_config(Config)),
    #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]),
    #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_affiliations(Config, Node, [{?config(master, Config), outcast},
					{my_jid(Config), owner}]),
    #stanza_error{reason = 'forbidden'} = delete_node(Config, Node),
    wait_for_master(Config),
    affiliations_slave(Config, get_event(Config));
affiliations_slave(Config, {publisher, Node, _ItemID}) ->
    #ps_subscription{type = subscribed} = subscribe_node(Config, Node),
    ok = unsubscribe_node(Config, Node),
    [_|_] = get_items(Config, Node),
    #ps_item{id = MyItemID} = publish_item(Config, Node),
    ok = delete_item(Config, Node, MyItemID),
    %% BUG: this should be fixed
    %% #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID),
    #stanza_error{reason = 'forbidden'} = purge_node(Config, Node),
    #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_node_config(Config, Node, default_node_config(Config)),
    #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]),
    #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node),
    #stanza_error{reason = 'forbidden'} =
	set_affiliations(Config, Node, [{?config(master, Config), outcast},
					{my_jid(Config), owner}]),
    #stanza_error{reason = 'forbidden'} = delete_node(Config, Node),
    wait_for_master(Config),
    affiliations_slave(Config, get_event(Config));
affiliations_slave(Config, {owner, Node, ItemID}) ->
    MyJID = my_jid(Config),
    Peer = ?config(master, Config),
    #ps_subscription{type = subscribed} = subscribe_node(Config, Node),
    ok = unsubscribe_node(Config, Node),
    [_|_] = get_items(Config, Node),
    #ps_item{id = MyItemID} = publish_item(Config, Node),
    ok = delete_item(Config, Node, MyItemID),
    ok = delete_item(Config, Node, ItemID),
    ok = purge_node(Config, Node),
    [_|_] = get_node_config(Config, Node),
    ok = set_node_config(Config, Node, default_node_config(Config)),
    ok = set_subscriptions(Config, Node, []),
    [] = get_subscriptions(Config, Node),
    ok = set_affiliations(Config, Node, [{Peer, outcast}, {MyJID, owner}]),
    [_, _] = get_affiliations(Config, Node),
    ok = delete_node(Config, Node),
    wait_for_master(Config),
    affiliations_slave(Config, get_event(Config));
affiliations_slave(Config, disconnect) ->
    disconnect(Config).

authorize_master(Config) ->
    send(Config, #presence{}),
    #presence{} = recv_presence(Config),
    Peer = ?config(slave, Config),
    PJID = pubsub_jid(Config),
    NodeConfig = set_opts(default_node_config(Config),
			  [{access_model, authorize}]),
    Node = ?config(pubsub_node, Config),
    Node = create_node(Config, Node, NodeConfig),
    wait_for_slave(Config),
    #message{sub_els = [#xdata{fields = F1}]} = recv_message(Config),
    C1 = pubsub_subscribe_authorization:decode(F1),
    Node = proplists:get_value(node, C1),
    Peer = proplists:get_value(subscriber_jid, C1),
    %% Deny it at first
    Deny = #xdata{type = submit,
		  fields = pubsub_subscribe_authorization:encode(
			     [{node, Node},
			      {subscriber_jid, Peer},
			      {allow, false}])},
    send(Config, #message{to = PJID, sub_els = [Deny]}),
    %% We should not have any subscriptions
    [] = get_subscriptions(Config, Node),
    wait_for_slave(Config),
    #message{sub_els = [#xdata{fields = F2}]} = recv_message(Config),
    C2 = pubsub_subscribe_authorization:decode(F2),
    Node = proplists:get_value(node, C2),
    Peer = proplists:get_value(subscriber_jid, C2),
    %% Now we accept is as the peer is very insisting ;)
    Approve = #xdata{type = submit,
		     fields = pubsub_subscribe_authorization:encode(
				[{node, Node},
				 {subscriber_jid, Peer},
				 {allow, true}])},
    send(Config, #message{to = PJID, sub_els = [Approve]}),
    wait_for_slave(Config),
    delete_node(Config, Node),
    disconnect(Config).

authorize_slave(Config) ->
    Node = ?config(pubsub_node, Config),
    MyJID = my_jid(Config),
    wait_for_master(Config),
    #ps_subscription{type = pending} = subscribe_node(Config, Node),
    %% We're denied at first
    #message{
       sub_els =
	   [#ps_event{
	       subscription = #ps_subscription{type = none,
					       jid = MyJID}}]} =
	recv_message(Config),
    wait_for_master(Config),
    #ps_subscription{type = pending} = subscribe_node(Config, Node),
    %% Now much better!
    #message{
       sub_els =
	   [#ps_event{
	       subscription = #ps_subscription{type = subscribed,
					       jid = MyJID}}]} =
	recv_message(Config),
    wait_for_master(Config),
    disconnect(Config).

%%%===================================================================
%%% Internal functions
%%%===================================================================
single_test(T) ->
    list_to_atom("pubsub_" ++ atom_to_list(T)).

master_slave_test(T) ->
    {list_to_atom("pubsub_" ++ atom_to_list(T)), [parallel],
     [list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_master"),
      list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_slave")]}.

set_opts(Config, Options) ->
    lists:foldl(
      fun({Opt, Val}, Acc) ->
	      lists:keystore(Opt, 1, Acc, {Opt, Val})
      end, Config, Options).

create_node(Config, Node) ->
    create_node(Config, Node, undefined).

create_node(Config, Node, Options) ->
    PJID = pubsub_jid(Config),
    NodeConfig = if is_list(Options) ->
			 #xdata{type = submit,
				fields = pubsub_node_config:encode(Options)};
		    true ->
			 undefined
		 end,
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub{create = Node,
					  configure = {<<>>, NodeConfig}}]}) of
	#iq{type = result, sub_els = [#pubsub{create = NewNode}]} ->
	    NewNode;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

delete_node(Config, Node) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}) of
	#iq{type = result, sub_els = []} ->
	    ok;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

purge_node(Config, Node) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub_owner{purge = Node}]}) of
	#iq{type = result, sub_els = []} ->
	    ok;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

get_default_node_config(Config) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = get, to = PJID,
		       sub_els = [#pubsub_owner{default = {<<>>, undefined}}]}) of
	#iq{type = result,
	    sub_els = [#pubsub_owner{default = {<<>>, NodeConfig}}]} ->
	    pubsub_node_config:decode(NodeConfig#xdata.fields);
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

get_node_config(Config, Node) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = get, to = PJID,
		       sub_els = [#pubsub_owner{configure = {Node, undefined}}]}) of
	#iq{type = result,
	    sub_els = [#pubsub_owner{configure = {Node, NodeConfig}}]} ->
	    pubsub_node_config:decode(NodeConfig#xdata.fields);
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

set_node_config(Config, Node, Options) ->
    PJID = pubsub_jid(Config),
    NodeConfig = #xdata{type = submit,
			fields = pubsub_node_config:encode(Options)},
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub_owner{configure =
						    {Node, NodeConfig}}]}) of
	#iq{type = result, sub_els = []} ->
	    ok;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

publish_item(Config, Node) ->
    PJID = pubsub_jid(Config),
    ItemID = p1_rand:get_string(),
    Item = #ps_item{id = ItemID, sub_els = [xmpp:encode(#presence{id = ItemID})]},
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub{publish = #ps_publish{
						       node = Node,
						       items = [Item]}}]}) of
	#iq{type = result,
	    sub_els = [#pubsub{publish = #ps_publish{
					    node = Node,
					    items = [#ps_item{id = ItemID}]}}]} ->
	    Item;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

get_items(Config, Node) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = get, to = PJID,
		       sub_els = [#pubsub{items = #ps_items{node = Node}}]}) of
	#iq{type = result,
	    sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} ->
	    Items;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

delete_item(Config, Node, I) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub{retract =
					      #ps_retract{
						 node = Node,
						 items = [#ps_item{id = I}]}}]}) of
	#iq{type = result, sub_els = []} ->
	    ok;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

subscribe_node(Config, Node) ->
    PJID = pubsub_jid(Config),
    MyJID = my_jid(Config),
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub{subscribe = #ps_subscribe{
							 node = Node,
							 jid = MyJID}}]}) of
	#iq{type = result,
	    sub_els = [#pubsub{
			  subscription = #ps_subscription{
					    node = Node,
					    jid = MyJID} = Sub}]} ->
	    Sub;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

unsubscribe_node(Config, Node) ->
    PJID = pubsub_jid(Config),
    MyJID = my_jid(Config),
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub{
				     unsubscribe = #ps_unsubscribe{
						      node = Node,
						      jid = MyJID}}]}) of
	#iq{type = result, sub_els = []} ->
	    ok;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

get_affiliations(Config) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = get, to = PJID,
		       sub_els = [#pubsub{affiliations = {<<>>, []}}]}) of
	#iq{type = result,
	    sub_els = [#pubsub{affiliations = {<<>>, Affs}}]} ->
	    Affs;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

get_affiliations(Config, Node) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = get, to = PJID,
		       sub_els = [#pubsub_owner{affiliations = {Node, []}}]}) of
	#iq{type = result,
	    sub_els = [#pubsub_owner{affiliations = {Node, Affs}}]} ->
	    Affs;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

set_affiliations(Config, Node, JTs) ->
    PJID = pubsub_jid(Config),
    Affs = [#ps_affiliation{jid = J, type = T} || {J, T} <- JTs],
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub_owner{affiliations =
						    {Node, Affs}}]}) of
	#iq{type = result, sub_els = []} ->
	    ok;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

get_subscriptions(Config) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = get, to = PJID,
		       sub_els = [#pubsub{subscriptions = {<<>>, []}}]}) of
	#iq{type = result, sub_els = [#pubsub{subscriptions = {<<>>, Subs}}]} ->
	    Subs;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

get_subscriptions(Config, Node) ->
    PJID = pubsub_jid(Config),
    case send_recv(Config,
		   #iq{type = get, to = PJID,
		       sub_els = [#pubsub_owner{subscriptions = {Node, []}}]}) of
	#iq{type = result,
	    sub_els = [#pubsub_owner{subscriptions = {Node, Subs}}]} ->
	    Subs;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

set_subscriptions(Config, Node, JTs) ->
    PJID = pubsub_jid(Config),
    Subs = [#ps_subscription{jid = J, type = T} || {J, T} <- JTs],
    case send_recv(Config,
		   #iq{type = set, to = PJID,
		       sub_els = [#pubsub_owner{subscriptions =
						    {Node, Subs}}]}) of
	#iq{type = result, sub_els = []} ->
	    ok;
	#iq{type = error} = IQ ->
	    xmpp:get_subtag(IQ, #stanza_error{})
    end.

default_node_config(Config) ->
    [{title, ?config(pubsub_node_title, Config)},
     {notify_delete, false},
     {send_last_published_item, never}].