1: -module(mongoose_graphql_SUITE).
    2: 
    3: -compile([export_all, nowarn_export_all]).
    4: 
    5: -include_lib("eunit/include/eunit.hrl").
    6: -include_lib("common_test/include/ct.hrl").
    7: -include_lib("graphql/src/graphql_schema.hrl").
    8: -include_lib("jid/include/jid.hrl").
    9: 
   10: -define(assertPermissionsFailed(Config, Doc),
   11:         ?assertThrow({error, #{error_term := {no_permissions, _}}},
   12:                      check_permissions(Config, Doc))).
   13: -define(assertPermissionsSuccess(Config, Doc),
   14:         ?assertMatch(ok, check_permissions(Config, Doc))).
   15: 
   16: -define(assertErrMsg(Code, MsgContains, ErrorMsg),
   17:         assert_err_msg(Code, MsgContains, ErrorMsg)).
   18: 
   19: all() ->
   20:     [can_create_endpoint,
   21:      can_load_split_schema,
   22:      unexpected_internal_error,
   23:      admin_and_user_load_global_types,
   24:      {group, unprotected_graphql},
   25:      {group, protected_graphql},
   26:      {group, error_handling},
   27:      {group, error_formatting},
   28:      {group, permissions},
   29:      {group, user_listener},
   30:      {group, admin_listener}].
   31: 
   32: groups() ->
   33:     [{protected_graphql, [parallel], protected_graphql()},
   34:      {unprotected_graphql, [parallel], unprotected_graphql()},
   35:      {error_handling, [parallel], error_handling()},
   36:      {error_formatting, [parallel], error_formatting()},
   37:      {permissions, [parallel], permissions()},
   38:      {admin_listener, [parallel], admin_listener()},
   39:      {user_listener, [parallel], user_listener()}].
   40: 
   41: protected_graphql() ->
   42:     [auth_can_execute_protected_query,
   43:      auth_can_execute_protected_mutation,
   44:      unauth_cannot_execute_protected_query,
   45:      unauth_cannot_execute_protected_mutation,
   46:      unauth_can_access_introspection].
   47: 
   48: unprotected_graphql() ->
   49:     [can_execute_query_with_vars,
   50:      auth_can_execute_query,
   51:      auth_can_execute_mutation,
   52:      unauth_can_execute_query,
   53:      unauth_can_execute_mutation].
   54: 
   55: error_handling() ->
   56:     [should_catch_parsing_error,
   57:      should_catch_type_check_params_error,
   58:      should_catch_type_check_error,
   59:      should_catch_validation_error].
   60: 
   61: error_formatting() ->
   62:     [format_internal_crash,
   63:      format_parse_errors,
   64:      format_decode_errors,
   65:      format_authorize_error,
   66:      format_validate_error,
   67:      format_type_check_error,
   68:      format_execute_error,
   69:      format_uncategorized_error,
   70:      format_any_error].
   71: 
   72: permissions() ->
   73:     [check_object_permissions,
   74:      check_field_permissions,
   75:      check_child_object_permissions,
   76:      check_child_object_field_permissions,
   77:      check_fragment_permissions,
   78:      check_interface_permissions,
   79:      check_interface_field_permissions,
   80:      check_inline_fragment_permissions,
   81:      check_union_permissions
   82:     ].
   83: 
   84: user_listener() ->
   85:     [auth_user_can_access_protected_types | common_tests()].
   86: admin_listener() ->
   87:     [no_creds_defined_admin_can_access_protected,
   88:      auth_admin_can_access_protected_types | common_tests()].
   89: 
   90: common_tests() ->
   91:     [malformed_auth_header_error,
   92:      auth_wrong_creds_error,
   93:      invalid_json_body_error,
   94:      no_query_supplied_error,
   95:      variables_invalid_json_error,
   96:      listener_reply_with_parsing_error,
   97:      listener_reply_with_type_check_error,
   98:      listener_reply_with_validation_error,
   99:      listener_unauth_cannot_access_protected_types,
  100:      listener_unauth_can_access_unprotected_types,
  101:      listener_can_execute_query_with_variables].
  102: 
  103: init_per_suite(Config) ->
  104:     application:ensure_all_started(cowboy),
  105:     application:ensure_all_started(jid),
  106:     Config.
  107: 
  108: end_per_suite(_Config) ->
  109:     ok.
  110: 
  111: init_per_group(user_listener, Config) ->
  112:     meck:new(mongoose_api_common, [no_link]),
  113:     meck:expect(mongoose_api_common, check_password,
  114:                 fun
  115:                     (#jid{user = <<"alice">>}, <<"makota">>) -> {true, {}};
  116:                     (_, _) -> false
  117:                 end),
  118:     ListenerOpts = [{schema_endpoint, <<"user">>}],
  119:     init_ep_listener(5557, user_schema_ep, ListenerOpts, Config);
  120: init_per_group(admin_listener, Config) ->
  121:     ListenerOpts = [{username, <<"admin">>},
  122:                     {password, <<"secret">>},
  123:                     {schema_endpoint, <<"admin">>}],
  124:     init_ep_listener(5558, admin_schema_ep, ListenerOpts, Config);
  125: init_per_group(no_creds_admin_listener, Config) ->
  126:     ListenerOpts = [{schema_endpoint, <<"admin">>}],
  127:     init_ep_listener(5559, admin_schema_ep, ListenerOpts, Config);
  128: init_per_group(_G, Config) ->
  129:     Config.
  130: 
  131: end_per_group(user_listener, Config) ->
  132:     meck:unload(mongoose_api_common),
  133:     ?config(test_process, Config) ! stop,
  134:     Config;
  135: end_per_group(admin_listener, Config) ->
  136:     ?config(test_process, Config) ! stop,
  137:     Config;
  138: end_per_group(_, Config) ->
  139:     Config.
  140: 
  141: init_per_testcase(C, Config) when C =:= auth_can_execute_protected_query;
  142:                                   C =:= auth_can_execute_protected_mutation;
  143:                                   C =:= unauth_cannot_execute_protected_query;
  144:                                   C =:= unauth_cannot_execute_protected_mutation;
  145:                                   C =:= unauth_can_access_introspection ->
  146:     {Mapping, Pattern} = example_schema_protected_data(Config),
  147:     {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]),
  148:     Ep = mongoose_graphql:get_endpoint(C),
  149:     [{endpoint, Ep} | Config];
  150: init_per_testcase(C, Config) when C =:= can_execute_query_with_vars;
  151:                                   C =:= auth_can_execute_query;
  152:                                   C =:= auth_can_execute_mutation;
  153:                                   C =:= unauth_can_execute_query;
  154:                                   C =:= unauth_can_execute_mutation;
  155:                                   C =:= should_catch_type_check_params_error;
  156:                                   C =:= should_catch_type_check_error;
  157:                                   C =:= should_catch_parsing_error;
  158:                                   C =:= should_catch_validation_error ->
  159:     {Mapping, Pattern} = example_schema_data(Config),
  160:     {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]),
  161:     Ep = mongoose_graphql:get_endpoint(C),
  162:     [{endpoint, Ep} | Config];
  163: init_per_testcase(C, Config) when C =:= check_object_permissions;
  164:                                   C =:= check_field_permissions;
  165:                                   C =:= check_child_object_permissions;
  166:                                   C =:= check_child_object_field_permissions;
  167:                                   C =:= check_fragment_permissions;
  168:                                   C =:= check_interface_permissions;
  169:                                   C =:= check_interface_field_permissions;
  170:                                   C =:= check_inline_fragment_permissions;
  171:                                   C =:= check_union_permissions ->
  172:     {Mapping, Pattern} = example_permissions_schema_data(Config),
  173:     {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]),
  174:     Ep = mongoose_graphql:get_endpoint(C),
  175:     [{endpoint, Ep} | Config];
  176: init_per_testcase(C, Config) ->
  177:     [{endpoint_name, C} | Config].
  178: 
  179: end_per_testcase(_, _Config) ->
  180:     ok.
  181: 
  182: can_create_endpoint(Config) ->
  183:     Name = ?config(endpoint_name, Config),
  184:     {Mapping, Pattern} = example_schema_protected_data(Config),
  185:     {ok, Pid} = mongoose_graphql:create_endpoint(Name, Mapping, [Pattern]),
  186: 
  187:     Ep = mongoose_graphql:get_endpoint(Name),
  188:     ?assertMatch({endpoint_context, Name, Pid, _, _}, Ep),
  189:     ?assertMatch(#root_schema{id = 'ROOT', query = <<"UserQuery">>,
  190:                               mutation = <<"UserMutation">>},
  191:                   graphql_schema:get(Ep, 'ROOT')).
  192: 
  193: can_load_split_schema(Config) ->
  194:     Name = ?config(endpoint_name, Config),
  195:     {Mapping, Pattern} = example_split_schema_data(Config),
  196:     {ok, Pid} = mongoose_graphql:create_endpoint(Name, Mapping, [Pattern]),
  197: 
  198:     Ep = mongoose_graphql:get_endpoint(Name),
  199:     ?assertMatch({endpoint_context, Name, Pid, _, _}, Ep),
  200:     ?assertMatch(#root_schema{id = 'ROOT', query = <<"Query">>,
  201:                               mutation = <<"Mutation">>},
  202:                   graphql_schema:get(Ep, 'ROOT')),
  203:     ?assertMatch(#object_type{id = <<"Query">>}, graphql_schema:get(Ep, <<"Query">>)),
  204:     ?assertMatch(#object_type{id = <<"Mutation">>}, graphql_schema:get(Ep, <<"Mutation">>)).
  205: 
  206: unexpected_internal_error(Config) ->
  207:     Name = ?config(endpoint_name, Config),
  208:     Doc = <<"mutation { field }">>,
  209:     Res = mongoose_graphql:execute(Name, undefined, Doc),
  210:     ?assertEqual({error, internal_crash}, Res).
  211: 
  212: admin_and_user_load_global_types(_Config) ->
  213:     mongoose_graphql:init(),
  214:     AdminEp = mongoose_graphql:get_endpoint(admin),
  215:     ?assertMatch(#scalar_type{id = <<"JID">>}, graphql_schema:get(AdminEp, <<"JID">>)),
  216:     ?assertMatch(#directive_type{id = <<"protected">>},
  217:                  graphql_schema:get(AdminEp, <<"protected">>)),
  218: 
  219:     UserEp = mongoose_graphql:get_endpoint(user),
  220:     ?assertMatch(#scalar_type{id = <<"JID">>}, graphql_schema:get(UserEp, <<"JID">>)),
  221:     ?assertMatch(#directive_type{id = <<"protected">>},
  222:                  graphql_schema:get(UserEp, <<"protected">>)).
  223: 
  224: %% Protected graphql
  225: 
  226: auth_can_execute_protected_query(Config) ->
  227:     Ep = ?config(endpoint, Config),
  228:     Doc = <<"{ field }">>,
  229:     Res = mongoose_graphql:execute(Ep, undefined, Doc),
  230:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  231: 
  232: auth_can_execute_protected_mutation(Config) ->
  233:     Ep = ?config(endpoint, Config),
  234:     Doc = <<"mutation { field }">>,
  235:     Res = mongoose_graphql:execute(Ep, undefined, Doc),
  236:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  237: 
  238: unauth_cannot_execute_protected_query(Config) ->
  239:     Ep = ?config(endpoint, Config),
  240:     Doc = <<"query Q1 { field }">>,
  241:     Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)),
  242:     ?assertMatch({error, #{error_term := {no_permissions, <<"Q1">>}, path := [<<"Q1">>]}}, Res).
  243: 
  244: unauth_cannot_execute_protected_mutation(Config) ->
  245:     Ep = ?config(endpoint, Config),
  246:     Doc = <<"mutation { field }">>,
  247:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  248:     ?assertMatch({error, #{error_term := {no_permissions, <<"ROOT">>}}}, Res).
  249: 
  250: unauth_can_access_introspection(Config) ->
  251:     Ep = ?config(endpoint, Config),
  252:     Doc = <<"{ __schema { queryType { name } } __type(name: \"UserQuery\") { name } }">>,
  253:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  254:     Expected =
  255:         {ok,
  256:             #{data =>
  257:                 #{<<"__schema">> =>
  258:                     #{<<"queryType">> =>
  259:                         #{<<"name">> => <<"UserQuery">>}
  260:                 },
  261:                 <<"__type">> =>
  262:                     #{<<"name">> =>
  263:                         <<"UserQuery">>
  264:                      }
  265:                  }
  266:              }
  267:         },
  268:     ?assertEqual(Expected, Res).
  269: 
  270: %% Unprotected graphql
  271: 
  272: can_execute_query_with_vars(Config) ->
  273:     Ep = ?config(endpoint, Config),
  274:     Doc = <<"query Q1($value: String!) { id(value: $value)}">>,
  275:     Req =
  276:         #{document => Doc,
  277:           operation_name => <<"Q1">>,
  278:           vars => #{<<"value">> => <<"Hello">>},
  279:           authorized => false,
  280:           ctx => #{}},
  281:     Res = mongoose_graphql:execute(Ep, Req),
  282:     ?assertEqual({ok, #{data => #{<<"id">> => <<"Hello">>}}}, Res).
  283: 
  284: unauth_can_execute_query(Config) ->
  285:     Ep = ?config(endpoint, Config),
  286:     Doc = <<"query { field }">>,
  287:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  288:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  289: 
  290: unauth_can_execute_mutation(Config) ->
  291:     Ep = ?config(endpoint, Config),
  292:     Doc = <<"mutation { field }">>,
  293:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  294:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  295: 
  296: auth_can_execute_query(Config) ->
  297:     Ep = ?config(endpoint, Config),
  298:     Doc = <<"query { field }">>,
  299:     Res = mongoose_graphql:execute(Ep, request(Doc, true)),
  300:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  301: 
  302: auth_can_execute_mutation(Config) ->
  303:     Ep = ?config(endpoint, Config),
  304:     Doc = <<"mutation { field }">>,
  305:     Res = mongoose_graphql:execute(Ep, request(Doc, true)),
  306:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  307: 
  308: %% Error handling
  309: 
  310: should_catch_parsing_error(Config) ->
  311:     Ep = ?config(endpoint, Config),
  312:     Doc = <<"query { field ">>,
  313:     DocScan = <<"query { id(value: \"ala) }">>,
  314:     ResParseErr = mongoose_graphql:execute(Ep, request(Doc, false)),
  315:     ?assertMatch({error, #{phase := parse, error_term := {parser_error, _}}}, ResParseErr),
  316:     ResScanErr = mongoose_graphql:execute(Ep, request(DocScan, false)),
  317:     ?assertMatch({error, #{phase := parse, error_term := {scanner_error, _}}}, ResScanErr).
  318: 
  319: should_catch_type_check_error(Config) ->
  320:     Ep = ?config(endpoint, Config),
  321:     Doc = <<"query { notExistingField(value: \"Hello\") }">>,
  322:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  323:     ?assertMatch({error, #{phase := type_check, error_term := unknown_field}}, Res).
  324: 
  325: should_catch_type_check_params_error(Config) ->
  326:     Ep = ?config(endpoint, Config),
  327:     Doc = <<"query { id(value: 12) }">>,
  328:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  329:     ?assertMatch({error, #{phase := type_check, error_term := {input_coercion, _, _, _}}}, Res).
  330: 
  331: should_catch_validation_error(Config) ->
  332:     Ep = ?config(endpoint, Config),
  333:     Doc = <<"query Q1{ id(value: \"ok\") } query Q1{ id(value: \"ok\") }">>,
  334:     % Query name must be unique
  335:     Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)),
  336:     ?assertMatch({error, #{phase := validate, error_term := {not_unique, _}}}, Res).
  337: 
  338: %% Permissions
  339: 
  340: check_object_permissions(Config) ->
  341:     Doc = <<"query { field }">>,
  342:     FDoc = <<"mutation { field }">>,
  343:     ?assertPermissionsSuccess(Config, Doc),
  344:     ?assertPermissionsFailed(Config, FDoc).
  345: 
  346: check_field_permissions(Config) ->
  347:     Doc = <<"{ field protectedField }">>,
  348:     ?assertPermissionsFailed(Config, Doc).
  349: 
  350: check_child_object_permissions(Config) ->
  351:     Doc = <<"{ protectedObj{ type } }">>,
  352:     ?assertPermissionsFailed(Config, Doc).
  353: 
  354: check_child_object_field_permissions(Config) ->
  355:     Doc = <<"{ obj { field } }">>,
  356:     FDoc = <<"{ obj { field protectedField } }">>,
  357:     ?assertPermissionsSuccess(Config, Doc),
  358:     ?assertPermissionsFailed(Config, FDoc).
  359: 
  360: check_fragment_permissions(Config) ->
  361:     Config2 = [{op, <<"Q1">>} | Config],
  362:     Doc = <<"query Q1{ obj { ...body } } fragment body on Object { name field }">>,
  363:     FDoc = <<"query Q1{ obj { ...body } } fragment body on Object { name field protectedField }">>,
  364:     ?assertPermissionsSuccess(Config2, Doc),
  365:     ?assertPermissionsFailed(Config2, FDoc).
  366: 
  367: check_interface_permissions(Config) ->
  368:     Doc = <<"{ interface { name } }">>,
  369:     FDoc = <<"{ protInterface { name } }">>,
  370:     ?assertPermissionsSuccess(Config, Doc),
  371:     ?assertPermissionsFailed(Config, FDoc).
  372: 
  373: check_interface_field_permissions(Config) ->
  374:     Doc = <<"{ interface { protectedName } }">>,
  375:     FieldProtectedNotEnaugh = <<"{ obj { protectedName } }">>,
  376:     FieldProtectedEnaugh = <<"{ obj { otherName } }">>,
  377:     % field is protected in interface and object, so cannnot be accessed.
  378:     ?assertPermissionsFailed(Config, Doc),
  379:     ?assertPermissionsFailed(Config, FieldProtectedEnaugh),
  380:     % field is protected only in interface, so can by accessed from implementing objects.
  381:     ?assertPermissionsSuccess(Config, FieldProtectedNotEnaugh).
  382: 
  383: check_inline_fragment_permissions(Config) ->
  384:     Doc = <<"{ interface { name otherName ... on Object { field } } }">>,
  385:     FDoc = <<"{ interface { name otherName ... on Object { field protectedField } } }">>,
  386:     FDoc2 = <<"{ interface { name ... on Object { field otherName} } }">>,
  387:     ?assertPermissionsSuccess(Config, Doc),
  388:     ?assertPermissionsFailed(Config, FDoc),
  389:     ?assertPermissionsFailed(Config, FDoc2).
  390: 
  391: check_union_permissions(Config) ->
  392:     Doc = <<"{ union { ... on O1 { field1 } } }">>,
  393:     FDoc = <<"{ union { ... on O1 { field1 field1Protected } } }">>,
  394:     FDoc2 = <<"{ union { ... on O1 { field1 } ... on O2 { field2 } } }">>,
  395:     ?assertPermissionsSuccess(Config, Doc),
  396:     ?assertPermissionsFailed(Config, FDoc),
  397:     ?assertPermissionsFailed(Config, FDoc2).
  398: 
  399: %% Error formatting
  400: 
  401: format_internal_crash(_Config) ->
  402:     {Code, Res} = mongoose_graphql_errors:format_error(internal_crash),
  403:     ?assertEqual(500, Code),
  404:     ?assertMatch(#{extensions := #{code := internal_server_error}}, Res).
  405: 
  406: format_parse_errors(_Config) ->
  407:     ParserError = make_error(parse, {parser_error, {0, graphql_parser, "parser_error_msg"}}),
  408:     ScannerError = make_error(parse, {scanner_error,
  409:                                       {0, graphql_scanner, {illegal, "illegal_characters"}}}),
  410:     ScannerError2 = make_error(parse, {scanner_error,
  411:                                       {0, graphql_scanner, {user, "user_scanner_err"}}}),
  412: 
  413:     {400, ResParser} = mongoose_graphql_errors:format_error(ParserError),
  414:     {400, ResScanner} = mongoose_graphql_errors:format_error(ScannerError),
  415:     {400, ResScanner2} = mongoose_graphql_errors:format_error(ScannerError2),
  416:     ?assertErrMsg(parser_error, <<"parser_error_msg">>, ResParser),
  417:     ?assertErrMsg(scanner_error, <<"illegal_characters">>, ResScanner),
  418:     ?assertErrMsg(scanner_error, <<"user_scanner_err">>, ResScanner2).
  419: 
  420: format_decode_errors(_Config) ->
  421:     {400, Msg1} = mongoose_graphql_errors:format_error(make_error(decode, no_query_supplied)),
  422:     {400, Msg2} = mongoose_graphql_errors:format_error(make_error(decode, invalid_json_body)),
  423:     {400, Msg3} = mongoose_graphql_errors:format_error(make_error(decode, variables_invalid_json)),
  424: 
  425:     ?assertErrMsg(no_query_supplied, <<"The query was not supplied">>, Msg1),
  426:     ?assertErrMsg(invalid_json_body, <<"invalid">>, Msg2),
  427:     ?assertErrMsg(variables_invalid_json, <<"invalid">>, Msg3).
  428: 
  429: format_authorize_error(_Config) ->
  430:     {401, Msg1} = mongoose_graphql_errors:format_error(make_error(authorize, wrong_credentials)),
  431:     {401, Msg2} = mongoose_graphql_errors:format_error(
  432:                     make_error([<<"ROOT">>], authorize, {no_permissions, <<"ROOT">>})),
  433:     {401, Msg3} = mongoose_graphql_errors:format_error(
  434:                     make_error(authorize, {request_error, {header, <<"authorization">>}, 'msg'})),
  435: 
  436:     ?assertErrMsg(wrong_credentials, <<"provided credentials are wrong">>, Msg1),
  437:     ?assertErrMsg(no_permissions, <<"without permissions">>, Msg2),
  438:     ?assertMatch(#{path := [<<"ROOT">>]}, Msg2),
  439:     ?assertErrMsg(request_error, <<"Malformed authorization header">>, Msg3).
  440: 
  441: format_validate_error(_Config) ->
  442:     % Ensure the module can format this phase
  443:     {400, Msg} = mongoose_graphql_errors:format_error(
  444:                    make_error(validate, {not_unique, <<"OpName">>})),
  445:     ?assertMatch(#{extensions := #{code := not_unique}}, Msg).
  446: 
  447: format_type_check_error(_Config) ->
  448:     % Ensure the module can format this phase
  449:     {400, Msg} = mongoose_graphql_errors:format_error(
  450:                    make_error(type_check, non_null)),
  451:     ?assertMatch(#{extensions := #{code := non_null}}, Msg).
  452: 
  453: format_execute_error(_Config) ->
  454:     % Ensure the module can format this phase
  455:     {400, Msg} = mongoose_graphql_errors:format_error(
  456:                    make_error(execute, {resolver_error, any_error})),
  457:     ?assertMatch(#{extensions := #{code := resolver_error}}, Msg).
  458: 
  459: format_uncategorized_error(_Config) ->
  460:     % Ensure the module can format this phase
  461:     {400, Msg} = mongoose_graphql_errors:format_error(
  462:                    make_error(uncategorized, any_error)),
  463:     ?assertMatch(#{extensions := #{code := any_error}}, Msg).
  464: 
  465: format_any_error(_Config) ->
  466:     {400, Msg1} = mongoose_graphql_errors:format_error(any_error),
  467:     {400, Msg2} = mongoose_graphql_errors:format_error(<<"any_error">>),
  468:     {400, Msg3} = mongoose_graphql_errors:format_error({1, any_error}),
  469:     {400, Msg4} = mongoose_graphql_errors:format_error(#{msg => any_error}),
  470:     ?assertErrMsg(uncategorized, <<"any_error">>, Msg1),
  471:     ?assertErrMsg(uncategorized, <<"any_error">>, Msg2),
  472:     ?assertErrMsg(uncategorized, <<"any_error">>, Msg3),
  473:     ?assertErrMsg(uncategorized, <<"any_error">>, Msg4).
  474: 
  475: %% Listeners
  476: 
  477: auth_user_can_access_protected_types(Config) ->
  478:     Ep = ?config(endpoint_addr, Config),
  479:     Body = #{query => "{ field }"},
  480:     {Status, Data} = execute(Ep, Body, {<<"alice@localhost">>, <<"makota">>}),
  481:     assert_access_granted(Status, Data).
  482: 
  483: no_creds_defined_admin_can_access_protected(_Config) ->
  484:     Port = 5559,
  485:     Ep = "http://localhost:" ++ integer_to_list(Port),
  486:     start_listener(no_creds_admin_listener, Port, [{schema_endpoint, <<"admin">>}]),
  487:     Body = #{<<"query">> => <<"{ field }">>},
  488:     {Status, Data} = execute(Ep, Body, undefined),
  489:     assert_access_granted(Status, Data).
  490: 
  491: auth_admin_can_access_protected_types(Config) ->
  492:     Ep = ?config(endpoint_addr, Config),
  493:     Body = #{query => "{ field }"},
  494:     {Status, Data} = execute(Ep, Body, {<<"admin">>, <<"secret">>}),
  495:     assert_access_granted(Status, Data).
  496: 
  497: malformed_auth_header_error(Config) ->
  498:     Ep = ?config(endpoint_addr, Config),
  499:     % The encoded credentials value is malformed and cannot be decoded.
  500:     Headers = [{<<"Authorization">>, <<"Basic YWRtaW46c2VjcmV">>}],
  501:     {Status, Data} = post_request(Ep, Headers, <<"">>),
  502:     assert_no_permissions(request_error, Status, Data).
  503: 
  504: auth_wrong_creds_error(Config) ->
  505:     Ep = ?config(endpoint_addr, Config),
  506:     Body = #{query => "{ field }"},
  507:     {Status, Data} = execute(Ep, Body, {<<"user">>, <<"wrong_password">>}),
  508:     assert_no_permissions(wrong_credentials, Status, Data).
  509: 
  510: invalid_json_body_error(Config) ->
  511:     Ep = ?config(endpoint_addr, Config),
  512:     Body = <<"">>,
  513:     {Status, Data} = execute(Ep, Body, undefined),
  514:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  515:     assert_code(invalid_json_body, Data).
  516: 
  517: no_query_supplied_error(Config) ->
  518:     Ep = ?config(endpoint_addr, Config),
  519:     Body = #{},
  520:     {Status, Data} = execute(Ep, Body, undefined),
  521:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  522:     assert_code(no_query_supplied, Data).
  523: 
  524: variables_invalid_json_error(Config) ->
  525:     Ep = ?config(endpoint_addr, Config),
  526:     Body = #{<<"query">> => <<"{ field }">>, <<"variables">> => <<"{1: 2}">>},
  527:     {Status, Data} = execute(Ep, Body, undefined),
  528:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  529:     assert_code(variables_invalid_json, Data).
  530: 
  531: listener_reply_with_parsing_error(Config) ->
  532:     Ep = ?config(endpoint_addr, Config),
  533:     Body = #{<<"query">> => <<"{ field ">>},
  534:     {Status, Data} = execute(Ep, Body, undefined),
  535:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  536:     assert_code(parser_error, Data),
  537: 
  538:     BodyScanner = #{<<"query">> => <<"mutation { id(value: \"asdfsad) } ">>},
  539:     {StatusScanner, DataScanner} = execute(Ep, BodyScanner, undefined),
  540:     ?assertEqual({<<"400">>,<<"Bad Request">>}, StatusScanner),
  541:     assert_code(scanner_error, DataScanner).
  542: 
  543: listener_reply_with_type_check_error(Config) ->
  544:     Ep = ?config(endpoint_addr, Config),
  545:     Body = #{<<"query">> => <<"mutation { id(value: 12) }">>},
  546:     {Status, Data} = execute(Ep, Body, undefined),
  547:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  548:     assert_code(input_coercion, Data).
  549: 
  550: listener_reply_with_validation_error(Config) ->
  551:     Ep = ?config(endpoint_addr, Config),
  552:     Body = #{<<"query">> => <<"query Q1 { field } query Q1 { field }">>,
  553:              <<"operationName">> => <<"Q1">>},
  554:     {Status, Data} = execute(Ep, Body, undefined),
  555:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  556:     assert_code(not_unique, Data).
  557: 
  558: listener_can_execute_query_with_variables(Config) ->
  559:     Ep = ?config(endpoint_addr, Config),
  560:     Body = #{query => "mutation M1($value: String!){ id(value: $value) } query Q1{ field }",
  561:              variables => #{value => <<"Hello">>},
  562:              operationName => <<"M1">>
  563:             },
  564:     {Status, Data} = execute(Ep, Body, undefined),
  565:     assert_access_granted(Status, Data),
  566:     ?assertMatch(#{<<"data">> := #{<<"id">> := <<"Hello">>}}, Data).
  567: 
  568: listener_unauth_cannot_access_protected_types(Config) ->
  569:     Ep = ?config(endpoint_addr, Config),
  570:     Body = #{query => "{ field }"},
  571:     {Status, Data} = execute(Ep, Body, undefined),
  572:     ?assertMatch(#{<<"errors">> := [#{<<"path">> := [<<"ROOT">>]}]}, Data),
  573:     assert_no_permissions(no_permissions, Status, Data).
  574: 
  575: listener_unauth_can_access_unprotected_types(Config) ->
  576:     Ep = ?config(endpoint_addr, Config),
  577:     Body = #{query => "mutation { field }"},
  578:     {Status, Data} = execute(Ep, Body, undefined),
  579:     assert_access_granted(Status, Data).
  580: 
  581: %% Helpers
  582: 
  583: assert_code(Code, Data) ->
  584:     BinCode = atom_to_binary(Code),
  585:     ?assertMatch(#{<<"errors">> := [#{<<"extensions">> := #{<<"code">> := BinCode}}]}, Data).
  586: 
  587: assert_no_permissions(ExpectedCode, Status, Data) ->
  588:     ?assertEqual({<<"401">>,<<"Unauthorized">>}, Status),
  589:     assert_code(ExpectedCode, Data).
  590: 
  591: assert_access_granted(Status, Data) ->
  592:     ?assertEqual({<<"200">>,<<"OK">>}, Status),
  593:     % access was granted, no error was returned
  594:     ?assertNotMatch(#{<<"errors">> := _}, Data).
  595: 
  596: assert_err_msg(Code, MsgContains, #{message := Msg} = ErrorMsg) ->
  597:     ?assertMatch(#{extensions := #{code := Code}}, ErrorMsg),
  598:     ?assertNotEqual(nomatch, binary:match(Msg, MsgContains)).
  599: 
  600: make_error(Phase, Term) ->
  601:     #{phase => Phase, error_term => Term}.
  602: 
  603: make_error(Path, Phase, Term) ->
  604:     #{path => Path, phase => Phase, error_term => Term}.
  605: 
  606: check_permissions(Config, Doc) ->
  607:     Ep = ?config(endpoint, Config),
  608:     Op = proplists:get_value(op, Config, undefined),
  609:     {ok, Ast} = graphql:parse(Doc),
  610:     {ok, #{ast := Ast2}} = graphql:type_check(Ep, Ast),
  611:     ok = graphql:validate(Ast2),
  612:     ok = mongoose_graphql_permissions:check_permissions(Op, false, Ast2).
  613: 
  614: request(Doc, Authorized) ->
  615:     request(undefined, Doc, Authorized).
  616: 
  617: request(Op, Doc, Authorized) ->
  618:     #{document => Doc,
  619:       operation_name => Op,
  620:       vars => #{},
  621:       authorized => Authorized,
  622:       ctx => #{}}.
  623: 
  624: example_split_schema_data(Config) ->
  625:     Pattern = filename:join([proplists:get_value(data_dir, Config),
  626:                              "split_schema", "*.gql"]),
  627:     Mapping =
  628:         #{objects =>
  629:               #{'Query' => mongoose_graphql_default_resolver,
  630:                 'Mutation' => mongoose_graphql_default_resolver,
  631:                 default => mongoose_graphql_default_resolver}},
  632:     {Mapping, Pattern}.
  633: 
  634: example_schema_protected_data(Config) ->
  635:     Pattern = filename:join([proplists:get_value(data_dir, Config), "protected_schema.gql"]),
  636:     Mapping =
  637:         #{objects =>
  638:               #{'UserQuery' => mongoose_graphql_default_resolver,
  639:                 'UserMutation' => mongoose_graphql_default_resolver,
  640:                 default => mongoose_graphql_default_resolver}},
  641:     {Mapping, Pattern}.
  642: 
  643: example_schema_data(Config) ->
  644:     Pattern = filename:join([proplists:get_value(data_dir, Config), "schema.gql"]),
  645:     Mapping =
  646:         #{objects =>
  647:               #{'UserQuery' => mongoose_graphql_default_resolver,
  648:                 'UserMutation' => mongoose_graphql_default_resolver,
  649:                 default => mongoose_graphql_default_resolver}},
  650:     {Mapping, Pattern}.
  651: 
  652: example_permissions_schema_data(Config) ->
  653:     Pattern = filename:join([proplists:get_value(data_dir, Config), "permissions_schema.gql"]),
  654:     Mapping =
  655:         #{objects =>
  656:               #{'UserQuery' => mongoose_graphql_default_resolver,
  657:                 'UserMutation' => mongoose_graphql_default_resolver,
  658:                 default => mongoose_graphql_default_resolver},
  659:          interfaces => #{default => mongoose_graphql_default_resolver},
  660:          unions => #{default => mongoose_graphql_default_resolver}},
  661:     {Mapping, Pattern}.
  662: 
  663: example_listener_schema_data(Config) ->
  664:     Pattern = filename:join([proplists:get_value(data_dir, Config), "listener_schema.gql"]),
  665:     Mapping =
  666:         #{objects =>
  667:               #{'UserQuery' => mongoose_graphql_default_resolver,
  668:                 'UserMutation' => mongoose_graphql_default_resolver,
  669:                 default => mongoose_graphql_default_resolver}},
  670:     {Mapping, Pattern}.
  671: 
  672: -spec init_ep_listener(integer(), atom(), [{atom(), term()}], [{atom(), term()}]) ->
  673:     [{atom(), term()}].
  674: init_ep_listener(Port, EpName, ListenerOpts, Config) ->
  675:     Pid = spawn(fun() ->
  676:                     Name = list_to_atom("gql_listener_" ++ atom_to_list(EpName)),
  677:                     ok = start_listener(Name, Port, ListenerOpts),
  678:                     {Mapping, Pattern} = example_listener_schema_data(Config),
  679:                     {ok, _} = mongoose_graphql:create_endpoint(EpName, Mapping, [Pattern]),
  680:                     receive
  681:                         stop ->
  682:                             ok
  683:                     end
  684:                 end),
  685:     [{test_process, Pid}, {endpoint_addr, "http://localhost:" ++ integer_to_list(Port)} | Config].
  686: 
  687: -spec start_listener(atom(), integer(), [{atom(), term()}]) -> ok.
  688: start_listener(Ref, Port, Opts) ->
  689:     Dispatch = cowboy_router:compile([
  690:         {'_', [{"/graphql", mongoose_graphql_cowboy_handler, Opts}]}
  691:     ]),
  692:     {ok, _} = cowboy:start_clear(Ref,
  693:                                  [{port, Port}],
  694:                                  #{env => #{dispatch => Dispatch}}),
  695:     ok.
  696: 
  697: -spec execute(binary(), map(), undefined | {binary(), binary()}) -> {{binary(), binary()}, map()}.
  698: execute(EpAddr, Body, undefined) ->
  699:     post_request(EpAddr, [], Body);
  700: execute(EpAddr, Body, {Username, Password}) ->
  701:     Creds = base64:encode(<<Username/binary, ":", Password/binary>>),
  702:     Headers = [{<<"Authorization">>, <<"Basic ", Creds/binary>>}],
  703:     post_request(EpAddr, Headers, Body).
  704: 
  705: post_request(EpAddr, HeadersIn, Body) when is_binary(Body) ->
  706:     {ok, Client} = fusco:start(EpAddr, []),
  707:     Headers = [{<<"Content-Type">>, <<"application/json">>},
  708:                {<<"Request-Id">>, random_request_id()} | HeadersIn],
  709:     {ok, {ResStatus, _, ResBody, _, _}} = Res =
  710:         fusco:request(Client, <<"/graphql">>, <<"POST">>, Headers, Body, 5000),
  711:     fusco:disconnect(Client),
  712:     ct:log("~p", [Res]),
  713:     {ResStatus, jiffy:decode(ResBody, [return_maps])};
  714: post_request(Ep, HeadersIn, Body) ->
  715:     post_request(Ep, HeadersIn, jiffy:encode(Body)).
  716: 
  717: random_request_id() ->
  718:     base16:encode(crypto:strong_rand_bytes(8)).