Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions big_tests/tests/graphql_muc_light_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ admin_muc_light_tests() ->
admin_list_user_rooms_non_existent_domain,
admin_list_room_users,
admin_list_room_users_non_existent_domain,
admin_list_rooms,
admin_list_rooms_pagination,
admin_list_rooms_filter,
admin_list_rooms_schema_without_roomname,
admin_list_rooms_non_existent_domain,
admin_get_room_config,
admin_get_room_config_non_existent_domain,
admin_blocking_list,
Expand Down Expand Up @@ -317,10 +322,20 @@ maybe_clear_db() ->
ok
end.

init_per_testcase(admin_list_rooms_schema_without_roomname = TC, Config) ->
%% A config_schema may not define the roomname field at all
NoRoomnameSchema = [{<<"subject">>, <<"Test">>, subject, binary}],
Opts = config([modules, mod_muc_light],
maps:merge(muc_light_opts(), #{config_schema => NoRoomnameSchema})),
dynamic_modules:ensure_modules(domain_helper:host_type(), [{mod_muc_light, Opts}]),
escalus:init_per_testcase(TC, Config);
init_per_testcase(TC, Config) ->
rest_helper:maybe_skip_mam_test_cases(TC, [user_get_room_messages,
admin_get_room_messages], Config).

end_per_testcase(admin_list_rooms_schema_without_roomname = TC, Config) ->
dynamic_modules:ensure_modules(domain_helper:host_type(), required_modules(suite)),
escalus:end_per_testcase(TC, Config);
end_per_testcase(TC, Config) ->
escalus:end_per_testcase(TC, Config).

Expand Down Expand Up @@ -1487,6 +1502,138 @@ admin_list_room_users_non_existent_domain_story(Config, Alice) ->
Res = list_room_users(make_bare_jid(RoomJID#jid.luser, ?UNKNOWN_DOMAIN), Config),
?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)).

admin_list_rooms(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}],
fun admin_list_rooms_story/3).

admin_list_rooms_story(Config, Alice, Bob) ->
AliceBin = escalus_client:short_jid(Alice),
BobBin = escalus_client:short_jid(Bob),
MUCServer = ?config(muc_light_host, Config),
%% Start from a clean slate so domain-wide counts are deterministic
%% (the admin group does not clear rooms between test cases).
ok = rpc(mim(), mod_muc_light_db_backend, force_clear, [domain_helper:host_type()]),
%% Empty domain
#{<<"rooms">> := [], <<"count">> := 0, <<"nextPage">> := false} =
get_ok_value(?USER_LIST_ROOMS_PATH, list_rooms(MUCServer, null, null, null, Config)),
%% Create two rooms (Alice owns room1, Bob owns room2), invite Bob into room1
{ok, #{jid := RoomJID1}} = create_room(MUCServer, <<"Room A">>, <<"subjectA">>, AliceBin),
{ok, #{jid := RoomJID2}} = create_room(MUCServer, <<"Room B">>, <<"subjectB">>, BobBin),
invite_user(RoomJID1, AliceBin, BobBin),
#{<<"rooms">> := Rooms, <<"count">> := 2, <<"nextPage">> := false} =
get_ok_value(?USER_LIST_ROOMS_PATH, list_rooms(MUCServer, null, null, null, Config)),
R1 = find_room_desc(jid:to_binary(RoomJID1), Rooms),
?assertEqual(<<"Room A">>, maps:get(<<"name">>, R1)),
?assertEqual(<<"subjectA">>, maps:get(<<"subject">>, R1)),
?assertEqual(2, maps:get(<<"usersNumber">>, R1)),
?assertEqual(escalus_utils:jid_to_lower(AliceBin), maps:get(<<"ownerJid">>, R1)),
R2 = find_room_desc(jid:to_binary(RoomJID2), Rooms),
?assertEqual(1, maps:get(<<"usersNumber">>, R2)),
?assertEqual(escalus_utils:jid_to_lower(BobBin), maps:get(<<"ownerJid">>, R2)).

admin_list_rooms_pagination(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}],
fun admin_list_rooms_pagination_story/2).

