diff --git a/big_tests/tests/graphql_SUITE.erl b/big_tests/tests/graphql_SUITE.erl index 707946a7fa..aa035af641 100644 --- a/big_tests/tests/graphql_SUITE.erl +++ b/big_tests/tests/graphql_SUITE.erl @@ -30,7 +30,11 @@ admin_handler() -> [auth_admin_can_access_protected_types | common_tests()]. common_tests() -> - [wrong_creds_cannot_access_protected_types, + [malformed_auth_header_error, + document_parse_error, + document_type_check_error, + document_validate_error, + wrong_creds_cannot_access_protected_types, unauth_cannot_access_protected_types, unauth_can_access_unprotected_types, can_execute_query_with_variables, @@ -86,7 +90,8 @@ unauth_cannot_access_protected_types(Config) -> Ep = ?config(schema_endpoint, Config), Body = #{query => "{ field }"}, {Status, Data} = execute(Ep, Body, undefined), - assert_no_permissions(Status, Data). + ?assertMatch(#{<<"errors">> := [#{<<"path">> := [<<"ROOT">>]}]}, Data), + assert_no_permissions(no_permissions, Status, Data). unauth_can_access_unprotected_types(Config) -> Ep = ?config(schema_endpoint, Config), @@ -98,7 +103,46 @@ wrong_creds_cannot_access_protected_types(Config) -> Ep = ?config(schema_endpoint, Config), Body = #{query => "{ field }"}, {Status, Data} = execute(Ep, Body, {<<"user">>, <<"wrong_password">>}), - assert_no_permissions(Status, Data). + assert_no_permissions(wrong_credentials, Status, Data). + +malformed_auth_header_error(Config) -> + EpName = ?config(schema_endpoint, Config), + Request = + #{port => get_port(EpName), + role => {graphql, atom_to_binary(EpName)}, + method => <<"POST">>, + headers => [{<<"Authorization">>, <<"Basic YWRtaW46c2VjcmV">>}], + return_maps => true, + path => "/graphql"}, + % The encoded credentials value is malformed and cannot be decoded. + {Status, Data} = rest_helper:make_request(Request), + assert_no_permissions(request_error, Status, Data). + +document_parse_error(Config) -> + Ep = ?config(schema_endpoint, Config), + Body = #{<<"query">> => <<"{ field ">>}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(parser_error, Data), + + BodyScanner = #{<<"query">> => <<"mutation { id(value: \"asdfsad) } ">>}, + {StatusScanner, DataScanner} = execute(Ep, BodyScanner, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, StatusScanner), + assert_code(scanner_error, DataScanner). + +document_type_check_error(Config) -> + Ep = ?config(schema_endpoint, Config), + Body = #{<<"query">> => <<"mutation { id(value: 12) }">>}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(input_coercion, Data). + +document_validate_error(Config) -> + Ep = ?config(schema_endpoint, Config), + Body = #{<<"query">> => <<"query Q1 { field } query Q1 { field }">>, <<"operationName">> => <<"Q1">>}, + {Status, Data} = execute(Ep, Body, undefined), + ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), + assert_code(not_unique, Data). auth_user_can_access_protected_types(Config) -> escalus:fresh_story( @@ -148,27 +192,31 @@ invalid_json_body_error(Config) -> Body = <<"">>, {Status, Data} = execute(Ep, Body, undefined), ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - ?assertMatch(#{<<"errors">> := [#{<<"message">> := <<"invalid_json_body">>}]}, Data). + assert_code(invalid_json_body, Data). no_query_supplied_error(Config) -> Ep = ?config(schema_endpoint, Config), Body = #{}, {Status, Data} = execute(Ep, Body, undefined), ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - ?assertMatch(#{<<"errors">> := [#{<<"message">> := <<"no_query_supplied">>}]}, Data). + assert_code(no_query_supplied, Data). variables_invalid_json_error(Config) -> Ep = ?config(schema_endpoint, Config), Body = #{<<"query">> => <<"{ field }">>, <<"variables">> => <<"{1: 2}">>}, {Status, Data} = execute(Ep, Body, undefined), ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - ?assertMatch(#{<<"errors">> := [#{<<"message">> := <<"variables_invalid_json">>}]}, Data). + assert_code(variables_invalid_json, Data). %% Helpers -assert_no_permissions(Status, Data) -> - ?assertEqual({<<"400">>,<<"Bad Request">>}, Status), - ?assertMatch(#{<<"errors">> := [#{<<"message">> := <<"no_permissions">>}]}, Data). +assert_code(Code, Data) -> + BinCode = atom_to_binary(Code), + ?assertMatch(#{<<"errors">> := [#{<<"extensions">> := #{<<"code">> := BinCode}}]}, Data). + +assert_no_permissions(ExpectedCode, Status, Data) -> + ?assertEqual({<<"401">>,<<"Unauthorized">>}, Status), + assert_code(ExpectedCode, Data). assert_access_granted(Status, Data) -> ?assertEqual({<<"200">>,<<"OK">>}, Status), diff --git a/big_tests/tests/mongooseimctl_SUITE.erl b/big_tests/tests/mongooseimctl_SUITE.erl index eb0eedbc08..99870dd8e6 100644 --- a/big_tests/tests/mongooseimctl_SUITE.erl +++ b/big_tests/tests/mongooseimctl_SUITE.erl @@ -1133,7 +1133,7 @@ can_handle_execution_error(Config) -> Res = mongooseimctl("graphql", [Query], Config), ?assertMatch({_, 0}, Res), Data = element(1, Res), - ?assertNotEqual(nomatch, string:find(Data, "{error,{parser_error")). + ?assertNotEqual(nomatch, string:find(Data, "parser_error")). graphql_wrong_arguments_number(Config) -> ExpectedFragment = "This command requires", diff --git a/src/mongoose_graphql.erl b/src/mongoose_graphql.erl index e050e9f10e..c0832736bb 100644 --- a/src/mongoose_graphql.erl +++ b/src/mongoose_graphql.erl @@ -72,7 +72,13 @@ execute(Ep, #{document := Doc, {ok, graphql:execute(Ep, Ctx2, Ast2)} catch throw:{error, Err} -> - {error, Err} + {error, Err}; + Class:Reason:Stacktrace -> + Err = #{what => graphql_internal_crash, + class => Class, reason => Reason, + stacktrace => Stacktrace}, + ?LOG_ERROR(Err), + {error, internal_crash} end. %% @doc Execute selected operation on a given endpoint with authorization. @@ -104,8 +110,8 @@ graphql_parse(Doc) -> case graphql:parse(Doc) of {ok, _} = Ok -> Ok; - {error, _} = Err -> - throw(Err) + {error, Err} -> + graphql_err:abort([], parse, Err) end. admin_mapping_rules() -> diff --git a/src/mongoose_graphql/mongoose_graphql_cowboy_handler.erl b/src/mongoose_graphql/mongoose_graphql_cowboy_handler.erl index 3a3aca78fa..280866249e 100644 --- a/src/mongoose_graphql/mongoose_graphql_cowboy_handler.erl +++ b/src/mongoose_graphql/mongoose_graphql_cowboy_handler.erl @@ -58,9 +58,19 @@ charsets_provided(Req, State) -> {[<<"utf-8">>], Req, State}. is_authorized(Req, State) -> - Auth = cowboy_req:parse_header(<<"authorization">>, Req), - State2 = check_auth(Auth, State), - {true, Req, State2}. + try cowboy_req:parse_header(<<"authorization">>, Req) of + Auth -> + case check_auth(Auth, State) of + {ok, State2} -> + {true, Req, State2}; + error -> + Msg = make_error(authorize, wrong_credentials), + reply_error(Msg, Req, State) + end + catch + exit:Err -> + reply_error(make_error(authorize, Err), Req, State) + end. resource_exists(#{method := <<"GET">>} = Req, State) -> {true, Req, State}; @@ -75,7 +85,7 @@ to_html(Req, #{index_location := {priv_file, App, FileLocation}} = State) -> json_request(Req, State) -> case gather(Req) of {error, Reason} -> - reply_error(400, Reason, Req, State); + reply_error(Reason, Req, State); {ok, Req2, Decoded} -> run_request(Decoded, Req2, State) end. @@ -92,22 +102,24 @@ check_auth(Auth, #{schema_endpoint := <<"user">>} = State) -> auth_user({basic, User, Password}, State) -> case mongoose_api_common:check_password(jid:from_binary(User), Password) of - {true, _} -> State#{authorized => true, schema_ctx => #{username => User}}; - _ -> State#{authorized => false} + {true, _} -> {ok, State#{authorized => true, schema_ctx => #{username => User}}}; + _ -> error end; auth_user(_, State) -> - State#{authorized => false}. + {ok, State#{authorized => false}}. auth_admin({basic, Username, Password}, #{username := Username, password := Password} = State) -> - State#{authorized => true}; + {ok, State#{authorized => true}}; +auth_admin({basic, _, _}, _) -> + error; auth_admin(_, #{username := _, password := _} = State) -> - State#{authorized => false}; + {ok, State#{authorized => false}}; auth_admin(_, State) -> % auth credentials not provided in config - State#{authorized => true}. + {ok, State#{authorized => true}}. run_request(#{document := undefined}, Req, State) -> - reply_error(400, no_query_supplied, Req, State); + reply_error(make_error(decode, no_query_supplied), Req, State); run_request(#{} = ReqCtx, Req, #{schema_endpoint := EpName, authorized := AuthStatus} = State) -> Ep = mongoose_graphql:get_endpoint(binary_to_existing_atom(EpName)), @@ -120,7 +132,7 @@ run_request(#{} = ReqCtx, Req, #{schema_endpoint := EpName, Reply = cowboy_req:reply(200, Req2), {stop, Reply, State}; {error, Reason} -> - reply_error(400, Reason, Req, State) + reply_error(Reason, Req, State) end. gather(Req) -> @@ -131,7 +143,7 @@ gather(Req) -> gather(Req2, JSON, Bindings) catch _:_ -> - {error, invalid_json_body} + {error, make_error(decode, invalid_json_body)} end. gather(Req, Body, Params) -> @@ -156,10 +168,10 @@ variables([#{<<"variables">> := Vars} | _]) -> try jiffy:decode(Vars, [return_maps]) of null -> {ok, #{}}; JSON when is_map(JSON) -> {ok, JSON}; - _ -> {error, variables_invalid_json} + _ -> {error, make_error(decode, variables_invalid_json)} catch _:_ -> - {error, variables_invalid_json} + {error, make_error(decode, variables_invalid_json)} end; is_map(Vars) -> {ok, Vars}; @@ -178,17 +190,12 @@ operation_name([_ | Next]) -> operation_name([]) -> undefined. -reply_error(Code, Msg, Req, State) -> - Errors = - case Msg of - {error, E} -> - graphql_err:format_errors(#{}, [E]); - _ -> - Formatted = iolist_to_binary(io_lib:format("~p", [Msg])), - [#{type => error, message => Formatted}] - end, - - Body = jiffy:encode(#{errors => Errors}), +make_error(Phase, Term) -> + #{error_term => Term, phase => Phase}. + +reply_error(Msg, Req, State) -> + {Code, Error} = mongoose_graphql_errors:format_error(Msg), + Body = jiffy:encode(#{errors => [Error]}), Req2 = cowboy_req:set_resp_body(Body, Req), Reply = cowboy_req:reply(Code, Req2), {stop, Reply, State}. diff --git a/src/mongoose_graphql/mongoose_graphql_errors.erl b/src/mongoose_graphql/mongoose_graphql_errors.erl index f47ed3b2ee..f3c4d0fbb1 100644 --- a/src/mongoose_graphql/mongoose_graphql_errors.erl +++ b/src/mongoose_graphql/mongoose_graphql_errors.erl @@ -1,18 +1,95 @@ -%% @doc Implements callbacks that format custom errors returned from resolvers or crashes. +%% @doc Implements callbacks that format custom errors returned from resolvers or crashes. +%% In addition, it can format each type of error that occurred in any graphql +%% or mongoose_graphql phase. +%% @end -module(mongoose_graphql_errors). --export([err/2, crash/2]). +-export([format_error/1, err/2, crash/2]). --ignore_xref([err/2, crash/2]). +-ignore_xref([format_error/1, err/2, crash/2]). -% callback invoked when resolver returns error tuple +-type err_msg() :: #{message := binary(), extensions => map(), path => list()}. + +%% callback invoked when resolver returns error tuple +-spec err(map(), term()) -> err_msg(). err(_Ctx, domain_not_found) -> #{message => <<"Given domain does not exist">>, extensions => #{code => resolver_error}}; err(_Ctx, ErrorTerm) -> #{message => iolist_to_binary(io_lib:format("~p", [ErrorTerm])), extensions => #{code => resolver_error}}. -% callback invoked when resolver crashes +%% callback invoked when resolver crashes +-spec crash(map(), term()) -> err_msg(). crash(_Ctx, #{type := Type}) -> #{message => <<"Unexpected ", Type/binary, " resolver crash">>, extensions => #{code => resolver_crash}}. + +%% @doc Format error that occurred in any phase including HTTP request decoding. +-spec format_error(term())-> {integer(), err_msg()}. +format_error(#{phase := Phase, error_term := Term} = Err) when Phase =:= authorize; + Phase =:= decode; + Phase =:= parse -> + Msg = #{extensions => #{code => err_code(Phase, Term)}, + message => iolist_to_binary(err_msg(Phase, Term))}, + {err_http_code(Phase), add_path(Err, Msg)}; +format_error(#{error_term := _, phase := Phase} = Err) when Phase =:= execute; + Phase =:= type_check; + Phase =:= validate; + Phase =:= uncategorized -> + Err2 = maps:merge(#{path => []}, Err), + [ErrMsg] = graphql:format_errors(#{}, [Err2]), + {400, ErrMsg}; +format_error(internal_crash) -> + Msg = #{message => <<"GraphQL Internal Server Error">>, + extensions => #{code => internal_server_error}}, + {500, Msg}; +format_error(Err) -> + Msg = #{extensions => #{code => uncategorized}, + message => iolist_to_binary(io_lib:format("~p", [Err]))}, + {400, Msg}. + +%% Internal + +err_http_code(authorize) -> + 401; +err_http_code(_) -> + 400. + +err_code(_, Term) -> + simplify(Term). + +simplify(A) when is_atom(A) -> A; +simplify(B) when is_binary(B) -> B; +simplify(T) when is_tuple(T) -> element(1, T). + +err_msg(parse, Result) -> + parse_err_msg(Result); +err_msg(decode, Result) -> + decode_err_msg(Result); +err_msg(authorize, Result) -> + authorize_err_msg(Result). + +authorize_err_msg({request_error, {header, <<"authorization">>}, _}) -> + "Malformed authorization header. Please consult the relevant specification"; +authorize_err_msg(wrong_credentials) -> + "The provided credentials are wrong"; +authorize_err_msg({no_permissions, Op}) -> + io_lib:format("Cannot execute query ~s without permissions", [Op]). + +parse_err_msg({parser_error, {Line, graphql_parser, Msg}}) -> + io_lib:format("Cannot parse line ~B because of ~s", [Line, Msg]); +parse_err_msg({scanner_error, {Line, graphql_scanner, Msg}}) -> + Formatted = lists:flatten(graphql_scanner:format_error(Msg)), + io_lib:format("Cannot scan line ~B because of ~s", [Line, Formatted]). + +decode_err_msg(no_query_supplied) -> + "The query was not supplied in the request body"; +decode_err_msg(invalid_json_body) -> + "The request JSON body is invalid"; +decode_err_msg(variables_invalid_json) -> + "The variables' JSON is invalid". + +add_path(#{path := Path}, ErrMsg) -> + ErrMsg#{path => Path}; +add_path(_, ErrMsg) -> + ErrMsg. diff --git a/src/mongoose_graphql/mongoose_graphql_permissions.erl b/src/mongoose_graphql/mongoose_graphql_permissions.erl index 117a287205..d165404b7f 100644 --- a/src/mongoose_graphql/mongoose_graphql_permissions.erl +++ b/src/mongoose_graphql/mongoose_graphql_permissions.erl @@ -50,7 +50,8 @@ check_permissions(OpName, false, #document{definitions = Definitions}) -> true -> ok; false -> - throw({error, no_permissions}) + OpName2 = op_name(OpName), + graphql_err:abort([OpName2], authorize, {no_permissions, OpName2}) end; false -> ok @@ -63,6 +64,11 @@ check_permissions(_, true, _) -> % Internal +op_name(undefined) -> + <<"ROOT">>; +op_name(Name) -> + Name. + is_req_operation(#op{id = 'ROOT'}, undefined) -> true; is_req_operation(#op{id = {name, _, Name}}, Name) -> diff --git a/test/mongoose_graphql_SUITE.erl b/test/mongoose_graphql_SUITE.erl index 9d8d07a3c4..51bc1c94b6 100644 --- a/test/mongoose_graphql_SUITE.erl +++ b/test/mongoose_graphql_SUITE.erl @@ -7,24 +7,30 @@ -include_lib("graphql/src/graphql_schema.hrl"). -define(assertPermissionsFailed(Config, Doc), - ?assertThrow({error, no_permissions}, + ?assertThrow({error, #{error_term := {no_permissions, _}}}, check_permissions(Config, Doc))). -define(assertPermissionsSuccess(Config, Doc), ?assertMatch(ok, check_permissions(Config, Doc))). +-define(assertErrMsg(Code, MsgContains, ErrorMsg), + assert_err_msg(Code, MsgContains, ErrorMsg)). + all() -> [can_create_endpoint, can_load_split_schema, + unexpected_internal_error, admin_and_user_load_global_types, {group, unprotected_graphql}, {group, protected_graphql}, {group, error_handling}, + {group, error_formatting}, {group, permissions}]. groups() -> [{protected_graphql, [parallel], protected_graphql()}, {unprotected_graphql, [parallel], unprotected_graphql()}, {error_handling, [parallel], error_handling()}, + {error_formatting, [parallel], error_formatting()}, {permissions, [parallel], permissions()}]. protected_graphql() -> @@ -42,9 +48,21 @@ unprotected_graphql() -> unauth_can_execute_mutation]. error_handling() -> - [should_catch_parsing_errors, - should_catch_type_check_params_errors, - should_catch_type_check_errors]. + [should_catch_parsing_error, + should_catch_type_check_params_error, + should_catch_type_check_error, + should_catch_validation_error]. + +error_formatting() -> + [format_internal_crash, + format_parse_errors, + format_decode_errors, + format_authorize_error, + format_validate_error, + format_type_check_error, + format_execute_error, + format_uncategorized_error, + format_any_error]. permissions() -> [check_object_permissions, @@ -72,9 +90,10 @@ init_per_testcase(C, Config) when C =:= can_execute_query_with_vars; C =:= auth_can_execute_mutation; C =:= unauth_can_execute_query; C =:= unauth_can_execute_mutation; - C =:= should_catch_type_check_params_errors; - C =:= should_catch_type_check_errors; - C =:= should_catch_parsing_errors -> + C =:= should_catch_type_check_params_error; + C =:= should_catch_type_check_error; + C =:= should_catch_parsing_error; + C =:= should_catch_validation_error -> {Mapping, Pattern} = example_schema_data(Config), {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]), Ep = mongoose_graphql:get_endpoint(C), @@ -122,6 +141,12 @@ can_load_split_schema(Config) -> ?assertMatch(#object_type{id = <<"Query">>}, graphql_schema:get(Ep, <<"Query">>)), ?assertMatch(#object_type{id = <<"Mutation">>}, graphql_schema:get(Ep, <<"Mutation">>)). +unexpected_internal_error(Config) -> + Name = ?config(endpoint_name, Config), + Doc = <<"mutation { field }">>, + Res = mongoose_graphql:execute(Name, undefined, Doc), + ?assertEqual({error, internal_crash}, Res). + admin_and_user_load_global_types(_Config) -> mongoose_graphql:init(), AdminEp = mongoose_graphql:get_endpoint(admin), @@ -148,15 +173,15 @@ auth_can_execute_protected_mutation(Config) -> unauth_cannot_execute_protected_query(Config) -> Ep = ?config(endpoint, Config), - Doc = <<"{ field }">>, - Res = mongoose_graphql:execute(Ep, request(Doc, false)), - ?assertEqual({error, no_permissions}, Res). + Doc = <<"query Q1 { field }">>, + Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)), + ?assertMatch({error, #{error_term := {no_permissions, <<"Q1">>}, path := [<<"Q1">>]}}, Res). unauth_cannot_execute_protected_mutation(Config) -> Ep = ?config(endpoint, Config), Doc = <<"mutation { field }">>, Res = mongoose_graphql:execute(Ep, request(Doc, false)), - ?assertEqual({error, no_permissions}, Res). + ?assertMatch({error, #{error_term := {no_permissions, <<"ROOT">>}}}, Res). unauth_can_access_introspection(Config) -> Ep = ?config(endpoint, Config), @@ -214,23 +239,33 @@ auth_can_execute_mutation(Config) -> Res = mongoose_graphql:execute(Ep, request(Doc, true)), ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res). -should_catch_parsing_errors(Config) -> +should_catch_parsing_error(Config) -> Ep = ?config(endpoint, Config), Doc = <<"query { field ">>, - Res = mongoose_graphql:execute(Ep, request(Doc, false)), - ?assertMatch({error, _}, Res). + DocScan = <<"query { id(value: \"ala) }">>, + ResParseErr = mongoose_graphql:execute(Ep, request(Doc, false)), + ?assertMatch({error, #{phase := parse, error_term := {parser_error, _}}}, ResParseErr), + ResScanErr = mongoose_graphql:execute(Ep, request(DocScan, false)), + ?assertMatch({error, #{phase := parse, error_term := {scanner_error, _}}}, ResScanErr). -should_catch_type_check_errors(Config) -> +should_catch_type_check_error(Config) -> Ep = ?config(endpoint, Config), Doc = <<"query { notExistingField(value: \"Hello\") }">>, Res = mongoose_graphql:execute(Ep, request(Doc, false)), - ?assertMatch({error, _}, Res). + ?assertMatch({error, #{phase := type_check, error_term := unknown_field}}, Res). -should_catch_type_check_params_errors(Config) -> +should_catch_type_check_params_error(Config) -> Ep = ?config(endpoint, Config), Doc = <<"query { id(value: 12) }">>, Res = mongoose_graphql:execute(Ep, request(Doc, false)), - ?assertMatch({error, _}, Res). + ?assertMatch({error, #{phase := type_check, error_term := {input_coercion, _, _, _}}}, Res). + +should_catch_validation_error(Config) -> + Ep = ?config(endpoint, Config), + Doc = <<"query Q1{ id(value: \"ok\") } query Q1{ id(value: \"ok\") }">>, + % Query name must be unique + Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)), + ?assertMatch({error, #{phase := validate, error_term := {not_unique, _}}}, Res). check_object_permissions(Config) -> Doc = <<"query { field }">>, @@ -291,8 +326,92 @@ check_union_permissions(Config) -> ?assertPermissionsFailed(Config, FDoc), ?assertPermissionsFailed(Config, FDoc2). +format_internal_crash(_Config) -> + {Code, Res} = mongoose_graphql_errors:format_error(internal_crash), + ?assertEqual(500, Code), + ?assertMatch(#{extensions := #{code := internal_server_error}}, Res). + +format_parse_errors(_Config) -> + ParserError = make_error(parse, {parser_error, {0, graphql_parser, "parser_error_msg"}}), + ScannerError = make_error(parse, {scanner_error, + {0, graphql_scanner, {illegal, "illegal_characters"}}}), + ScannerError2 = make_error(parse, {scanner_error, + {0, graphql_scanner, {user, "user_scanner_err"}}}), + + {400, ResParser} = mongoose_graphql_errors:format_error(ParserError), + {400, ResScanner} = mongoose_graphql_errors:format_error(ScannerError), + {400, ResScanner2} = mongoose_graphql_errors:format_error(ScannerError2), + ?assertErrMsg(parser_error, <<"parser_error_msg">>, ResParser), + ?assertErrMsg(scanner_error, <<"illegal_characters">>, ResScanner), + ?assertErrMsg(scanner_error, <<"user_scanner_err">>, ResScanner2). + +format_decode_errors(_Config) -> + {400, Msg1} = mongoose_graphql_errors:format_error(make_error(decode, no_query_supplied)), + {400, Msg2} = mongoose_graphql_errors:format_error(make_error(decode, invalid_json_body)), + {400, Msg3} = mongoose_graphql_errors:format_error(make_error(decode, variables_invalid_json)), + + ?assertErrMsg(no_query_supplied, <<"The query was not supplied">>, Msg1), + ?assertErrMsg(invalid_json_body, <<"invalid">>, Msg2), + ?assertErrMsg(variables_invalid_json, <<"invalid">>, Msg3). + +format_authorize_error(_Config) -> + {401, Msg1} = mongoose_graphql_errors:format_error(make_error(authorize, wrong_credentials)), + {401, Msg2} = mongoose_graphql_errors:format_error( + make_error([<<"ROOT">>], authorize, {no_permissions, <<"ROOT">>})), + {401, Msg3} = mongoose_graphql_errors:format_error( + make_error(authorize, {request_error, {header, <<"authorization">>}, 'msg'})), + + ?assertErrMsg(wrong_credentials, <<"provided credentials are wrong">>, Msg1), + ?assertErrMsg(no_permissions, <<"without permissions">>, Msg2), + ?assertMatch(#{path := [<<"ROOT">>]}, Msg2), + ?assertErrMsg(request_error, <<"Malformed authorization header">>, Msg3). + +format_validate_error(_Config) -> + % Ensure the module can format this phase + {400, Msg} = mongoose_graphql_errors:format_error( + make_error(validate, {not_unique, <<"OpName">>})), + ?assertMatch(#{extensions := #{code := not_unique}}, Msg). + +format_type_check_error(_Config) -> + % Ensure the module can format this phase + {400, Msg} = mongoose_graphql_errors:format_error( + make_error(type_check, non_null)), + ?assertMatch(#{extensions := #{code := non_null}}, Msg). + +format_execute_error(_Config) -> + % Ensure the module can format this phase + {400, Msg} = mongoose_graphql_errors:format_error( + make_error(execute, {resolver_error, any_error})), + ?assertMatch(#{extensions := #{code := resolver_error}}, Msg). + +format_uncategorized_error(_Config) -> + % Ensure the module can format this phase + {400, Msg} = mongoose_graphql_errors:format_error( + make_error(uncategorized, any_error)), + ?assertMatch(#{extensions := #{code := any_error}}, Msg). + +format_any_error(_Config) -> + {400, Msg1} = mongoose_graphql_errors:format_error(any_error), + {400, Msg2} = mongoose_graphql_errors:format_error(<<"any_error">>), + {400, Msg3} = mongoose_graphql_errors:format_error({1, any_error}), + {400, Msg4} = mongoose_graphql_errors:format_error(#{msg => any_error}), + ?assertErrMsg(uncategorized, <<"any_error">>, Msg1), + ?assertErrMsg(uncategorized, <<"any_error">>, Msg2), + ?assertErrMsg(uncategorized, <<"any_error">>, Msg3), + ?assertErrMsg(uncategorized, <<"any_error">>, Msg4). + %% Helpers +assert_err_msg(Code, MsgContains, #{message := Msg} = ErrorMsg) -> + ?assertMatch(#{extensions := #{code := Code}}, ErrorMsg), + ?assertNotEqual(nomatch, binary:match(Msg, MsgContains)). + +make_error(Phase, Term) -> + #{phase => Phase, error_term => Term}. + +make_error(Path, Phase, Term) -> + #{path => Path, phase => Phase, error_term => Term}. + check_permissions(Config, Doc) -> Ep = ?config(endpoint, Config), Op = proplists:get_value(op, Config, undefined), @@ -302,8 +421,11 @@ check_permissions(Config, Doc) -> ok = mongoose_graphql_permissions:check_permissions(Op, false, Ast2). request(Doc, Authorized) -> + request(undefined, Doc, Authorized). + +request(Op, Doc, Authorized) -> #{document => Doc, - operation_name => undefined, + operation_name => Op, vars => #{}, authorized => Authorized, ctx => #{}}.