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.