1: -module(graphql_last_SUITE).
    2: 
    3: -compile([export_all, nowarn_export_all]).
    4: 
    5: -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]).
    6: -import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1, user_to_jid/1,
    7:                          get_ok_value/2, get_err_msg/1, get_err_code/1]).
    8: 
    9: -include_lib("eunit/include/eunit.hrl").
   10: 
   11: -define(assertErrMsg(Res, ContainsPart), assert_err_msg(ContainsPart, Res)).
   12: -define(assertErrCode(Res, Code), assert_err_code(Code, Res)).
   13: 
   14: -define(NONEXISTENT_JID, <<"user@user.com">>).
   15: -define(DEFAULT_DT, <<"2022-04-17T12:58:30.000000Z">>).
   16: 
   17: suite() ->
   18:     require_rpc_nodes([mim]) ++ escalus:suite().
   19: 
   20: all() ->
   21:     [{group, user_last},
   22:      {group, admin_last},
   23:      {group, admin_last_old_users}].
   24: 
   25: groups() ->
   26:     [{user_last, [parallel], user_last_handler()},
   27:      {admin_last, [parallel], admin_last_handler()},
   28:      {admin_last_old_users, [], admin_old_users_handler()}].
   29: 
   30: user_last_handler() ->
   31:     [user_set_last,
   32:      user_get_last,
   33:      user_get_other_user_last].
   34: 
   35: admin_last_handler() ->
   36:     [admin_set_last,
   37:      admin_try_set_nonexistent_user_last,
   38:      admin_get_last,
   39:      admin_get_nonexistent_user_last,
   40:      admin_try_get_nonexistent_last,
   41:      admin_count_active_users,
   42:      admin_try_count_nonexistent_domain_active_users].
   43: 
   44: admin_old_users_handler() ->
   45:     [admin_list_old_users_domain,
   46:      admin_try_list_old_users_nonexistent_domain,
   47:      admin_list_old_users_global,
   48:      admin_remove_old_users_domain,
   49:      admin_try_remove_old_users_nonexistent_domain,
   50:      admin_remove_old_users_global,
   51:      admin_user_without_last_info_is_old_user,
   52:      admin_logged_user_is_not_old_user].
   53: 
   54: init_per_suite(Config) ->
   55:     HostType = domain_helper:host_type(),
   56:     SecHostType = domain_helper:secondary_host_type(),
   57:     Config1 = escalus:init_per_suite(Config),
   58:     Config2 = dynamic_modules:save_modules([HostType, SecHostType], Config1),
   59:     Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType),
   60:     SecBackend = mongoose_helper:get_backend_mnesia_rdbms_riak(SecHostType),
   61:     dynamic_modules:ensure_modules(HostType, required_modules(Backend)),
   62:     dynamic_modules:ensure_modules(SecHostType, required_modules(SecBackend)),
   63:     escalus:init_per_suite(Config2).
   64: 
   65: end_per_suite(Config) ->
   66:     dynamic_modules:restore_modules(Config),
   67:     escalus:end_per_suite(Config).
   68: 
   69: init_per_group(admin_last, Config) ->
   70:     graphql_helper:init_admin_handler(Config);
   71: init_per_group(admin_last_old_users, Config) ->
   72:     AuthMods = mongoose_helper:auth_modules(),
   73:     case lists:member(ejabberd_auth_ldap, AuthMods) of
   74:         true -> {skip, not_fully_supported_with_ldap};
   75:         false -> graphql_helper:init_admin_handler(Config)
   76:     end;
   77: init_per_group(user_last, Config) ->
   78:     [{schema_endpoint, user} | Config].
   79: 
   80: end_per_group(_, _Config) ->
   81:     escalus_fresh:clean().
   82: 
   83: init_per_testcase(C, Config) when C =:= admin_remove_old_users_domain;
   84:                                   C =:= admin_remove_old_users_global;
   85:                                   C =:= admin_list_old_users_domain;
   86:                                   C =:= admin_list_old_users_global;
   87:                                   C =:= admin_user_without_last_info_is_old_user ->
   88:     Config1 = escalus:create_users(Config, escalus:get_users([alice, bob, alice_bis])),
   89:     escalus:init_per_testcase(C, Config1);
   90: init_per_testcase(CaseName, Config) ->
   91:     escalus:init_per_testcase(CaseName, Config).
   92: 
   93: end_per_testcase(C, Config) when C =:= admin_remove_old_users_domain;
   94:                                  C =:= admin_remove_old_users_global;
   95:                                  C =:= admin_list_old_users_domain;
   96:                                  C =:= admin_list_old_users_global;
   97:                                  C =:= admin_user_without_last_info_is_old_user ->
   98:     escalus:delete_users(Config, escalus:get_users([alice, bob, alice_bis])),
   99:     escalus:end_per_testcase(C, Config);
  100: end_per_testcase(CaseName, Config) ->
  101:     escalus:end_per_testcase(CaseName, Config).
  102: 
  103: required_modules(riak) ->
  104:     [{mod_last, #{backend => riak,
  105:                   iqdisc => one_queue,
  106:                   riak => #{bucket_type => <<"last">>}}}];
  107: required_modules(Backend) ->
  108:     [{mod_last, #{backend => Backend,
  109:                   iqdisc => one_queue}}].
  110: 
  111: %% Admin test cases
  112: 
  113: admin_set_last(Config) ->
  114:     escalus:fresh_story_with_config(Config, [{alice, 1}],
  115:                                     fun admin_set_last/2).
  116: 
  117: admin_set_last(Config, Alice) ->
  118:     Status = <<"First status">>,
  119:     JID = escalus_utils:jid_to_lower(user_to_bin(Alice)),
  120:     % With timestamp provided
  121:     Res = execute_auth(admin_set_last_body(Alice, Status, ?DEFAULT_DT), Config),
  122:     #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} =
  123:         get_ok_value(p(setLast), Res),
  124:     % Without timestamp provided
  125:     Status2 = <<"Second status">>,
  126:     Res2 = execute_auth(admin_set_last_body(Alice, Status2, null), Config),
  127:     #{<<"user">> := JID, <<"status">> := Status2, <<"timestamp">> := DateTime2} =
  128:         get_ok_value(p(setLast), Res2),
  129:     ?assert(os:system_time(second) - dt_to_unit(DateTime2, second) < 2).
  130: 
  131: admin_try_set_nonexistent_user_last(Config) ->
  132:     Res = execute_auth(admin_set_last_body(?NONEXISTENT_JID, <<"status">>, null), Config),
  133:     ?assertErrMsg(Res, <<"not exist">>),
  134:     ?assertErrCode(Res, user_does_not_exist).
  135: 
  136: admin_get_last(Config) ->
  137:     escalus:fresh_story_with_config(Config, [{alice, 1}],
  138:                                     fun admin_get_last/2).
  139: 
  140: admin_get_last(Config, Alice) ->
  141:     Status = <<"I love ducks">>,
  142:     JID = escalus_utils:jid_to_lower(user_to_bin(Alice)),
  143:     execute_auth(admin_set_last_body(Alice, Status, ?DEFAULT_DT), Config),
  144:     Res = execute_auth(admin_get_last_body(Alice), Config),
  145:     #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} =
  146:         get_ok_value(p(getLast), Res).
  147: 
  148: admin_get_nonexistent_user_last(Config) ->
  149:     Res = execute_auth(admin_get_last_body(?NONEXISTENT_JID), Config),
  150:     ?assertErrMsg(Res, <<"not exist">>),
  151:     ?assertErrCode(Res, user_does_not_exist).
  152: 
  153: admin_try_get_nonexistent_last(Config) ->
  154:     escalus:fresh_story_with_config(Config, [{alice, 1}],
  155:                                     fun admin_try_get_nonexistent_last/2).
  156: 
  157: admin_try_get_nonexistent_last(Config, Alice) ->
  158:     Res = execute_auth(admin_get_last_body(Alice), Config),
  159:     ?assertErrMsg(Res, <<"not found">>),
  160:     ?assertErrCode(Res, last_not_found).
  161: 
  162: admin_count_active_users(Config) ->
  163:     escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}],
  164:                                     fun admin_count_active_users/3).
  165: 
  166: admin_count_active_users(Config, Alice, Bob) ->
  167:     Domain = domain_helper:domain(),
  168:     set_last(Alice, now_dt_with_offset(5), Config),
  169:     set_last(Bob, now_dt_with_offset(10), Config),
  170:     Res = execute_auth(admin_count_active_users_body(Domain, null), Config),
  171:     ?assertEqual(2, get_ok_value(p(countActiveUsers), Res)),
  172:     Res2 = execute_auth(admin_count_active_users_body(Domain, now_dt_with_offset(30)), Config),
  173:     ?assertEqual(0, get_ok_value(p(countActiveUsers), Res2)).
  174: 
  175: admin_try_count_nonexistent_domain_active_users(Config) ->
  176:     Res = execute_auth(admin_count_active_users_body(<<"unknown-domain.com">>, null), Config),
  177:     ?assertErrMsg(Res, <<"not found">>),
  178:     ?assertErrCode(Res, domain_not_found).
  179: 
  180: %% Admin old users test cases
  181: 
  182: admin_remove_old_users_domain(Config) ->
  183:     jids_with_config(Config, [alice, alice_bis, bob], fun admin_remove_old_users_domain/4).
  184: 
  185: admin_remove_old_users_domain(Config, Alice, AliceBis, Bob) ->
  186:     Domain = domain_helper:domain(),
  187:     ToRemoveDateTime = now_dt_with_offset(100),
  188: 
  189:     set_last(Bob, ToRemoveDateTime, Config),
  190:     set_last(AliceBis, ToRemoveDateTime, Config),
  191:     set_last(Alice, now_dt_with_offset(200), Config),
  192: 
  193:     Resp = execute_auth(admin_remove_old_users_body(Domain, now_dt_with_offset(150)), Config),
  194:     [#{<<"jid">> := Bob, <<"timestamp">> := BobDateTimeRes}] = get_ok_value(p(removeOldUsers), Resp),
  195:     ?assertEqual(dt_to_unit(ToRemoveDateTime, second), dt_to_unit(BobDateTimeRes, second)),
  196:     ?assertMatch({user_does_not_exist, _}, check_account(Bob)),
  197:     ?assertMatch({ok, _}, check_account(Alice)),
  198:     ?assertMatch({ok, _}, check_account(AliceBis)).
  199: 
  200: admin_try_remove_old_users_nonexistent_domain(Config) ->
  201:     Res = execute_auth(admin_remove_old_users_body(<<"nonexistent">>, now_dt_with_offset(0)), Config),
  202:     ?assertErrMsg(Res, <<"not found">>),
  203:     ?assertErrCode(Res, domain_not_found).
  204: 
  205: admin_remove_old_users_global(Config) ->
  206:     jids_with_config(Config, [alice, alice_bis, bob], fun admin_remove_old_users_global/4).
  207: 
  208: admin_remove_old_users_global(Config, Alice, AliceBis, Bob) ->
  209:     ToRemoveDateTime = now_dt_with_offset(100),
  210:     ToRemoveTimestamp = dt_to_unit(ToRemoveDateTime, second),
  211: 
  212:     set_last(Bob, ToRemoveDateTime, Config),
  213:     set_last(AliceBis, ToRemoveDateTime, Config),
  214:     set_last(Alice, now_dt_with_offset(200), Config),
  215: 
  216:     Resp = execute_auth(admin_remove_old_users_body(null, now_dt_with_offset(150)), Config),
  217:     [#{<<"jid">> := AliceBis, <<"timestamp">> := AliceBisDateTime},
  218:      #{<<"jid">> := Bob, <<"timestamp">> := BobDateTime}] =
  219:         lists:sort(get_ok_value(p(removeOldUsers), Resp)),
  220:     ?assertEqual(ToRemoveTimestamp, dt_to_unit(BobDateTime, second)),
  221:     ?assertEqual(ToRemoveTimestamp, dt_to_unit(AliceBisDateTime, second)),
  222:     ?assertMatch({user_does_not_exist, _}, check_account(Bob)),
  223:     ?assertMatch({user_does_not_exist, _}, check_account(AliceBis)),
  224:     ?assertMatch({ok, _}, check_account(Alice)).
  225: 
  226: admin_list_old_users_domain(Config) ->
  227:     jids_with_config(Config, [alice, bob], fun admin_list_old_users_domain/3).
  228: 
  229: admin_list_old_users_domain(Config, Alice, Bob) ->
  230:     Domain = domain_helper:domain(),
  231:     OldDateTime = now_dt_with_offset(100),
  232: 
  233:     set_last(Bob, OldDateTime, Config),
  234:     set_last(Alice, now_dt_with_offset(200), Config),
  235: 
  236:     Res = execute_auth(admin_list_old_users_body(Domain, now_dt_with_offset(150)), Config),
  237:     [#{<<"jid">> := Bob, <<"timestamp">> := BobDateTime}] = get_ok_value(p(listOldUsers), Res),
  238:     ?assertEqual(dt_to_unit(OldDateTime, second), dt_to_unit(BobDateTime, second)).
  239: 
  240: admin_try_list_old_users_nonexistent_domain(Config) ->
  241:     Res = execute_auth(admin_list_old_users_body(<<"nonexistent">>, now_dt_with_offset(0)), Config),
  242:     ?assertErrMsg(Res, <<"not found">>),
  243:     ?assertErrCode(Res, domain_not_found).
  244: 
  245: admin_list_old_users_global(Config) ->
  246:     jids_with_config(Config, [alice, alice_bis, bob], fun admin_list_old_users_global/4).
  247: 
  248: admin_list_old_users_global(Config, Alice, AliceBis, Bob) ->
  249:     OldDateTime = now_dt_with_offset(100),
  250: 
  251:     set_last(Bob, OldDateTime, Config),
  252:     set_last(AliceBis, OldDateTime, Config),
  253:     set_last(Alice, now_dt_with_offset(200), Config),
  254: 
  255:     Res = execute_auth(admin_list_old_users_body(null, now_dt_with_offset(150)), Config),
  256:     [#{<<"jid">> := AliceBis, <<"timestamp">> := AliceBisDateTime},
  257:      #{<<"jid">> := Bob, <<"timestamp">> := BobDateTime}] =
  258:         lists:sort(get_ok_value(p(listOldUsers), Res)),
  259:     ?assertEqual(dt_to_unit(OldDateTime, second), dt_to_unit(BobDateTime, second)),
  260:     ?assertEqual(dt_to_unit(OldDateTime, second), dt_to_unit(AliceBisDateTime, second)).
  261: 
  262: admin_user_without_last_info_is_old_user(Config) ->
  263:     Res = execute_auth(admin_list_old_users_body(null, now_dt_with_offset(150)), Config),
  264:     OldUsers = get_ok_value(p(listOldUsers), Res),
  265:     ?assertEqual(3, length(OldUsers)),
  266:     [?assertEqual(null, TS) || #{<<"timestamp">> := TS} <- OldUsers].
  267: 
  268: admin_logged_user_is_not_old_user(Config) ->
  269:     escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_logged_user_is_not_old_user/2).
  270: 
  271: admin_logged_user_is_not_old_user(Config, _Alice) ->
  272:     Res = execute_auth(admin_list_old_users_body(null, now_dt_with_offset(100)), Config),
  273:     ?assertEqual([], get_ok_value(p(listOldUsers), Res)).
  274: 
  275: %% User test cases
  276: 
  277: user_set_last(Config) ->
  278:     escalus:fresh_story_with_config(Config, [{alice, 1}],
  279:                                     fun user_set_last/2).
  280: 
  281: user_set_last(Config, Alice) ->
  282:     Status = <<"My first status">>,
  283:     JID = escalus_utils:jid_to_lower(user_to_bin(Alice)),
  284:     Res = execute_user(user_set_last_body(Status, ?DEFAULT_DT), Alice, Config),
  285:     #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} =
  286:         get_ok_value(p(setLast), Res),
  287:     Status2 = <<"Quack Quack">>,
  288:     Res2 = execute_user(user_set_last_body(Status2, null), Alice, Config),
  289:     #{<<"user">> := JID, <<"status">> := Status2, <<"timestamp">> := DateTime2} =
  290:         get_ok_value(p(setLast), Res2),
  291:     ?assert(os:system_time(second) - dt_to_unit(DateTime2, second) < 2).
  292: 
  293: user_get_last(Config) ->
  294:     escalus:fresh_story_with_config(Config, [{alice, 1}],
  295:                                     fun user_get_last/2).
  296: user_get_last(Config, Alice) ->
  297:     Status = <<"I love ducks">>,
  298:     JID = escalus_utils:jid_to_lower(user_to_bin(Alice)),
  299:     execute_user(user_set_last_body(Status, ?DEFAULT_DT), Alice, Config),
  300:     Res = execute_user(user_get_last_body(null), Alice, Config),
  301:     #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} =
  302:         get_ok_value(p(getLast), Res).
  303: 
  304: user_get_other_user_last(Config) ->
  305:     escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}],
  306:                                     fun user_get_other_user_last/3).
  307: 
  308: user_get_other_user_last(Config, Alice, Bob) ->
  309:     Status = <<"In good mood">>,
  310:     JID = escalus_utils:jid_to_lower(user_to_bin(Bob)),
  311:     execute_user(user_set_last_body(Status, ?DEFAULT_DT), Bob, Config),
  312:     Res = execute_user(admin_get_last_body(Bob), Alice, Config),
  313:     #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} =
  314:         get_ok_value(p(getLast), Res).
  315: 
  316: %% Helpers
  317: 
  318: jids_with_config(Config, Users, Fun) ->
  319:     Args = [escalus_utils:jid_to_lower(escalus_users:get_jid(Config, User)) || User <- Users],
  320:     apply(Fun, [Config | Args]).
  321: 
  322: set_last(UserJID, DateTime, Config) ->
  323:     execute_auth(admin_set_last_body(UserJID, <<>>, DateTime), Config).
  324: 
  325: check_account(User) ->
  326:     {Username, LServer} = jid:to_lus(user_to_jid(User)),
  327:     rpc(mim(), mongoose_account_api, check_account, [Username, LServer]).
  328: 
  329: assert_err_msg(Contains, Res) ->
  330:     ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), Contains)).
  331: 
  332: assert_err_code(Code, Res) ->
  333:     ?assertEqual(atom_to_binary(Code), get_err_code(Res)).
  334: 
  335: p(Cmd) when is_atom(Cmd) ->
  336:     [data, last, Cmd];
  337: p(Path) when is_list(Path) ->
  338:     [data, last] ++ Path.
  339: 
  340: now_dt_with_offset(SecondsOffset) ->
  341:     Seconds = erlang:system_time(second) + SecondsOffset,
  342:     list_to_binary(calendar:system_time_to_rfc3339(Seconds, [{unit, second}, {offset, "Z"}])).
  343: 
  344: dt_to_unit(ISODateTime, Unit) ->
  345:     calendar:rfc3339_to_system_time(binary_to_list(ISODateTime), [{unit, Unit}]).
  346: 
  347: %% Request bodies
  348: 
  349: admin_set_last_body(User, Status, DateTime) ->
  350:     Query = <<"mutation M1($user: JID!, $timestamp: DateTime, $status: String!)
  351:               { last { setLast (user: $user, timestamp: $timestamp, status: $status)
  352:               { user timestamp status } } }">>,
  353:     OpName = <<"M1">>,
  354:     Vars = #{user => user_to_bin(User), timestamp => DateTime, status => Status},
  355:     #{query => Query, operationName => OpName, variables => Vars}.
  356: 
  357: admin_get_last_body(User) ->
  358:     Query = <<"query Q1($user: JID!)
  359:               { last { getLast(user: $user)
  360:               { user timestamp status } } }">>,
  361:     OpName = <<"Q1">>,
  362:     Vars = #{user => user_to_bin(User)},
  363:     #{query => Query, operationName => OpName, variables => Vars}.
  364: 
  365: admin_count_active_users_body(Domain, Timestamp) ->
  366:     Query = <<"query Q1($domain: String!, $timestamp: DateTime)
  367:               { last { countActiveUsers(domain: $domain, timestamp: $timestamp) } }">>,
  368:     OpName = <<"Q1">>,
  369:     Vars = #{domain => Domain, timestamp => Timestamp},
  370:     #{query => Query, operationName => OpName, variables => Vars}.
  371: 
  372: admin_remove_old_users_body(Domain, Timestamp) ->
  373:     Query = <<"mutation M1($domain: String, $timestamp: DateTime!)
  374:               { last { removeOldUsers(domain: $domain, timestamp: $timestamp) { jid timestamp } } }">>,
  375:     OpName = <<"M1">>,
  376:     Vars = #{domain => Domain, timestamp => Timestamp},
  377:     #{query => Query, operationName => OpName, variables => Vars}.
  378: 
  379: admin_list_old_users_body(Domain, Timestamp) ->
  380:     Query = <<"query Q1($domain: String, $timestamp: DateTime!)
  381:               { last { listOldUsers(domain: $domain, timestamp: $timestamp) { jid timestamp } } }">>,
  382:     OpName = <<"Q1">>,
  383:     Vars = #{domain => Domain, timestamp => Timestamp},
  384:     #{query => Query, operationName => OpName, variables => Vars}.
  385: 
  386: user_set_last_body(Status, DateTime) ->
  387:     Query = <<"mutation M1($timestamp: DateTime, $status: String!)
  388:               { last { setLast (timestamp: $timestamp, status: $status)
  389:               { user timestamp status } } }">>,
  390:     OpName = <<"M1">>,
  391:     Vars = #{timestamp => DateTime, status => Status},
  392:     #{query => Query, operationName => OpName, variables => Vars}.
  393: 
  394: user_get_last_body(User) ->
  395:     Query = <<"query Q1($user: JID)
  396:               { last { getLast(user: $user)
  397:               { user timestamp status } } }">>,
  398:     OpName = <<"Q1">>,
  399:     Vars = #{user => user_to_bin(User)},
  400:     #{query => Query, operationName => OpName, variables => Vars}.