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.