Skip to content

Commit

Permalink
Merge pull request #3694 from esl/graphql/inbox-api
Browse files Browse the repository at this point in the history
GraphQL - Implement inbox API
  • Loading branch information
chrzaszcz committed Jun 30, 2022
2 parents fe70ffa + cb6ae8f commit bbdc781
Show file tree
Hide file tree
Showing 19 changed files with 409 additions and 14 deletions.
1 change: 1 addition & 0 deletions big_tests/default.spec
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
{suites, "tests", graphql_SUITE}.
{suites, "tests", graphql_account_SUITE}.
{suites, "tests", graphql_domain_SUITE}.
{suites, "tests", graphql_inbox_SUITE}.
{suites, "tests", graphql_last_SUITE}.
{suites, "tests", graphql_muc_SUITE}.
{suites, "tests", graphql_muc_light_SUITE}.
Expand Down
1 change: 1 addition & 0 deletions big_tests/dynamic_domains.spec
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
{suites, "tests", graphql_SUITE}.
{suites, "tests", graphql_account_SUITE}.
{suites, "tests", graphql_domain_SUITE}.
{suites, "tests", graphql_inbox_SUITE}.
{suites, "tests", graphql_last_SUITE}.
{suites, "tests", graphql_muc_SUITE}.
{suites, "tests", graphql_muc_light_SUITE}.
Expand Down
206 changes: 206 additions & 0 deletions big_tests/tests/graphql_inbox_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
-module(graphql_inbox_SUITE).

-compile([export_all, nowarn_export_all]).

-import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]).
-import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1, user_to_jid/1,
get_ok_value/2, get_err_msg/1, get_err_code/1]).

-include_lib("eunit/include/eunit.hrl").
-include("inbox.hrl").

-define(assertErrMsg(Res, ContainsPart), assert_err_msg(ContainsPart, Res)).
-define(assertErrCode(Res, Code), assert_err_code(Code, Res)).

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

all() ->
inbox_helper:skip_or_run_inbox_tests(tests()).

tests() ->
[{group, user_inbox},
{group, admin_inbox}].

groups() ->
[{user_inbox, [], user_inbox_handler()},
{admin_inbox, [], admin_inbox_handler()}].

user_inbox_handler() ->
[user_flush_own_bin].

admin_inbox_handler() ->
[admin_flush_user_bin,
admin_try_flush_nonexistent_user_bin,
admin_flush_domain_bin,
admin_try_flush_nonexistent_domain_bin,
admin_flush_global_bin,
admin_flush_global_bin_after_days,
admin_try_flush_nonexistent_host_type_bin].

init_per_suite(Config) ->
HostType = domain_helper:host_type(),
SecHostType = domain_helper:secondary_host_type(),
Config1 = dynamic_modules:save_modules([HostType, SecHostType], Config),
Modules = [{mod_inbox, inbox_helper:inbox_opts(async_pools)} | inbox_helper:muclight_modules()],
ok = dynamic_modules:ensure_modules(HostType, Modules),
ok = dynamic_modules:ensure_modules(SecHostType, Modules),
escalus:init_per_suite(Config1).

end_per_suite(Config) ->
dynamic_modules:restore_modules(Config),
escalus:end_per_suite(Config).

init_per_group(admin_inbox, Config) ->
graphql_helper:init_admin_handler(Config);
init_per_group(user_inbox, Config) ->
[{schema_endpoint, user} | Config].

end_per_group(_, _) ->
ok.

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

end_per_testcase(CaseName, Config) ->
%% Clean users after each test case to keep inbox empty
escalus_fresh:clean(),
escalus:end_per_testcase(CaseName, Config).

%% Admin test cases

admin_flush_user_bin(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
fun admin_flush_user_bin/4).

admin_flush_user_bin(Config, Alice, Bob, Kate) ->
RoomBinJID = create_room_and_make_users_leave(Alice, Bob, Kate),
Res = execute_auth(admin_flush_user_bin_body(Bob, null), Config),
NumOfRows = get_ok_value(p(flushUserBin), Res),
?assertEqual(1, NumOfRows),
inbox_helper:check_inbox(Bob, [], #{box => bin}),
check_aff_msg_in_inbox_bin(Kate, RoomBinJID).

admin_flush_domain_bin(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {kate, 1}],
fun admin_flush_domain_bin/4).

