1: -module(graphql_session_SUITE).
    2: 
    3: -include_lib("common_test/include/ct.hrl").
    4: -include_lib("eunit/include/eunit.hrl").
    5: -include_lib("escalus/include/escalus.hrl").
    6: -include_lib("exml/include/exml.hrl").
    7: 
    8: -compile([export_all, nowarn_export_all]).
    9: 
   10: -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]).
   11: -import(graphql_helper, [execute/3, execute_auth/2, get_listener_port/1,
   12:                          get_listener_config/1, get_ok_value/2, get_err_msg/1]).
   13: 
   14: suite() ->
   15:     require_rpc_nodes([mim]) ++ escalus:suite().
   16: 
   17: all() ->
   18:     [{group, user_session},
   19:      {group, admin_session}].
   20: 
   21: groups() ->
   22:     [{user_session, [parallel], user_session_handler()},
   23:      {admin_session, [], admin_session_handler()}].
   24: 
   25: user_session_handler() ->
   26:     [user_list_resources,
   27:      user_count_resources,
   28:      user_sessions_info].
   29: 
   30: admin_session_handler() ->
   31:     [admin_list_sessions,
   32:      admin_count_sessions,
   33:      admin_list_user_sessions,
   34:      admin_count_user_resources,
   35:      admin_get_user_resource,
   36:      admin_list_users_with_status,
   37:      admin_count_users_with_status,
   38:      admin_kick_session,
   39:      admin_set_presence,
   40:      admin_set_presence_away,
   41:      admin_set_presence_unavailable].
   42: 
   43: init_per_suite(Config) ->
   44:     Config2 = escalus:init_per_suite(Config),
   45:     dynamic_modules:save_modules(domain_helper:host_type(), Config2).
   46: 
   47: end_per_suite(Config) ->
   48:     dynamic_modules:restore_modules(Config),
   49:     escalus:end_per_suite(Config).
   50: 
   51: init_per_group(admin_session, Config) ->
   52:     Config1 = escalus:create_users(Config, escalus:get_users([alice, alice_bis, bob])),
   53:     graphql_helper:init_admin_handler(Config1);
   54: init_per_group(user_session, Config) ->
   55:     [{schema_endpoint, user} | Config].
   56: 
   57: end_per_group(admin_session, Config) ->
   58:     escalus_fresh:clean(),
   59:     escalus:delete_users(Config, escalus:get_users([alice, alice_bis, bob]));
   60: end_per_group(user_session, _Config) ->
   61:     escalus_fresh:clean().
   62: 
   63: init_per_testcase(CaseName, Config) ->
   64:     escalus:init_per_testcase(CaseName, Config).
   65: 
   66: end_per_testcase(CaseName, Config) ->
   67:     escalus:end_per_testcase(CaseName, Config).
   68: 
   69: %% Test cases
   70: 
   71: user_list_resources(Config) ->
   72:     escalus:fresh_story_with_config(Config, [{alice, 2}], fun user_list_resources_story/3).
   73: 
   74: user_list_resources_story(Config, Alice, Alice2) ->
   75:     Ep = ?config(schema_endpoint, Config),
   76:     Creds = make_creds(Alice),
   77: 
   78:     Query = <<"query{ session { listResources } }">>,
   79:     Result = execute(Ep, #{query => Query}, Creds),
   80: 
   81:     Path = [data, session, listResources],
   82:     ExpectedRes = [escalus_client:resource(Alice), escalus_client:resource(Alice2)],
   83:     ?assertMatch(ExpectedRes, lists:sort(get_ok_value(Path, Result))).
   84: 
   85: user_count_resources(Config) ->
   86:     escalus:fresh_story_with_config(Config, [{alice, 3}], fun user_count_resources_story/4).
   87: 
   88: user_count_resources_story(Config, Alice, _Alice2, _Alice3) ->
   89:     Ep = ?config(schema_endpoint, Config),
   90:     Creds = make_creds(Alice),
   91: 
   92:     Query = <<"query{ session { countResources } }">>,
   93:     Result = execute(Ep, #{query => Query}, Creds),
   94: 
   95:     Path = [data, session, countResources],
   96:     ?assertEqual(3, get_ok_value(Path, Result)).
   97: 
   98: user_sessions_info(Config) ->
   99:     escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_sessions_info_story/2).
  100: 
  101: user_sessions_info_story(Config, Alice) ->
  102:     Ep = ?config(schema_endpoint, Config),
  103:     Creds = make_creds(Alice),
  104: 
  105:     Query = <<"query{ session { listSessions { user } } }">>,
  106:     Result = execute(Ep, #{query => Query}, Creds),
  107:     ExpectedUser = escalus_utils:jid_to_lower(escalus_client:full_jid(Alice)),
  108: 
  109:     Path = [data, session, listSessions],
  110:     ?assertMatch([#{<<"user">> := ExpectedUser}], get_ok_value(Path, Result)).
  111: 
  112: admin_list_sessions(Config) ->
  113:     escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {bob, 1}],
  114:                                     fun admin_list_sessions_story/4).
  115: 
  116: admin_list_sessions_story(Config, _Alice, AliceB, _Bob) ->
  117:     BisDomain = escalus_client:server(AliceB),
  118:     Path = [data, session, listSessions],
  119:     % List all sessions
  120:     Res = execute_auth(list_sessions_body(null), Config),
  121:     Sessions = get_ok_value(Path, Res),
  122:     ?assertEqual(3, length(Sessions)),
  123:     % List sessions for a domain
  124:     Res2 = execute_auth(list_sessions_body(BisDomain), Config),
  125:     Sessions2 = get_ok_value(Path, Res2),
  126:     ?assertEqual(1, length(Sessions2)).
  127: 
  128: admin_count_sessions(Config) ->
  129:     escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {bob, 1}],
  130:                                     fun admin_count_sessions_story/4).
  131: 
  132: admin_count_sessions_story(Config, _Alice, AliceB, _Bob) ->
  133:     BisDomain = escalus_client:server(AliceB),
  134:     Path = [data, session, countSessions],
  135:     % Count all sessions
  136:     Res = execute_auth(count_sessions_body(null), Config),
  137:     Number = get_ok_value(Path, Res),
  138:     ?assertEqual(3, Number),
  139:     % Count sessions for a domain
  140:     Res2 = execute_auth(count_sessions_body(BisDomain), Config),
  141:     Number2 = get_ok_value(Path, Res2),
  142:     ?assertEqual(1, Number2).
  143: 
  144: admin_list_user_sessions(Config) ->
  145:     escalus:fresh_story_with_config(Config, [{alice, 2}, {bob, 1}],
  146:                                     fun admin_list_user_sessions_story/4).
  147: 
  148: admin_list_user_sessions_story(Config, Alice, Alice2, _Bob) ->
  149:     S1JID = escalus_client:full_jid(Alice),
  150:     S2JID = escalus_client:full_jid(Alice2),
  151:     Path = [data, session, listUserSessions],
  152:     Res = execute_auth(list_user_sessions_body(S1JID), Config),
  153:     ExpectedRes = lists:map(fun escalus_utils:jid_to_lower/1, [S1JID, S2JID]),
  154:     Sessions = get_ok_value(Path, Res),
  155:     ?assertEqual(2, length(Sessions)),
  156:     ?assert(users_match(ExpectedRes, Sessions)).
  157: 
  158: admin_count_user_resources(Config) ->
  159:     escalus:fresh_story_with_config(Config, [{alice, 3}], fun admin_count_user_resources_story/4).
  160: 
  161: admin_count_user_resources_story(Config, Alice, _Alice2, _Alice3) ->
  162:     Path = [data, session, countUserResources],
  163:     JID = escalus_client:full_jid(Alice),
  164:     Res = execute_auth(count_user_resources_body(JID), Config),
  165:     ?assertEqual(3, get_ok_value(Path, Res)).
  166: 
  167: admin_get_user_resource(Config) ->
  168:     escalus:fresh_story_with_config(Config, [{alice, 3}], fun admin_get_user_resource_story/4).
  169: 
  170: admin_get_user_resource_story(Config, Alice, Alice2, _Alice3) ->
  171:     Path = [data, session, getUserResource],
  172:     JID = escalus_client:short_jid(Alice),
  173:     % Provide a correct resource number
  174:     Res = execute_auth(get_user_resource_body(JID, 2), Config),
  175:     ?assertEqual(escalus_client:resource(Alice2), get_ok_value(Path, Res)),
  176:     % Provide a wrong resource number
  177:     Res2 = execute_auth(get_user_resource_body(JID, 4), Config),
  178:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"Wrong resource number">>)).
  179: 
  180: admin_count_users_with_status(Config) ->
  181:     escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}],
  182:                                     fun admin_count_users_with_status_story/3).
  183: 
  184: admin_count_users_with_status_story(Config, Alice, AliceB) ->
  185:     Path = [data, session, countUsersWithStatus],
  186:     AwayStatus = <<"away">>,
  187:     AwayPresence = escalus_stanza:presence_show(AwayStatus),
  188:     DndStatus = <<"dnd">>,
  189:     DndPresence = escalus_stanza:presence_show(DndStatus),
  190:     % Count users with away status globally
  191:     escalus_client:send(Alice, AwayPresence),
  192:     escalus_client:send(AliceB, AwayPresence),
  193:     Res = execute_auth(count_users_with_status_body(null, AwayStatus), Config),
  194:     ?assertEqual(2, get_ok_value(Path, Res)),
  195:     % Count users with away status for a domain
  196:     Res2 = execute_auth(count_users_with_status_body(domain_helper:domain(), AwayStatus), Config),
  197:     ?assertEqual(1, get_ok_value(Path, Res2)),
  198:     % Count users with dnd status globally
  199:     escalus_client:send(AliceB, DndPresence),
  200:     Res3 = execute_auth(count_users_with_status_body(null, DndStatus), Config),
  201:     ?assertEqual(1, get_ok_value(Path, Res3)).
  202: 
  203: admin_list_users_with_status(Config) ->
  204:     escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}],
  205:                                     fun admin_list_users_with_status_story/3).
  206: 
  207: admin_list_users_with_status_story(Config, Alice, AliceB) ->
  208:     AliceJID = escalus_client:full_jid(Alice),
  209:     AliceBJID = escalus_client:full_jid(AliceB),
  210:     Path = [data, session, listUsersWithStatus],
  211:     AwayStatus = <<"away">>,
  212:     AwayPresence = escalus_stanza:presence_show(AwayStatus),
  213:     DndStatus = <<"dnd">>,
  214:     DndPresence = escalus_stanza:presence_show(DndStatus),
  215:     % List users with away status globally
  216:     escalus_client:send(Alice, AwayPresence),
  217:     escalus_client:send(AliceB, AwayPresence),
  218:     Res = execute_auth(list_users_with_status_body(null, AwayStatus), Config),
  219:     StatusUsers = get_ok_value(Path, Res),
  220:     ?assertEqual(2, length(StatusUsers)),
  221:     ?assert(users_match([AliceJID, AliceBJID], StatusUsers)),
  222:     % List users with away status for a domain
  223:     Res2 = execute_auth(list_users_with_status_body(domain_helper:domain(), AwayStatus), Config),
  224:     StatusUsers2 = get_ok_value(Path, Res2),
  225:     ?assertEqual(1, length(StatusUsers2)),
  226:     ?assert(users_match([AliceJID], StatusUsers2)),
  227:     % List users with dnd status globally
  228:     escalus_client:send(AliceB, DndPresence),
  229:     Res3 = execute_auth(list_users_with_status_body(null, DndStatus), Config),
  230:     StatusUsers3 = get_ok_value(Path, Res3),
  231:     ?assertEqual(1, length(StatusUsers3)),
  232:     ?assert(users_match([AliceBJID], StatusUsers3)).
  233: 
  234: admin_kick_session(Config) ->
  235:     escalus:fresh_story_with_config(Config, [{alice, 2}], fun admin_kick_session_story/3).
  236: 
  237: admin_kick_session_story(Config, Alice1, Alice2) ->
  238:     JIDA1 = escalus_client:full_jid(Alice1),
  239:     JIDA2 = escalus_client:full_jid(Alice2),
  240:     Reason = <<"Test kick">>,
  241:     Path = [data, session, kickUser, message],
  242:     Res = execute_auth(kick_session_body(JIDA1, Reason), Config),
  243:     % Kick an active session
  244:     ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Res), <<"kicked">>)),
  245:     ?assertEqual(JIDA1, get_ok_value([data, session, kickUser, jid], Res)),
  246:     % Try to kick an offline session
  247:     Res2 = execute_auth(kick_session_body(JIDA1, Reason), Config),
  248:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"No active session">>)),
  249:     % Try to kick a session with JID without a resource
  250:     Res3 = execute_auth(kick_session_body(escalus_client:short_jid(Alice2), Reason), Config),
  251:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Res3), <<"No active session">>)),
  252:     % Kick another active session
  253:     Res4 = execute_auth(kick_session_body(JIDA2, Reason), Config),
  254:     ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Res4), <<"kicked">>)).
  255: 
  256: admin_set_presence(Config) ->
  257:     escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_set_presence_story/2).
  258: 
  259: admin_set_presence_story(Config, Alice) ->
  260:     JID = escalus_client:full_jid(Alice),
  261:     ShortJID = escalus_client:short_jid(Alice),
  262:     Type = <<"AVAILABLE">>,
  263:     Show = <<"ONLINE">>,
  264:     Status = <<"Be right back">>,
  265:     Priority = 1,
  266:     % Send short JID
  267:     Res = execute_auth(set_presence_body(ShortJID, Type, Show, Status, Priority), Config),
  268:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), <<"resource is empty">>)),
  269:     % Send full JID
  270:     Path = [data, session, setPresence, message],
  271:     Res2 = execute_auth(set_presence_body(JID, Type, Show, Status, Priority), Config),
  272:     ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Res2), <<"set successfully">>)),
  273:     Presence = escalus:wait_for_stanza(Alice),
  274:     ?assertNot(escalus_pred:is_presence_with_show(<<"online">>, Presence)),
  275:     escalus:assert(is_presence_with_type, [<<"available">>], Presence),
  276:     escalus:assert(is_presence_with_status, [Status], Presence),
  277:     escalus:assert(is_presence_with_priority, [<<"1">>], Presence).
  278: 
  279: admin_set_presence_away(Config) ->
  280:     escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_set_presence_away_story/2).
  281: 
  282: admin_set_presence_away_story(Config, Alice) ->
  283:     JID = escalus_client:full_jid(Alice),
  284:     Type = <<"AVAILABLE">>,
  285:     Show = <<"AWAY">>,
  286:     Path = [data, session, setPresence, message],
  287:     Res2 = execute_auth(set_presence_body(JID, Type, Show, null, null), Config),
  288:     ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Res2), <<"set successfully">>)),
  289:     Presence = escalus:wait_for_stanza(Alice),
  290:     escalus:assert(is_presence_with_type, [<<"available">>], Presence),
  291:     escalus:assert(is_presence_with_show, [<<"away">>], Presence).
  292: 
  293: admin_set_presence_unavailable(Config) ->
  294:     escalus:fresh_story_with_config(Config, [{alice, 2}], fun admin_set_presence_unavailable_story/3).
  295: 
  296: admin_set_presence_unavailable_story(Config, Alice, Alice2) ->
  297:     JID = escalus_client:full_jid(Alice),
  298:     Type = <<"UNAVAILABLE">>,
  299:     Status = <<"I'm sleeping">>,
  300:     Path = [data, session, setPresence, message],
  301:     Res2 = execute_auth(set_presence_body(JID, Type, null, Status, null), Config),
  302:     ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Res2), <<"set successfully">>)),
  303:     Presence = escalus:wait_for_stanza(Alice2),
  304:     escalus:assert(is_presence_with_type, [<<"unavailable">>], Presence),
  305:     escalus:assert(is_presence_with_status, [Status], Presence).
  306: 
  307: %% Helpers
  308: 
  309: -spec users_match([jid:literal_jid()], [#{user := jid:literal_jid()}]) -> boolean().
  310: users_match(Expected, Actual) ->
  311:     lists:all(fun(#{<<"user">> := JID}) -> lists:member(JID, Expected) end, Actual).
  312: 
  313: make_creds(#client{props = Props} = Client) ->
  314:     JID = escalus_utils:jid_to_lower(escalus_client:short_jid(Client)),
  315:     Password = proplists:get_value(password, Props),
  316:     {JID, Password}.
  317: 
  318: %% Request bodies
  319: 
  320: list_sessions_body(Domain) ->
  321:     Query = <<"query Q1($domain: String)
  322:               { session { listSessions(domain: $domain) { user } } }">>,
  323:     OpName = <<"Q1">>,
  324:     Vars = #{<<"domain">> => Domain},
  325:     #{query => Query, operationName => OpName, variables => Vars}.
  326: 
  327: count_sessions_body(Domain) ->
  328:     Query = <<"query Q1($domain: String)
  329:               { session { countSessions(domain: $domain) } }">>,
  330:     OpName = <<"Q1">>,
  331:     Vars = #{<<"domain">> => Domain},
  332:     #{query => Query, operationName => OpName, variables => Vars}.
  333: 
  334: list_user_sessions_body(JID) ->
  335:     Query = <<"query Q1($user: JID!)
  336:               { session { listUserSessions(user: $user) { user } } }">>,
  337:     OpName = <<"Q1">>,
  338:     Vars = #{<<"user">> => JID},
  339:     #{query => Query, operationName => OpName, variables => Vars}.
  340: 
  341: count_user_resources_body(JID) ->
  342:     Query = <<"query Q1($user: JID!)
  343:               { session { countUserResources(user: $user) } }">>,
  344:     OpName = <<"Q1">>,
  345:     Vars = #{<<"user">> => JID},
  346:     #{query => Query, operationName => OpName, variables => Vars}.
  347: 
  348: get_user_resource_body(JID, Number) ->
  349:     Query = <<"query Q1($user: JID!, $number: Int!)
  350:               { session { getUserResource(user: $user, number: $number) } }">>,
  351:     OpName = <<"Q1">>,
  352:     Vars = #{<<"user">> => JID, <<"number">> => Number},
  353:     #{query => Query, operationName => OpName, variables => Vars}.
  354: 
  355: list_users_with_status_body(Domain, Status) ->
  356:     Query = <<"query Q1($domain: String, $status: String!)
  357:               { session { listUsersWithStatus(domain: $domain, status: $status) { user } } }">>,
  358:     OpName = <<"Q1">>,
  359:     Vars = #{<<"domain">> => Domain, <<"status">> => Status},
  360:     #{query => Query, operationName => OpName, variables => Vars}.
  361: 
  362: count_users_with_status_body(Domain, Status) ->
  363:     Query = <<"query Q1($domain: String, $status: String!)
  364:               { session { countUsersWithStatus(domain: $domain, status: $status) } }">>,
  365:     OpName = <<"Q1">>,
  366:     Vars = #{<<"domain">> => Domain, <<"status">> => Status},
  367:     #{query => Query, operationName => OpName, variables => Vars}.
  368: 
  369: kick_session_body(JID, Reason) ->
  370:     Query = <<"mutation M1($user: JID!, $reason: String!)
  371:               { session { kickUser(user: $user, reason: $reason) { jid message }} }">>,
  372:     OpName = <<"M1">>,
  373:     Vars = #{<<"user">> => JID, <<"reason">> => Reason},
  374:     #{query => Query, operationName => OpName, variables => Vars}.
  375: 
  376: set_presence_body(JID, Type, Show, Status, Priority) ->
  377:     Query = <<"mutation M1($user: JID!, $type: PresenceType!, $show: PresenceShow, $status: String, $priority: Int)
  378:               { session { setPresence(user: $user, type: $type, show: $show, status: $status, priority: $priority)
  379:               { message jid } } }">>,
  380:     OpName = <<"M1">>,
  381:     Vars = #{<<"user">> => JID, <<"type">> => Type, <<"show">> => Show, <<"status">> => Status, <<"priority">> => Priority},
  382:     #{query => Query, operationName => OpName, variables => Vars}.