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