admin_list_rooms_pagination_story(Config, Alice) ->
AliceBin = escalus_client:short_jid(Alice),
MUCServer = ?config(muc_light_host, Config),
%% Start from a clean slate so pagination boundaries and counts are deterministic.
ok = rpc(mim(), mod_muc_light_db_backend, force_clear, [domain_helper:host_type()]),
[{ok, _} = create_room(MUCServer, Name, <<"s">>, AliceBin)
|| Name <- [<<"R1">>, <<"R2">>, <<"R3">>]],
%% Full listing (limit defaults to 50), capture the global sort order
#{<<"rooms">> := All, <<"count">> := 3, <<"nextPage">> := false} =
get_ok_value(?USER_LIST_ROOMS_PATH, list_rooms(MUCServer, null, null, null, Config)),
AllJids = [maps:get(<<"jid">>, R) || R <- All],
?assertEqual(lists:sort(AllJids), AllJids),
%% First page: limit 2 -> first two rooms, more pages ahead
#{<<"rooms">> := Page1, <<"count">> := 3, <<"nextPage">> := true} =
get_ok_value(?USER_LIST_ROOMS_PATH, list_rooms(MUCServer, null, null, 2, Config)),
Page1Jids = [maps:get(<<"jid">>, R) || R <- Page1],
?assertEqual(lists:sublist(AllJids, 1, 2), Page1Jids),
%% Second page: after the last JID of the first page -> last room only
#{<<"rooms">> := Page2, <<"count">> := 3, <<"nextPage">> := false} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, null, lists:last(Page1Jids), 2, Config)),
?assertEqual(lists:sublist(AllJids, 3, 1), [maps:get(<<"jid">>, R) || R <- Page2]),
%% An exactly-full last page also reports no further pages
#{<<"rooms">> := All, <<"count">> := 3, <<"nextPage">> := false} =
get_ok_value(?USER_LIST_ROOMS_PATH, list_rooms(MUCServer, null, null, 3, Config)),
%% After the last room -> empty page, count unchanged
#{<<"rooms">> := [], <<"count">> := 3, <<"nextPage">> := false} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, null, lists:last(AllJids), 2, Config)).

admin_list_rooms_filter(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}],
fun admin_list_rooms_filter_story/2).

admin_list_rooms_filter_story(Config, Alice) ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very long test case - difficult to follow, and (IMO) with an unnecessary part at the end.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ma not sure how you would like it to be modified.

AliceBin = escalus_client:short_jid(Alice),
MUCServer = ?config(muc_light_host, Config),
%% Start from a clean slate so filtered counts are deterministic.
ok = rpc(mim(), mod_muc_light_db_backend, force_clear, [domain_helper:host_type()]),
[{ok, _} = create_identified_room(MUCServer, Name, <<"s">>, AliceBin, Id)
|| {Id, Name} <- [{<<"apple-room">>, <<"Fruit HQ">>},
{<<"banana-room">>, <<"Yellow">>},
{<<"cherry-room">>, <<"fruit annex">>}]],
%% Filter by a substring of the room JID localpart
#{<<"rooms">> := [Apple], <<"count">> := 1, <<"nextPage">> := false} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, <<"apple">>, null, null, Config)),
?assertEqual(<<"apple-room@", MUCServer/binary>>, maps:get(<<"jid">>, Apple)),
%% Filter by room name, case-insensitively
#{<<"rooms">> := FruitRooms, <<"count">> := 2} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, <<"FRUIT">>, null, null, Config)),
?assertEqual([<<"apple-room@", MUCServer/binary>>, <<"cherry-room@", MUCServer/binary>>],
[maps:get(<<"jid">>, R) || R <- FruitRooms]),
%% Filter composes with cursor pagination
#{<<"rooms">> := [RoomA], <<"count">> := 3, <<"nextPage">> := true} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, <<"room">>, null, 1, Config)),
#{<<"rooms">> := [RoomB], <<"count">> := 3, <<"nextPage">> := true} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, <<"room">>, maps:get(<<"jid">>, RoomA), 1, Config)),
?assertEqual([<<"apple-room@", MUCServer/binary>>, <<"banana-room@", MUCServer/binary>>],
[maps:get(<<"jid">>, RoomA), maps:get(<<"jid">>, RoomB)]),
%% No match -> empty page and zero count
#{<<"rooms">> := [], <<"count">> := 0, <<"nextPage">> := false} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, <<"does-not-match-anything">>, null, null, Config)),
%% An empty filter behaves like no filter
#{<<"count">> := 3} =
get_ok_value(?USER_LIST_ROOMS_PATH, list_rooms(MUCServer, <<>>, null, null, Config)).

admin_list_rooms_schema_without_roomname(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}],
fun admin_list_rooms_schema_without_roomname_story/2).

