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).