Skip to content

Commit

Permalink
Merge pull request #3354 from esl/mim-graphql-poc
Browse files Browse the repository at this point in the history
GraphQL in MongooseIM proof of concept
  • Loading branch information
DenysGonchar committed Dec 10, 2021
2 parents e1e1e08 + a719db0 commit d35b68b
Show file tree
Hide file tree
Showing 45 changed files with 1,577 additions and 30 deletions.
1 change: 1 addition & 0 deletions big_tests/default.spec
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
{suites, "tests", disco_and_caps_SUITE}.
{suites, "tests", extdisco_SUITE}.
{suites, "tests", gdpr_SUITE}.
{suites, "tests", graphql_SUITE}.
{suites, "tests", inbox_SUITE}.
{suites, "tests", inbox_extensions_SUITE}.
{suites, "tests", jingle_SUITE}.
Expand Down
2 changes: 2 additions & 0 deletions big_tests/dynamic_domains.spec
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
remove_personal_data_pubsub],
"at the moment mod_pubsub doesn't support dynamic domains"}.

{suites, "tests", graphql_SUITE}.

{suites, "tests", inbox_SUITE}.

{suites, "tests", inbox_extensions_SUITE}.
Expand Down
234 changes: 234 additions & 0 deletions big_tests/tests/graphql_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
-module(graphql_SUITE).

-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").

-compile([export_all, nowarn_export_all]).
-import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]).
-import(graphql_helper, [load_test_schema/2]).

suite() ->
require_rpc_nodes([mim]) ++ escalus:suite().

all() ->
[{group, cowboy_handler},
{group, admin_handler},
{group, user_handler}].

groups() ->
[{cowboy_handler, [parallel], cowboy_handler()},
{user_handler, [parallel], user_handler()},
{admin_handler, [parallel], admin_handler()}].

cowboy_handler() ->
[can_connect_to_admin,
can_connect_to_user].

user_handler() ->
[auth_user_can_access_protected_types | common_tests()].
admin_handler() ->
[auth_admin_can_access_protected_types | common_tests()].

common_tests() ->
[wrong_creds_cannot_access_protected_types,
unauth_cannot_access_protected_types,
unauth_can_access_unprotected_types,
can_execute_query_with_variables,
invalid_json_body_error,
no_query_supplied_error,
variables_invalid_json_error,
can_load_graphiql].

init_per_suite(Config) ->
% reset endpoints and load test schema
ok = load_test_schema(admin, Config),
ok = load_test_schema(user, Config),
escalus:init_per_suite(Config).

end_per_suite(Config) ->
% reinit endpoints with original schemas
ok = rpc(mim(), mongoose_graphql, init, []),
escalus:end_per_suite(Config).

init_per_group(admin_handler, Config) ->
Endpoint = admin,
Opts = get_listener_opts(Endpoint),
case proplists:is_defined(username, Opts) of
true ->
[{schema_endpoint, Endpoint} | Config];
false ->
{skipped, <<"Admin credentials are not defined in config">>}
end;
init_per_group(user_handler, Config) ->
Config1 = escalus:create_users(Config, escalus:get_users([alice])),
[{schema_endpoint, user} | Config1];
init_per_group(_, Config) ->
Config.

end_per_group(user_handler, Config) ->
escalus:delete_users(Config, escalus:get_users([alice]));
end_per_group(_, _Config) ->
ok.

init_per_testcase(CaseName, Config) ->
escalus:init_per_testcase(CaseName, Config).

end_per_testcase(CaseName, Config) ->
escalus:end_per_testcase(CaseName, Config).