admin_flush_domain_bin(Config, Alice, AliceBis, Kate) ->
RoomBinJID = create_room_and_make_users_leave(Alice, AliceBis, Kate),
Domain = domain_helper:domain(),
Res = execute_auth(admin_flush_domain_bin_body(Domain, null), Config),
NumOfRows = get_ok_value(p(flushDomainBin), Res),
?assertEqual(1, NumOfRows),
inbox_helper:check_inbox(Kate, [], #{box => bin}),
check_aff_msg_in_inbox_bin(AliceBis, RoomBinJID).

admin_flush_global_bin(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {kate, 1}],
fun admin_flush_global_bin/4).

admin_flush_global_bin(Config, Alice, AliceBis, Kate) ->
SecHostType = domain_helper:host_type(),
create_room_and_make_users_leave(Alice, AliceBis, Kate),
Res = execute_auth(admin_flush_global_bin_body(SecHostType, null), Config),
NumOfRows = get_ok_value(p(flushGlobalBin), Res),
?assertEqual(2, NumOfRows),
inbox_helper:check_inbox(AliceBis, [], #{box => bin}),
inbox_helper:check_inbox(Kate, [], #{box => bin}).

admin_flush_global_bin_after_days(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}, {alice_bis, 1}, {kate, 1}],
fun admin_flush_global_bin_after_days/4).

admin_flush_global_bin_after_days(Config, Alice, AliceBis, Kate) ->
SecHostType = domain_helper:host_type(),
RoomBinJID = create_room_and_make_users_leave(Alice, AliceBis, Kate),
Res = execute_auth(admin_flush_global_bin_body(SecHostType, 1), Config),
NumOfRows = get_ok_value(p(flushGlobalBin), Res),
?assertEqual(0, NumOfRows),
check_aff_msg_in_inbox_bin(AliceBis, RoomBinJID),
check_aff_msg_in_inbox_bin(Kate, RoomBinJID).

admin_try_flush_nonexistent_user_bin(Config) ->
%% Check nonexistent domain error
Res = execute_auth(admin_flush_user_bin_body(<<"[email protected]">>, null), Config),
?assertErrMsg(Res, <<"not found">>),
?assertErrCode(Res, domain_not_found),
%% Check nonexistent user error
User = <<"nonexistent-user@", (domain_helper:domain())/binary>>,
Res2 = execute_auth(admin_flush_user_bin_body(User, null), Config),
?assertErrMsg(Res2, <<"does not exist">>),
?assertErrCode(Res2, user_does_not_exist).

admin_try_flush_nonexistent_domain_bin(Config) ->
Res = execute_auth(admin_flush_domain_bin_body(<<"unknown-domain">>, null), Config),
?assertErrMsg(Res, <<"not found">>),
?assertErrCode(Res, domain_not_found).

admin_try_flush_nonexistent_host_type_bin(Config) ->
Res = execute_auth(admin_flush_global_bin_body(<<"nonexistent host type">>, null), Config),
?assertErrMsg(Res, <<"not found">>),
?assertErrCode(Res, host_type_not_found).

%% User test cases

user_flush_own_bin(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
fun user_flush_own_bin/4).