admin_list_rooms_schema_without_roomname_story(Config, Alice) ->
AliceBin = escalus_client:short_jid(Alice),
MUCServer = ?config(muc_light_host, Config),
%% Start from a clean slate so domain-wide counts are deterministic.
ok = rpc(mim(), mod_muc_light_db_backend, force_clear, [domain_helper:host_type()]),
CreatorJID = jid:from_binary(AliceBin),
{ok, _} = rpc(mim(), mod_muc_light_api, create_room,
[MUCServer, <<"noname-room">>, CreatorJID, #{<<"subject">> => <<"s">>}]),
#{<<"rooms">> := [Room], <<"count">> := 1} =
get_ok_value(?USER_LIST_ROOMS_PATH, list_rooms(MUCServer, null, null, null, Config)),
?assertEqual(null, maps:get(<<"name">>, Room)),
?assertEqual(<<"s">>, maps:get(<<"subject">>, Room)),
%% A filter can only match the JID, as there is no name to match
#{<<"rooms">> := [], <<"count">> := 0} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, <<"qqq">>, null, null, Config)),
#{<<"count">> := 1} =
get_ok_value(?USER_LIST_ROOMS_PATH,
list_rooms(MUCServer, <<"noname">>, null, null, Config)).

admin_list_rooms_non_existent_domain(Config) ->
Res = list_rooms(<<"non-existent-muclight.example.com">>, null, null, null, Config),
?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"not found">>)).

admin_get_room_config(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_get_room_config_story/2).

Expand Down Expand Up @@ -1739,6 +1886,11 @@ create_room(Domain, Name, Subject, CreatorBin) ->
Config = #{<<"roomname">> => Name, <<"subject">> => Subject},
rpc(mim(), mod_muc_light_api, create_room, [Domain, CreatorJID, Config]).

create_identified_room(Domain, Name, Subject, CreatorBin, Id) ->
CreatorJID = jid:from_binary(CreatorBin),
Config = #{<<"roomname">> => Name, <<"subject">> => Subject},
rpc(mim(), mod_muc_light_api, create_room, [Domain, Id, CreatorJID, Config]).

invite_user(RoomJID, SenderBin, RecipientBin) ->
SenderJID = jid:from_binary(SenderBin),
RecipientJID = jid:from_binary(RecipientBin),
Expand Down Expand Up @@ -1799,6 +1951,11 @@ list_user_rooms(User, Config) ->
Vars = #{<<"user">> => User},
execute_command(<<"muc_light">>, <<"listUserRooms">>, Vars, Config).

list_rooms(MUCDomain, Filter, After, Limit, Config) ->
Vars = #{<<"mucDomain">> => MUCDomain, <<"filter">> => Filter,
<<"after">> => After, <<"limit">> => Limit},
execute_command(<<"muc_light">>, <<"listRooms">>, Vars, Config).

delete_room(RoomJID, Config) ->
Vars = #{<<"room">> => RoomJID},
execute_command(<<"muc_light">>, <<"deleteRoom">>, Vars, Config).
Expand Down Expand Up @@ -1904,3 +2061,7 @@ member_is_affiliated(Stanza, User) ->
MemberJID = escalus_utils:jid_to_lower(escalus_utils:get_short_jid(User)),
Data = exml_query:path(Stanza, [{element, <<"x">>}, {element, <<"user">>}, cdata]),
MemberJID == Data.

find_room_desc(JIDBin, Rooms) ->
[Room] = [R || #{<<"jid">> := J} = R <- Rooms, J =:= JIDBin],
Room.
3 changes: 3 additions & 0 deletions priv/graphql/schemas/admin/muc_light.gql
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ type MUCLightAdminQuery @protected @use(modules: ["mod_muc_light"]){
"Get the user's list of blocked entities"
getBlockingList(user: JID!): [BlockingItem!]
@protected(type: DOMAIN, args: ["user"]) @use(args: ["user"])
"Get rooms of the given MUC Light domain, ordered by JID. To get the next page, pass the JID of the last room as 'after'. 'filter' matches a substring of the room JID or name"
listRooms(mucDomain: DomainName!, filter: String, after: BareJID, limit: PosInt = 50): MUCLightRoomsPayload
@protected(type: DOMAIN, args: ["mucDomain"]) @use(args: ["mucDomain"])
}
24 changes: 24 additions & 0 deletions priv/graphql/schemas/global/muc_light.gql
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,27 @@ type RoomUser{
"User's affiliation"
affiliation: Affiliation!
}

"MUC Light rooms payload"
type MUCLightRoomsPayload{
"List of room descriptions, ordered by room JID"
rooms: [MUCLightRoomDesc!]
"Total number of rooms in the domain matching the filter"
count: NonNegInt
"Whether more rooms exist after this page"
nextPage: Boolean
}

"MUC Light room description"
type MUCLightRoomDesc{
"Room's JID"
jid: BareJID!
"Room's display name (may be empty)"
name: String
"Room's subject (may be empty)"
subject: String
"Number of room members"
usersNumber: NonNegInt
"JID of one of the room's owners (there is normally exactly one); null if no owner can be resolved"
ownerJid: JID
}
30 changes: 28 additions & 2 deletions src/graphql/admin/mongoose_graphql_muc_light_admin_query.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
-import(mongoose_graphql_helper, [make_error/2, format_result/2, null_to_undefined/1]).
-import(mongoose_graphql_muc_light_helper, [make_room/1,
make_ok_user/1,
page_size_or_max_limit/2]).
page_size_or_max_limit/2,
null_to_default/2]).

