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