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