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