1: -module(muc_light_legacy_SUITE).
    2: 
    3: -include_lib("escalus/include/escalus.hrl").
    4: -include_lib("escalus/include/escalus_xmlns.hrl").
    5: -include_lib("common_test/include/ct.hrl").
    6: -include_lib("exml/include/exml.hrl").
    7: 
    8: -export([
    9:          disco_service/1,
   10:          disco_features/1,
   11:          disco_features_with_mam/1,
   12:          disco_info/1,
   13:          disco_info_with_mam/1,
   14:          disco_rooms/1,
   15:          disco_rooms_rsm/1,
   16:          unauthorized_stanza/1
   17:         ]).
   18: -export([
   19:          send_message/1,
   20:          change_subject/1,
   21:          all_can_configure/1,
   22:          set_config_deny/1,
   23:          get_room_config/1,
   24:          get_room_occupants/1,
   25:          leave_room/1,
   26:          change_other_aff_deny/1
   27:         ]).
   28: -export([
   29:          create_room/1,
   30:          create_room_with_equal_occupants/1,
   31:          create_existing_room_deny/1,
   32:          destroy_room/1,
   33:          set_config/1,
   34:          assorted_config_doesnt_lead_to_duplication/1,
   35:          remove_and_add_users/1,
   36:          explicit_owner_change/1,
   37:          implicit_owner_change/1,
   38:          edge_case_owner_change/1
   39:         ]).
   40: -export([
   41:          manage_blocklist/1,
   42:          block_room/1,
   43:          block_user/1,
   44:          blocking_disabled/1
   45:         ]).
   46: 
   47: -export([all/0, groups/0, suite/0,
   48:          init_per_suite/1, end_per_suite/1,
   49:          init_per_group/2, end_per_group/2,
   50:          init_per_testcase/2, end_per_testcase/2]).
   51: 
   52: -import(escalus_ejabberd, [rpc/3]).
   53: -import(muc_helper, [foreach_occupant/3, foreach_recipient/2]).
   54: -import(distributed_helper, [subhost_pattern/1]).
   55: -import(domain_helper, [host_type/0, domain/0]).
   56: -import(muc_light_helper, [
   57:                            bin_aff_users/1,
   58:                            to_lus/2,
   59:                            lbin/1,
   60:                            create_room/6,
   61:                            default_config/1,
   62:                            default_schema/0
   63:                           ]).
   64: -import(config_parser_helper, [mod_config/2]).
   65: 
   66: -define(ROOM, <<"testroom">>).
   67: -define(ROOM2, <<"testroom2">>).
   68: 
   69: -define(NS_MUC_LIGHT, <<"urn:xmpp:muclight:0">>).
   70: -define(NS_MUC_ROOMCONFIG, <<"http://jabber.org/protocol/muc#roomconfig">>).
   71: 
   72: -define(MUCHOST, (muc_helper:muc_host())).
   73: 
   74: -type ct_aff_user() :: {EscalusClient :: escalus:client(), Aff :: atom()}.
   75: -type ct_aff_users() :: [ct_aff_user()].
   76: -type ct_block_item() :: {What :: atom(), Action :: atom(), Who :: binary()}.
   77: -type verify_fun() :: muc_helper:verify_fun().
   78: -type xmlel() :: exml:element().
   79: 
   80: -define(DEFAULT_AFF_USERS, [{Alice, owner}, {Bob, member}, {Kate, member}]).
   81: 
   82: %%--------------------------------------------------------------------
   83: %% Suite configuration
   84: %%--------------------------------------------------------------------
   85: 
   86: all() ->
   87:     [
   88:      {group, entity},
   89:      {group, occupant},
   90:      {group, owner},
   91:      {group, blocking}
   92:     ].
   93: 
   94: groups() ->
   95:     G = [
   96:          {entity, [sequence], [
   97:                                disco_service,
   98:                                disco_features,
   99:                                disco_features_with_mam,
  100:                                disco_info,
  101:                                disco_info_with_mam,
  102:                                disco_rooms,
  103:                                disco_rooms_rsm,
  104:                                unauthorized_stanza
  105:                               ]},
  106:          {occupant, [sequence], [
  107:                                  send_message,
  108:                                  change_subject,
  109:                                  all_can_configure,
  110:                                  set_config_deny,
  111:                                  get_room_config,
  112:                                  get_room_occupants,
  113:                                  leave_room,
  114:                                  change_other_aff_deny
  115:                                 ]},
  116:          {owner, [sequence], [
  117:                               create_room,
  118:                               create_room_with_equal_occupants,
  119:                               create_existing_room_deny,
  120:                               destroy_room,
  121:                               set_config,
  122:                               assorted_config_doesnt_lead_to_duplication,
  123:                               remove_and_add_users,
  124:                               explicit_owner_change,
  125:                               implicit_owner_change,
  126:                               edge_case_owner_change
  127:                              ]},
  128:          {blocking, [sequence], [
  129:                                  manage_blocklist,
  130:                                  block_room,
  131:                                  block_user,
  132:                                  blocking_disabled
  133:                                 ]}
  134:         ],
  135:     ct_helper:repeat_all_until_all_ok(G).
  136: 
  137: suite() ->
  138:     escalus:suite().
  139: 
  140: %%--------------------------------------------------------------------
  141: %% Init & teardown
  142: %%--------------------------------------------------------------------
  143: 
  144: init_per_suite(Config) ->
  145:     Config1 = dynamic_modules:save_modules(host_type(), Config),
  146:     Config2 = escalus:init_per_suite(Config1),
  147:     escalus:create_users(Config2, escalus:get_users([alice, bob, kate, mike])).
  148: 
  149: end_per_suite(Config) ->
  150:     clear_db(),
  151:     Config1 = escalus:delete_users(Config, escalus:get_users([alice, bob, kate, mike])),
  152:     dynamic_modules:restore_modules(Config),
  153:     escalus:end_per_suite(Config1).
  154: 
  155: init_per_group(_GroupName, Config) ->
  156:     Config.
  157: 
  158: end_per_group(_GroupName, Config) ->
  159:     Config.
  160: 
  161: init_per_testcase(CaseName, Config) when CaseName =:= disco_rooms_rsm;
  162:                                          CaseName =:= block_room;
  163:                                          CaseName =:= block_user ->
  164:     dynamic_modules:ensure_modules(host_type(), required_modules(CaseName)),
  165:     create_room(?ROOM, ?MUCHOST, alice, [bob, kate], Config),
  166:     create_room(?ROOM2, ?MUCHOST, alice, [kate], Config),
  167:     escalus:init_per_testcase(CaseName, Config);
  168: init_per_testcase(create_existing_room_deny = CaseName, Config) ->
  169:     dynamic_modules:ensure_modules(host_type(), required_modules(CaseName)),
  170:     create_room(?ROOM, ?MUCHOST, alice, [], Config),
  171:     escalus:init_per_testcase(CaseName, Config);
  172: init_per_testcase(CaseName, Config) when CaseName =:= disco_features_with_mam;
  173:                                          CaseName =:= disco_info_with_mam ->
  174:     case mam_helper:backend() of
  175:         disabled ->
  176:             {skip, "No MAM backend available"};
  177:         Backend ->
  178:             dynamic_modules:ensure_modules(host_type(), required_modules(CaseName, Backend)),
  179:             create_room(?ROOM, ?MUCHOST, alice, [bob, kate], Config),
  180:             escalus:init_per_testcase(CaseName, Config)
  181:     end;
  182: init_per_testcase(CaseName, Config) ->
  183:     dynamic_modules:ensure_modules(host_type(), required_modules(CaseName)),
  184:     create_room(?ROOM, ?MUCHOST, alice, [bob, kate], Config),
  185:     escalus:init_per_testcase(CaseName, Config).
  186: 
  187: end_per_testcase(CaseName, Config) ->
  188:     clear_db(),
  189:     escalus:end_per_testcase(CaseName, Config).
  190: 
  191: %% Module configuration per test case
  192: 
  193: required_modules(CaseName, MAMBackend) ->
  194:     [{mod_mam_meta, mam_helper:config_opts(#{backend => MAMBackend,
  195:                                              muc => #{host => subhost_pattern(?MUCHOST)}})} |
  196:      common_required_modules(CaseName)].
  197: 
  198: required_modules(CaseName) ->
  199:     [{mod_mam_meta, stopped} | common_required_modules(CaseName)].
  200: 
  201: common_required_modules(CaseName) ->
  202:     Opts = maps:merge(common_muc_light_opts(), muc_light_opts(CaseName)),
  203:     [{mod_muc_light, mod_config(mod_muc_light, Opts)}].
  204: 
  205: muc_light_opts(disco_rooms_rsm) ->
  206:     #{rooms_per_page => 1};
  207: muc_light_opts(all_can_configure) ->
  208:     #{all_can_configure => true};
  209: muc_light_opts(create_room_with_equal_occupants) ->
  210:     #{equal_occupants => true};
  211: muc_light_opts(block_user) ->
  212:     #{all_can_invite => true};
  213: muc_light_opts(blocking_disabled) ->
  214:     #{blocking => false};
  215: muc_light_opts(_) ->
  216:     #{}.
  217: 
  218: common_muc_light_opts() ->
  219:     #{host => subhost_pattern(muc_helper:muc_host_pattern()),
  220:       backend => mongoose_helper:mnesia_or_rdbms_backend(),
  221:       legacy_mode => true}.
  222: 
  223: %% ---------------------- Helpers ----------------------
  224: 
  225: create_room(RoomU, MUCHost, Owner, Members, Config) ->
  226:     create_room(RoomU, MUCHost, Owner, Members, Config, <<"-">>).
  227: 
  228: clear_db() ->
  229:     muc_light_helper:clear_db(host_type()).
  230: 
  231: %%--------------------------------------------------------------------
  232: %% MUC light tests
  233: %%--------------------------------------------------------------------
  234: 
  235: %% ---------------------- Disco ----------------------
  236: 
  237: disco_service(Config) ->
  238:     muc_helper:disco_service_story(Config).
  239: 
  240: disco_features(Config) ->
  241:     muc_helper:disco_features_story(Config, [?NS_MUC]).
  242: 
  243: disco_features_with_mam(Config) ->
  244:     muc_helper:disco_features_story(Config, [?NS_MUC] ++ mam_helper:namespaces()).
  245: 
  246: disco_info(Config) ->
  247:     muc_helper:disco_info_story(Config, [?NS_MUC]).
  248: 
  249: disco_info_with_mam(Config) ->
  250:     muc_helper:disco_info_story(Config, [?NS_MUC] ++ mam_helper:namespaces()).
  251: 
  252: disco_rooms(Config) ->
  253:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  254:             {ok, {?ROOM2, _}} = create_room(?ROOM2, ?MUCHOST, kate, [], Config),
  255:             DiscoStanza = escalus_stanza:to(escalus_stanza:iq_get(?NS_DISCO_ITEMS, []), ?MUCHOST),
  256:             escalus:send(Alice, DiscoStanza),
  257:             %% we should get 1 room, Alice is not in the second one
  258:             Stanza = escalus:wait_for_stanza(Alice),
  259:             [Item] = exml_query:paths(Stanza, [{element, <<"query">>}, {element, <<"item">>}]),
  260:             ProperJID = room_bin_jid(?ROOM),
  261:             ProperJID = exml_query:attr(Item, <<"jid">>),
  262:             escalus:assert(is_stanza_from, [?MUCHOST], Stanza)
  263:         end).
  264: 
  265: disco_rooms_rsm(Config) ->
  266:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  267:             DiscoStanza = escalus_stanza:to(escalus_stanza:iq_get(?NS_DISCO_ITEMS, []), ?MUCHOST),
  268:             escalus:send(Alice, DiscoStanza),
  269:             %% we should get 1 room with RSM info
  270:             Stanza = escalus:wait_for_stanza(Alice),
  271:             [Item] = exml_query:paths(Stanza, [{element, <<"query">>}, {element, <<"item">>}]),
  272:             ProperJID = room_bin_jid(?ROOM),
  273:             ProperJID = exml_query:attr(Item, <<"jid">>),
  274: 
  275:             RSM = #xmlel{ name = <<"set">>,
  276:                           attrs = [{<<"xmlns">>, ?NS_RSM}],
  277:                           children = [ #xmlel{ name = <<"max">>,
  278:                                                children = [#xmlcdata{ content = <<"10">> }] },
  279:                                        #xmlel{ name = <<"before">> } ]  },
  280:             DiscoStanza2 = escalus_stanza:to(
  281:                              escalus_stanza:iq_get(?NS_DISCO_ITEMS, [RSM]), ?MUCHOST),
  282:             escalus:send(Alice, DiscoStanza2),
  283:             %% we should get second room
  284:             Stanza2 = escalus:wait_for_stanza(Alice),
  285:             [Item2] = exml_query:paths(Stanza2, [{element, <<"query">>}, {element, <<"item">>}]),
  286:             ProperJID2 = room_bin_jid(?ROOM2),
  287:             ProperJID2 = exml_query:attr(Item2, <<"jid">>),
  288: 
  289:             BadAfter = #xmlel{ name = <<"after">>,
  290:                                children = [#xmlcdata{ content = <<"oops@", (?MUCHOST)/binary>> }] },
  291:             RSM2 = #xmlel{ name = <<"set">>,
  292:                           attrs = [{<<"xmlns">>, ?NS_RSM}],
  293:                           children = [ #xmlel{ name = <<"max">>,
  294:                                                children = [#xmlcdata{ content = <<"10">> }] },
  295:                                        BadAfter ]  },
  296:             DiscoStanza3 = escalus_stanza:to(
  297:                              escalus_stanza:iq_get(?NS_DISCO_ITEMS, [RSM2]), ?MUCHOST),
  298:             escalus:send(Alice, DiscoStanza3),
  299:             escalus:assert(is_error, [<<"cancel">>, <<"item-not-found">>],
  300:                            escalus:wait_for_stanza(Alice))
  301:         end).
  302: 
  303: unauthorized_stanza(Config) ->
  304:     escalus:story(Config, [{alice, 1}, {kate, 1}], fun(Alice, Kate) ->
  305:             {ok, {?ROOM2, _}} = create_room(?ROOM2, ?MUCHOST, kate, [], Config),
  306:             MsgStanza = escalus_stanza:groupchat_to(room_bin_jid(?ROOM2), <<"malicious">>),
  307:             escalus:send(Alice, MsgStanza),
  308:             escalus:assert(is_error, [<<"cancel">>, <<"item-not-found">>],
  309:                            escalus:wait_for_stanza(Alice)),
  310:             verify_no_stanzas([Alice, Kate])
  311:         end).
  312: 
  313: %% ---------------------- Occupant ----------------------
  314: 
  315: send_message(Config) ->
  316:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  317:             Msg = <<"Heyah!">>,
  318:             Id = <<"LegacyId">>,
  319:             Stanza = escalus_stanza:set_id(
  320:                        escalus_stanza:groupchat_to(room_bin_jid(?ROOM), Msg), Id),
  321:             foreach_occupant([Alice, Bob, Kate], Stanza, gc_message_verify_fun(?ROOM, Msg, Id))
  322:         end).
  323: 
  324: change_subject(Config) ->
  325:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  326:             Subject = <<"new subject">>,
  327:             SubjectStanza = #xmlel{name = <<"message">>,
  328:                                    attrs = [{<<"type">>, <<"groupchat">>}],
  329:                                    children = [#xmlel{
  330:                                                   name = <<"subject">>,
  331:                                                   children = [#xmlcdata{content = Subject}]
  332:                                                  }]
  333:                                   },
  334:             foreach_occupant([Alice, Bob, Kate],
  335:                              escalus_stanza:to(SubjectStanza, room_bin_jid(?ROOM)),
  336:                              subject_message_verify_fun(?ROOM, Subject))
  337:         end).
  338: 
  339: all_can_configure(Config) ->
  340:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  341:             ConfigChange = [{<<"roomname">>, <<"new subject">>}],
  342:             Stanza = stanza_config_set(?ROOM, ConfigChange),
  343:             foreach_occupant([Alice, Bob, Kate], Stanza, config_msg_verify_fun())
  344:         end).
  345: 
  346: set_config_deny(Config) ->
  347:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  348:             ConfigChange = [{<<"roomname">>, <<"new subject">>}],
  349:             Stanza = stanza_config_set(?ROOM, ConfigChange),
  350:             escalus:send(Kate, Stanza),
  351:             escalus:assert(is_error, [<<"cancel">>, <<"not-allowed">>],
  352:                            escalus:wait_for_stanza(Kate)),
  353:             verify_no_stanzas([Alice, Bob, Kate])
  354:         end).
  355: 
  356: get_room_config(Config) ->
  357:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  358:             Stanza = stanza_config_get(?ROOM),
  359:             ConfigKV = default_config(default_schema()),
  360:             foreach_occupant([Alice, Bob, Kate], Stanza, config_iq_verify_fun(ConfigKV))
  361:         end).
  362: 
  363: get_room_occupants(Config) ->
  364:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  365:             AffUsers = [{Alice, owner}, {Bob, member}, {Kate, member}],
  366:             foreach_occupant([Alice, Bob, Kate], stanza_aff_get(?ROOM), aff_iq_verify_fun(AffUsers))
  367:         end).
  368: 
  369: leave_room(Config) ->
  370:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  371:             % Users will leave one by one, owner last
  372:             lists:foldr(
  373:               fun(User, {Occupants, Outsiders}) ->
  374:                       NewOccupants = lists:keydelete(User, 1, Occupants),
  375:                       user_leave(User, [ U || {U, _} <- NewOccupants]),
  376:                       verify_no_stanzas(Outsiders),
  377:                       {NewOccupants, [User | Outsiders]}
  378:               end, {?DEFAULT_AFF_USERS, []}, [Alice, Bob, Kate])
  379:         end).
  380: 
  381: change_other_aff_deny(Config) ->
  382:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}, {mike, 1}],
  383:                   fun(Alice, Bob, Kate, Mike) ->
  384:             AffUsersChanges1 = [{Bob, none}],
  385:             escalus:send(Kate, stanza_aff_set(?ROOM, AffUsersChanges1)),
  386:             escalus:assert(is_error, [<<"cancel">>, <<"not-allowed">>],
  387:                            escalus:wait_for_stanza(Kate)),
  388: 
  389:             AffUsersChanges2 = [{Alice, member}, {Kate, owner}],
  390:             escalus:send(Kate, stanza_aff_set(?ROOM, AffUsersChanges2)),
  391:             escalus:assert(is_error, [<<"cancel">>, <<"not-allowed">>],
  392:                            escalus:wait_for_stanza(Kate)),
  393: 
  394:             AffUsersChanges3 = [{Mike, member}],
  395:             escalus:send(Kate, stanza_aff_set(?ROOM, AffUsersChanges3)),
  396:             escalus:assert(is_error, [<<"cancel">>, <<"not-allowed">>],
  397:                            escalus:wait_for_stanza(Kate)),
  398: 
  399:             verify_no_stanzas([Alice, Bob, Kate, Mike])
  400:         end).
  401: 
  402: %% ---------------------- owner ----------------------
  403: 
  404: create_room(Config) ->
  405:     escalus:story(Config, [{bob, 1}], fun(Bob) ->
  406:             escalus:send(Bob, stanza_create_room(<<"bobroom">>, Bob)),
  407:             Result = escalus:wait_for_stanza(Bob),
  408:             presence_verify(Bob, owner, Result)
  409:         end).
  410: 
  411: create_room_with_equal_occupants(Config) ->
  412:     escalus:story(Config, [{bob, 1}], fun(Bob) ->
  413:             escalus:send(Bob, stanza_create_room(<<"bobroom">>, Bob)),
  414:             Result = escalus:wait_for_stanza(Bob),
  415:             presence_verify(Bob, member, Result)
  416:         end).
  417: 
  418: create_existing_room_deny(Config) ->
  419:     escalus:story(Config, [{bob, 1}], fun(Bob) ->
  420:             escalus:send(Bob, stanza_create_room(?ROOM, Bob)),
  421:             Result = escalus:wait_for_stanza(Bob),
  422:             escalus:assert(is_presence_with_type, [<<"error">>], Result),
  423:             escalus:assert(is_error, [<<"auth">>, <<"registration-required">>], Result),
  424:             X = exml_query:subelement(Result, <<"x">>),
  425:             ?NS_MUC = exml_query:attr(X, <<"xmlns">>)
  426:         end).
  427: 
  428: destroy_room(Config) ->
  429:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  430:             escalus:send(Alice, stanza_destroy_room(?ROOM)),
  431:             AffUsersChanges = [{Alice, none}, {Bob, none}, {Kate, none}],
  432:             verify_aff_bcast([], AffUsersChanges, [], Alice),
  433:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice))
  434:         end).
  435: 
  436: set_config(Config) ->
  437:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  438:             ConfigChange = [{<<"roomname">>, <<"The Coven">>}],
  439:             escalus:send(Alice, stanza_config_set(?ROOM, ConfigChange)),
  440:             foreach_recipient([Alice, Bob, Kate], config_msg_verify_fun()),
  441:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  442:             Stanza = stanza_config_get(?ROOM),
  443:             foreach_occupant([Alice, Bob, Kate], Stanza, config_iq_verify_fun(ConfigChange))
  444:         end).
  445: 
  446: assorted_config_doesnt_lead_to_duplication(Config) ->
  447:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  448:             ConfigChange = [{<<"subject">>, <<"Elixirs">>},
  449:                             {<<"roomname">>, <<"The Coven">>},
  450:                             {<<"subject">>, <<"Elixirs">>}],
  451:             escalus:send(Alice, stanza_config_set(?ROOM, ConfigChange)),
  452:             foreach_recipient([Alice, Bob, Kate], config_msg_verify_fun()),
  453:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  454: 
  455:             Stanza = stanza_config_get(?ROOM),
  456:             VerifyFun = fun(Incoming) ->
  457:                                 Fields = exml_query:paths(Incoming, [{element, <<"query">>},
  458:                                                                      {element, <<"x">>},
  459:                                                                      {element, <<"field">>}]),
  460:                                 ConfigKV = [{exml_query:attr(F, <<"var">>),
  461:                                              exml_query:path(F, [{element, <<"value">>}, cdata])}
  462:                                             || F <- Fields],
  463:                                 Length = length(ConfigKV),
  464:                                 Length = length(lists:ukeysort(1, ConfigKV))
  465:                         end,
  466:             foreach_occupant([Alice, Bob, Kate], Stanza, VerifyFun)
  467:          end).
  468: 
  469: remove_and_add_users(Config) ->
  470:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  471:             AffUsersChanges1 = [{Bob, none}, {Kate, none}],
  472:             escalus:send(Alice, stanza_aff_set(?ROOM, AffUsersChanges1)),
  473:             verify_aff_bcast([Alice], AffUsersChanges1, [], Alice),
  474:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  475:             AffUsersChanges2 = [{Bob, member}, {Kate, member}],
  476:             escalus:send(Alice, stanza_aff_set(?ROOM, AffUsersChanges2)),
  477:             verify_aff_bcast([Alice, Bob, Kate], AffUsersChanges2, [Bob, Kate], Alice),
  478:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice))
  479:         end).
  480: 
  481: explicit_owner_change(Config) ->
  482:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  483:             AffUsersChanges1 = [{Bob, none}, {Alice, none}, {Kate, owner}],
  484:             escalus:send(Alice, stanza_aff_set(?ROOM, AffUsersChanges1)),
  485:             verify_aff_bcast([Kate], AffUsersChanges1, [], Alice),
  486:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice))
  487:         end).
  488: 
  489: implicit_owner_change(Config) ->
  490:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  491:             AffUsersChanges1 = [{Bob, none}, {Alice, member}],
  492:             escalus:send(Alice, stanza_aff_set(?ROOM, AffUsersChanges1)),
  493:             verify_aff_bcast([Kate, Alice], [{Kate, owner} | AffUsersChanges1], [], Alice),
  494:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice))
  495:         end).
  496: 
  497: edge_case_owner_change(Config) ->
  498:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  499:             AffUsersChanges1 = [{Alice, member}, {Bob, none}, {Kate, none}],
  500:             escalus:send(Alice, stanza_aff_set(?ROOM, AffUsersChanges1)),
  501:             verify_aff_bcast([Alice], [{Kate, none}, {Bob, none}], [], Alice),
  502:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice))
  503:         end).
  504: 
  505: %% ---------------------- blocking ----------------------
  506: 
  507: manage_blocklist(Config) ->
  508:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  509:             escalus:send(Alice, stanza_blocking_get()),
  510:             GetResult1 = escalus:wait_for_stanza(Alice),
  511:             escalus:assert(is_iq_result, GetResult1),
  512:             QueryEl1 = exml_query:subelement(GetResult1, <<"query">>),
  513:             verify_blocklist(QueryEl1, []),
  514:             Domain = domain(),
  515:             BlocklistChange1 = [{user, deny, <<"user@", Domain/binary>>},
  516:                                 {room, deny, room_bin_jid(?ROOM)}],
  517:             escalus:send(Alice, stanza_blocking_set(BlocklistChange1)),
  518:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  519:             escalus:send(Alice, stanza_blocking_get()),
  520:             GetResult2 = escalus:wait_for_stanza(Alice),
  521:             escalus:assert(is_iq_result, GetResult2),
  522:             QueryEl2 = exml_query:subelement(GetResult2, <<"query">>),
  523:             verify_blocklist(QueryEl2, BlocklistChange1),
  524: 
  525:             BlocklistChange2 = [{user, allow, <<"user@", Domain/binary>>},
  526:                                 {room, allow, room_bin_jid(?ROOM)}],
  527:             escalus:send(Alice, stanza_blocking_set(BlocklistChange2)),
  528:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  529:             escalus:send(Alice, stanza_blocking_get()),
  530:             GetResult3 = escalus:wait_for_stanza(Alice),
  531:             escalus:assert(is_iq_result, GetResult3),
  532:             % Match below checks for empty list
  533:             QueryEl1 = exml_query:subelement(GetResult3, <<"query">>)
  534:         end).
  535: 
  536: block_room(Config) ->
  537:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  538:             BlocklistChange = [{room, deny, room_bin_jid(?ROOM)}],
  539:             escalus:send(Bob, stanza_blocking_set(BlocklistChange)),
  540:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  541:             user_leave(Bob, [Alice, Kate]),
  542: 
  543:             % Alice tries to readd Bob to the room but fails
  544:             BobReadd = [{Bob, member}],
  545:             escalus:send(Alice, stanza_aff_set(?ROOM, BobReadd)),
  546:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  547:             verify_no_stanzas([Alice, Bob, Kate]),
  548: 
  549:             % But Alice can add Bob to another room!
  550:             escalus:send(Alice, stanza_aff_set(?ROOM2, BobReadd)),
  551:             verify_aff_bcast([Alice, Bob, Kate], BobReadd, [Bob], Alice),
  552:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice))
  553:         end).
  554: 
  555: block_user(Config) ->
  556:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  557:             AliceJIDBin = lbin(escalus_client:short_jid(Alice)),
  558:             BlocklistChange = [{user, deny, AliceJIDBin}],
  559:             escalus:send(Bob, stanza_blocking_set(BlocklistChange)),
  560:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  561: 
  562:             % Alice tries to add Bob to the room but fails
  563:             BobAdd = [{Bob, member}],
  564:             escalus:send(Alice, stanza_aff_set(?ROOM2, BobAdd)),
  565:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  566:             verify_no_stanzas([Alice, Bob, Kate]),
  567: 
  568:             % But Kate can add Bob to the room!
  569:             escalus:send(Kate, stanza_aff_set(?ROOM2, BobAdd)),
  570:             verify_aff_bcast([Alice, Bob, Kate], BobAdd, [Bob], Kate),
  571:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Kate)),
  572:             verify_no_stanzas([Alice, Bob, Kate])
  573:         end).
  574: 
  575: blocking_disabled(Config) ->
  576:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  577:             escalus:send(Alice, stanza_blocking_get()),
  578:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  579:                            escalus:wait_for_stanza(Alice)),
  580:             Domain = domain(),
  581:             BlocklistChange1 = [{user, deny, <<"user@", Domain/binary>>},
  582:                                 {room, deny, room_bin_jid(?ROOM)}],
  583:             escalus:send(Alice, stanza_blocking_set(BlocklistChange1)),
  584:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  585:                            escalus:wait_for_stanza(Alice))
  586:         end).
  587: 
  588: %%--------------------------------------------------------------------
  589: %% Subroutines
  590: %%--------------------------------------------------------------------
  591: 
  592: -spec user_leave(User :: escalus:client(), RemainingOccupants :: [escalus:client()]) -> ok.
  593: user_leave(User, RemainingOccupants) ->
  594:     AffUsersChanges = [{User, none}],
  595:     Stanza = stanza_aff_set(?ROOM, AffUsersChanges),
  596:     escalus:send(User, Stanza),
  597:     verify_aff_bcast(RemainingOccupants, AffUsersChanges, [], User),
  598:     escalus:assert(is_iq_result, escalus:wait_for_stanza(User)).
  599: 
  600: %%--------------------------------------------------------------------
  601: %% IQ getters
  602: %%--------------------------------------------------------------------
  603: 
  604: -spec stanza_blocking_get() -> xmlel().
  605: stanza_blocking_get() ->
  606:     escalus_stanza:privacy_get_lists([?NS_MUC_LIGHT]).
  607: 
  608: -spec stanza_config_get(Room :: binary()) -> xmlel().
  609: stanza_config_get(Room) ->
  610:     escalus_stanza:to(
  611:       escalus_stanza:iq_get(?NS_MUC_OWNER, []), room_bin_jid(Room)).
  612: 
  613: -spec stanza_aff_get(Room :: binary()) -> xmlel().
  614: stanza_aff_get(Room) ->
  615:     escalus_stanza:to(
  616:       escalus_stanza:iq_get(?NS_MUC_ADMIN, []), room_bin_jid(Room)).
  617: 
  618: %%--------------------------------------------------------------------
  619: %% IQ setters
  620: %%--------------------------------------------------------------------
  621: 
  622: -spec stanza_blocking_set(BlocklistChanges :: [ct_block_item()]) -> xmlel().
  623: stanza_blocking_set(BlocklistChanges) ->
  624:     Items = [ encode_privacy_item(What, Action, Who) || {What, Action, Who} <- BlocklistChanges ],
  625:     escalus_stanza:privacy_set_list(escalus_stanza:privacy_list(?NS_MUC_LIGHT, Items)).
  626: 
  627: encode_privacy_item(What, Action, Who) ->
  628:     Value = case What of
  629:                 room -> Who;
  630:                 user -> <<(?MUCHOST)/binary, $/, Who/binary>>
  631:             end,
  632:     ActionBin = atom_to_binary(Action, utf8),
  633:     escalus_stanza:privacy_list_item(<<"1">>, ActionBin, <<"jid">>, Value, []).
  634: 
  635: -spec stanza_create_room(RoomNode :: binary(), Creator :: escalus:client()) -> xmlel().
  636: stanza_create_room(RoomNode, Creator) ->
  637:     ToBinJID = <<(room_bin_jid(RoomNode))/binary, $/,
  638:                  (lbin(escalus_client:short_jid(Creator)))/binary>>,
  639:     X = #xmlel{ name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_MUC}] },
  640:     escalus_stanza:to(escalus_stanza:presence(<<"available">>, [X]), ToBinJID).
  641: 
  642: -spec stanza_destroy_room(Room :: binary()) -> xmlel().
  643: stanza_destroy_room(Room) ->
  644:     escalus_stanza:to(escalus_stanza:iq_set(?NS_MUC_OWNER, [#xmlel{ name = <<"destroy">> }]),
  645:                       room_bin_jid(Room)).
  646: 
  647: -spec stanza_config_set(Room :: binary(), ConfigChanges :: [muc_light_helper:config_item()]) -> xmlel().
  648: stanza_config_set(Room, ConfigChanges) ->
  649:     IQ = escalus_stanza:iq_set(?NS_MUC_OWNER, [form_x_el(ConfigChanges)]),
  650:     escalus_stanza:to(IQ, room_bin_jid(Room)).
  651: 
  652: -spec form_x_el(Fields :: [xmlel()]) -> xmlel().
  653: form_x_el(Fields) ->
  654:     #xmlel{
  655:        name = <<"x">>,
  656:        attrs = [{<<"xmlns">>, <<"jabber:x:data">>}, {<<"type">>, <<"submit">>}],
  657:        children = [form_field(<<"FORM_TYPE">>, ?NS_MUC_ROOMCONFIG, <<"hidden">>)
  658:                    | [form_field(K, V, <<"text-single">>) || {K, V} <- Fields ]]
  659:       }.
  660: 
  661: -spec form_field(Var :: binary(), Value :: binary(), Type :: binary()) -> xmlel().
  662: form_field(Var, Value, Type) ->
  663:     #xmlel{ name  = <<"field">>,
  664:             attrs = [{<<"type">>, Type}, {<<"var">>, Var}],
  665:             children  = [#xmlel{name = <<"value">>,
  666:                                 children = [#xmlcdata{content = Value}] }] }.
  667: 
  668: -spec stanza_aff_set(Room :: binary(), AffUsers :: ct_aff_users()) -> xmlel().
  669: stanza_aff_set(Room, AffUsers) ->
  670:     Items = [#xmlel{ name = <<"item">>, attrs = [{<<"affiliation">>, AffBin},
  671:                                                  {<<"jid">>, UserBin}] }
  672:              || {UserBin, AffBin} <- bin_aff_users(AffUsers)],
  673:     escalus_stanza:to(escalus_stanza:iq_set(?NS_MUC_ADMIN, Items), room_bin_jid(Room)).
  674: 
  675: %%--------------------------------------------------------------------
  676: %% Verifiers
  677: %%--------------------------------------------------------------------
  678: 
  679: -spec verify_blocklist(Query :: xmlel(), ProperBlocklist :: [ct_block_item()]) -> [].
  680: verify_blocklist(Query, ProperBlocklist) ->
  681:     ?NS_PRIVACY = exml_query:attr(Query, <<"xmlns">>),
  682:     RawItems = exml_query:paths(Query, [{element, <<"list">>}, {element, <<"item">>}]),
  683:     Blocklist = [ parse_blocked_item(Item) || Item <- RawItems ],
  684:     ProperBlocklistLen = length(ProperBlocklist),
  685:     ProperBlocklistLen = length(Blocklist),
  686:     [] = lists:foldl(fun lists:delete/2, Blocklist, ProperBlocklist).
  687: 
  688: -spec parse_blocked_item(Item :: xmlel()) -> ct_block_item().
  689: parse_blocked_item(Item) ->
  690:     <<"deny">> = exml_query:attr(Item, <<"action">>),
  691:     <<"jid">> = exml_query:attr(Item, <<"type">>),
  692:     Value = exml_query:attr(Item, <<"value">>),
  693:     MucHost = ?MUCHOST,
  694:     case binary:split(Value, <<"/">>) of
  695:         [MucHost, User] -> {user, deny, User};
  696:         [Room] -> {room, deny, Room};
  697:         Other ->
  698:             CfgHost = rpc(gen_mod, get_module_opt, [host_type(), mod_muc_light, host, undefined]),
  699:             ct:fail(#{what => parse_blocked_item_failed,
  700:                       muc_host => MucHost, other => Other,
  701:                       cfg_host => CfgHost})
  702:     end.
  703: 
  704: -spec verify_aff_bcast(CurrentOccupants :: [escalus:client()], AffUsersChanges :: ct_aff_users(),
  705:                        Newcomers :: [escalus:client()], Changer :: escalus:client()) -> ok.
  706: verify_aff_bcast(CurrentOccupants, AffUsersChanges, Newcomers, Changer) ->
  707:     MucHost = ?MUCHOST,
  708:     PredList = [ presence_verify_fun(AffUser) || AffUser <- AffUsersChanges ],
  709:     lists:foreach(
  710:       fun(Occupant) ->
  711:               case lists:member(Occupant, Newcomers) of
  712:                   false ->
  713:                       Stanzas = escalus:wait_for_stanzas(Occupant, length(PredList)),
  714:                       escalus_new_assert:mix_match(PredList, Stanzas);
  715:                   true ->
  716:                       ok
  717:               end
  718:       end, CurrentOccupants),
  719:     lists:foreach(
  720:       fun(Newcomer) ->
  721:               #xmlel{ name = <<"message">> } = Incoming = escalus:wait_for_stanza(Newcomer),
  722:               RoomBareJIDBin = exml_query:attr(Incoming, <<"from">>),
  723:               [_, MucHost] = binary:split(RoomBareJIDBin, <<"@">>),
  724:               X = exml_query:subelement(Incoming, <<"x">>),
  725:               ?NS_MUC_USER = exml_query:attr(X, <<"xmlns">>),
  726:               [Invite] = exml_query:subelements(X, <<"invite">>),
  727:               ChangerBareJIDBin = lbin(escalus_client:short_jid(Changer)),
  728:               ChangerBareJIDBin = exml_query:attr(Invite, <<"from">>)
  729:       end, Newcomers),
  730:     lists:foreach(
  731:       fun({Leaver, none}) ->
  732:               presence_verify(Leaver, none, escalus:wait_for_stanza(Leaver));
  733:          (_) ->
  734:               ignore
  735:       end, AffUsersChanges).
  736: 
  737: -spec verify_no_stanzas(Users :: [escalus:client()]) -> ok.
  738: verify_no_stanzas(Users) ->
  739:     lists:foreach(
  740:       fun(User) ->
  741:               {false, _} = {escalus_client:has_stanzas(User), User}
  742:       end, Users).
  743: 
  744: -spec verify_config(ConfigFields :: [xmlel()], Config :: [muc_light_helper:config_item()]) -> ok.
  745: verify_config(ConfigFields, Config) ->
  746:     [] = lists:foldl(
  747:            fun(Field, ConfigAcc) ->
  748:                    Key = exml_query:attr(Field, <<"var">>),
  749:                    Val = exml_query:path(Field, [{element, <<"value">>}, cdata]),
  750:                    case lists:keytake(Key, 1, ConfigAcc) of
  751:                        {value, {_, ProperVal}, NewConfig} when Val =:= ProperVal -> NewConfig;
  752:                        false -> ConfigAcc
  753:                    end
  754:            end, Config, ConfigFields).
  755: 
  756: -spec verify_aff_users(Items :: [xmlel()], BinAffUsers :: [{binary(), binary()}]) -> [].
  757: verify_aff_users(Items, BinAffUsers) ->
  758:     true = (length(Items) == length(BinAffUsers)),
  759:     [] = lists:foldl(
  760:            fun(Item, AffAcc) ->
  761:                    JID = exml_query:attr(Item, <<"jid">>),
  762:                    JID = exml_query:attr(Item, <<"nick">>),
  763:                    Aff = exml_query:attr(Item, <<"affiliation">>),
  764:                    verify_keytake(lists:keytake(JID, 1, AffAcc), JID, Aff, AffAcc)
  765:            end, BinAffUsers, Items).
  766: 
  767: -spec verify_keytake(Result :: {value, Item :: tuple(), Acc :: list()}, JID :: binary(),
  768:                      Aff :: binary(), AffAcc :: list()) -> list().
  769: verify_keytake({value, {_, Aff}, NewAffAcc}, _JID, Aff, _AffAcc) -> NewAffAcc.
  770: 
  771: %%--------------------------------------------------------------------
  772: %% Verification funs generators
  773: %%--------------------------------------------------------------------
  774: 
  775: -spec gc_message_verify_fun(Room :: binary(), MsgText :: binary(), Id :: binary()) -> verify_fun().
  776: gc_message_verify_fun(Room, MsgText, Id) ->
  777:     MucHost = ?MUCHOST,
  778:     fun(Incoming) ->
  779:             escalus:assert(is_groupchat_message, [MsgText], Incoming),
  780:             [RoomBareJID, FromNick] = binary:split(exml_query:attr(Incoming, <<"from">>), <<"/">>),
  781:             [Room, MucHost] = binary:split(RoomBareJID, <<"@">>),
  782:             [_] = binary:split(FromNick, <<"/">>), % nick is bare JID
  783:             Id = exml_query:attr(Incoming, <<"id">>)
  784:     end.
  785: 
  786: -spec subject_message_verify_fun(Room :: binary(), Subject :: binary()) -> verify_fun().
  787: subject_message_verify_fun(Room, Subject) ->
  788:     MucHost = ?MUCHOST,
  789:     fun(Incoming) ->
  790:             escalus:assert(is_groupchat_message, Incoming),
  791:             Subject = exml_query:path(Incoming, [{element, <<"subject">>}, cdata]),
  792:             RoomBareJID = exml_query:attr(Incoming, <<"from">>),
  793:             [Room, MucHost] = binary:split(RoomBareJID, <<"@">>)
  794:     end.
  795: 
  796: -spec config_msg_verify_fun() -> verify_fun().
  797: config_msg_verify_fun() ->
  798:     fun(Incoming) ->
  799:             escalus:assert(is_groupchat_message, Incoming),
  800:             [X] = exml_query:subelements(Incoming, <<"x">>),
  801:             ?NS_MUC_USER = exml_query:attr(X, <<"xmlns">>),
  802:             <<"104">> = exml_query:path(X, [{element, <<"status">>}, {attr, <<"code">>}])
  803:     end.
  804: 
  805: -spec config_iq_verify_fun(RoomConfig :: [muc_light_helper:config_item()]) -> verify_fun().
  806: config_iq_verify_fun(RoomConfig) ->
  807:     fun(Incoming) ->
  808:             Fields = exml_query:paths(Incoming, [{element, <<"query">>}, {element, <<"x">>},
  809:                                                   {element, <<"field">>}]),
  810:             ?NS_MUC_OWNER = exml_query:path(Incoming, [{element, <<"query">>},
  811:                                                        {attr, <<"xmlns">>}]),
  812:             verify_config(Fields, RoomConfig)
  813:     end.
  814: 
  815: -spec aff_iq_verify_fun(AffUsers :: ct_aff_users()) -> verify_fun().
  816: aff_iq_verify_fun(AffUsers) ->
  817:     BinAffUsers = bin_aff_users(AffUsers),
  818:     fun(Incoming) ->
  819:             [Query] = exml_query:subelements(Incoming, <<"query">>),
  820:             ?NS_MUC_ADMIN = exml_query:attr(Query, <<"xmlns">>),
  821:             Items = exml_query:subelements(Query, <<"item">>),
  822:             verify_aff_users(Items, BinAffUsers)
  823:     end.
  824: 
  825: -spec presence_verify_fun(AffUser :: ct_aff_user()) -> verify_fun().
  826: presence_verify_fun({User, UserAff}) ->
  827:     fun(Incoming) ->
  828:             true == (catch presence_verify(User, UserAff, Incoming))
  829:     end.
  830: 
  831: -spec presence_verify(User :: escalus:client(), UserAff :: none | member | owner,
  832:                       Incoming :: xmlel()) -> true.
  833: presence_verify(User, UserAff, #xmlel{ name = <<"presence">> } = Incoming) ->
  834:     MucHost = ?MUCHOST,
  835:     UserJIDBin = lbin(escalus_client:short_jid(User)),
  836:     [RoomBareJIDBin, UserJIDBin] = binary:split(exml_query:attr(Incoming, <<"from">>), <<"/">>),
  837:     [_, MucHost] = binary:split(RoomBareJIDBin, <<"@">>),
  838:     X = exml_query:subelement(Incoming, <<"x">>),
  839:     HasDestroy = exml_query:subelement(X, <<"destroy">>) =/= undefined,
  840:     {ProperAff, ProperRole}
  841:     = case {UserAff, exml_query:attr(Incoming, <<"type">>)} of
  842:           {none, _} ->
  843:               <<"unavailable">> = exml_query:attr(Incoming, <<"type">>),
  844:               case HasDestroy of
  845:                   false ->
  846:                       <<"321">> = exml_query:path(X, [{element, <<"status">>}, {attr, <<"code">>}]);
  847:                   true ->
  848:                       ok
  849:               end,
  850:               {<<"none">>, <<"none">>};
  851:           {_, Type} when Type =/= <<"unavailable">> ->
  852:               case UserAff of
  853:                   member -> {<<"member">>, <<"participant">>};
  854:                   owner -> {<<"owner">>, <<"moderator">>}
  855:               end
  856:       end,
  857:     ?NS_MUC_USER = exml_query:attr(X, <<"xmlns">>),
  858:     [Item] = exml_query:subelements(X, <<"item">>),
  859:     ProperAff = exml_query:attr(Item, <<"affiliation">>),
  860:     ProperRole = exml_query:attr(Item, <<"role">>),
  861:     case exml_query:subelements(X, <<"status">>) of
  862:         [_, _] -> % room create request
  863:             [<<"110">>, <<"201">>]
  864:             = lists:sort(exml_query:paths(X, [{element, <<"status">>}, {attr, <<"code">>}]));
  865:         _ ->
  866:             case HasDestroy of
  867:                 false -> UserJIDBin = exml_query:attr(Item, <<"jid">>);
  868:                 true -> ok
  869:             end
  870:     end,
  871:     true.
  872: 
  873: %%--------------------------------------------------------------------
  874: %% Other helpers
  875: %%--------------------------------------------------------------------
  876: 
  877: -spec room_bin_jid(Room :: binary()) -> binary().
  878: room_bin_jid(Room) ->
  879:     <<Room/binary, $@, (muc_helper:muc_host())/binary>>.