diff --git a/big_tests/tests/graphql_account_SUITE.erl b/big_tests/tests/graphql_account_SUITE.erl index d80cbe3f620..f8e2892cfcc 100644 --- a/big_tests/tests/graphql_account_SUITE.erl +++ b/big_tests/tests/graphql_account_SUITE.erl @@ -6,7 +6,7 @@ -compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). --import(graphql_helper, [execute/3, execute_auth/2, get_listener_port/1, +-import(graphql_helper, [execute/3, execute_auth/2, execute_command/4, get_listener_port/1, get_listener_config/1, get_ok_value/2, get_err_msg/1]). -define(NOT_EXISTING_JID, <<"unknown987@unknown">>). @@ -16,17 +16,19 @@ suite() -> all() -> [{group, user_account_handler}, - {group, admin_account_handler}]. + {group, admin_account_handler}, + {group, admin_account_cli}]. groups() -> - [{user_account_handler, [parallel], user_account_handler()}, - {admin_account_handler, [], admin_account_handler()}]. + [{user_account_handler, [parallel], user_account_tests()}, + {admin_account_handler, [], admin_account_tests()}, + {admin_account_cli, [], admin_account_tests()}]. -user_account_handler() -> +user_account_tests() -> [user_unregister, user_change_password]. -admin_account_handler() -> +admin_account_tests() -> [admin_list_users, admin_count_users, admin_check_password, @@ -43,28 +45,38 @@ admin_account_handler() -> init_per_suite(Config) -> Config1 = [{ctl_auth_mods, mongoose_helper:auth_modules()} | Config], Config2 = escalus:init_per_suite(Config1), - dynamic_modules:save_modules(domain_helper:host_type(), Config2). + Config3 = ejabberd_node_utils:init(mim(), Config2), + dynamic_modules:save_modules(domain_helper:host_type(), Config3). end_per_suite(Config) -> dynamic_modules:restore_modules(Config), escalus:end_per_suite(Config). init_per_group(admin_account_handler, Config) -> - Config1 = escalus:create_users(Config, escalus:get_users([alice])), - graphql_helper:init_admin_handler(Config1); + graphql_helper:init_admin_handler(init_users(Config)); +init_per_group(admin_account_cli, Config) -> + graphql_helper:init_admin_cli(init_users(Config)); init_per_group(user_account_handler, Config) -> [{schema_endpoint, user} | Config]; init_per_group(_, Config) -> Config. end_per_group(admin_account_handler, Config) -> - escalus_fresh:clean(), - escalus:delete_users(Config, escalus:get_users([alice])); + clean_users(Config); +end_per_group(admin_account_cli, Config) -> + clean_users(Config); end_per_group(user_account_handler, _Config) -> escalus_fresh:clean(); end_per_group(_, _Config) -> ok. +init_users(Config) -> + escalus:create_users(Config, escalus:get_users([alice])). + +clean_users(Config) -> + escalus_fresh:clean(), + escalus:delete_users(Config, escalus:get_users([alice])). + init_per_testcase(admin_register_user = C, Config) -> Config1 = [{user, {<<"gql_admin_registration_test">>, domain_helper:domain()}} | Config], escalus:init_per_testcase(C, Config1); @@ -110,7 +122,7 @@ user_unregister_story(Config, Alice) -> AllUsers = rpc(mim(), mongoose_account_api, list_users, [domain_helper:domain()]), LAliceJID = jid:to_binary(jid:to_lower((jid:binary_to_bare(BinJID)))), ?assertNot(lists:member(LAliceJID, AllUsers)). - + user_change_password(Config) -> escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_change_password_story/2). @@ -128,23 +140,23 @@ user_change_password_story(Config, Alice) -> admin_list_users(Config) -> % An unknown domain - Resp = execute_auth(list_users_body(<<"unknown-domain">>), Config), + Resp = list_users(<<"unknown-domain">>, Config), ?assertEqual([], get_ok_value([data, account, listUsers], Resp)), % A domain with users Domain = domain_helper:domain(), Username = jid:nameprep(escalus_users:get_username(Config, alice)), JID = <>, - Resp2 = execute_auth(list_users_body(Domain), Config), + Resp2 = list_users(Domain, Config), Users = get_ok_value([data, account, listUsers], Resp2), ?assert(lists:member(JID, Users)). - + admin_count_users(Config) -> % An unknown domain - Resp = execute_auth(count_users_body(<<"unknown-domain">>), Config), + Resp = count_users(<<"unknown-domain">>, Config), ?assertEqual(0, get_ok_value([data, account, countUsers], Resp)), % A domain with at least one user Domain = domain_helper:domain(), - Resp2 = execute_auth(count_users_body(Domain), Config), + Resp2 = count_users(Domain, Config), ?assert(0 < get_ok_value([data, account, countUsers], Resp2)). admin_check_password(Config) -> @@ -152,24 +164,24 @@ admin_check_password(Config) -> BinJID = escalus_users:get_jid(Config, alice), Path = [data, account, checkPassword], % A correct password - Resp1 = execute_auth(check_password_body(BinJID, Password), Config), + Resp1 = check_password(BinJID, Password, Config), ?assertMatch(#{<<"correct">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)), % An incorrect password - Resp2 = execute_auth(check_password_body(BinJID, <<"incorrect_pw">>), Config), + Resp2 = check_password(BinJID, <<"incorrect_pw">>, Config), ?assertMatch(#{<<"correct">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)), % A non-existing user - Resp3 = execute_auth(check_password_body(?NOT_EXISTING_JID, Password), Config), - ?assertEqual(null, get_ok_value(Path, Resp3)). + Resp3 = check_password(?NOT_EXISTING_JID, Password, Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp3), <<"not exist">>)). admin_check_password_hash(Config) -> UserSCRAM = escalus_users:get_jid(Config, alice), EmptyHash = list_to_binary(get_md5(<<>>)), Method = <<"md5">>, % SCRAM password user - Resp1 = execute_auth(check_password_hash_body(UserSCRAM, EmptyHash, Method), Config), + Resp1 = check_password_hash(UserSCRAM, EmptyHash, Method, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"SCRAM password">>)), % A non-existing user - Resp2 = execute_auth(check_password_hash_body(?NOT_EXISTING_JID, EmptyHash, Method), Config), + Resp2 = check_password_hash(?NOT_EXISTING_JID, EmptyHash, Method, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"not exist">>)). admin_check_plain_password_hash(Config) -> @@ -180,23 +192,23 @@ admin_check_plain_password_hash(Config) -> WrongHash = list_to_binary(get_md5(<<"wrong password">>)), Path = [data, account, checkPasswordHash], % A correct hash - Resp = execute_auth(check_password_hash_body(UserJID, Hash, Method), Config), + Resp = check_password_hash(UserJID, Hash, Method, Config), ?assertMatch(#{<<"correct">> := true, <<"message">> := _}, get_ok_value(Path, Resp)), % An incorrect hash - Resp2 = execute_auth(check_password_hash_body(UserJID, WrongHash, Method), Config), + Resp2 = check_password_hash(UserJID, WrongHash, Method, Config), ?assertMatch(#{<<"correct">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)), % A not-supported hash method - Resp3 = execute_auth(check_password_hash_body(UserJID, Hash, <<"a">>), Config), + Resp3 = check_password_hash(UserJID, Hash, <<"a">>, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp3), <<"not supported">>)). admin_check_user(Config) -> BinJID = escalus_users:get_jid(Config, alice), Path = [data, account, checkUser], % An existing user - Resp1 = execute_auth(check_user_body(BinJID), Config), + Resp1 = check_user(BinJID, Config), ?assertMatch(#{<<"exist">> := true, <<"message">> := _}, get_ok_value(Path, Resp1)), % A non-existing user - Resp2 = execute_auth(check_user_body(?NOT_EXISTING_JID), Config), + Resp2 = check_user(?NOT_EXISTING_JID, Config), ?assertMatch(#{<<"exist">> := false, <<"message">> := _}, get_ok_value(Path, Resp2)). admin_register_user(Config) -> @@ -204,10 +216,10 @@ admin_register_user(Config) -> {Username, Domain} = proplists:get_value(user, Config), Path = [data, account, registerUser, message], % Register a new user - Resp1 = execute_auth(register_user_body(Domain, Username, Password), Config), + Resp1 = register_user(Domain, Username, Password, Config), ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp1), <<"successfully registered">>)), % Try to register a user with existing name - Resp2 = execute_auth(register_user_body(Domain, Username, Password), Config), + Resp2 = register_user(Domain, Username, Password, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"already registered">>)). admin_register_random_user(Config) -> @@ -215,7 +227,7 @@ admin_register_random_user(Config) -> Domain = domain_helper:domain(), Path = [data, account, registerUser], % Register a new user - Resp1 = execute_auth(register_user_body(Domain, null, Password), Config), + Resp1 = register_random_user(Domain, Password, Config), #{<<"message">> := Msg, <<"jid">> := JID} = get_ok_value(Path, Resp1), {Username, Server} = jid:to_lus(jid:from_binary(JID)), @@ -223,14 +235,14 @@ admin_register_random_user(Config) -> {ok, _} = rpc(mim(), mongoose_account_api, unregister_user, [Username, Server]). admin_remove_non_existing_user(Config) -> - Resp = execute_auth(remove_user_body(?NOT_EXISTING_JID), Config), + Resp = remove_user(?NOT_EXISTING_JID, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp), <<"not exist">>)). admin_remove_existing_user(Config) -> escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> Path = [data, account, removeUser, message], BinJID = escalus_client:full_jid(Alice), - Resp4 = execute_auth(remove_user_body(BinJID), Config), + Resp4 = remove_user(BinJID, Config), ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp4), <<"successfully unregister">>)) end). @@ -239,12 +251,12 @@ admin_ban_user(Config) -> Path = [data, account, banUser, message], Reason = <<"annoying">>, % Ban not existing user - Resp1 = execute_auth(ban_user_body(?NOT_EXISTING_JID, Reason), Config), + Resp1 = ban_user(?NOT_EXISTING_JID, Reason, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"not allowed">>)), % Ban an existing user escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> BinJID = escalus_client:full_jid(Alice), - Resp2 = execute_auth(ban_user_body(BinJID, Reason), Config), + Resp2 = ban_user(BinJID, Reason, Config), ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp2), <<"successfully banned">>)) end). @@ -252,15 +264,15 @@ admin_change_user_password(Config) -> Path = [data, account, changeUserPassword, message], NewPassword = <<"new password">>, % Change password of not existing user - Resp1 = execute_auth(change_user_password_body(?NOT_EXISTING_JID, NewPassword), Config), + Resp1 = change_user_password(?NOT_EXISTING_JID, NewPassword, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp1), <<"not allowed">>)), % Set an empty password - Resp2 = execute_auth(change_user_password_body(?NOT_EXISTING_JID, <<>>), Config), + Resp2 = change_user_password(?NOT_EXISTING_JID, <<>>, Config), ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp2), <<"Empty password">>)), % Change password of an existing user escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> BinJID = escalus_client:full_jid(Alice), - Resp3 = execute_auth(change_user_password_body(BinJID, NewPassword), Config), + Resp3 = change_user_password(BinJID, NewPassword, Config), ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp3), <<"Password changed">>)) end). @@ -270,70 +282,49 @@ get_md5(AccountPass) -> lists:flatten([io_lib:format("~.16B", [X]) || X <- binary_to_list(crypto:hash(md5, AccountPass))]). -%% Request bodies +%% Admin commands -list_users_body(Domain) -> - Query = <<"query Q1($domain: String!) { account { listUsers(domain: $domain) } }">>, - OpName = <<"Q1">>, +list_users(Domain, Config) -> Vars = #{<<"domain">> => Domain}, - #{query => Query, operationName => OpName, variables => Vars}. + execute_command(<<"account">>, <<"listUsers">>, Vars, Config). -count_users_body(Domain) -> - Query = <<"query Q1($domain: String!) { account { countUsers(domain: $domain) } }">>, - OpName = <<"Q1">>, +count_users(Domain, Config) -> Vars = #{<<"domain">> => Domain}, - #{query => Query, operationName => OpName, variables => Vars}. + execute_command(<<"account">>, <<"countUsers">>, Vars, Config). -check_password_body(User, Password) -> - Query = <<"query Q1($user: JID!, $password: String!) - { account { checkPassword(user: $user, password: $password) {correct message} } }">>, - OpName = <<"Q1">>, +check_password(User, Password, Config) -> Vars = #{<<"user">> => User, <<"password">> => Password}, - #{query => Query, operationName => OpName, variables => Vars}. + execute_command(<<"account">>, <<"checkPassword">>, Vars, Config). -check_password_hash_body(User, PasswordHash, HashMethod) -> - Query = <<"query Q1($user: JID!, $hash: String!, $method: String!) - { account { checkPasswordHash(user: $user, passwordHash: $hash, hashMethod: $method) - {correct message} } }">>, - OpName = <<"Q1">>, - Vars = #{<<"user">> => User, <<"hash">> => PasswordHash, <<"method">> => HashMethod}, - #{query => Query, operationName => OpName, variables => Vars}. +check_password_hash(User, PasswordHash, HashMethod, Config) -> + Vars = #{<<"user">> => User, <<"passwordHash">> => PasswordHash, <<"hashMethod">> => HashMethod}, + execute_command(<<"account">>, <<"checkPasswordHash">>, Vars, Config). -check_user_body(User) -> - Query = <<"query Q1($user: JID!) - { account { checkUser(user: $user) {exist message} } }">>, - OpName = <<"Q1">>, +check_user(User, Config) -> Vars = #{<<"user">> => User}, - #{query => Query, operationName => OpName, variables => Vars}. + execute_command(<<"account">>, <<"checkUser">>, Vars, Config). -register_user_body(Domain, Username, Password) -> - Query = <<"mutation M1($domain: String!, $username: String, $password: String!) - { account { registerUser(domain: $domain, username: $username, password: $password) - { jid message } } }">>, - OpName = <<"M1">>, +register_user(Domain, Username, Password, Config) -> Vars = #{<<"domain">> => Domain, <<"username">> => Username, <<"password">> => Password}, - #{query => Query, operationName => OpName, variables => Vars}. + execute_command(<<"account">>, <<"registerUser">>, Vars, Config). -remove_user_body(User) -> - Query = <<"mutation M1($user: JID!) - { account { removeUser(user: $user) { jid message } } }">>, - OpName = <<"M1">>, +register_random_user(Domain, Password, Config) -> + Vars = #{<<"domain">> => Domain, <<"password">> => Password}, + execute_command(<<"account">>, <<"registerUser">>, Vars, Config). + +remove_user(User, Config) -> Vars = #{<<"user">> => User}, - #{query => Query, operationName => OpName, variables => Vars}. + execute_command(<<"account">>, <<"removeUser">>, Vars, Config). -ban_user_body(JID, Reason) -> - Query = <<"mutation M1($user: JID!, $reason: String!) - { account { banUser(user: $user, reason: $reason) { jid message } } }">>, - OpName = <<"M1">>, +ban_user(JID, Reason, Config) -> Vars = #{<<"user">> => JID, <<"reason">> => Reason}, - #{query => Query, operationName => OpName, variables => Vars}. + execute_command(<<"account">>, <<"banUser">>, Vars, Config). -change_user_password_body(JID, NewPassword) -> - Query = <<"mutation M1($user: JID!, $newPassword: String!) - { account { changeUserPassword(user: $user, newPassword: $newPassword) { jid message } } }">>, - OpName = <<"M1">>, +change_user_password(JID, NewPassword, Config) -> Vars = #{<<"user">> => JID, <<"newPassword">> => NewPassword}, - #{query => Query, operationName => OpName, variables => Vars}. + execute_command(<<"account">>, <<"changeUserPassword">>, Vars, Config). + +%% Request bodies user_change_password_body(NewPassword) -> Query = <<"mutation M1($newPassword: String!) diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index cd5f64220b9..547b870de48 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -1,12 +1,8 @@ -module(graphql_helper). --import(distributed_helper, [mim/0, rpc/4]). +-compile([export_all, nowarn_export_all]). --export([execute/3, execute_auth/2, execute_domain_auth/2, execute_user/3]). --export([init_admin_handler/1, init_domain_admin_handler/1, end_domain_admin_handler/1]). --export([get_listener_port/1, get_listener_config/1]). --export([get_ok_value/2, get_err_msg/1, get_err_msg/2, get_err_code/1, make_creds/1, - user_to_bin/1, user_to_full_bin/1, user_to_jid/1, user_to_lower_jid/1]). +-import(distributed_helper, [mim/0, rpc/4]). -include_lib("common_test/include/ct.hrl"). -include_lib("escalus/include/escalus.hrl"). @@ -24,6 +20,19 @@ execute(EpName, Body, Creds) -> body => Body}, rest_helper:make_request(Request). +execute_command(Category, Command, Args, Config) -> + Protocol = ?config(protocol, Config), + execute_command(Category, Command, Args, Config, Protocol). + +execute_command(Category, Command, Args, Config, http) -> + #{Category := #{Command := #{doc := Doc}}} = + rpc(mim(), mongoose_graphql_commands, get_specs, []), + execute_auth(#{query => Doc, variables => Args}, Config); +execute_command(Category, Command, Args, Config, cli) -> + VarsJSON = jiffy:encode(Args), + {Result, Code} = mongooseimctl_helper:mongooseimctl(Category, [Command, VarsJSON], Config), + {{exit_status, Code}, rest_helper:decode(Result, #{return_maps => true})}. + execute_auth(Body, Config) -> Ep = ?config(schema_endpoint, Config), #{username := Username, password := Password} = get_listener_opts(Ep), @@ -56,11 +65,14 @@ init_admin_handler(Config) -> Opts = get_listener_opts(Endpoint), case maps:is_key(username, Opts) of true -> - [{schema_endpoint, Endpoint}, {listener_opts, Opts} | Config]; + [{protocol, http}, {schema_endpoint, Endpoint}, {listener_opts, Opts} | Config]; false -> ct:fail(<<"Admin credentials are not defined in config">>) end. +init_admin_cli(Config) -> + [{protocol, cli} | Config]. + init_domain_admin_handler(Config) -> Domain = domain_helper:domain(), Password = base16:encode(crypto:strong_rand_bytes(8)), @@ -78,22 +90,28 @@ get_listener_opts(EpName) -> Opts. get_err_code(Resp) -> - get_ok_value([errors, 1, extensions, code], Resp). + get_value([extensions, code], get_error(1, Resp)). --spec get_err_msg(#{errors := [#{message := binary()}]}) -> binary(). get_err_msg(Resp) -> - get_ok_value([errors, 1, message], Resp). + get_err_msg(1, Resp). --spec get_err_msg(pos_integer(), #{errors := [#{message := binary()}]}) -> binary(). get_err_msg(N, Resp) -> - get_ok_value([errors, N, message], Resp). + get_value([message], get_error(N, Resp)). + +get_error(N, {Code, #{<<"errors">> := Errors}}) -> + assert_response_code(error, Code), + lists:nth(N, Errors). --spec get_ok_value([atom()], {tuple(), map()}) -> binary(). -get_ok_value([errors, N | Path], {{<<"200">>, <<"OK">>}, #{<<"errors">> := Errors}}) -> - get_value(Path, lists:nth(N, Errors)); -get_ok_value(Path, {{<<"200">>, <<"OK">>}, Data}) -> +get_ok_value(Path, {Code, Data}) -> + assert_response_code(ok, Code), get_value(Path, Data). +assert_response_code(_, {<<"200">>, <<"OK">>}) -> ok; +assert_response_code(error, {exit_status, 1}) -> ok; +assert_response_code(ok, {exit_status, 0}) -> ok; +assert_response_code(Type, Code) -> + error(#{what => invalid_response_code, expected_type => Type, response_code => Code}). + make_creds(#client{props = Props} = Client) -> JID = escalus_utils:jid_to_lower(escalus_client:short_jid(Client)), Password = proplists:get_value(password, Props), diff --git a/big_tests/tests/mongooseimctl_SUITE.erl b/big_tests/tests/mongooseimctl_SUITE.erl index 1532a2d6c79..4816e4ac4ac 100644 --- a/big_tests/tests/mongooseimctl_SUITE.erl +++ b/big_tests/tests/mongooseimctl_SUITE.erl @@ -177,7 +177,15 @@ upload_enabled() -> graphql() -> [graphql_wrong_arguments_number, can_execute_admin_queries_with_permissions, - can_handle_execution_error]. + can_handle_execution_error, + graphql_error_unknown_command_with_args, + graphql_error_unknown_command_without_args, + graphql_error_no_command, + graphql_error_invalid_vars, + graphql_error_no_vars, + graphql_error_too_many_args, + graphql_error_missing_variable, + graphql_command]. suite() -> require_rpc_nodes([mim]) ++ escalus:suite(). @@ -1147,6 +1155,44 @@ graphql_wrong_arguments_number(Config) -> Data2 = element(1, ResTooManyArgs), ?assertNotEqual(nomatch, string:find(Data2, ExpectedFragment)). +%% Generic GraphQL command tests +%% Specific commands are tested in graphql_*_SUITE + +graphql_error_unknown_command_with_args(Config) -> + {Res, 1} = mongooseimctl("account", ["makeCoffee", "{}"], Config), + ?assertMatch({match, _}, re:run(Res, "Unknown command")). + +graphql_error_unknown_command_without_args(Config) -> + {Res, 1} = mongooseimctl("account", ["makeCoffee"], Config), + ?assertMatch({match, _}, re:run(Res, "Unknown command")). + +graphql_error_no_command(Config) -> + {Res, 1} = mongooseimctl("account", [], Config), + ?assertMatch({match, _}, re:run(Res, "No command")). + +graphql_error_invalid_vars(Config) -> + {Res, 1} = mongooseimctl("account", ["countUsers", "now"], Config), + ?assertMatch({match, _}, re:run(Res, "Could not parse")). + +graphql_error_no_vars(Config) -> + {Res, 1} = mongooseimctl("account", ["countUsers"], Config), + ?assertMatch({match, _}, re:run(Res, "This command requires variables")). + +graphql_error_too_many_args(Config) -> + {Res, 1} = mongooseimctl("account", ["countUsers", "{}", "{}"], Config), + ?assertMatch({match, _}, re:run(Res, "Too many arguments")). + +graphql_error_missing_variable(Config) -> + {ResJSON, 1} = mongooseimctl("account", ["countUsers", "{}"], Config), + #{<<"errors">> := Errors} = rest_helper:decode(ResJSON, #{return_maps => true}), + ?assertMatch([#{<<"extensions">> := #{<<"code">> := <<"missing_non_null_param">>}}], Errors). + +graphql_command(Config) -> + VarsJSON = jiffy:encode(#{domain => domain()}), + {ResJSON, 0} = mongooseimctl("account", ["countUsers", VarsJSON], Config), + #{<<"data">> := Data} = rest_helper:decode(ResJSON, #{return_maps => true}), + ?assertMatch(#{<<"account">> := #{<<"countUsers">> := _}}, Data). + %%----------------------------------------------------------------- %% Improve coverage %%----------------------------------------------------------------- diff --git a/big_tests/tests/rest_helper.erl b/big_tests/tests/rest_helper.erl index cef561ab15f..3a9ffd678ed 100644 --- a/big_tests/tests/rest_helper.erl +++ b/big_tests/tests/rest_helper.erl @@ -1,37 +1,7 @@ -module(rest_helper). -author("bartekgorny"). -%% API --export([ - assert_inlist/2, - assert_notinlist/2, - assert_status/2, - decode_maplist/1, - gett/2, - gett/3, - post/3, - post/4, - putt/3, - putt/4, - delete/2, - delete/3, - delete/4, - make_request/1, - simple_request/2, - simple_request/3, - simple_request/4, - maybe_enable_mam/3, - maybe_skip_mam_test_cases/3, - fill_archive/2, - fill_room_archive/3, - make_timestamp/2, - change_admin_creds/1, - make_msg_stanza_with_props/2, - make_malformed_msg_stanza_with_props/2, - make_msg_stanza_with_thread_and_parent/4, - make_msg_stanza_with_thread/3, - make_msg_stanza_without_thread/2 -]). +-compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, subhost_pattern/1, diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 187bd9ee567..70b0dd77615 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -179,7 +179,8 @@ process(["mnesia", Arg]) when is_list(Arg) -> ?STATUS_SUCCESS; process(["graphql", Arg]) when is_list(Arg) -> Doc = list_to_binary(Arg), - Result = mongoose_graphql_commands:execute(Doc, #{}), % TODO Support vars? + Ep = mongoose_graphql:get_endpoint(admin), + Result = mongoose_graphql:execute(Ep, undefined, Doc), handle_graphql_result(Result); process(["graphql" | _]) -> ?PRINT("This command requires one string type argument!\n", []), @@ -213,24 +214,41 @@ process(["help" | Mode]) -> print_usage_commands(CmdStringU, MaxC, ShCode), ?STATUS_SUCCESS end; -process([CategoryStr, CommandStr, VarsStr] = Args) -> - Category = list_to_binary(CategoryStr), - Command = list_to_binary(CommandStr), - case mongoose_graphql_commands:find_document(Category, Command) of - {ok, Doc} -> - Vars = jiffy:decode(VarsStr, [return_maps]), - Result = mongoose_graphql_commands:execute(Doc, Vars), - handle_graphql_result(Result); - {error, not_found} -> - run_command(Args) - end; process(Args) -> - run_command(Args). + case mongoose_graphql_commands:process(Args) of + {error, #{reason := Reason}} when Reason =:= no_args; + Reason =:= unknown_category -> + run_command(Args); % Fallback to the old commands + {error, #{reason := unknown_command, category := Category, cat_spec := CatSpec, + command := Command}} -> + ?PRINT("Unknown command '~s'. Available commands in category '~s':~n", + [Command, Category]), + [?PRINT(" ~s~n", [Cmd]) || Cmd <- lists:sort(maps:keys(CatSpec))], + ?STATUS_ERROR; + {error, #{reason := no_command, category := Category, cat_spec := CatSpec}} -> + ?PRINT("No command was specified. Available commands in category '~s':~n", [Category]), + [?PRINT(" ~s~n", [Cmd]) || Cmd <- lists:sort(maps:keys(CatSpec))], + ?STATUS_ERROR; + {error, #{reason := invalid_vars}} -> + ?PRINT("Could not parse command variables from JSON~n", []), + ?STATUS_ERROR; + {error, #{reason := no_vars}} -> + ?PRINT("This command requires variables in JSON~n", []), + ?STATUS_ERROR; + {error, #{reason := too_many_args}} -> + ?PRINT("Too many arguments~n", []), + ?STATUS_ERROR; + Result -> + handle_graphql_result(Result) + end. handle_graphql_result({ok, Result}) -> JSONResult = mongoose_graphql_response:term_to_pretty_json(Result), ?PRINT("~s\n", [JSONResult]), - ?STATUS_SUCCESS; + case Result of + #{errors := _} -> ?STATUS_ERROR; + _ -> ?STATUS_SUCCESS + end; handle_graphql_result({error, Reason}) -> {_Code, Error} = mongoose_graphql_errors:format_error(Reason), JSONResult = jiffy:encode(#{errors => [Error]}, [pretty]), diff --git a/src/graphql/mongoose_graphql_commands.erl b/src/graphql/mongoose_graphql_commands.erl index a9640b0a02b..8ebf2ec68a5 100644 --- a/src/graphql/mongoose_graphql_commands.erl +++ b/src/graphql/mongoose_graphql_commands.erl @@ -2,25 +2,39 @@ -module(mongoose_graphql_commands). --export([start/0, stop/0, find_document/2, execute/2]). +%% API +-export([start/0, stop/0, process/1]). + +%% Only for tests +-export([get_specs/0]). + +-ignore_xref([get_specs/0]). %% This level of nesting is needed for basic type introspection, e.g. see below for [String!]! %% NON_NULL LIST NON_NULL SCALAR -define(TYPE_QUERY, "{name kind ofType {name kind ofType {name kind ofType {name kind}}}}"). +-type context() :: #{args := [string()], + category => category(), + cat_spec => category_spec(), + command => command(), + doc => doc(), + vars => json_map(), + reason => atom()}. +-type result() :: {ok, #{atom() => graphql:json()}} | {error, any()}. +-type specs() :: #{category() => category_spec()}. -type category() :: binary(). +-type category_spec() :: #{command() => command_spec()}. -type command() :: binary(). --type doc() :: binary(). --type ep() :: graphql:endpoint_context(). --type op_type() :: binary(). -type command_spec() :: #{op_type := op_type(), args := [arg_spec()], fields := [field_spec()], doc := doc()}. -type arg_spec() :: #{name := binary(), type := binary(), wrap := [list | required]}. -type field_spec() :: #{name := binary(), fields => [field_spec()]}. --type category_spec() :: #{command() => command_spec()}. --type specs() :: #{category() => category_spec()}. +-type op_type() :: binary(). +-type doc() :: binary(). +-type ep() :: graphql:endpoint_context(). -type json_map() :: #{binary() => graphql:json()}. %% API @@ -39,18 +53,62 @@ stop() -> persistent_term:erase(?MODULE), ok. --spec find_document(category(), command()) -> {ok, doc()} | {error, not_found}. -find_document(Category, Command) -> - case persistent_term:get(?MODULE) of - #{Category := #{Command := CommandSpec}} -> - #{doc := Doc} = CommandSpec, - {ok, Doc}; - _ -> - {error, not_found} - end. +-spec process([string()]) -> result(). +process(Args) -> + lists:foldl(fun(_, {error, _} = Error) -> Error; + (StepF, Ctx) -> StepF(Ctx) + end, + #{args => Args}, + [fun find_category/1, fun find_command/1, fun parse_vars/1, fun execute/1]). + +-spec get_specs() -> specs(). +get_specs() -> + persistent_term:get(?MODULE). --spec execute(doc(), json_map()) -> {ok, map()} | {error, term()}. -execute(Doc, Vars) -> +%% Internals + +-spec find_category(context()) -> context() | {error, context()}. +find_category(CtxIn = #{args := [CategoryStr | Args]}) -> + Category = list_to_binary(CategoryStr), + Ctx = CtxIn#{category => Category, args => Args}, + case get_specs() of + #{Category := CatSpec} -> + Ctx#{cat_spec => CatSpec}; + #{} -> + {error, Ctx#{reason => unknown_category}} + end; +find_category(Ctx = #{args := []}) -> + {error, Ctx#{reason => no_args}}. + +-spec find_command(context()) -> context() | {error, context()}. +find_command(CtxIn = #{args := [CommandStr | Args]}) -> + Command = list_to_binary(CommandStr), + Ctx = #{cat_spec := CatSpec} = CtxIn#{command => Command, args => Args}, + case CatSpec of + #{Command := CommandSpec} -> + #{doc := Doc} = CommandSpec, + Ctx#{doc => Doc}; + #{} -> + {error, Ctx#{reason => unknown_command}} + end; +find_command(Ctx) -> + {error, Ctx#{reason => no_command}}. + +-spec parse_vars(context()) -> context() | {error, context()}. +parse_vars(Ctx = #{args := [VarsStr]}) -> + try jiffy:decode(VarsStr, [return_maps]) of + Vars -> + Ctx#{vars => Vars} + catch _:_ -> + {error, Ctx#{reason => invalid_vars}} + end; +parse_vars(Ctx = #{args := []}) -> + {error, Ctx#{reason => no_vars}}; +parse_vars(Ctx = #{args := [_|_]}) -> + {error, Ctx#{reason => too_many_args}}. + +-spec execute(context()) -> result(). +execute(#{doc := Doc, vars := Vars}) -> execute(mongoose_graphql:get_endpoint(admin), Doc, Vars). %% Internals @@ -186,7 +244,7 @@ return_field(#{name := Name, fields := Fields}) -> return_field(#{name := Name}) -> Name. --spec execute(ep(), doc(), json_map()) -> {ok, #{atom() => graphql:json()}} | {error, term()}. +-spec execute(ep(), doc(), json_map()) -> result(). execute(Ep, Doc, Vars) -> mongoose_graphql:execute(Ep, #{document => Doc, operation_name => undefined,