can_connect_to_admin(_Config) ->
?assertMatch({{<<"400">>,<<"Bad Request">>}, _}, execute(admin, #{}, undefined)).

can_connect_to_user(_Config) ->
?assertMatch({{<<"400">>,<<"Bad Request">>}, _}, execute(user, #{}, undefined)).

unauth_cannot_access_protected_types(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{query => "{ field }"},
{Status, Data} = execute(Ep, Body, undefined),
assert_no_permissions(Status, Data).

unauth_can_access_unprotected_types(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{query => "mutation { field }"},
{Status, Data} = execute(Ep, Body, undefined),
assert_access_granted(Status, Data).

wrong_creds_cannot_access_protected_types(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{query => "{ field }"},
{Status, Data} = execute(Ep, Body, {<<"user">>, <<"wrong_password">>}),
assert_no_permissions(Status, Data).

auth_user_can_access_protected_types(Config) ->
escalus:fresh_story(
Config, [{alice, 1}],
fun(Alice) ->
Password = user_password(alice),
AliceJID = escalus_client:short_jid(Alice),
Ep = ?config(schema_endpoint, Config),
Body = #{query => "{ field }"},
{Status, Data} = execute(Ep, Body, {AliceJID, Password}),
assert_access_granted(Status, Data)
end).

auth_admin_can_access_protected_types(Config) ->
Ep = ?config(schema_endpoint, Config),
Opts = get_listener_opts(Ep),
User = proplists:get_value(username, Opts),
Password = proplists:get_value(password, Opts),
Body = #{query => "{ field }"},
{Status, Data} = execute(Ep, Body, {User, Password}),
assert_access_granted(Status, Data).

can_execute_query_with_variables(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{query => "mutation M1($value: String!){ id(value: $value) } query Q1{ field }",
variables => #{value => <<"Hello">>},
operationName => <<"M1">>
},
{Status, Data} = execute(Ep, Body, undefined),
?assertEqual({<<"200">>,<<"OK">>}, Status),
% operation M1 was executed, because id is in path
% access was granted, an error was returned because valid resolver was not defined
?assertMatch(#{<<"data">> := #{<<"id">> := null},
<<"errors">> :=
[#{<<"extensions">> := #{<<"code">> := <<"resolver_crash">>},
<<"path">> := [<<"id">>]}]},
Data).

can_load_graphiql(Config) ->
Ep = ?config(schema_endpoint, Config),
{Status, Html} = get_graphiql_website(Ep),
?assertEqual({<<"200">>,<<"OK">>}, Status),
?assertNotEqual(nomatch, binary:match(Html, <<"Loading...">>)).

invalid_json_body_error(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = <<"">>,
{Status, Data} = execute(Ep, Body, undefined),
?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
?assertMatch(#{<<"errors">> := [#{<<"message">> := <<"invalid_json_body">>}]}, Data).

no_query_supplied_error(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{},
{Status, Data} = execute(Ep, Body, undefined),
?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
?assertMatch(#{<<"errors">> := [#{<<"message">> := <<"no_query_supplied">>}]}, Data).

variables_invalid_json_error(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{<<"query">> => <<"{ field }">>, <<"variables">> => <<"{1: 2}">>},
{Status, Data} = execute(Ep, Body, undefined),
?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
?assertMatch(#{<<"errors">> := [#{<<"message">> := <<"variables_invalid_json">>}]}, Data).

%% Helpers

assert_no_permissions(Status, Data) ->
?assertEqual({<<"400">>,<<"Bad Request">>}, Status),
?assertMatch(#{<<"errors">> := [#{<<"message">> := <<"no_permissions">>}]}, Data).

assert_access_granted(Status, Data) ->
?assertEqual({<<"200">>,<<"OK">>}, Status),
% access was granted, an error was returned because valid resolver was not defined
?assertMatch(#{<<"errors">> :=
[#{<<"extensions">> :=
#{<<"code">> := <<"resolver_crash">>}}]}, Data).

user_password(User) ->
[{User, Props}] = escalus:get_users([User]),
proplists:get_value(password, Props).

get_port(EpName) ->
{PortIpNet, ejabberd_cowboy, _Opts} = get_listener_config(EpName),
element(1, PortIpNet).

get_listener_opts(EpName) ->
{_, ejabberd_cowboy, Opts} = get_listener_config(EpName),
{value, {modules, Modules}} = lists:keysearch(modules, 1, Opts),
[Opts2] = lists:filtermap(
fun
({_, _Path, mongoose_graphql_cowboy_handler, Args}) ->
{true, Args};
(_) ->
false
end, Modules),
Opts2.

get_listener_config(EpName) ->
Listeners = rpc(mim(), mongoose_config, get_opt, [listen]),
[{_, ejabberd_cowboy, _} = Config] =
lists:filter(fun(Config) -> is_graphql_config(Config, EpName) end, Listeners),
Config.

is_graphql_config({_PortIpNet, ejabberd_cowboy, Opts}, EpName) ->
{value, {modules, Modules}} = lists:keysearch(modules, 1, Opts),
lists:any(fun({_, _Path, mongoose_graphql_cowboy_handler, Args}) ->
atom_to_binary(EpName) == proplists:get_value(schema_endpoint, Args);
(_) -> false
end, Modules);
is_graphql_config(_, _EpName) ->
false.

execute(EpName, Body, Creds) ->
Request =
#{port => get_port(EpName),
role => {graphql, atom_to_binary(EpName)},
method => <<"POST">>,
return_maps => true,
creds => Creds,
path => "/graphql",
body => Body},
rest_helper:make_request(Request).

get_graphiql_website(EpName) ->
Request =
#{port => get_port(EpName),
role => {graphql, atom_to_binary(EpName)},
method => <<"GET">>,
headers => [{<<"Accept">>, <<"text/html">>}],
return_maps => true,
path => "/graphql"},
rest_helper:make_request(Request).
21 changes: 21 additions & 0 deletions big_tests/tests/graphql_SUITE_data/schema.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
schema{
query: Query,
mutation: Mutation
}

directive @protected on FIELD_DEFINITION | OBJECT

"""
Contains all available queries.
"""
type Query @protected{
field: String
}

"""
Contains all available mutations.
"""
type Mutation{
field: String
id(value: String!): String
}
24 changes: 24 additions & 0 deletions big_tests/tests/graphql_helper.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-module(graphql_helper).

-include_lib("common_test/include/ct.hrl").

-import(distributed_helper, [mim/0, rpc/4]).

-export([load_test_schema/2]).

load_test_schema(Name, Config) ->
Path = filename:join([?config(mim_data_dir, Config), "schema.gql"]),
{ok, SchemaData} = file:read_file(Path),
Ep = rpc(mim(), mongoose_graphql, get_endpoint, [Name]),
ok = rpc(mim(), graphql_schema, reset, [Ep]),
ok = rpc(mim(), graphql, load_schema, [Ep, test_schema_mapping(), SchemaData]),
ok = rpc(mim(), graphql, validate_schema, [Ep]),
ok.

test_schema_mapping() ->
#{objects => #{
'Query' => mongoose_graphql_default,
'Mutation' => mongoose_graphql_default,
default => mongoose_graphql_default
}
}.
Loading

0 comments on commit d35b68b

Please sign in to comment.