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: -type listener_opts() :: #{endpoint_schema := binary(),
   11:                            atom() => any()}.
   12: 
   13: -define(assertPermissionsFailed(Config, Doc),
   14:         ?assertThrow({error, #{error_term := {no_permissions, _}}},
   15:                      check_permissions(Config, false, Doc))).
   16: -define(assertPermissionsSuccess(Config, Doc),
   17:         ?assertMatch(ok, check_permissions(Config, false, Doc))).
   18: 
   19: -define(assertDomainPermissionsFailed(Config, Domain, Args, Doc),
   20:         ?assertThrow({error, #{error_term := {no_permissions, _,#{type := domain,
   21:                                                                   invalid_args := Args}}}},
   22:                      check_domain_permissions(Config, Domain, Doc))).
   23: -define(assertPermissionsSuccess(Config, Domain, Doc),
   24:         ?assertMatch(ok, check_domain_permissions(Config, Domain, Doc))).
   25: 
   26: -define(assertErrMsg(Code, MsgContains, ErrorMsg),
   27:         assert_err_msg(Code, MsgContains, ErrorMsg)).
   28: 
   29: all() ->
   30:     [can_create_endpoint,
   31:      can_load_split_schema,
   32:      unexpected_internal_error,
   33:      admin_and_user_load_global_types,
   34:      {group, unprotected_graphql},
   35:      {group, protected_graphql},
   36:      {group, error_handling},
   37:      {group, error_formatting},
   38:      {group, permissions},
   39:      {group, domain_permissions},
   40:      {group, user_listener},
   41:      {group, admin_listener},
   42:      {group, domain_admin_listener}].
   43: 
   44: groups() ->
   45:     [{protected_graphql, [parallel], protected_graphql()},
   46:      {unprotected_graphql, [parallel], unprotected_graphql()},
   47:      {error_handling, [parallel], error_handling()},
   48:      {error_formatting, [parallel], error_formatting()},
   49:      {permissions, [parallel], permissions()},
   50:      {domain_permissions, [parallel], domain_permissions()},
   51:      {admin_listener, [parallel], admin_listener()},
   52:      {domain_admin_listener, [parallel], domain_admin_listener()},
   53:      {user_listener, [parallel], user_listener()}].
   54: 
   55: protected_graphql() ->
   56:     [auth_can_execute_protected_query,
   57:      auth_can_execute_protected_mutation,
   58:      unauth_cannot_execute_protected_query,
   59:      unauth_cannot_execute_protected_mutation,
   60:      unauth_can_access_introspection].
   61: 
   62: unprotected_graphql() ->
   63:     [can_execute_query_with_vars,
   64:      auth_can_execute_query,
   65:      auth_can_execute_mutation,
   66:      unauth_can_execute_query,
   67:      unauth_can_execute_mutation].
   68: 
   69: error_handling() ->
   70:     [should_catch_parsing_error,
   71:      should_catch_type_check_params_error,
   72:      should_catch_type_check_error,
   73:      should_catch_validation_error].
   74: 
   75: error_formatting() ->
   76:     [format_internal_crash,
   77:      format_parse_errors,
   78:      format_decode_errors,
   79:      format_authorize_error,
   80:      format_validate_error,
   81:      format_type_check_error,
   82:      format_execute_error,
   83:      format_uncategorized_error,
   84:      format_any_error].
   85: 
   86: permissions() ->
   87:     [check_object_permissions,
   88:      check_field_permissions,
   89:      check_child_object_permissions,
   90:      check_child_object_field_permissions,
   91:      check_fragment_permissions,
   92:      check_interface_permissions,
   93:      check_interface_field_permissions,
   94:      check_inline_fragment_permissions,
   95:      check_union_permissions
   96:     ].
   97: 
   98: domain_permissions() ->
   99:     [check_field_domain_permissions,
  100:      check_field_input_arg_domain_permissions,
  101:      check_field_list_arg_domain_permissions,
  102:      check_field_null_arg_domain_permissions,
  103:      check_field_jid_arg_domain_permissions,
  104:      check_child_object_field_domain_permissions,
  105:      check_field_subdomain_permissions,
  106:      check_field_global_permissions,
  107:      check_interface_field_domain_permissions
  108:     ].
  109: 
  110: user_listener() ->
  111:     [auth_user_can_access_protected_types | common_tests()].
  112: 
  113: admin_listener() ->
  114:     [no_creds_defined_admin_can_access_protected,
  115:      auth_admin_can_access_protected_types | common_tests()].
  116: 
  117: domain_admin_listener() ->
  118:     [auth_domain_admin_can_access_protected_types,
  119:      auth_domain_admin_wrong_password_error,
  120:      auth_domain_admin_nonexistent_domain_error,
  121:      auth_domain_admin_cannot_access_other_domain,
  122:      auth_domain_admin_cannot_access_global,
  123:      auth_domain_admin_can_access_owned_domain
  124:      | common_tests()].
  125: 
  126: common_tests() ->
  127:     [malformed_auth_header_error,
  128:      auth_wrong_creds_error,
  129:      invalid_json_body_error,
  130:      no_query_supplied_error,
  131:      variables_invalid_json_error,
  132:      listener_reply_with_parsing_error,
  133:      listener_reply_with_type_check_error,
  134:      listener_reply_with_validation_error,
  135:      listener_unauth_cannot_access_protected_types,
  136:      listener_unauth_can_access_unprotected_types,
  137:      listener_can_execute_query_with_variables].
  138: 
  139: init_per_suite(Config) ->
  140:     application:ensure_all_started(cowboy),
  141:     application:ensure_all_started(jid),
  142:     Config.
  143: 
  144: end_per_suite(_Config) ->
  145:     ok.
  146: 
  147: init_per_group(user_listener, Config) ->
  148:     meck:new(mongoose_api_common, [no_link]),
  149:     meck:expect(mongoose_api_common, check_password,
  150:                 fun
  151:                     (#jid{luser = <<"alice">>}, <<"makota">>) -> {true, {}};
  152:                     (_, _) -> false
  153:                 end),
  154:     ListenerOpts = #{schema_endpoint => <<"user">>},
  155:     init_ep_listener(5557, user_schema_ep, ListenerOpts, Config);
  156: init_per_group(admin_listener, Config) ->
  157:     ListenerOpts = #{username => <<"admin">>,
  158:                      password => <<"secret">>,
  159:                      schema_endpoint => <<"admin">>},
  160:     init_ep_listener(5558, admin_schema_ep, ListenerOpts, Config);
  161: init_per_group(domain_admin_listener, Config) ->
  162:     meck:new(mongoose_domain_api, [no_link]),
  163:     meck:expect(mongoose_domain_api, check_domain_password,
  164:                 fun
  165:                     (<<"localhost">>, <<"makota">>) -> ok;
  166:                     (<<"localhost">>, _) -> {error, wrong_password};
  167:                     (_, _) -> {error, not_found}
  168:                 end),
  169:     meck:expect(mongoose_domain_api, get_subdomain_info,
  170:                 fun (_) -> {error, not_found} end),
  171:     ListenerOpts = #{schema_endpoint => <<"domain_admin">>},
  172:     init_ep_listener(5560, domain_admin_schema_ep, ListenerOpts, Config);
  173: init_per_group(domain_permissions, Config) ->
  174:     meck:new(mongoose_domain_api, [no_link]),
  175:     meck:expect(mongoose_domain_api, get_subdomain_info,
  176:                 fun
  177:                     (<<"subdomain.test-domain.com">>) ->
  178:                         {ok, #{parent_domain => <<"test-domain.com">>}};
  179:                     (<<"subdomain.test-domain2.com">>) ->
  180:                         {ok, #{parent_domain => <<"test-domain2.com">>}};
  181:                     (_) ->
  182:                         {error, not_found}
  183:                 end),
  184:     Domains = [{<<"subdomain.test-domain.com">>, <<"test-domain.com">>},
  185:                {<<"subdomain.test-domain2.com">>, <<"test-domain2.com">>}],
  186:     [{domains, Domains} | Config];
  187: init_per_group(_G, Config) ->
  188:     Config.
  189: 
  190: end_per_group(user_listener, Config) ->
  191:     meck:unload(mongoose_api_common),
  192:     ?config(test_process, Config) ! stop,
  193:     Config;
  194: end_per_group(admin_listener, Config) ->
  195:     ?config(test_process, Config) ! stop,
  196:     Config;
  197: end_per_group(domain_admin_listener, Config) ->
  198:     meck:unload(mongoose_domain_api),
  199:     ?config(test_process, Config) ! stop,
  200:     Config;
  201: end_per_group(domain_permissions, _Config) ->
  202:     meck:unload(mongoose_domain_api);
  203: end_per_group(_, Config) ->
  204:     Config.
  205: 
  206: init_per_testcase(C, Config) when C =:= auth_can_execute_protected_query;
  207:                                   C =:= auth_can_execute_protected_mutation;
  208:                                   C =:= unauth_cannot_execute_protected_query;
  209:                                   C =:= unauth_cannot_execute_protected_mutation;
  210:                                   C =:= unauth_can_access_introspection ->
  211:     {Mapping, Pattern} = example_schema_protected_data(Config),
  212:     {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]),
  213:     Ep = mongoose_graphql:get_endpoint(C),
  214:     [{endpoint, Ep} | Config];
  215: init_per_testcase(C, Config) when C =:= can_execute_query_with_vars;
  216:                                   C =:= auth_can_execute_query;
  217:                                   C =:= auth_can_execute_mutation;
  218:                                   C =:= unauth_can_execute_query;
  219:                                   C =:= unauth_can_execute_mutation;
  220:                                   C =:= should_catch_type_check_params_error;
  221:                                   C =:= should_catch_type_check_error;
  222:                                   C =:= should_catch_parsing_error;
  223:                                   C =:= should_catch_validation_error ->
  224:     {Mapping, Pattern} = example_schema_data(Config),
  225:     {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]),
  226:     Ep = mongoose_graphql:get_endpoint(C),
  227:     [{endpoint, Ep} | Config];
  228: init_per_testcase(C, Config) when C =:= check_object_permissions;
  229:                                   C =:= check_field_permissions;
  230:                                   C =:= check_child_object_permissions;
  231:                                   C =:= check_child_object_field_permissions;
  232:                                   C =:= check_fragment_permissions;
  233:                                   C =:= check_interface_permissions;
  234:                                   C =:= check_interface_field_permissions;
  235:                                   C =:= check_inline_fragment_permissions;
  236:                                   C =:= check_union_permissions;
  237:                                   C =:= check_field_domain_permissions;
  238:                                   C =:= check_field_input_arg_domain_permissions;
  239:                                   C =:= check_field_list_arg_domain_permissions;
  240:                                   C =:= check_field_null_arg_domain_permissions;
  241:                                   C =:= check_field_jid_arg_domain_permissions;
  242:                                   C =:= check_field_subdomain_permissions;
  243:                                   C =:= check_field_global_permissions;
  244:                                   C =:= check_child_object_field_domain_permissions;
  245:                                   C =:= check_interface_field_domain_permissions ->
  246:     {Mapping, Pattern} = example_permissions_schema_data(Config),
  247:     {ok, _} = mongoose_graphql:create_endpoint(C, Mapping, [Pattern]),
  248:     Ep = mongoose_graphql:get_endpoint(C),
  249:     [{endpoint, Ep} | Config];
  250: init_per_testcase(C, Config) ->
  251:     [{endpoint_name, C} | Config].
  252: 
  253: end_per_testcase(_, _Config) ->
  254:     ok.
  255: 
  256: can_create_endpoint(Config) ->
  257:     Name = ?config(endpoint_name, Config),
  258:     {Mapping, Pattern} = example_schema_protected_data(Config),
  259:     {ok, Pid} = mongoose_graphql:create_endpoint(Name, Mapping, [Pattern]),
  260: 
  261:     Ep = mongoose_graphql:get_endpoint(Name),
  262:     ?assertMatch({endpoint_context, Name, Pid, _, _}, Ep),
  263:     ?assertMatch(#root_schema{id = 'ROOT', query = <<"UserQuery">>,
  264:                               mutation = <<"UserMutation">>},
  265:                   graphql_schema:get(Ep, 'ROOT')).
  266: 
  267: can_load_split_schema(Config) ->
  268:     Name = ?config(endpoint_name, Config),
  269:     {Mapping, Pattern} = example_split_schema_data(Config),
  270:     {ok, Pid} = mongoose_graphql:create_endpoint(Name, Mapping, [Pattern]),
  271: 
  272:     Ep = mongoose_graphql:get_endpoint(Name),
  273:     ?assertMatch({endpoint_context, Name, Pid, _, _}, Ep),
  274:     ?assertMatch(#root_schema{id = 'ROOT', query = <<"Query">>,
  275:                               mutation = <<"Mutation">>},
  276:                   graphql_schema:get(Ep, 'ROOT')),
  277:     ?assertMatch(#object_type{id = <<"Query">>}, graphql_schema:get(Ep, <<"Query">>)),
  278:     ?assertMatch(#object_type{id = <<"Mutation">>}, graphql_schema:get(Ep, <<"Mutation">>)).
  279: 
  280: unexpected_internal_error(Config) ->
  281:     Name = ?config(endpoint_name, Config),
  282:     Doc = <<"mutation { field }">>,
  283:     Res = mongoose_graphql:execute(Name, undefined, Doc),
  284:     ?assertEqual({error, internal_crash}, Res).
  285: 
  286: admin_and_user_load_global_types(_Config) ->
  287:     mongoose_graphql:init(),
  288:     AdminEp = mongoose_graphql:get_endpoint(admin),
  289:     ?assertMatch(#scalar_type{id = <<"JID">>}, graphql_schema:get(AdminEp, <<"JID">>)),
  290:     ?assertMatch(#directive_type{id = <<"protected">>},
  291:                  graphql_schema:get(AdminEp, <<"protected">>)),
  292: 
  293:     UserEp = mongoose_graphql:get_endpoint(user),
  294:     ?assertMatch(#scalar_type{id = <<"JID">>}, graphql_schema:get(UserEp, <<"JID">>)),
  295:     ?assertMatch(#directive_type{id = <<"protected">>},
  296:                  graphql_schema:get(UserEp, <<"protected">>)).
  297: 
  298: %% Protected graphql
  299: 
  300: auth_can_execute_protected_query(Config) ->
  301:     Ep = ?config(endpoint, Config),
  302:     Doc = <<"{ field }">>,
  303:     Res = mongoose_graphql:execute(Ep, undefined, Doc),
  304:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  305: 
  306: auth_can_execute_protected_mutation(Config) ->
  307:     Ep = ?config(endpoint, Config),
  308:     Doc = <<"mutation { field }">>,
  309:     Res = mongoose_graphql:execute(Ep, undefined, Doc),
  310:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  311: 
  312: unauth_cannot_execute_protected_query(Config) ->
  313:     Ep = ?config(endpoint, Config),
  314:     Doc = <<"query Q1 { field }">>,
  315:     Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)),
  316:     ?assertMatch({error, #{error_term := {no_permissions, <<"Q1">>}, path := [<<"Q1">>]}}, Res).
  317: 
  318: unauth_cannot_execute_protected_mutation(Config) ->
  319:     Ep = ?config(endpoint, Config),
  320:     Doc = <<"mutation { field }">>,
  321:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  322:     ?assertMatch({error, #{error_term := {no_permissions, <<"ROOT">>}}}, Res).
  323: 
  324: unauth_can_access_introspection(Config) ->
  325:     Ep = ?config(endpoint, Config),
  326:     Doc = <<"{ __schema { queryType { name } } __type(name: \"UserQuery\") { name } }">>,
  327:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  328:     Expected =
  329:         {ok,
  330:             #{data =>
  331:                 #{<<"__schema">> =>
  332:                     #{<<"queryType">> =>
  333:                         #{<<"name">> => <<"UserQuery">>}
  334:                 },
  335:                 <<"__type">> =>
  336:                     #{<<"name">> =>
  337:                         <<"UserQuery">>
  338:                      }
  339:                  }
  340:              }
  341:         },
  342:     ?assertEqual(Expected, Res).
  343: 
  344: %% Unprotected graphql
  345: 
  346: can_execute_query_with_vars(Config) ->
  347:     Ep = ?config(endpoint, Config),
  348:     Doc = <<"query Q1($value: String!) { id(value: $value)}">>,
  349:     Req =
  350:         #{document => Doc,
  351:           operation_name => <<"Q1">>,
  352:           vars => #{<<"value">> => <<"Hello">>},
  353:           authorized => false,
  354:           ctx => #{}},
  355:     Res = mongoose_graphql:execute(Ep, Req),
  356:     ?assertEqual({ok, #{data => #{<<"id">> => <<"Hello">>}}}, Res).
  357: 
  358: unauth_can_execute_query(Config) ->
  359:     Ep = ?config(endpoint, Config),
  360:     Doc = <<"query { field }">>,
  361:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  362:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  363: 
  364: unauth_can_execute_mutation(Config) ->
  365:     Ep = ?config(endpoint, Config),
  366:     Doc = <<"mutation { field }">>,
  367:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  368:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  369: 
  370: auth_can_execute_query(Config) ->
  371:     Ep = ?config(endpoint, Config),
  372:     Doc = <<"query { field }">>,
  373:     Res = mongoose_graphql:execute(Ep, request(Doc, true)),
  374:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  375: 
  376: auth_can_execute_mutation(Config) ->
  377:     Ep = ?config(endpoint, Config),
  378:     Doc = <<"mutation { field }">>,
  379:     Res = mongoose_graphql:execute(Ep, request(Doc, true)),
  380:     ?assertEqual({ok, #{data => #{<<"field">> => <<"Test field">>}}}, Res).
  381: 
  382: %% Error handling
  383: 
  384: should_catch_parsing_error(Config) ->
  385:     Ep = ?config(endpoint, Config),
  386:     Doc = <<"query { field ">>,
  387:     DocScan = <<"query { id(value: \"ala) }">>,
  388:     ResParseErr = mongoose_graphql:execute(Ep, request(Doc, false)),
  389:     ?assertMatch({error, #{phase := parse, error_term := {parser_error, _}}}, ResParseErr),
  390:     ResScanErr = mongoose_graphql:execute(Ep, request(DocScan, false)),
  391:     ?assertMatch({error, #{phase := parse, error_term := {scanner_error, _}}}, ResScanErr).
  392: 
  393: should_catch_type_check_error(Config) ->
  394:     Ep = ?config(endpoint, Config),
  395:     Doc = <<"query { notExistingField(value: \"Hello\") }">>,
  396:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  397:     ?assertMatch({error, #{phase := type_check, error_term := unknown_field}}, Res).
  398: 
  399: should_catch_type_check_params_error(Config) ->
  400:     Ep = ?config(endpoint, Config),
  401:     Doc = <<"query { id(value: 12) }">>,
  402:     Res = mongoose_graphql:execute(Ep, request(Doc, false)),
  403:     ?assertMatch({error, #{phase := type_check, error_term := {input_coercion, _, _, _}}}, Res).
  404: 
  405: should_catch_validation_error(Config) ->
  406:     Ep = ?config(endpoint, Config),
  407:     Doc = <<"query Q1{ id(value: \"ok\") } query Q1{ id(value: \"ok\") }">>,
  408:     % Query name must be unique
  409:     Res = mongoose_graphql:execute(Ep, request(<<"Q1">>, Doc, false)),
  410:     ?assertMatch({error, #{phase := validate, error_term := {not_unique, _}}}, Res).
  411: 
  412: %% Permissions
  413: 
  414: check_object_permissions(Config) ->
  415:     Doc = <<"query { field }">>,
  416:     FDoc = <<"mutation { field }">>,
  417:     ?assertPermissionsSuccess(Config, Doc),
  418:     ?assertPermissionsFailed(Config, FDoc).
  419: 
  420: check_field_permissions(Config) ->
  421:     Doc = <<"{ field protectedField }">>,
  422:     ?assertPermissionsFailed(Config, Doc).
  423: 
  424: check_child_object_permissions(Config) ->
  425:     Doc = <<"{ protectedObj{ type } }">>,
  426:     ?assertPermissionsFailed(Config, Doc).
  427: 
  428: check_child_object_field_permissions(Config) ->
  429:     Doc = <<"{ obj { field } }">>,
  430:     FDoc = <<"{ obj { field protectedField } }">>,
  431:     ?assertPermissionsSuccess(Config, Doc),
  432:     ?assertPermissionsFailed(Config, FDoc).
  433: 
  434: check_fragment_permissions(Config) ->
  435:     Config2 = [{op, <<"Q1">>} | Config],
  436:     Doc = <<"query Q1{ obj { ...body } } fragment body on Object { name field }">>,
  437:     FDoc = <<"query Q1{ obj { ...body } } fragment body on Object { name field protectedField }">>,
  438:     ?assertPermissionsSuccess(Config2, Doc),
  439:     ?assertPermissionsFailed(Config2, FDoc).
  440: 
  441: check_interface_permissions(Config) ->
  442:     Doc = <<"{ interface { name } }">>,
  443:     FDoc = <<"{ protInterface { name } }">>,
  444:     ?assertPermissionsSuccess(Config, Doc),
  445:     ?assertPermissionsFailed(Config, FDoc).
  446: 
  447: check_interface_field_permissions(Config) ->
  448:     Doc = <<"{ interface { protectedName } }">>,
  449:     FieldProtectedNotEnough = <<"{ obj { protectedName } }">>,
  450:     FieldProtectedEnough = <<"{ obj { otherName } }">>,
  451:     % Field is protected in interface and object, so it cannot be accessed.
  452:     ?assertPermissionsFailed(Config, Doc),
  453:     ?assertPermissionsFailed(Config, FieldProtectedEnough),
  454:     % Field is protected only in an interface, so it can be accessed from implementing objects.
  455:     ?assertPermissionsSuccess(Config, FieldProtectedNotEnough).
  456: 
  457: check_inline_fragment_permissions(Config) ->
  458:     Doc = <<"{ interface { name otherName ... on Object { field } } }">>,
  459:     FDoc = <<"{ interface { name otherName ... on Object { field protectedField } } }">>,
  460:     FDoc2 = <<"{ interface { name ... on Object { field otherName} } }">>,
  461:     ?assertPermissionsSuccess(Config, Doc),
  462:     ?assertPermissionsFailed(Config, FDoc),
  463:     ?assertPermissionsFailed(Config, FDoc2).
  464: 
  465: check_union_permissions(Config) ->
  466:     Doc = <<"{ union { ... on O1 { field1 } } }">>,
  467:     FDoc = <<"{ union { ... on O1 { field1 field1Protected } } }">>,
  468:     FDoc2 = <<"{ union { ... on O1 { field1 } ... on O2 { field2 } } }">>,
  469:     ?assertPermissionsSuccess(Config, Doc),
  470:     ?assertPermissionsFailed(Config, FDoc),
  471:     ?assertPermissionsFailed(Config, FDoc2).
  472: 
  473: %% Domain permissions
  474: 
  475: check_field_domain_permissions(Config) ->
  476:     Domain = <<"my-domain.com">>,
  477:     Config2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => Domain}} | Config],
  478:     Doc = <<"{ field protectedField }">>,
  479:     Doc2 = <<"query Q1($domain: String) { protectedField domainProtectedField(argA: $domain"
  480:              ", argB: \"domain\") }">>,
  481:     FDoc = <<"{protectedField domainProtectedField(argA: \"domain.com\","
  482:              " argB: \"domain.com\") }">>,
  483:     ?assertPermissionsSuccess(Config, Domain, Doc),
  484:     ?assertPermissionsSuccess(Config2, Domain, Doc2),
  485:     ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>], FDoc).
  486: 
  487: check_child_object_field_domain_permissions(Config) ->
  488:     Domain = <<"my-domain.com">>,
  489:     Config2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => Domain}} | Config],
  490:     Doc = <<"{ obj { field protectedField } }">>,
  491:     Doc2 = <<"query Q1($domain: String) { obj { protectedField domainProtectedField(argA: $domain"
  492:              ", argB: \"domain\") } }">>,
  493:     FDoc = <<"{ obj {protectedField domainProtectedField(argA: \"domain.com\","
  494:              " argB: \"domain.com\") } }">>,
  495:     ?assertPermissionsSuccess(Config, Domain, Doc),
  496:     ?assertPermissionsSuccess(Config2, Domain, Doc2),
  497:     ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>], FDoc).
  498: 
  499: check_interface_field_domain_permissions(Config) ->
  500:     Domain = <<"my-domain.com">>,
  501:     OkDomain = <<"{ interface { protectedDomainName(domain: \"my-domain.com\") } }">>,
  502:     OkDomain1 = <<"{ obj { protectedDomainName(domain: \"my-domain.com\") } }">>,
  503:     OkDomain2 = <<"{ obj { domainName(domain: \"my-domain.com\") } }">>,
  504:     WrongDomain = <<"{ interface { protectedDomainName(domain: \"domain.com\") } }">>,
  505:     WrongDomain1 = <<"{ obj { domainName(domain: \"domain.com\") } }">>,
  506:     ProtectedNotEnough = <<"{ obj { protectedDomainName(domain: \"domain.com\") } }">>,
  507:     ?assertPermissionsSuccess(Config, Domain, OkDomain),
  508:     ?assertPermissionsSuccess(Config, Domain, OkDomain1),
  509:     ?assertPermissionsSuccess(Config, Domain, OkDomain2),
  510:     % Field is protected in interface and object, so it cannot be accessed with the wrong domain.
  511:     ?assertDomainPermissionsFailed(Config, Domain, [<<"domain">>], WrongDomain),
  512:     ?assertDomainPermissionsFailed(Config, Domain, [<<"domain">>], WrongDomain1),
  513:     % Field is protected only in an interface, so it can be accessed from implementing objects
  514:     % with the wrong domain.
  515:     ?assertPermissionsSuccess(Config, Domain, ProtectedNotEnough).
  516: 
  517: check_field_input_arg_domain_permissions(Config) ->
  518:     Domain = <<"my-domain.com">>,
  519:     DomainInput = #{<<"domain">> => Domain, <<"notDomain">> => <<"random text here">>},
  520:     Config2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => Domain,
  521:                                         <<"domainInput">> => DomainInput}} | Config],
  522:     Doc = <<"query Q1($domain: String, $domainInput: DomainInput!) "
  523:             "{ domainInputProtectedField(argA: $domain, argB: $domainInput)"
  524:             "  domainProtectedField(argA: $domain, argB: \"domain.com\") }">>,
  525: 
  526:     FDoc = <<"{ domainInputProtectedField(argA: \"do.com\", argB: { domain: \"do.com\" }) }">>,
  527:     ?assertPermissionsSuccess(Config2, Domain, Doc),
  528:     ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>, <<"argB.domain">>], FDoc).
  529: 
  530: 
  531: check_field_list_arg_domain_permissions(Config) ->
  532:     [{Subdomain, Domain} | _] = ?config(domains, Config),
  533:     Domains = [#{<<"domain">> => Domain, <<"notDomain">> => <<"random text here">>},
  534:                #{<<"domain">> => Subdomain}],
  535:     Config2 = [{op, <<"Q1">>}, {args, #{<<"domains">> => Domains}} | Config],
  536:     Doc = <<"query Q1($domains: [DomainInput!]) "
  537:             "{ domainListInputProtectedField(domains: $domains) }">>,
  538: 
  539:     FDoc = <<"{ domainListInputProtectedField(domains: [{ domain: \"do.com\" }]) }">>,
  540:     ?assertPermissionsSuccess(Config2, Domain, Doc),
  541:     ?assertDomainPermissionsFailed(Config, Domain, [<<"domains.domain">>], FDoc).
  542: 
  543: check_field_null_arg_domain_permissions(Config) ->
  544:     [{_, Domain} | _] = ?config(domains, Config),
  545:     Doc = <<"{ domainProtectedField domainInputProtectedField }">>,
  546:     ?assertPermissionsSuccess(Config, Domain, Doc).
  547: 
  548: check_field_jid_arg_domain_permissions(Config) ->
  549:     Domain = <<"my-domain.com">>,
  550:     Config2 = [{op, <<"Q1">>},
  551:                {args, #{<<"jid">> => <<"bob@", Domain/binary>>}} | Config],
  552:     Doc = <<"query Q1($jid: JID) { domainJIDProtectedField(argA: $jid, argB: \"bob@bob\") }">>,
  553:     FDoc = <<"{ domainJIDProtectedField(argA: \"bob@do.com\", argB: \"bob@do.com\") }">>,
  554:     ?assertPermissionsSuccess(Config2, Domain, Doc),
  555:     ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>], FDoc).
  556: 
  557: check_field_subdomain_permissions(Config) ->
  558:     [{Subdomain, Domain}, {FSubdomain, _Domain2}] = ?config(domains, Config),
  559:     Config2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => Subdomain}} | Config],
  560:     FConfig2 = [{op, <<"Q1">>}, {args, #{<<"domain">> => FSubdomain}} | Config],
  561:     Doc = <<"query Q1($domain: String) "
  562:              "{ protectedField domainProtectedField(argA: $domain, argB: \"do.com\") }">>,
  563:     ?assertPermissionsSuccess(Config2, Domain, Doc),
  564:     ?assertDomainPermissionsFailed(FConfig2, Domain, [<<"argA">>], Doc).
  565: 
  566: check_field_global_permissions(Config) ->
  567:     Domain = <<"my-domain.com">>,
  568:     Doc = <<"{ protectedField onlyForGlobalAdmin }">>,
  569:     ?assertMatch(ok, check_permissions(Config, true, Doc)),
  570:     ?assertThrow({error, #{error_term := {no_permissions, _, #{type := global}}}},
  571:                  check_domain_permissions(Config, Domain, Doc)).
  572: 
  573: %% Error formatting
  574: 
  575: format_internal_crash(_Config) ->
  576:     {Code, Res} = mongoose_graphql_errors:format_error(internal_crash),
  577:     ?assertEqual(500, Code),
  578:     ?assertMatch(#{extensions := #{code := internal_server_error}}, Res).
  579: 
  580: format_parse_errors(_Config) ->
  581:     ParserError = make_error(parse, {parser_error, {0, graphql_parser, "parser_error_msg"}}),
  582:     ScannerError = make_error(parse, {scanner_error,
  583:                                       {0, graphql_scanner, {illegal, "illegal_characters"}}}),
  584:     ScannerError2 = make_error(parse, {scanner_error,
  585:                                       {0, graphql_scanner, {user, "user_scanner_err"}}}),
  586: 
  587:     {400, ResParser} = mongoose_graphql_errors:format_error(ParserError),
  588:     {400, ResScanner} = mongoose_graphql_errors:format_error(ScannerError),
  589:     {400, ResScanner2} = mongoose_graphql_errors:format_error(ScannerError2),
  590:     ?assertErrMsg(parser_error, <<"parser_error_msg">>, ResParser),
  591:     ?assertErrMsg(scanner_error, <<"illegal_characters">>, ResScanner),
  592:     ?assertErrMsg(scanner_error, <<"user_scanner_err">>, ResScanner2).
  593: 
  594: format_decode_errors(_Config) ->
  595:     {400, Msg1} = mongoose_graphql_errors:format_error(make_error(decode, no_query_supplied)),
  596:     {400, Msg2} = mongoose_graphql_errors:format_error(make_error(decode, invalid_json_body)),
  597:     {400, Msg3} = mongoose_graphql_errors:format_error(make_error(decode, variables_invalid_json)),
  598: 
  599:     ?assertErrMsg(no_query_supplied, <<"The query was not supplied">>, Msg1),
  600:     ?assertErrMsg(invalid_json_body, <<"invalid">>, Msg2),
  601:     ?assertErrMsg(variables_invalid_json, <<"invalid">>, Msg3).
  602: 
  603: format_authorize_error(_Config) ->
  604:     {401, Msg1} = mongoose_graphql_errors:format_error(make_error(authorize, wrong_credentials)),
  605:     {401, Msg2} = mongoose_graphql_errors:format_error(
  606:                     make_error([<<"ROOT">>], authorize, {no_permissions, <<"ROOT">>})),
  607:     {401, Msg3} = mongoose_graphql_errors:format_error(
  608:                     make_error(authorize, {request_error, {header, <<"authorization">>}, 'msg'})),
  609: 
  610:     ?assertErrMsg(wrong_credentials, <<"provided credentials are wrong">>, Msg1),
  611:     ?assertErrMsg(no_permissions, <<"without permissions">>, Msg2),
  612:     ?assertMatch(#{path := [<<"ROOT">>]}, Msg2),
  613:     ?assertErrMsg(request_error, <<"Malformed authorization header">>, Msg3).
  614: 
  615: format_validate_error(_Config) ->
  616:     % Ensure the module can format this phase
  617:     {400, Msg} = mongoose_graphql_errors:format_error(
  618:                    make_error(validate, {not_unique, <<"OpName">>})),
  619:     ?assertMatch(#{extensions := #{code := not_unique}}, Msg).
  620: 
  621: format_type_check_error(_Config) ->
  622:     % Ensure the module can format this phase
  623:     {400, Msg} = mongoose_graphql_errors:format_error(
  624:                    make_error(type_check, non_null)),
  625:     ?assertMatch(#{extensions := #{code := non_null}}, Msg).
  626: 
  627: format_execute_error(_Config) ->
  628:     % Ensure the module can format this phase
  629:     {400, Msg} = mongoose_graphql_errors:format_error(
  630:                    make_error(execute, {resolver_error, any_error})),
  631:     ?assertMatch(#{extensions := #{code := resolver_error}}, Msg).
  632: 
  633: format_uncategorized_error(_Config) ->
  634:     % Ensure the module can format this phase
  635:     {400, Msg} = mongoose_graphql_errors:format_error(
  636:                    make_error(uncategorized, any_error)),
  637:     ?assertMatch(#{extensions := #{code := any_error}}, Msg).
  638: 
  639: format_any_error(_Config) ->
  640:     {400, Msg1} = mongoose_graphql_errors:format_error(any_error),
  641:     {400, Msg2} = mongoose_graphql_errors:format_error(<<"any_error">>),
  642:     {400, Msg3} = mongoose_graphql_errors:format_error({1, any_error}),
  643:     {400, Msg4} = mongoose_graphql_errors:format_error(#{msg => any_error}),
  644:     ?assertErrMsg(uncategorized, <<"any_error">>, Msg1),
  645:     ?assertErrMsg(uncategorized, <<"any_error">>, Msg2),
  646:     ?assertErrMsg(uncategorized, <<"any_error">>, Msg3),
  647:     ?assertErrMsg(uncategorized, <<"any_error">>, Msg4).
  648: 
  649: %% Listeners
  650: 
  651: auth_user_can_access_protected_types(Config) ->
  652:     Ep = ?config(endpoint_addr, Config),
  653:     Body = #{query => "{ field }"},
  654:     {Status, Data} = execute(Ep, Body, {<<"alice@localhost">>, <<"makota">>}),
  655:     assert_access_granted(Status, Data).
  656: 
  657: no_creds_defined_admin_can_access_protected(_Config) ->
  658:     Port = 5559,
  659:     Ep = "http://localhost:" ++ integer_to_list(Port),
  660:     start_listener(no_creds_admin_listener, Port, #{schema_endpoint => <<"admin">>}),
  661:     Body = #{<<"query">> => <<"{ field }">>},
  662:     {Status, Data} = execute(Ep, Body, undefined),
  663:     assert_access_granted(Status, Data).
  664: 
  665: auth_admin_can_access_protected_types(Config) ->
  666:     Ep = ?config(endpoint_addr, Config),
  667:     Body = #{query => "{ field }"},
  668:     {Status, Data} = execute(Ep, Body, {<<"admin">>, <<"secret">>}),
  669:     assert_access_granted(Status, Data).
  670: 
  671: auth_domain_admin_can_access_protected_types(Config) ->
  672:     Ep = ?config(endpoint_addr, Config),
  673:     Body = #{query => "{ field }"},
  674:     {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"makota">>}),
  675:     assert_access_granted(Status, Data).
  676: 
  677: auth_domain_admin_wrong_password_error(Config) ->
  678:     Ep = ?config(endpoint_addr, Config),
  679:     Body = #{query => "{ field }"},
  680:     {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"mapsa">>}),
  681:     assert_no_permissions(wrong_credentials, Status, Data).
  682: 
  683: auth_domain_admin_nonexistent_domain_error(Config) ->
  684:     Ep = ?config(endpoint_addr, Config),
  685:     Body = #{query => "{ field }"},
  686:     {Status, Data} = execute(Ep, Body, {<<"admin@localhost2">>, <<"makota">>}),
  687:     assert_no_permissions(wrong_credentials, Status, Data).
  688: 
  689: auth_domain_admin_can_access_owned_domain(Config) ->
  690:     Ep = ?config(endpoint_addr, Config),
  691:     Body = #{query => "{ fieldDP(argA: \"localhost\") }"},
  692:     {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"makota">>}),
  693:     assert_access_granted(Status, Data).
  694: 
  695: auth_domain_admin_cannot_access_other_domain(Config) ->
  696:     Ep = ?config(endpoint_addr, Config),
  697:     Body = #{query => "{ field fieldDP(argA: \"domain.com\") }"},
  698:     {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"makota">>}),
  699:     assert_no_permissions(no_permissions, Status, Data).
  700: 
  701: auth_domain_admin_cannot_access_global(Config) ->
  702:     Ep = ?config(endpoint_addr, Config),
  703:     Body = #{query => "{ fieldGlobal(argA: \"localhost\") }"},
  704:     {Status, Data} = execute(Ep, Body, {<<"admin@localhost">>, <<"makota">>}),
  705:     assert_no_permissions(no_permissions, Status, Data).
  706: 
  707: malformed_auth_header_error(Config) ->
  708:     Ep = ?config(endpoint_addr, Config),
  709:     % The encoded credentials value is malformed and cannot be decoded.
  710:     Headers = [{<<"Authorization">>, <<"Basic YWRtaW46c2VjcmV">>}],
  711:     {Status, Data} = post_request(Ep, Headers, <<"">>),
  712:     assert_no_permissions(request_error, Status, Data).
  713: 
  714: auth_wrong_creds_error(Config) ->
  715:     Ep = ?config(endpoint_addr, Config),
  716:     Body = #{query => "{ field }"},
  717:     {Status, Data} = execute(Ep, Body, {<<"user">>, <<"wrong_password">>}),
  718:     assert_no_permissions(wrong_credentials, Status, Data).
  719: 
  720: invalid_json_body_error(Config) ->
  721:     Ep = ?config(endpoint_addr, Config),
  722:     Body = <<"">>,
  723:     {Status, Data} = execute(Ep, Body, undefined),
  724:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  725:     assert_code(invalid_json_body, Data).
  726: 
  727: no_query_supplied_error(Config) ->
  728:     Ep = ?config(endpoint_addr, Config),
  729:     Body = #{},
  730:     {Status, Data} = execute(Ep, Body, undefined),
  731:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  732:     assert_code(no_query_supplied, Data).
  733: 
  734: variables_invalid_json_error(Config) ->
  735:     Ep = ?config(endpoint_addr, Config),
  736:     Body = #{<<"query">> => <<"{ field }">>, <<"variables">> => <<"{1: 2}">>},
  737:     {Status, Data} = execute(Ep, Body, undefined),
  738:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  739:     assert_code(variables_invalid_json, Data).
  740: 
  741: listener_reply_with_parsing_error(Config) ->
  742:     Ep = ?config(endpoint_addr, Config),
  743:     Body = #{<<"query">> => <<"{ field ">>},
  744:     {Status, Data} = execute(Ep, Body, undefined),
  745:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  746:     assert_code(parser_error, Data),
  747: 
  748:     BodyScanner = #{<<"query">> => <<"mutation { id(value: \"asdfsad) } ">>},
  749:     {StatusScanner, DataScanner} = execute(Ep, BodyScanner, undefined),
  750:     ?assertEqual({<<"400">>,<<"Bad Request">>}, StatusScanner),
  751:     assert_code(scanner_error, DataScanner).
  752: 
  753: listener_reply_with_type_check_error(Config) ->
  754:     Ep = ?config(endpoint_addr, Config),
  755:     Body = #{<<"query">> => <<"mutation { id(value: 12) }">>},
  756:     {Status, Data} = execute(Ep, Body, undefined),
  757:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  758:     assert_code(input_coercion, Data).
  759: 
  760: listener_reply_with_validation_error(Config) ->
  761:     Ep = ?config(endpoint_addr, Config),
  762:     Body = #{<<"query">> => <<"query Q1 { field } query Q1 { field }">>,
  763:              <<"operationName">> => <<"Q1">>},
  764:     {Status, Data} = execute(Ep, Body, undefined),
  765:     ?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
  766:     assert_code(not_unique, Data).
  767: 
  768: listener_can_execute_query_with_variables(Config) ->
  769:     Ep = ?config(endpoint_addr, Config),
  770:     Body = #{query => "mutation M1($value: String!){ id(value: $value) } query Q1{ field }",
  771:              variables => #{value => <<"Hello">>},
  772:              operationName => <<"M1">>
  773:             },
  774:     {Status, Data} = execute(Ep, Body, undefined),
  775:     assert_access_granted(Status, Data),
  776:     ?assertMatch(#{<<"data">> := #{<<"id">> := <<"Hello">>}}, Data).
  777: 
  778: listener_unauth_cannot_access_protected_types(Config) ->
  779:     Ep = ?config(endpoint_addr, Config),
  780:     Body = #{query => "{ field }"},
  781:     {Status, Data} = execute(Ep, Body, undefined),
  782:     ?assertMatch(#{<<"errors">> := [#{<<"path">> := [<<"ROOT">>]}]}, Data),
  783:     assert_no_permissions(no_permissions, Status, Data).
  784: 
  785: listener_unauth_can_access_unprotected_types(Config) ->
  786:     Ep = ?config(endpoint_addr, Config),
  787:     Body = #{query => "mutation { field }"},
  788:     {Status, Data} = execute(Ep, Body, undefined),
  789:     assert_access_granted(Status, Data).
  790: 
  791: %% Helpers
  792: 
  793: assert_code(Code, Data) ->
  794:     BinCode = atom_to_binary(Code),
  795:     ?assertMatch(#{<<"errors">> := [#{<<"extensions">> := #{<<"code">> := BinCode}}]}, Data).
  796: 
  797: assert_no_permissions(ExpectedCode, Status, Data) ->
  798:     ?assertEqual({<<"401">>,<<"Unauthorized">>}, Status),
  799:     assert_code(ExpectedCode, Data).
  800: 
  801: assert_access_granted(Status, Data) ->
  802:     ?assertEqual({<<"200">>,<<"OK">>}, Status),
  803:     % access was granted, no error was returned
  804:     ?assertNotMatch(#{<<"errors">> := _}, Data).
  805: 
  806: assert_err_msg(Code, MsgContains, #{message := Msg} = ErrorMsg) ->
  807:     ?assertMatch(#{extensions := #{code := Code}}, ErrorMsg),
  808:     ?assertNotEqual(nomatch, binary:match(Msg, MsgContains)).
  809: 
  810: make_error(Phase, Term) ->
  811:     #{phase => Phase, error_term => Term}.
  812: 
  813: make_error(Path, Phase, Term) ->
  814:     #{path => Path, phase => Phase, error_term => Term}.
  815: 
  816: check_permissions(Config, Auth, Doc) ->
  817:     Ep = ?config(endpoint, Config),
  818:     Op = proplists:get_value(op, Config, undefined),
  819:     {ok, Ast} = graphql:parse(Doc),
  820:     {ok, #{ast := Ast2}} = graphql:type_check(Ep, Ast),
  821:     ok = graphql:validate(Ast2),
  822:     Ctx = #{operation_name => Op, authorized => Auth, params => #{}},
  823:     ok = mongoose_graphql_permissions:check_permissions(Ctx, Ast2).
  824: 
  825: check_domain_permissions(Config, Domain, Doc) ->
  826:     Ep = ?config(endpoint, Config),
  827:     Args = proplists:get_value(args, Config, #{}),
  828:     Op = proplists:get_value(op, Config, undefined),
  829:     {ok, Ast} = graphql:parse(Doc),
  830:     {ok, #{ast := Ast2, fun_env := FunEnv}} = graphql:type_check(Ep, Ast),
  831:     ok = graphql:validate(Ast2),
  832:     Coerced = graphql:type_check_params(Ep, FunEnv, Op, Args),
  833:     Admin = jid:make_bare(<<"admin">>, Domain),
  834:     Ctx = #{operation_name => Op, authorized => true, authorized_as => domain_admin,
  835:             admin => Admin, params => Coerced},
  836:     ok = mongoose_graphql_permissions:check_permissions(Ctx, Ast2).
  837: 
  838: request(Doc, Authorized) ->
  839:     request(undefined, Doc, Authorized).
  840: 
  841: request(Op, Doc, Authorized) ->
  842:     #{document => Doc,
  843:       operation_name => Op,
  844:       vars => #{},
  845:       authorized => Authorized,
  846:       ctx => #{}}.
  847: 
  848: example_split_schema_data(Config) ->
  849:     Pattern = filename:join([proplists:get_value(data_dir, Config),
  850:                              "split_schema", "*.gql"]),
  851:     Mapping =
  852:         #{objects =>
  853:               #{'Query' => mongoose_graphql_default_resolver,
  854:                 'Mutation' => mongoose_graphql_default_resolver,
  855:                 default => mongoose_graphql_default_resolver}},
  856:     {Mapping, Pattern}.
  857: 
  858: example_schema_protected_data(Config) ->
  859:     Pattern = filename:join([proplists:get_value(data_dir, Config), "protected_schema.gql"]),
  860:     Mapping =
  861:         #{objects =>
  862:               #{'UserQuery' => mongoose_graphql_default_resolver,
  863:                 'UserMutation' => mongoose_graphql_default_resolver,
  864:                 default => mongoose_graphql_default_resolver}},
  865:     {Mapping, Pattern}.
  866: 
  867: example_schema_data(Config) ->
  868:     Pattern = filename:join([proplists:get_value(data_dir, Config), "schema.gql"]),
  869:     Mapping =
  870:         #{objects =>
  871:               #{'UserQuery' => mongoose_graphql_default_resolver,
  872:                 'UserMutation' => mongoose_graphql_default_resolver,
  873:                 default => mongoose_graphql_default_resolver}},
  874:     {Mapping, Pattern}.
  875: 
  876: example_permissions_schema_data(Config) ->
  877:     Pattern = filename:join([proplists:get_value(data_dir, Config), "permissions_schema.gql"]),
  878:     Mapping =
  879:         #{objects =>
  880:               #{'UserQuery' => mongoose_graphql_default_resolver,
  881:                 'UserMutation' => mongoose_graphql_default_resolver,
  882:                 default => mongoose_graphql_default_resolver},
  883:           enums => #{default => mongoose_graphql_default_resolver},
  884:           scalars => #{default => mongoose_graphql_scalar},
  885:           interfaces => #{default => mongoose_graphql_default_resolver},
  886:           unions => #{default => mongoose_graphql_default_resolver}},
  887:     {Mapping, Pattern}.
  888: 
  889: example_listener_schema_data(Config) ->
  890:     Pattern = filename:join([proplists:get_value(data_dir, Config), "listener_schema.gql"]),
  891:     Mapping =
  892:         #{objects =>
  893:               #{'UserQuery' => mongoose_graphql_default_resolver,
  894:                 'UserMutation' => mongoose_graphql_default_resolver,
  895:                 default => mongoose_graphql_default_resolver},
  896:           enums => #{default => mongoose_graphql_default_resolver}},
  897:     {Mapping, Pattern}.
  898: 
  899: -spec init_ep_listener(integer(), atom(), listener_opts(), [{atom(), term()}]) ->
  900:     [{atom(), term()}].
  901: init_ep_listener(Port, EpName, ListenerOpts, Config) ->
  902:     Pid = spawn(fun() ->
  903:                     Name = list_to_atom("gql_listener_" ++ atom_to_list(EpName)),
  904:                     ok = start_listener(Name, Port, ListenerOpts),
  905:                     {Mapping, Pattern} = example_listener_schema_data(Config),
  906:                     {ok, _} = mongoose_graphql:create_endpoint(EpName, Mapping, [Pattern]),
  907:                     receive
  908:                         stop ->
  909:                             ok
  910:                     end
  911:                 end),
  912:     [{test_process, Pid}, {endpoint_addr, "http://localhost:" ++ integer_to_list(Port)} | Config].
  913: 
  914: -spec start_listener(atom(), integer(), listener_opts()) -> ok.
  915: start_listener(Ref, Port, Opts) ->
  916:     Dispatch = cowboy_router:compile([
  917:         {'_', [{"/graphql", mongoose_graphql_cowboy_handler, Opts}]}
  918:     ]),
  919:     {ok, _} = cowboy:start_clear(Ref,
  920:                                  [{port, Port}],
  921:                                  #{env => #{dispatch => Dispatch}}),
  922:     ok.
  923: 
  924: -spec execute(binary(), map(), undefined | {binary(), binary()}) -> {{binary(), binary()}, map()}.
  925: execute(EpAddr, Body, undefined) ->
  926:     post_request(EpAddr, [], Body);
  927: execute(EpAddr, Body, {Username, Password}) ->
  928:     Creds = base64:encode(<<Username/binary, ":", Password/binary>>),
  929:     Headers = [{<<"Authorization">>, <<"Basic ", Creds/binary>>}],
  930:     post_request(EpAddr, Headers, Body).
  931: 
  932: post_request(EpAddr, HeadersIn, Body) when is_binary(Body) ->
  933:     {ok, Client} = fusco:start(EpAddr, []),
  934:     Headers = [{<<"Content-Type">>, <<"application/json">>},
  935:                {<<"Request-Id">>, random_request_id()} | HeadersIn],
  936:     {ok, {ResStatus, _, ResBody, _, _}} = Res =
  937:         fusco:request(Client, <<"/graphql">>, <<"POST">>, Headers, Body, 5000),
  938:     fusco:disconnect(Client),
  939:     ct:log("~p", [Res]),
  940:     {ResStatus, jiffy:decode(ResBody, [return_maps])};
  941: post_request(Ep, HeadersIn, Body) ->
  942:     post_request(Ep, HeadersIn, jiffy:encode(Body)).
  943: 
  944: random_request_id() ->
  945:     base16:encode(crypto:strong_rand_bytes(8)).