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