user_flush_own_bin(Config, Alice, Bob, Kate) ->
create_room_and_make_users_leave(Alice, Bob, Kate),
Res = execute_user(user_flush_own_bin_body(null), Bob, Config),
NumOfRows = get_ok_value(p(flushBin), Res),
?assertEqual(1, NumOfRows),
inbox_helper:check_inbox(Bob, [], #{box => bin}).

%% Helpers

create_room_and_make_users_leave(Alice, Bob, Kate) ->
RoomName = inbox_SUITE:create_room_and_make_users_leave(Alice, Bob, Kate),
muc_light_helper:room_bin_jid(RoomName).

check_aff_msg_in_inbox_bin(User, RoomBinJID) ->
UserShort = escalus_client:short_jid(User),
Convs = [#conv{unread = 1, from = RoomBinJID, to = UserShort, content = <<>>}],
inbox_helper:check_inbox(User, Convs, #{box => bin}).

assert_err_msg(Contains, Res) ->
?assertNotEqual(nomatch, binary:match(get_err_msg(Res), Contains)).

assert_err_code(Code, Res) ->
?assertEqual(atom_to_binary(Code), get_err_code(Res)).

p(Cmd) when is_atom(Cmd) ->
[data, inbox, Cmd];
p(Path) when is_list(Path) ->
[data, inbox] ++ Path.

%% Request bodies

admin_flush_user_bin_body(User, Days) ->
Query = <<"mutation M1($user: JID!, $days: PosInt)
{ inbox { flushUserBin(user: $user, days: $days) } }">>,
OpName = <<"M1">>,
Vars = #{user => user_to_bin(User), days => Days},
#{query => Query, operationName => OpName, variables => Vars}.

admin_flush_domain_bin_body(Domain, Days) ->
Query = <<"mutation M1($domain: String!, $days: PosInt)
{ inbox { flushDomainBin(domain: $domain, days: $days) } }">>,
OpName = <<"M1">>,
Vars = #{domain => Domain, days => Days},
#{query => Query, operationName => OpName, variables => Vars}.

admin_flush_global_bin_body(HostType, Days) ->
Query = <<"mutation M1($hostType: String!, $days: PosInt)
{ inbox { flushGlobalBin(hostType: $hostType, days: $days) } }">>,
OpName = <<"M1">>,
Vars = #{hostType => HostType, days => Days},
#{query => Query, operationName => OpName, variables => Vars}.

user_flush_own_bin_body(Days) ->
Query = <<"mutation M1($days: PosInt) { inbox { flushBin(days: $days) } }">>,
OpName = <<"M1">>,
Vars = #{days => Days},
#{query => Query, operationName => OpName, variables => Vars}.
2 changes: 2 additions & 0 deletions priv/graphql/schemas/admin/admin_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type AdminMutation @protected{
account: AccountAdminMutation
"Domain management"
domains: DomainAdminMutation
"Inbox bin management"
inbox: InboxAdminMutation
"Last activity management"
last: LastAdminMutation
"MUC room management"
Expand Down
28 changes: 28 additions & 0 deletions priv/graphql/schemas/admin/inbox.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Allow admin to flush the inbox bin".
"""
type InboxAdminMutation @protected{
"Flush the user's bin and return the number of deleted rows"
flushUserBin(
"User to clear a bin"
user: JID!,
"Remove older than given days or all if null"
days: PosInt
): Int @protected(type: DOMAIN, args: ["user"])

"Flush the whole domain bin and return the number of deleted rows"
flushDomainBin(
"Domain to be cleared"
domain: String!,
"Remove older than given days or all if null"
days: PosInt
): Int @protected(type: Domain, args: ["domain"])

"Flush the global bin and return the number of deleted rows"
flushGlobalBin(
"Required to identify the DB backend"
hostType: String!,
"Remove older than given days or all if null"
days: PosInt
): Int @protected(type: GLOBAL)
}
1 change: 1 addition & 0 deletions priv/graphql/schemas/global/scalar_types.gql
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ scalar JID
"The JID with resource e.g. alice@localhost/res1"
scalar FullJID
scalar NonEmptyString
scalar PosInt
10 changes: 10 additions & 0 deletions priv/graphql/schemas/user/inbox.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Allow user to flush own inbox bin".
"""
type InboxUserMutation @protected{
"Flush the user's bin and return the number of deleted rows"
flushBin(
"Remove older than given days or all if null"
days: PosInt
): Int
}
2 changes: 2 additions & 0 deletions priv/graphql/schemas/user/user_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Only an authenticated user can execute these mutations.
type UserMutation @protected{
"Account management"
account: AccountUserMutation
"Inbox bin management"
inbox: InboxUserMutation
"Last activity management"
last: LastUserMutation
"MUC room management"
Expand Down
8 changes: 4 additions & 4 deletions src/graphql/admin/mongoose_graphql_admin_mutation.erl
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@

-ignore_xref([execute/4]).

-include("../mongoose_graphql_types.hrl").

execute(_Ctx, _Obj, <<"account">>, _Args) ->
{ok, account};
execute(_Ctx, _Obj, <<"domains">>, _Args) ->
{ok, admin};
execute(_Ctx, _Obj, <<"httpUpload">>, _Args) ->
{ok, httpUpload};
execute(_Ctx, _Obj, <<"inbox">>, _Args) ->
{ok, inbox};
execute(_Ctx, _Obj, <<"last">>, _Args) ->
{ok, last};
execute(_Ctx, _Obj, <<"muc">>, _Args) ->
{ok, muc};
execute(_Ctx, _Obj, <<"httpUpload">>, _Args) ->
{ok, httpUpload};
execute(_Ctx, _Obj, <<"muc_light">>, _Args) ->
{ok, muc_light};
execute(_Ctx, _Obj, <<"offline">>, _Args) ->
Expand Down
19 changes: 19 additions & 0 deletions src/graphql/admin/mongoose_graphql_inbox_admin_mutation.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-module(mongoose_graphql_inbox_admin_mutation).

-behaviour(mongoose_graphql).

-export([execute/4]).

-import(mongoose_graphql_helper, [format_result/2, null_to_default/2]).

-ignore_xref([execute/4]).

execute(_Ctx, inbox, <<"flushUserBin">>, #{<<"user">> := UserJID, <<"days">> := Days}) ->
Res = mod_inbox_api:flush_user_bin(UserJID, null_to_default(Days, 0)),
format_result(Res, #{user => jid:to_binary(UserJID)});
execute(_Ctx, inbox, <<"flushDomainBin">>, #{<<"domain">> := Domain, <<"days">> := Days}) ->
Res = mod_inbox_api:flush_domain_bin(Domain, null_to_default(Days, 0)),
format_result(Res, #{domain => Domain});
execute(_Ctx, inbox, <<"flushGlobalBin">>, #{<<"hostType">> := HostType, <<"days">> := Days}) ->
Res = mod_inbox_api:flush_global_bin(HostType, null_to_default(Days, 0)),
format_result(Res, #{host_type => HostType}).
2 changes: 2 additions & 0 deletions src/graphql/mongoose_graphql.erl
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ admin_mapping_rules() ->
'DomainAdminQuery' => mongoose_graphql_domain_admin_query,
'AdminMutation' => mongoose_graphql_admin_mutation,
'DomainAdminMutation' => mongoose_graphql_domain_admin_mutation,
'InboxAdminMutation' => mongoose_graphql_inbox_admin_mutation,
'SessionAdminMutation' => mongoose_graphql_session_admin_mutation,
'SessionAdminQuery' => mongoose_graphql_session_admin_query,
'StanzaAdminMutation' => mongoose_graphql_stanza_admin_mutation,
Expand Down Expand Up @@ -166,6 +167,7 @@ user_mapping_rules() ->
'UserMutation' => mongoose_graphql_user_mutation,
'AccountUserQuery' => mongoose_graphql_account_user_query,
'AccountUserMutation' => mongoose_graphql_account_user_mutation,
'InboxUserMutation' => mongoose_graphql_inbox_user_mutation,
'MUCUserMutation' => mongoose_graphql_muc_user_mutation,
'MUCUserQuery' => mongoose_graphql_muc_user_query,
'MUCLightUserMutation' => mongoose_graphql_muc_light_user_mutation,
Expand Down
7 changes: 7 additions & 0 deletions src/graphql/mongoose_graphql_scalar.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ input(<<"Stanza">>, Value) -> exml:parse(Value);
input(<<"JID">>, Jid) -> jid_from_binary(Jid);
input(<<"FullJID">>, Jid) -> full_jid_from_binary(Jid);
input(<<"NonEmptyString">>, Value) -> non_empty_string_to_binary(Value);
input(<<"PosInt">>, Value) -> validate_pos_integer(Value);
input(Ty, V) ->
error_logger:info_report({coercing_generic_scalar, Ty, V}),
{ok, V}.
Expand All @@ -29,6 +30,7 @@ output(<<"DateTime">>, DT) -> {ok, microseconds_to_binary(DT)};
output(<<"Stanza">>, Elem) -> {ok, exml:to_binary(Elem)};
output(<<"JID">>, Jid) -> {ok, jid:to_binary(Jid)};
output(<<"NonEmptyString">>, Value) -> binary_to_non_empty_string(Value);
output(<<"PosInt">>, Value) -> validate_pos_integer(Value);
output(Ty, V) ->
error_logger:info_report({output_generic_scalar, Ty, V}),
{ok, V}.
Expand Down Expand Up @@ -67,6 +69,11 @@ binary_to_non_empty_string(<<>>) ->
binary_to_non_empty_string(Val) ->
{ok, Val}.

validate_pos_integer(PosInt) when is_integer(PosInt), PosInt > 0 ->
{ok, PosInt};
validate_pos_integer(_Value) ->
{error, "Value is not a positive integer"}.

microseconds_to_binary(Microseconds) ->
Opts = [{offset, "Z"}, {unit, microsecond}],
list_to_binary(calendar:system_time_to_rfc3339(Microseconds, Opts)).
13 changes: 13 additions & 0 deletions src/graphql/user/mongoose_graphql_inbox_user_mutation.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-module(mongoose_graphql_inbox_user_mutation).

-behaviour(mongoose_graphql).

-export([execute/4]).

-import(mongoose_graphql_helper, [format_result/2, null_to_default/2]).

-ignore_xref([execute/4]).

execute(#{user := UserJID}, inbox, <<"flushBin">>, #{<<"days">> := Days}) ->
Res = mod_inbox_api:flush_user_bin(UserJID, null_to_default(Days, 0)),
format_result(Res, #{user => jid:to_binary(UserJID)}).
Loading

0 comments on commit bbdc781

Please sign in to comment.