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