%% Matches the schema default of the listRooms limit argument; only needed
%% when the client passes an explicit null.
-define(DEFAULT_ROOMS_PAGE_SIZE, 50).

execute(_Ctx, _Obj, <<"listUserRooms">>, Args) ->
list_user_rooms(Args);
Expand All @@ -21,7 +26,9 @@ execute(_Ctx, _Obj, <<"getRoomConfig">>, Args) ->
execute(_Ctx, _Obj, <<"getRoomMessages">>, Args) ->
get_room_messages(Args);
execute(_Ctx, _Obj, <<"getBlockingList">>, Args) ->
get_blocking_list(Args).
get_blocking_list(Args);
execute(_Ctx, _Obj, <<"listRooms">>, Args) ->
list_rooms(Args).

-spec list_user_rooms(map()) -> {ok, [{ok, binary()}]} | {error, resolver_error()}.
list_user_rooms(#{<<"user">> := UserJID}) ->
Expand Down Expand Up @@ -72,3 +79,22 @@ get_blocking_list(#{<<"user">> := UserJID}) ->
Err ->
make_error(Err, #{user => jid:to_binary(UserJID)})
end.

-spec list_rooms(map()) -> {ok, map()} | {error, resolver_error()}.
list_rooms(#{<<"mucDomain">> := MUCDomain, <<"filter">> := Filter,
<<"after">> := After, <<"limit">> := Limit}) ->
Filter2 = null_to_undefined(Filter),
After2 = after_to_luser(After),
Limit2 = null_to_default(Limit, ?DEFAULT_ROOMS_PAGE_SIZE),
case mod_muc_light_api:get_rooms(MUCDomain, Filter2, After2, Limit2) of
{ok, {Rooms, Count, HasNextPage}} ->
{ok, mongoose_graphql_muc_light_helper:make_rooms_payload(Rooms, Count, HasNextPage)};
Err ->
make_error(Err, #{mucDomain => MUCDomain})
end.

after_to_luser(null) ->
undefined;
after_to_luser(JID) ->
{LUser, _} = jid:to_lus(JID),
LUser.
22 changes: 21 additions & 1 deletion src/graphql/mongoose_graphql_muc_light_helper.erl
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
-module(mongoose_graphql_muc_light_helper).

-import(mongoose_graphql_helper, [undefined_to_null/1]).

-export([make_room/1, make_ok_user/1, blocking_item_to_map/1, prepare_blocking_items/1,
page_size_or_max_limit/2, null_to_default/2, config_to_map/3]).
page_size_or_max_limit/2, null_to_default/2, config_to_map/3,
make_rooms_payload/3]).

-spec page_size_or_max_limit(null | integer(), integer()) -> integer().
page_size_or_max_limit(null, MaxLimit) ->
Expand Down Expand Up @@ -46,3 +49,20 @@ options_to_map(null) ->
#{};
options_to_map(Options) ->
maps:from_list([{K, V} || #{<<"key">> := K, <<"value">> := V} <- Options]).

-spec make_rooms_payload([mod_muc_light_api:room_desc()], non_neg_integer(),
boolean()) -> map().
make_rooms_payload(RoomDescs, Count, HasNextPage) ->
Rooms = [{ok, make_room_desc(D)} || D <- RoomDescs],
#{<<"rooms">> => Rooms,
<<"count">> => Count,
<<"nextPage">> => HasNextPage}.

-spec make_room_desc(mod_muc_light_api:room_desc()) -> map().
make_room_desc(#{jid := JID, name := Name, subject := Subject,
users_number := UsersNumber, owner := Owner}) ->
#{<<"jid">> => jid:to_binary(JID),
<<"name">> => undefined_to_null(Name),
<<"subject">> => undefined_to_null(Subject),
<<"usersNumber">> => UsersNumber,
<<"ownerJid">> => undefined_to_null(Owner)}.
Loading