1: -module(muc_light_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("proper/include/proper.hrl").
    5: -include_lib("eunit/include/eunit.hrl").
    6: -include_lib("common_test/include/ct.hrl").
    7: -include("mod_muc_light.hrl").
    8: -include("jlib.hrl").
    9: -include("mongoose_rsm.hrl").
   10: -include("mongoose.hrl").
   11: 
   12: -define(DOMAIN, <<"localhost">>).
   13: 
   14: %% ------------------------------------------------------------------
   15: %% Common Test callbacks
   16: %% ------------------------------------------------------------------
   17: 
   18: all() ->
   19:     [
   20:      {group, aff_changes},
   21:      {group, rsm_disco},
   22:      {group, codec}
   23:     ].
   24: 
   25: groups() ->
   26:     [
   27:      {aff_changes, [parallel], [
   28:                                 aff_change_success,
   29:                                 aff_change_bad_request
   30:                                ]},
   31:      {rsm_disco, [parallel], [
   32:                               rsm_disco_success,
   33:                               rsm_disco_item_not_found
   34:                              ]},
   35:      {codec, [sequence], [codec_calls]}
   36:     ].
   37: 
   38: init_per_suite(Config) ->
   39:     application:ensure_all_started(jid),
   40:     Config.
   41: 
   42: end_per_suite(Config) ->
   43:     Config.
   44: 
   45: init_per_group(rsm_disco, Config) ->
   46:     Config;
   47: init_per_group(_, Config) ->
   48:     Config.
   49: 
   50: end_per_group(_, Config) ->
   51:     Config.
   52: 
   53: init_per_testcase(codec_calls, Config) ->
   54:     meck_mongoose_subdomain_core(),
   55:     ok = mnesia:create_schema([node()]),
   56:     ok = mnesia:start(),
   57:     mongoose_config:set_opt(all_metrics_are_global, false),
   58:     {ok, _} = application:ensure_all_started(exometer_core),
   59:     gen_hook:start_link(),
   60:     ejabberd_router:start_link(),
   61:     mim_ct_sup:start_link(ejabberd_sup),
   62:     mod_muc_light:start(?DOMAIN, []),
   63:     ets:new(testcalls, [named_table]),
   64:     ets:insert(testcalls, {hooks, 0}),
   65:     ets:insert(testcalls, {handlers, 0}),
   66:     Config;
   67: init_per_testcase(_, Config) ->
   68:     Config.
   69: 
   70: end_per_testcase(codec_calls, Config) ->
   71:     mod_muc_light:stop(?DOMAIN),
   72:     mongoose_config:unset_opt(all_metrics_are_global),
   73:     mnesia:stop(),
   74:     mnesia:delete_schema([node()]),
   75:     application:stop(exometer_core),
   76:     meck:unload(),
   77:     Config;
   78: end_per_testcase(_, Config) ->
   79:     Config.
   80: 
   81: %% ------------------------------------------------------------------
   82: %% Test cases
   83: %% ------------------------------------------------------------------
   84: 
   85: %% ----------------- Aff changes ----------------------
   86: 
   87: aff_change_success(_Config) ->
   88:     ?assert(proper:quickcheck(prop_aff_change_success())).
   89: 
   90: aff_change_bad_request(_Config) ->
   91:     ?assert(proper:quickcheck(prop_aff_change_bad_request())).
   92: 
   93: %% ----------------- RSM disco ----------------------
   94: 
   95: rsm_disco_success(_Config) ->
   96:     ?assert(proper:quickcheck(prop_rsm_disco_success())).
   97: 
   98: rsm_disco_item_not_found(_Config) ->
   99:     ?assert(proper:quickcheck(prop_rsm_disco_item_not_found())).
  100: 
  101: %% ----------------- Codecs ----------------------
  102: 
  103: %% @doc This is a regression test for a bug that was fixed in #01506f5a
  104: %% Basically it makes sure that codes have a proper setup of hook calls
  105: %% and all hooks and handlers are called as they should.
  106: codec_calls(_Config) ->
  107:     AffUsers = [{{<<"alice">>, <<"localhost">>}, member}, {{<<"bob">>, <<"localhost">>}, member}],
  108:     Sender = jid:from_binary(<<"bob@localhost/bbb">>),
  109:     RoomJid = jid:make(<<"pokoik">>, <<"muc.localhost">>, <<>>),
  110:     HandleFun = fun(_, _, _) -> count_call(handler) end,
  111:     gen_hook:add_handler(filter_room_packet,
  112:                          <<"localhost">>,
  113:                          fun ?MODULE:filter_room_packet_handler/3,
  114:                          #{},
  115:                          50),
  116: 
  117:     % count_call/1 should've been called twice - by handler fun (for each affiliated user,
  118:     % we have one) and by a filter_room_packet hook handler.
  119: 
  120:     Acc = mongoose_acc:new(#{ location => ?LOCATION,
  121:                               lserver => <<"localhost">>,
  122:                               host_type => <<"localhost">>,
  123:                               element => undefined,
  124:                               from_jid => jid:make_noprep(<<"a">>, <<"localhost">>, <<>>),
  125:                               to_jid => jid:make_noprep(<<>>, <<"muc.localhost">>, <<>>) }),
  126:     mod_muc_light_codec_modern:encode({#msg{id = <<"ajdi">>}, AffUsers},
  127:                                       Sender, RoomJid, HandleFun, Acc),
  128:     % 1 filter packet, sent 1 msg to 2 users
  129:     check_count(1, 2),
  130:     mod_muc_light_codec_modern:encode({set, #affiliations{}, [], []},
  131:                                       Sender, RoomJid, HandleFun, Acc),
  132:     % 1 filter packet, sent 1 IQ response to Sender
  133:     check_count(1, 1),
  134:     mod_muc_light_codec_modern:encode({set, #create{id = <<"ajdi">>, aff_users = AffUsers}, false},
  135:                                       Sender, RoomJid, HandleFun, Acc),
  136:     % 1 filter, 1 IQ response to Sender, 1 notification to 2 users
  137:     check_count(1, 3),
  138:     mod_muc_light_codec_modern:encode({set, #config{id = <<"ajdi">>}, AffUsers},
  139:         Sender, RoomJid, HandleFun, Acc),
  140:     % 1 filter, 1 IQ response to Sender, 1 notification to 2 users
  141:     check_count(1, 3),
  142:     mod_muc_light_codec_legacy:encode({#msg{id = <<"ajdi">>}, AffUsers},
  143:         Sender, RoomJid, HandleFun, Acc),
  144:     % 1 filter, 1 msg to 2 users
  145:     check_count(1, 2),
  146:     ok.
  147: 
  148: filter_room_packet_handler(Acc, _Params, _Extra) ->
  149:     count_call(hook),
  150:     {ok, Acc}.
  151: 
  152: %% ------------------------------------------------------------------
  153: %% Properties and validators
  154: %% ------------------------------------------------------------------
  155: 
  156: prop_aff_change_success() ->
  157:     ?FORALL({AffUsers, Changes, Joining, Leaving, WithOwner}, change_aff_params(),
  158:             begin
  159:                 case mod_muc_light_utils:change_aff_users(AffUsers, Changes) of
  160:                     {ok, NewAffUsers0, AffUsersChanged, Joining0, Leaving0} ->
  161:                         Joining = lists:sort(Joining0),
  162:                         Leaving = lists:sort(Leaving0),
  163:                         % is the length correct?
  164:                         CorrectLen = length(AffUsers) + length(Joining) - length(Leaving),
  165:                         CorrectLen = length(NewAffUsers0),
  166:                         % is the list unique and sorted?
  167:                         NewAffUsers0 = uuser_sort(NewAffUsers0),
  168:                         % are there no 'none' items?
  169:                         false = lists:keyfind(none, 2, NewAffUsers0),
  170:                         % are there no owners or there is exactly one?
  171:                         true = validate_owner(NewAffUsers0, false, WithOwner),
  172:                         % changes list applied to old list should produce the same result
  173:                         {ok, NewAffUsers1, _, _, _} = mod_muc_light_utils:change_aff_users(AffUsers, AffUsersChanged),
  174:                         NewAffUsers0 = NewAffUsers1,
  175:                         true;
  176:                     _ ->
  177:                         false
  178:                 end
  179:             end).
  180: 
  181: -spec validate_owner(NewAffUsers :: aff_users(), OneOwnerFound :: boolean(),
  182:                      AreOwnersAllowed :: boolean()) -> boolean().
  183: validate_owner([{_, owner} | _], true, _) -> false; % more than one owner
  184: validate_owner([{_, owner} | _], _, false) -> false; % there should be no owners
  185: validate_owner([{_, owner} | R], _, true) -> validate_owner(R, true, true); % there should be no owners
  186: validate_owner([_ | R], Found, WithOwner) -> validate_owner(R, Found, WithOwner);
  187: validate_owner([], _, _) -> true.
  188: 
  189: prop_aff_change_bad_request() ->
  190:     ?FORALL({AffUsers, Changes}, bad_change_aff(),
  191:             begin
  192:                 {error, {bad_request, _}} =
  193:                     mod_muc_light_utils:change_aff_users(AffUsers, Changes),
  194:                 true
  195:             end).
  196: 
  197: prop_rsm_disco_success() ->
  198:     ?FORALL({RoomsInfo, RSMIn, ProperSlice, FirstIndex}, valid_rsm_disco(),
  199:             begin
  200:                 RoomsInfoLen = length(RoomsInfo),
  201:                 {ok, ProperSlice, RSMOut} = mod_muc_light:apply_rsm(
  202:                                               RoomsInfo, RoomsInfoLen, RSMIn),
  203:                 RoomsInfoLen = RSMOut#rsm_out.count,
  204:                 case RSMIn#rsm_in.max of
  205:                     0 ->
  206:                         true;
  207:                     _ ->
  208:                         #rsm_out{ first = RSMFirst, last = RSMLast } = RSMOut,
  209:                         FirstIndex = RSMOut#rsm_out.index,
  210:                         {FirstRoom, _, _} = hd(ProperSlice),
  211:                         {LastRoom, _, _} = lists:last(ProperSlice),
  212:                         RSMFirst = jid:to_binary(FirstRoom),
  213:                         RSMLast = jid:to_binary(LastRoom),
  214:                         true
  215:                 end
  216:             end).
  217: 
  218: prop_rsm_disco_item_not_found() ->
  219:     ?FORALL({RoomsInfo, RSMIn}, invalid_rsm_disco(),
  220:             begin
  221:                 {error, item_not_found} = mod_muc_light:apply_rsm(
  222:                                             RoomsInfo, length(RoomsInfo), RSMIn),
  223:                 true
  224:             end).
  225: 
  226: %% ------------------------------------------------------------------
  227: %% Complex generators
  228: %% ------------------------------------------------------------------
  229: 
  230: %% ----------------------- Affilliations -----------------------
  231: 
  232: change_aff_params() ->
  233:     ?LET(WithOwner, with_owner(),
  234:          ?LET(AffUsers, aff_users_list(WithOwner, 10),
  235:               ?LET(
  236:                  {Joining,                   Leaving,           PromoteOwner,
  237:                   PromotedOwnerPos,                              DegradeOwner},
  238:                  {aff_users_list(false, 15), sublist(AffUsers), boolean(),
  239:                   integer(length(AffUsers), length(AffUsers)*2), boolean()},
  240:                  begin
  241:                      Changes1 = Joining ++ [{U, none} || {U, _} <- Leaving],
  242:                      Survivors = (AffUsers -- Leaving) ++ Joining,
  243:                      Changes2 = promote_owner(PromoteOwner andalso WithOwner, PromotedOwnerPos,
  244:                                               Changes1, Survivors),
  245:                      Changes3 = degrade_owner(DegradeOwner, AffUsers, Changes2),
  246:                      {AffUsers,
  247:                       Changes3,
  248:                       [ U || {U, _} <- Joining ],
  249:                       [ U || {U, _} <- Leaving ],
  250:                       WithOwner}
  251:                  end))).
  252: 
  253: -spec promote_owner(Promote :: boolean(), PromotedOwnerPos :: pos_integer(),
  254:                     Changes1 :: aff_users(), Survivors :: aff_users()) ->
  255:     ChangesWithOwnerPromotion :: aff_users().
  256: promote_owner(true, PromotedOwnerPos, Changes1, Survivors) ->
  257:     SurvivorsNoOwner = lists:keydelete(owner, 2, Survivors),
  258:     {NewOwner, _} = lists:nth((PromotedOwnerPos rem length(SurvivorsNoOwner)) + 1, SurvivorsNoOwner),
  259:     lists:keystore(NewOwner, 1, Changes1, {NewOwner, owner});
  260: promote_owner(false, _, Changes1, _) ->
  261:     Changes1.
  262: 
  263: -spec degrade_owner(Degrade :: boolean(), AffUsers :: aff_users(), Changes :: aff_users()) ->
  264:     ChangesWithDegradedOwner :: aff_users().
  265: degrade_owner(false, _, Changes1) ->
  266:     Changes1;
  267: degrade_owner(true, AffUsers, Changes1) ->
  268:     case lists:keyfind(owner, 2, AffUsers) of
  269:         {Owner, _} ->
  270:             case lists:keyfind(Owner, 1, Changes1) of
  271:                 false -> [{Owner, member} | Changes1];
  272:                 _ -> Changes1 % Owner is already downgraded somehow
  273:             end;
  274:         _ ->
  275:             Changes1
  276:     end.
  277: 
  278: bad_change_aff() ->
  279:     ?LET({{AffUsers, Changes, _Joining, _Leaving, WithOwner}, FailGenerator},
  280:          {change_aff_params(), fail_gen_fun()},
  281:          ?MODULE:FailGenerator(AffUsers, Changes, WithOwner)).
  282: 
  283: aff_users_list(WithOwner, NameLen) ->
  284:     ?LET(AffUsers, non_empty(list(aff_user(NameLen))), with_owner(uuser_sort(AffUsers), WithOwner)).
  285: 
  286: sublist(L) ->
  287:     ?LET(SurvivorVector,
  288:          vector(length(L), boolean()),
  289:          pick_survivors(L, SurvivorVector)).
  290: 
  291: owner_problem(AffUsers, Changes, false) ->
  292:     % Change aff to owner but owners are not allowed
  293:     ?LET({{User, _Aff}, InsertPos}, {aff_user(5), integer(1, length(Changes))},
  294:          {AffUsers, insert({User, owner}, Changes, InsertPos)});
  295: owner_problem(AffUsers, Changes, true) ->
  296:     % Promote two users to owner
  297:     ?LET({{User1, _Aff}, {User2, _Aff}, InsertPos1, InsertPos2},
  298:          {aff_user(5), aff_user(5), integer(1, length(Changes)), integer(1, length(Changes))},
  299:          {AffUsers, insert({User2, owner},
  300:                            insert({User1, owner}, Changes, InsertPos1),
  301:                            InsertPos2)}).
  302: 
  303: duplicated_user(AffUsers, Changes, _) ->
  304:      ?LET(DuplicatePos, integer(1, length(Changes)), {AffUsers, duplicate(Changes, DuplicatePos)}).
  305: 
  306: meaningless_change(AffUsers, Changes, _) ->
  307:     ?LET(MeaninglessAff, oneof([other, none]),
  308:          meaningless_change_by_aff(AffUsers, Changes, MeaninglessAff)).
  309: 
  310: meaningless_change_by_aff(AffUsers, Changes, none) ->
  311:     ?LET({{User, _Aff}, InsertPos}, {aff_user(5), integer(1, length(Changes))},
  312:          {AffUsers, insert({User, none}, Changes, InsertPos)});
  313: meaningless_change_by_aff(AffUsers, Changes0, other) ->
  314:     ?LET({UserPos, InsertPos}, {integer(1, length(AffUsers)), integer(1, length(Changes0))},
  315:          begin
  316:              {User, _} = AffUser = lists:nth(UserPos, AffUsers),
  317:              Changes = insert(AffUser, lists:keydelete(User, 1, Changes0), InsertPos),
  318:              {AffUsers, Changes}
  319:          end).
  320: 
  321: %% ----------------------- Disco RSM -----------------------
  322: 
  323: valid_rsm_disco() ->
  324:     ?LET(RSMType, rsm_type(), valid_rsm_disco(RSMType)).
  325: 
  326: valid_rsm_disco(RSMType) ->
  327:     ?LET({BeforeL0, ProperSlice, AfterL0},
  328:          {rooms_info(<<"-">>, RSMType == aft), rooms_info(<<>>), rooms_info(<<"+">>)},
  329:          begin
  330:              BeforeL = lists:usort(BeforeL0),
  331:              AfterL = lists:usort(AfterL0),
  332:              RoomsInfo = BeforeL ++ ProperSlice ++ AfterL,
  333:              FirstIndex = length(BeforeL),
  334:              RSMIn = make_rsm_in(RSMType, ProperSlice, FirstIndex, BeforeL, AfterL),
  335:              {RoomsInfo, RSMIn, ProperSlice, FirstIndex}
  336:          end).
  337: 
  338: make_rsm_in(index, ProperSlice, FirstIndex, _BeforeL, _AfterL) ->
  339:     #rsm_in{
  340:        max = length(ProperSlice),
  341:        index = FirstIndex
  342:       };
  343: make_rsm_in(aft, ProperSlice, _FirstIndex, BeforeL, _AfterL) ->
  344:     #rsm_in{
  345:        max = length(ProperSlice),
  346:        direction = aft,
  347:        id = jid:to_binary(element(1, lists:last(BeforeL)))
  348:       };
  349: make_rsm_in(before, ProperSlice, _FirstIndex, _BeforeL, AfterL) ->
  350:     #rsm_in{
  351:        max = length(ProperSlice),
  352:        direction = before,
  353:        id = case AfterL of
  354:                 [] -> <<>>;
  355:                 _ -> jid:to_binary(element(1, hd(AfterL)))
  356:             end
  357:       }.
  358: 
  359: invalid_rsm_disco() ->
  360:     ?LET({RoomsInfo, Nonexistent, RSMType}, {rooms_info(<<"-">>), room_us(<<"+">>), rsm_type()},
  361:              {RoomsInfo, make_invalid_rsm_in(RSMType, RoomsInfo, Nonexistent)}).
  362: 
  363: make_invalid_rsm_in(index, RoomsInfo, _Nonexistent) ->
  364:     #rsm_in{
  365:        max = 10,
  366:        index = length(RoomsInfo) + 1
  367:       };
  368: make_invalid_rsm_in(Direction, _RoomsInfo, Nonexistent) ->
  369:     #rsm_in{
  370:        max = 10,
  371:        direction = Direction,
  372:        id = jid:to_binary(Nonexistent)
  373:       }.
  374: 
  375: %% ------------------------------------------------------------------
  376: %% Simple generators
  377: %% ------------------------------------------------------------------
  378: 
  379: aff_user(NameLen) ->
  380:     ?LET(U, bitstring(NameLen*8), {{U, ?DOMAIN}, member}).
  381: 
  382: with_owner() ->
  383:     boolean().
  384: 
  385: with_owner(L, true) -> ?LET(OwnerPos, integer(1, length(L)), make_owner(L, OwnerPos));
  386: with_owner(L, _) -> L.
  387: 
  388: fail_gen_fun() ->
  389:     oneof([owner_problem, duplicated_user, meaningless_change]).
  390: 
  391: rsm_type() ->
  392:     oneof([before, aft, index]).
  393: 
  394: rooms_info(Prefix) ->
  395:     rooms_info(Prefix, false).
  396: 
  397: rooms_info(Prefix, true = _NonEmpty) ->
  398:     non_empty(rooms_info(Prefix, false));
  399: rooms_info(Prefix, false) ->
  400:     list({room_us(Prefix), any, any}).
  401: 
  402: room_us(Prefix) ->
  403:     ?LET({U, S}, {prop_helper:alnum_bitstring(), prop_helper:alnum_bitstring()},
  404:          {<<Prefix/binary, U/binary>>, S}).
  405: 
  406: %% ------------------------------------------------------------------
  407: %% Utils
  408: %% ------------------------------------------------------------------
  409: 
  410: -spec insert(E :: term(), L :: list(), Pos :: pos_integer()) -> [term(), ...].
  411: insert(E, L, 1) -> [E | L];
  412: insert(E, [EL | L], Pos) -> [EL | insert(E, L, Pos - 1)];
  413: insert(E, [], _) -> [E].
  414: 
  415: -spec duplicate(L :: [term(),...], Pos :: pos_integer()) -> [term(),...].
  416: duplicate([E | L], 1) -> [E, E | L];
  417: duplicate([E | L], Pos) -> [E | duplicate(L, Pos - 1)];
  418: duplicate([], _) -> [].
  419: 
  420: -spec uuser_sort(AffUsers :: aff_users()) -> aff_users().
  421: uuser_sort(AffUsers) ->
  422:     lists:usort(fun({A, _}, {B, _}) -> A =< B end, AffUsers).
  423: 
  424: -spec pick_survivors(List :: list(), SurvivorVector :: [boolean()]) -> list().
  425: pick_survivors([], []) -> [];
  426: pick_survivors([_ | RL], [false | RVec]) -> pick_survivors(RL, RVec);
  427: pick_survivors([E | RL], [_ | RVec]) -> [E | pick_survivors(RL, RVec)].
  428: 
  429: -spec make_owner(AffUsers :: aff_users(), OwnerPos :: pos_integer()) -> aff_users().
  430: make_owner([{User, _} | RAffUsers], 1) -> [{User, owner} | RAffUsers];
  431: make_owner([AffUser | RAffUsers], OwnerPos) -> [AffUser | make_owner(RAffUsers, OwnerPos - 1)];
  432: make_owner([], _OwnerPos) -> [].
  433: 
  434: count_call(hook) ->
  435:     ets:update_counter(testcalls, hooks, 1);
  436: count_call(handler) ->
  437:     ets:update_counter(testcalls, handlers, 1).
  438: 
  439: check_count(Hooks, Handlers) ->
  440:     [{hooks, Ho}] = ets:lookup(testcalls, hooks),
  441:     [{handlers, Ha}] = ets:lookup(testcalls, handlers),
  442:     ?assertEqual(Hooks, Ho),
  443:     ?assertEqual(Handlers, Ha),
  444:     ets:insert(testcalls, {hooks, 0}),
  445:     ets:insert(testcalls, {handlers, 0}).
  446: 
  447: meck_mongoose_subdomain_core() ->
  448:     meck:new(mongoose_subdomain_core),
  449:     meck:expect(mongoose_subdomain_core, register_subdomain,
  450:                 fun(HostType, SubdomainPattern, PacketHandler) -> ok end),
  451:     meck:expect(mongoose_subdomain_core, unregister_subdomain,
  452:                 fun(HostType, SubdomainPattern) -> ok end).