mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-24 16:23:40 +01:00
Commands refactor, first pass.
- add API versionning - changed error handling, based on exception - commands moved/merged from mod_admin_p1 to mod_admin_extra - command bufixes - add some elixir unit test cases Squashed commit of the following: commit dd59855b3486f78a9349756e4f102e79b3accff8 Merge: 14e8ffc 506e08e Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Fri Oct 30 11:43:18 2015 +0100 Merge branch '3.2.x' into api commit 14e8ffce78cbea6c8605371d1fc50a0c1d1e012c Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Tue Oct 27 16:35:17 2015 +0100 Added OAuth tests to ejabberd_commands commit f81c550c14628edfe4861c228576cb767924366a Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Tue Oct 27 16:34:55 2015 +0100 Added some mod_http_api tests commit 6a64578d5b2ba532a2feb6503ed98561e56d5d53 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Mon Oct 26 15:29:36 2015 +0100 Fix get_last command test Previous version won't work with dst. commit 27e0cde9e9c1f001effe68f8424a365ad947c068 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Fri Oct 23 17:59:34 2015 +0200 Add tests on admin command policy commit 19dad8d54f54c9fabd454280483cccfb06c8e78a Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Fri Oct 23 16:49:36 2015 +0200 Added command related tests (http api & user policy) commit e0e596ab4a3f3a70aba5f374f028939ab794de33 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Fri Oct 23 16:49:16 2015 +0200 Fix command call. commit 128cd7d1ede3c47a34f8ec3a750c980ccad2c61d Merge: 60c4c4c 447313c Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Thu Oct 22 14:48:39 2015 +0200 Merge branch '3.2.x' into api commit 60c4c4c0751302524c14219c6bc8c56a6069a689 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Thu Oct 22 14:45:57 2015 +0200 Fix ejabberd_commands spec. commit 8e145c28c5da762c2b93ee32327eff1db94ebfed Merge: 397273a f13dc94 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Wed Oct 21 18:26:07 2015 +0200 Merge branch '3.2.x' into api commit 397273a23ed415feac87aed33da6452229793387 Merge: c30e89b f289e27 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Wed Oct 21 15:27:45 2015 +0200 Merge branch '3.2.x' into api commit c30e89bb8a0013bff37e61e4c6953350c9c1f313 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Wed Oct 21 12:47:02 2015 +0200 Merge mod_http_api commit 7b0db22b4acd48ff6fabce41c1b2525e6580a3c5 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Fri Oct 16 11:55:48 2015 +0200 Fix exunit tests to run with common_test suites commit d8b1a89800ac7379a57a7eb4a09c3c93c3e1e5eb Merge: 2879ae8 63455b3 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Thu Oct 15 11:39:45 2015 +0200 Merge branch '3.2.x' into api commit 2879ae87ff3eee369ef3d780136b96ecff5285d1 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Wed Oct 14 14:53:44 2015 +0200 Fix update_roster command. commit a1d453dd7a3afda9861a8d747494a45057ad574b Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Tue Oct 13 16:14:28 2015 +0200 API commands refactor Moving and/or merging commands from mod_admin_p1 to mod_admin_extra commit b709ed26b0fc0ca4f3bdd5a59fa58ec7e3db97fa Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Wed Oct 7 15:10:01 2015 +0200 Add tests on commands commit 6711687bee9c672cb3d5aed0744e13420ecf6dbd Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Tue Sep 29 15:58:16 2015 +0200 Add ejabberd_commands tests commit df8682f419cf3877e77e36a19bca0fc55dc991f8 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Mon Sep 28 14:54:39 2015 +0200 Added API versioning for ejabberdctl and rest commands commit cd017b0e3aac431bc3ee807ceb7f8641e1523ef5 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Fri Sep 18 11:21:45 2015 +0200 Better error handling of HTTP API commands. commit ca5cb6acd8e4643f9d6c484d2277b0d7e88471e5 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Tue Sep 15 15:03:05 2015 +0200 add commands to mod_admin_extra: - get_offline_count - get_presence - change_password commit 7f583fa099e30ac2b0915669fd8f102ac565b833 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Tue Sep 15 15:02:16 2015 +0200 Improve REST API error handling commit 14753b1c02cdce434a786b7f80f6c09f0d210075 Author: Jerome Sautret <jerome.sautret@process-one.net> Date: Mon Sep 14 10:51:17 2015 +0200 Change REST API return codes for integer type.
This commit is contained in:
parent
c00cfca8e7
commit
3dc55c6d47
@ -336,6 +336,9 @@ test:
|
|||||||
quicktest:
|
quicktest:
|
||||||
$(REBAR) skip_deps=true ct suites=elixir
|
$(REBAR) skip_deps=true ct suites=elixir
|
||||||
|
|
||||||
|
eunit:
|
||||||
|
$(REBAR) skip_deps=true exunit
|
||||||
|
|
||||||
.PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \
|
.PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \
|
||||||
install uninstall uninstall-binary uninstall-all translations deps test spec \
|
install uninstall uninstall-binary uninstall-all translations deps test spec \
|
||||||
quicktest erlang_plt deps_plt ejabberd_plt
|
quicktest erlang_plt deps_plt ejabberd_plt
|
||||||
|
@ -31,8 +31,10 @@
|
|||||||
tags = [] :: [atom()] | '_' | '$2',
|
tags = [] :: [atom()] | '_' | '$2',
|
||||||
desc = "" :: string() | '_' | '$3',
|
desc = "" :: string() | '_' | '$3',
|
||||||
longdesc = "" :: string() | '_',
|
longdesc = "" :: string() | '_',
|
||||||
module :: atom(),
|
version = 0 :: integer(),
|
||||||
function :: atom(),
|
jabs = 1 :: integer(),
|
||||||
|
module :: atom() | '_',
|
||||||
|
function :: atom() | '_',
|
||||||
args = [] :: [aterm()] | '_' | '$1' | '$2',
|
args = [] :: [aterm()] | '_' | '$1' | '$2',
|
||||||
policy = restricted :: open | restricted | admin | user,
|
policy = restricted :: open | restricted | admin | user,
|
||||||
result = {res, rescode} :: rterm() | '_' | '$2',
|
result = {res, rescode} :: rterm() | '_' | '$2',
|
||||||
|
@ -43,6 +43,8 @@
|
|||||||
{tag, "1.0.0"}}}},
|
{tag, "1.0.0"}}}},
|
||||||
{if_var_true, tools, {meck, "0.8.2", {git, "https://github.com/eproxus/meck",
|
{if_var_true, tools, {meck, "0.8.2", {git, "https://github.com/eproxus/meck",
|
||||||
{tag, "0.8.2"}}}},
|
{tag, "0.8.2"}}}},
|
||||||
|
{if_var_true, tools, {moka, ".*", {git, "git://github.com/processone/moka.git",
|
||||||
|
{tag, "1.0.5"}}}},
|
||||||
{if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis",
|
{if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis",
|
||||||
{tag, "v1.0.8"}}}}]}.
|
{tag, "v1.0.8"}}}}]}.
|
||||||
|
|
||||||
|
@ -90,7 +90,8 @@
|
|||||||
%%% PowFloat = math:pow(Base, Exponent),
|
%%% PowFloat = math:pow(Base, Exponent),
|
||||||
%%% round(PowFloat).</pre>
|
%%% round(PowFloat).</pre>
|
||||||
%%%
|
%%%
|
||||||
%%% Since this function will be called by ejabberd_commands, it must be exported.
|
%%% Since this function will be called by ejabberd_commands, it must
|
||||||
|
%%% be exported.
|
||||||
%%% Add to your module:
|
%%% Add to your module:
|
||||||
%%% <pre>-export([calc_power/2]).</pre>
|
%%% <pre>-export([calc_power/2]).</pre>
|
||||||
%%%
|
%%%
|
||||||
@ -201,24 +202,33 @@
|
|||||||
%%% TODO: consider this feature:
|
%%% TODO: consider this feature:
|
||||||
%%% All commands are catched. If an error happens, return the restuple:
|
%%% All commands are catched. If an error happens, return the restuple:
|
||||||
%%% {error, flattened error string}
|
%%% {error, flattened error string}
|
||||||
%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this.
|
%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc)
|
||||||
%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response.
|
%%% need to allows this. And ejabberd_xmlrpc must be prepared to
|
||||||
|
%%% handle such an unexpected response.
|
||||||
|
|
||||||
|
|
||||||
-module(ejabberd_commands).
|
-module(ejabberd_commands).
|
||||||
-author('badlop@process-one.net').
|
-author('badlop@process-one.net').
|
||||||
|
|
||||||
|
-define(DEFAULT_VERSION, 1000000).
|
||||||
|
|
||||||
-export([init/0,
|
-export([init/0,
|
||||||
list_commands/0,
|
list_commands/0,
|
||||||
|
list_commands/1,
|
||||||
get_command_format/1,
|
get_command_format/1,
|
||||||
get_command_format/2,
|
get_command_format/2,
|
||||||
|
get_command_format/3,
|
||||||
get_command_definition/1,
|
get_command_definition/1,
|
||||||
|
get_command_definition/2,
|
||||||
get_tags_commands/0,
|
get_tags_commands/0,
|
||||||
|
get_tags_commands/1,
|
||||||
get_commands/0,
|
get_commands/0,
|
||||||
register_commands/1,
|
register_commands/1,
|
||||||
unregister_commands/1,
|
unregister_commands/1,
|
||||||
execute_command/2,
|
execute_command/2,
|
||||||
|
execute_command/3,
|
||||||
execute_command/4,
|
execute_command/4,
|
||||||
|
execute_command/5,
|
||||||
opt_type/1,
|
opt_type/1,
|
||||||
get_commands_spec/0
|
get_commands_spec/0
|
||||||
]).
|
]).
|
||||||
@ -226,6 +236,7 @@
|
|||||||
-include("ejabberd_commands.hrl").
|
-include("ejabberd_commands.hrl").
|
||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
-include("logger.hrl").
|
-include("logger.hrl").
|
||||||
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
|
|
||||||
-define(POLICY_ACCESS, '$policy').
|
-define(POLICY_ACCESS, '$policy').
|
||||||
|
|
||||||
@ -260,23 +271,26 @@ get_commands_spec() ->
|
|||||||
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
|
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
|
||||||
result_example = ok}].
|
result_example = ok}].
|
||||||
init() ->
|
init() ->
|
||||||
ets:new(ejabberd_commands, [named_table, set, public,
|
mnesia:delete_table(ejabberd_commands),
|
||||||
{keypos, #ejabberd_commands.name}]),
|
mnesia:create_table(ejabberd_commands,
|
||||||
|
[{ram_copies, [node()]},
|
||||||
|
{local_content, true},
|
||||||
|
{attributes, record_info(fields, ejabberd_commands)},
|
||||||
|
{type, bag}]),
|
||||||
|
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
|
||||||
register_commands(get_commands_spec()).
|
register_commands(get_commands_spec()).
|
||||||
|
|
||||||
-spec register_commands([ejabberd_commands()]) -> ok.
|
-spec register_commands([ejabberd_commands()]) -> ok.
|
||||||
|
|
||||||
%% @doc Register ejabberd commands.
|
%% @doc Register ejabberd commands.
|
||||||
%% If a command is already registered, a warning is printed and the old command is preserved.
|
%% If a command is already registered, a warning is printed and the
|
||||||
|
%% old command is preserved.
|
||||||
register_commands(Commands) ->
|
register_commands(Commands) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Command) ->
|
fun(Command) ->
|
||||||
case ets:insert_new(ejabberd_commands, Command) of
|
% XXX check if command exists
|
||||||
true ->
|
mnesia:dirty_write(Command)
|
||||||
ok;
|
% ?DEBUG("This command is already defined:~n~p", [Command])
|
||||||
false ->
|
|
||||||
?DEBUG("This command is already defined:~n~p", [Command])
|
|
||||||
end
|
|
||||||
end,
|
end,
|
||||||
Commands).
|
Commands).
|
||||||
|
|
||||||
@ -286,7 +300,7 @@ register_commands(Commands) ->
|
|||||||
unregister_commands(Commands) ->
|
unregister_commands(Commands) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Command) ->
|
fun(Command) ->
|
||||||
ets:delete_object(ejabberd_commands, Command)
|
mnesia:dirty_delete_object(Command)
|
||||||
end,
|
end,
|
||||||
Commands).
|
Commands).
|
||||||
|
|
||||||
@ -294,94 +308,183 @@ unregister_commands(Commands) ->
|
|||||||
|
|
||||||
%% @doc Get a list of all the available commands, arguments and description.
|
%% @doc Get a list of all the available commands, arguments and description.
|
||||||
list_commands() ->
|
list_commands() ->
|
||||||
Commands = ets:match(ejabberd_commands,
|
list_commands(?DEFAULT_VERSION).
|
||||||
#ejabberd_commands{name = '$1',
|
|
||||||
args = '$2',
|
|
||||||
desc = '$3',
|
|
||||||
_ = '_'}),
|
|
||||||
[{A, B, C} || [A, B, C] <- Commands].
|
|
||||||
|
|
||||||
-spec list_commands_policy() -> [{atom(), [aterm()], string(), atom()}].
|
-spec list_commands(integer()) -> [{atom(), [aterm()], string()}].
|
||||||
|
|
||||||
%% @doc Get a list of all the available commands, arguments, description, and
|
%% @doc Get a list of all the available commands, arguments and
|
||||||
%% policy.
|
%% description in a given API verion.
|
||||||
list_commands_policy() ->
|
list_commands(Version) ->
|
||||||
Commands = ets:match(ejabberd_commands,
|
Commands = get_commands_definition(Version),
|
||||||
#ejabberd_commands{name = '$1',
|
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
|
||||||
args = '$2',
|
args = Args,
|
||||||
desc = '$3',
|
desc = Desc} <- Commands].
|
||||||
policy = '$4',
|
|
||||||
_ = '_'}),
|
|
||||||
[{A, B, C, D} || [A, B, C, D] <- Commands].
|
|
||||||
|
|
||||||
-spec get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}.
|
|
||||||
|
-spec list_commands_policy(integer()) ->
|
||||||
|
[{atom(), [aterm()], string(), atom()}].
|
||||||
|
|
||||||
|
%% @doc Get a list of all the available commands, arguments,
|
||||||
|
%% description, and policy in a given API version.
|
||||||
|
list_commands_policy(Version) ->
|
||||||
|
Commands = get_commands_definition(Version),
|
||||||
|
[{Name, Args, Desc, Policy} ||
|
||||||
|
#ejabberd_commands{name = Name,
|
||||||
|
args = Args,
|
||||||
|
desc = Desc,
|
||||||
|
policy = Policy} <- Commands].
|
||||||
|
|
||||||
|
-spec get_command_format(atom()) -> {[aterm()], rterm()}.
|
||||||
|
|
||||||
%% @doc Get the format of arguments and result of a command.
|
%% @doc Get the format of arguments and result of a command.
|
||||||
get_command_format(Name) ->
|
get_command_format(Name) ->
|
||||||
get_command_format(Name, noauth).
|
get_command_format(Name, noauth, ?DEFAULT_VERSION).
|
||||||
|
get_command_format(Name, Version) when is_integer(Version) ->
|
||||||
|
get_command_format(Name, noauth, Version);
|
||||||
get_command_format(Name, Auth) ->
|
get_command_format(Name, Auth) ->
|
||||||
|
get_command_format(Name, Auth, ?DEFAULT_VERSION).
|
||||||
|
|
||||||
|
-spec get_command_format(atom(),
|
||||||
|
{binary(), binary(), binary(), boolean()} |
|
||||||
|
noauth | admin,
|
||||||
|
integer()) ->
|
||||||
|
{[aterm()], rterm()}.
|
||||||
|
|
||||||
|
get_command_format(Name, Auth, Version) ->
|
||||||
Admin = is_admin(Name, Auth),
|
Admin = is_admin(Name, Auth),
|
||||||
Matched = ets:match(ejabberd_commands,
|
#ejabberd_commands{args = Args,
|
||||||
#ejabberd_commands{name = Name,
|
result = Result,
|
||||||
args = '$1',
|
policy = Policy} =
|
||||||
result = '$2',
|
get_command_definition(Name, Version),
|
||||||
policy = '$3',
|
case Policy of
|
||||||
_ = '_'}),
|
user when Admin;
|
||||||
case Matched of
|
|
||||||
[] ->
|
|
||||||
{error, command_unknown};
|
|
||||||
[[Args, Result, user]] when Admin;
|
|
||||||
Auth == noauth ->
|
Auth == noauth ->
|
||||||
{[{user, binary}, {server, binary} | Args], Result};
|
{[{user, binary}, {server, binary} | Args], Result};
|
||||||
[[Args, Result, _]] ->
|
_ ->
|
||||||
{Args, Result}
|
{Args, Result}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get_command_definition(atom()) -> ejabberd_commands() | command_not_found.
|
-spec get_command_definition(atom()) -> ejabberd_commands().
|
||||||
|
|
||||||
%% @doc Get the definition record of a command.
|
%% @doc Get the definition record of a command.
|
||||||
get_command_definition(Name) ->
|
get_command_definition(Name) ->
|
||||||
case ets:lookup(ejabberd_commands, Name) of
|
get_command_definition(Name, ?DEFAULT_VERSION).
|
||||||
[E] -> E;
|
|
||||||
[] -> command_not_found
|
-spec get_command_definition(atom(), integer()) -> ejabberd_commands().
|
||||||
|
|
||||||
|
%% @doc Get the definition record of a command in a given API version.
|
||||||
|
get_command_definition(Name, Version) ->
|
||||||
|
case lists:reverse(
|
||||||
|
lists:sort(
|
||||||
|
mnesia:dirty_select(
|
||||||
|
ejabberd_commands,
|
||||||
|
ets:fun2ms(
|
||||||
|
fun(#ejabberd_commands{name = N, version = V} = C)
|
||||||
|
when N == Name, V =< Version ->
|
||||||
|
{V, C}
|
||||||
|
end)))) of
|
||||||
|
[{_, Command} | _ ] -> Command;
|
||||||
|
_E -> throw(unknown_command)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown}
|
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
|
||||||
%% @doc Execute a command.
|
|
||||||
execute_command(Name, Arguments) ->
|
|
||||||
execute_command([], noauth, Name, Arguments).
|
|
||||||
|
|
||||||
-spec execute_command([{atom(), [atom()], [any()]}] | undefined,
|
% @doc Returns all commands for a given API version
|
||||||
|
get_commands_definition(Version) ->
|
||||||
|
L = lists:reverse(
|
||||||
|
lists:sort(
|
||||||
|
mnesia:dirty_select(
|
||||||
|
ejabberd_commands,
|
||||||
|
ets:fun2ms(
|
||||||
|
fun(#ejabberd_commands{name = Name, version = V} = C)
|
||||||
|
when V =< Version ->
|
||||||
|
{Name, V, C}
|
||||||
|
end)))),
|
||||||
|
F = fun({_Name, _V, Command}, []) ->
|
||||||
|
[Command];
|
||||||
|
({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
|
||||||
|
Acc;
|
||||||
|
({_Name, _V, Command}, Acc) -> [Command | Acc]
|
||||||
|
end,
|
||||||
|
lists:foldl(F, [], L).
|
||||||
|
|
||||||
|
%% @spec (Name::atom(), Arguments) -> ResultTerm
|
||||||
|
%% where
|
||||||
|
%% Arguments = [any()]
|
||||||
|
%% @doc Execute a command.
|
||||||
|
%% Can return the following exceptions:
|
||||||
|
%% command_unknown | account_unprivileged | invalid_account_data |
|
||||||
|
%% no_auth_provided
|
||||||
|
execute_command(Name, Arguments) ->
|
||||||
|
execute_command(Name, Arguments, ?DEFAULT_VERSION).
|
||||||
|
|
||||||
|
-spec execute_command(atom(),
|
||||||
|
[any()],
|
||||||
|
integer() |
|
||||||
{binary(), binary(), binary(), boolean()} |
|
{binary(), binary(), binary(), boolean()} |
|
||||||
noauth | admin,
|
noauth | admin
|
||||||
atom(),
|
|
||||||
[any()]
|
|
||||||
) -> any().
|
) -> any().
|
||||||
|
|
||||||
%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error}
|
%% @spec (Name::atom(), Arguments, integer() | Auth) -> ResultTerm
|
||||||
|
%% where
|
||||||
|
%% Auth = {User::string(), Server::string(), Password::string(),
|
||||||
|
%% Admin::boolean()}
|
||||||
|
%% | noauth
|
||||||
|
%% | admin
|
||||||
|
%% Arguments = [any()]
|
||||||
|
%%
|
||||||
|
%% @doc Execute a command in a given API version
|
||||||
|
%% Can return the following exceptions:
|
||||||
|
%% command_unknown | account_unprivileged | invalid_account_data |
|
||||||
|
%% no_auth_provided
|
||||||
|
execute_command(Name, Arguments, Version) when is_integer(Version) ->
|
||||||
|
execute_command([], noauth, Name, Arguments, Version);
|
||||||
|
execute_command(Name, Arguments, Auth) ->
|
||||||
|
execute_command([], Auth, Name, Arguments, ?DEFAULT_VERSION).
|
||||||
|
|
||||||
|
%% @spec (AccessCommands, Auth, Name::atom(), Arguments) ->
|
||||||
|
%% ResultTerm | {error, Error}
|
||||||
%% where
|
%% where
|
||||||
%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
|
%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
|
||||||
%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
|
%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
|
||||||
%% | noauth
|
%% | noauth
|
||||||
%% | admin
|
%% | admin
|
||||||
%% Method = atom()
|
|
||||||
%% Arguments = [any()]
|
%% Arguments = [any()]
|
||||||
%% Error = command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
|
%%
|
||||||
execute_command(AccessCommands1, Auth1, Name, Arguments) ->
|
%% @doc Execute a command
|
||||||
|
%% Can return the following exceptions:
|
||||||
|
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
|
||||||
|
execute_command(AccessCommands, Auth, Name, Arguments) ->
|
||||||
|
execute_command(AccessCommands, Auth, Name, Arguments, ?DEFAULT_VERSION).
|
||||||
|
|
||||||
|
-spec execute_command([{atom(), [atom()], [any()]}] | undefined,
|
||||||
|
{binary(), binary(), binary(), boolean()} |
|
||||||
|
noauth | admin,
|
||||||
|
atom(),
|
||||||
|
[any()],
|
||||||
|
integer()
|
||||||
|
) -> any().
|
||||||
|
|
||||||
|
%% @spec (AccessCommands, Auth, Name::atom(), Arguments, integer()) -> ResultTerm
|
||||||
|
%% where
|
||||||
|
%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
|
||||||
|
%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
|
||||||
|
%% | noauth
|
||||||
|
%% | admin
|
||||||
|
%% Arguments = [any()]
|
||||||
|
%%
|
||||||
|
%% @doc Execute a command in a given API version
|
||||||
|
%% Can return the following exceptions:
|
||||||
|
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
|
||||||
|
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
|
||||||
Auth = case is_admin(Name, Auth1) of
|
Auth = case is_admin(Name, Auth1) of
|
||||||
true -> admin;
|
true -> admin;
|
||||||
false -> Auth1
|
false -> Auth1
|
||||||
end,
|
end,
|
||||||
case ets:lookup(ejabberd_commands, Name) of
|
Command = get_command_definition(Name, Version),
|
||||||
[Command] ->
|
AccessCommands = get_access_commands(AccessCommands1, Version),
|
||||||
AccessCommands = get_access_commands(AccessCommands1),
|
case check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
|
||||||
try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
|
|
||||||
ok -> execute_command2(Auth, Command, Arguments)
|
ok -> execute_command2(Auth, Command, Arguments)
|
||||||
catch
|
|
||||||
{error, Error} -> {error, Error}
|
|
||||||
end;
|
|
||||||
[] -> {error, command_unknown}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
execute_command2(
|
execute_command2(
|
||||||
@ -407,26 +510,25 @@ execute_command2(Command, Arguments) ->
|
|||||||
Module = Command#ejabberd_commands.module,
|
Module = Command#ejabberd_commands.module,
|
||||||
Function = Command#ejabberd_commands.function,
|
Function = Command#ejabberd_commands.function,
|
||||||
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
|
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
|
||||||
try apply(Module, Function, Arguments) of
|
apply(Module, Function, Arguments).
|
||||||
Response ->
|
|
||||||
Response
|
|
||||||
catch
|
|
||||||
Problem ->
|
|
||||||
{error, Problem}
|
|
||||||
end.
|
|
||||||
|
|
||||||
-spec get_tags_commands() -> [{string(), [string()]}].
|
-spec get_tags_commands() -> [{string(), [string()]}].
|
||||||
|
|
||||||
%% @spec () -> [{Tag::string(), [CommandName::string()]}]
|
%% @spec () -> [{Tag::string(), [CommandName::string()]}]
|
||||||
%% @doc Get all the tags and associated commands.
|
%% @doc Get all the tags and associated commands.
|
||||||
get_tags_commands() ->
|
get_tags_commands() ->
|
||||||
CommandTags = ets:match(ejabberd_commands,
|
get_tags_commands(?DEFAULT_VERSION).
|
||||||
#ejabberd_commands{
|
|
||||||
name = '$1',
|
-spec get_tags_commands(integer()) -> [{string(), [string()]}].
|
||||||
tags = '$2',
|
|
||||||
_ = '_'}),
|
%% @spec (integer) -> [{Tag::string(), [CommandName::string()]}]
|
||||||
|
%% @doc Get all the tags and associated commands in a given API version
|
||||||
|
get_tags_commands(Version) ->
|
||||||
|
CommandTags = [{Name, Tags} ||
|
||||||
|
#ejabberd_commands{name = Name, tags = Tags}
|
||||||
|
<- get_commands_definition(Version)],
|
||||||
Dict = lists:foldl(
|
Dict = lists:foldl(
|
||||||
fun([CommandNameAtom, CTags], D) ->
|
fun({CommandNameAtom, CTags}, D) ->
|
||||||
CommandName = atom_to_list(CommandNameAtom),
|
CommandName = atom_to_list(CommandNameAtom),
|
||||||
case CTags of
|
case CTags of
|
||||||
[] ->
|
[] ->
|
||||||
@ -445,7 +547,6 @@ get_tags_commands() ->
|
|||||||
CommandTags),
|
CommandTags),
|
||||||
orddict:to_list(Dict).
|
orddict:to_list(Dict).
|
||||||
|
|
||||||
|
|
||||||
%% -----------------------------
|
%% -----------------------------
|
||||||
%% Access verification
|
%% Access verification
|
||||||
%% -----------------------------
|
%% -----------------------------
|
||||||
@ -479,7 +580,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
|
|||||||
fun({Access, Commands, ArgumentRestrictions}) ->
|
fun({Access, Commands, ArgumentRestrictions}) ->
|
||||||
case check_access(Command, Access, Auth) of
|
case check_access(Command, Access, Auth) of
|
||||||
true ->
|
true ->
|
||||||
check_access_command(Commands, Command, ArgumentRestrictions,
|
check_access_command(Commands, Command,
|
||||||
|
ArgumentRestrictions,
|
||||||
Method, Arguments);
|
Method, Arguments);
|
||||||
false ->
|
false ->
|
||||||
false
|
false
|
||||||
@ -488,7 +590,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
|
|||||||
ArgumentRestrictions = [],
|
ArgumentRestrictions = [],
|
||||||
case check_access(Command, Access, Auth) of
|
case check_access(Command, Access, Auth) of
|
||||||
true ->
|
true ->
|
||||||
check_access_command(Commands, Command, ArgumentRestrictions,
|
check_access_command(Commands, Command,
|
||||||
|
ArgumentRestrictions,
|
||||||
Method, Arguments);
|
Method, Arguments);
|
||||||
false ->
|
false ->
|
||||||
false
|
false
|
||||||
@ -551,9 +654,11 @@ check_access2(Access, User, Server) ->
|
|||||||
deny -> false
|
deny -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) ->
|
check_access_command(Commands, Command, ArgumentRestrictions,
|
||||||
|
Method, Arguments) ->
|
||||||
case Commands==all orelse lists:member(Method, Commands) of
|
case Commands==all orelse lists:member(Method, Commands) of
|
||||||
true -> check_access_arguments(Command, ArgumentRestrictions, Arguments);
|
true -> check_access_arguments(Command, ArgumentRestrictions,
|
||||||
|
Arguments);
|
||||||
false -> false
|
false -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@ -577,18 +682,20 @@ tag_arguments(ArgsDefs, Args) ->
|
|||||||
Args).
|
Args).
|
||||||
|
|
||||||
|
|
||||||
get_access_commands(undefined) ->
|
get_access_commands(undefined, Version) ->
|
||||||
Cmds = get_commands(),
|
Cmds = get_commands(Version),
|
||||||
[{?POLICY_ACCESS, Cmds, []}];
|
[{?POLICY_ACCESS, Cmds, []}];
|
||||||
get_access_commands(AccessCommands) ->
|
get_access_commands(AccessCommands, _Version) ->
|
||||||
AccessCommands.
|
AccessCommands.
|
||||||
|
|
||||||
get_commands() ->
|
get_commands() ->
|
||||||
|
get_commands(?DEFAULT_VERSION).
|
||||||
|
get_commands(Version) ->
|
||||||
Opts = ejabberd_config:get_option(
|
Opts = ejabberd_config:get_option(
|
||||||
commands,
|
commands,
|
||||||
fun(V) when is_list(V) -> V end,
|
fun(V) when is_list(V) -> V end,
|
||||||
[]),
|
[]),
|
||||||
CommandsList = list_commands_policy(),
|
CommandsList = list_commands_policy(Version),
|
||||||
OpenCmds = [N || {N, _, _, open} <- CommandsList],
|
OpenCmds = [N || {N, _, _, open} <- CommandsList],
|
||||||
RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList],
|
RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList],
|
||||||
AdminCmds = [N || {N, _, _, admin} <- CommandsList],
|
AdminCmds = [N || {N, _, _, admin} <- CommandsList],
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
-behaviour(ejabberd_config).
|
-behaviour(ejabberd_config).
|
||||||
-author('alexey@process-one.net').
|
-author('alexey@process-one.net').
|
||||||
|
|
||||||
-export([start/0, init/0, process/1, process2/2,
|
-export([start/0, init/0, process/1,
|
||||||
register_commands/3, unregister_commands/3,
|
register_commands/3, unregister_commands/3,
|
||||||
opt_type/1]).
|
opt_type/1]).
|
||||||
|
|
||||||
@ -57,6 +57,8 @@
|
|||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
-include("logger.hrl").
|
-include("logger.hrl").
|
||||||
|
|
||||||
|
-define(DEFAULT_VERSION, 1000000).
|
||||||
|
|
||||||
|
|
||||||
%%-----------------------------
|
%%-----------------------------
|
||||||
%% Module
|
%% Module
|
||||||
@ -69,7 +71,7 @@ start() ->
|
|||||||
[SNode3 | Args3] ->
|
[SNode3 | Args3] ->
|
||||||
[SNode3, 60000, Args3];
|
[SNode3, 60000, Args3];
|
||||||
_ ->
|
_ ->
|
||||||
print_usage(),
|
print_usage(?DEFAULT_VERSION),
|
||||||
halt(?STATUS_USAGE)
|
halt(?STATUS_USAGE)
|
||||||
end,
|
end,
|
||||||
SNode1 = case string:tokens(SNode, "@") of
|
SNode1 = case string:tokens(SNode, "@") of
|
||||||
@ -93,6 +95,9 @@ start() ->
|
|||||||
[Node, Reason]),
|
[Node, Reason]),
|
||||||
%% TODO: show minimal start help
|
%% TODO: show minimal start help
|
||||||
?STATUS_BADRPC;
|
?STATUS_BADRPC;
|
||||||
|
{invalid_version, V} ->
|
||||||
|
print("Invalid API version number: ~p~n", [V]),
|
||||||
|
?STATUS_ERROR;
|
||||||
S ->
|
S ->
|
||||||
S
|
S
|
||||||
end,
|
end,
|
||||||
@ -126,11 +131,17 @@ unregister_commands(CmdDescs, Module, Function) ->
|
|||||||
%% Process
|
%% Process
|
||||||
%%-----------------------------
|
%%-----------------------------
|
||||||
|
|
||||||
|
|
||||||
-spec process([string()]) -> non_neg_integer().
|
-spec process([string()]) -> non_neg_integer().
|
||||||
|
process(Args) ->
|
||||||
|
process(Args, ?DEFAULT_VERSION).
|
||||||
|
|
||||||
|
|
||||||
|
-spec process([string()], non_neg_integer()) -> non_neg_integer().
|
||||||
|
|
||||||
%% The commands status, stop and restart are defined here to ensure
|
%% The commands status, stop and restart are defined here to ensure
|
||||||
%% they are usable even if ejabberd is completely stopped.
|
%% they are usable even if ejabberd is completely stopped.
|
||||||
process(["status"]) ->
|
process(["status"], _Version) ->
|
||||||
{InternalStatus, ProvidedStatus} = init:get_status(),
|
{InternalStatus, ProvidedStatus} = init:get_status(),
|
||||||
print("The node ~p is ~p with status: ~p~n",
|
print("The node ~p is ~p with status: ~p~n",
|
||||||
[node(), InternalStatus, ProvidedStatus]),
|
[node(), InternalStatus, ProvidedStatus]),
|
||||||
@ -146,24 +157,24 @@ process(["status"]) ->
|
|||||||
?STATUS_SUCCESS
|
?STATUS_SUCCESS
|
||||||
end;
|
end;
|
||||||
|
|
||||||
process(["stop"]) ->
|
process(["stop"], _Version) ->
|
||||||
%%ejabberd_cover:stop(),
|
%%ejabberd_cover:stop(),
|
||||||
init:stop(),
|
init:stop(),
|
||||||
?STATUS_SUCCESS;
|
?STATUS_SUCCESS;
|
||||||
|
|
||||||
process(["restart"]) ->
|
process(["restart"], _Version) ->
|
||||||
init:restart(),
|
init:restart(),
|
||||||
?STATUS_SUCCESS;
|
?STATUS_SUCCESS;
|
||||||
|
|
||||||
process(["mnesia"]) ->
|
process(["mnesia"], _Version) ->
|
||||||
print("~p~n", [mnesia:system_info(all)]),
|
print("~p~n", [mnesia:system_info(all)]),
|
||||||
?STATUS_SUCCESS;
|
?STATUS_SUCCESS;
|
||||||
|
|
||||||
process(["mnesia", "info"]) ->
|
process(["mnesia", "info"], _Version) ->
|
||||||
mnesia:info(),
|
mnesia:info(),
|
||||||
?STATUS_SUCCESS;
|
?STATUS_SUCCESS;
|
||||||
|
|
||||||
process(["mnesia", Arg]) ->
|
process(["mnesia", Arg], _Version) ->
|
||||||
case catch mnesia:system_info(list_to_atom(Arg)) of
|
case catch mnesia:system_info(list_to_atom(Arg)) of
|
||||||
{'EXIT', Error} -> print("Error: ~p~n", [Error]);
|
{'EXIT', Error} -> print("Error: ~p~n", [Error]);
|
||||||
Return -> print("~p~n", [Return])
|
Return -> print("~p~n", [Return])
|
||||||
@ -172,23 +183,23 @@ process(["mnesia", Arg]) ->
|
|||||||
|
|
||||||
%% The arguments --long and --dual are not documented because they are
|
%% The arguments --long and --dual are not documented because they are
|
||||||
%% automatically selected depending in the number of columns of the shell
|
%% automatically selected depending in the number of columns of the shell
|
||||||
process(["help" | Mode]) ->
|
process(["help" | Mode], Version) ->
|
||||||
{MaxC, ShCode} = get_shell_info(),
|
{MaxC, ShCode} = get_shell_info(),
|
||||||
case Mode of
|
case Mode of
|
||||||
[] ->
|
[] ->
|
||||||
print_usage(dual, MaxC, ShCode),
|
print_usage(dual, MaxC, ShCode, Version),
|
||||||
?STATUS_USAGE;
|
?STATUS_USAGE;
|
||||||
["--dual"] ->
|
["--dual"] ->
|
||||||
print_usage(dual, MaxC, ShCode),
|
print_usage(dual, MaxC, ShCode, Version),
|
||||||
?STATUS_USAGE;
|
?STATUS_USAGE;
|
||||||
["--long"] ->
|
["--long"] ->
|
||||||
print_usage(long, MaxC, ShCode),
|
print_usage(long, MaxC, ShCode, Version),
|
||||||
?STATUS_USAGE;
|
?STATUS_USAGE;
|
||||||
["--tags"] ->
|
["--tags"] ->
|
||||||
print_usage_tags(MaxC, ShCode),
|
print_usage_tags(MaxC, ShCode, Version),
|
||||||
?STATUS_SUCCESS;
|
?STATUS_SUCCESS;
|
||||||
["--tags", Tag] ->
|
["--tags", Tag] ->
|
||||||
print_usage_tags(Tag, MaxC, ShCode),
|
print_usage_tags(Tag, MaxC, ShCode, Version),
|
||||||
?STATUS_SUCCESS;
|
?STATUS_SUCCESS;
|
||||||
["help"] ->
|
["help"] ->
|
||||||
print_usage_help(MaxC, ShCode),
|
print_usage_help(MaxC, ShCode),
|
||||||
@ -196,13 +207,22 @@ process(["help" | Mode]) ->
|
|||||||
[CmdString | _] ->
|
[CmdString | _] ->
|
||||||
CmdStringU = ejabberd_regexp:greplace(
|
CmdStringU = ejabberd_regexp:greplace(
|
||||||
list_to_binary(CmdString), <<"-">>, <<"_">>),
|
list_to_binary(CmdString), <<"-">>, <<"_">>),
|
||||||
print_usage_commands(binary_to_list(CmdStringU), MaxC, ShCode),
|
print_usage_commands2(binary_to_list(CmdStringU), MaxC, ShCode, Version),
|
||||||
?STATUS_SUCCESS
|
?STATUS_SUCCESS
|
||||||
end;
|
end;
|
||||||
|
|
||||||
process(Args) ->
|
process(["--version", Arg | Args], _) ->
|
||||||
|
Version =
|
||||||
|
try
|
||||||
|
list_to_integer(Arg)
|
||||||
|
catch _:_ ->
|
||||||
|
throw({invalid_version, Arg})
|
||||||
|
end,
|
||||||
|
process(Args, Version);
|
||||||
|
|
||||||
|
process(Args, Version) ->
|
||||||
AccessCommands = get_accesscommands(),
|
AccessCommands = get_accesscommands(),
|
||||||
{String, Code} = process2(Args, AccessCommands),
|
{String, Code} = process2(Args, AccessCommands, Version),
|
||||||
case String of
|
case String of
|
||||||
[] -> ok;
|
[] -> ok;
|
||||||
_ ->
|
_ ->
|
||||||
@ -211,18 +231,21 @@ process(Args) ->
|
|||||||
Code.
|
Code.
|
||||||
|
|
||||||
%% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()}
|
%% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()}
|
||||||
process2(["--auth", User, Server, Pass | Args], AccessCommands) ->
|
process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) ->
|
||||||
process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, AccessCommands);
|
process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server),
|
||||||
process2(Args, AccessCommands) ->
|
list_to_binary(Pass), true}, Version);
|
||||||
process2(Args, noauth, AccessCommands).
|
process2(Args, AccessCommands, Version) ->
|
||||||
|
process2(Args, AccessCommands, admin, Version).
|
||||||
|
|
||||||
process2(Args, Auth, AccessCommands) ->
|
|
||||||
case try_run_ctp(Args, Auth, AccessCommands) of
|
|
||||||
|
process2(Args, AccessCommands, Auth, Version) ->
|
||||||
|
case try_run_ctp(Args, Auth, AccessCommands, Version) of
|
||||||
{String, wrong_command_arguments}
|
{String, wrong_command_arguments}
|
||||||
when is_list(String) ->
|
when is_list(String) ->
|
||||||
io:format(lists:flatten(["\n" | String]++["\n"])),
|
io:format(lists:flatten(["\n" | String]++["\n"])),
|
||||||
[CommandString | _] = Args,
|
[CommandString | _] = Args,
|
||||||
process(["help" | [CommandString]]),
|
process(["help" | [CommandString]], Version),
|
||||||
{lists:flatten(String), ?STATUS_ERROR};
|
{lists:flatten(String), ?STATUS_ERROR};
|
||||||
{String, Code}
|
{String, Code}
|
||||||
when is_list(String) and is_integer(Code) ->
|
when is_list(String) and is_integer(Code) ->
|
||||||
@ -246,29 +269,29 @@ get_accesscommands() ->
|
|||||||
%%-----------------------------
|
%%-----------------------------
|
||||||
|
|
||||||
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
|
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
|
||||||
try_run_ctp(Args, Auth, AccessCommands) ->
|
try_run_ctp(Args, Auth, AccessCommands, Version) ->
|
||||||
try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of
|
try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of
|
||||||
false when Args /= [] ->
|
false when Args /= [] ->
|
||||||
try_call_command(Args, Auth, AccessCommands);
|
try_call_command(Args, Auth, AccessCommands, Version);
|
||||||
false ->
|
false ->
|
||||||
print_usage(),
|
print_usage(Version),
|
||||||
{"", ?STATUS_USAGE};
|
{"", ?STATUS_USAGE};
|
||||||
Status ->
|
Status ->
|
||||||
{"", Status}
|
{"", Status}
|
||||||
catch
|
catch
|
||||||
exit:Why ->
|
exit:Why ->
|
||||||
print_usage(),
|
print_usage(Version),
|
||||||
{io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE};
|
{io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE};
|
||||||
Error:Why ->
|
Error:Why ->
|
||||||
%% In this case probably ejabberd is not started, so let's show Status
|
%% In this case probably ejabberd is not started, so let's show Status
|
||||||
process(["status"]),
|
process(["status"], Version),
|
||||||
print("~n", []),
|
print("~n", []),
|
||||||
{io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE}
|
{io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
|
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
|
||||||
try_call_command(Args, Auth, AccessCommands) ->
|
try_call_command(Args, Auth, AccessCommands, Version) ->
|
||||||
try call_command(Args, Auth, AccessCommands) of
|
try call_command(Args, Auth, AccessCommands, Version) of
|
||||||
{error, command_unknown} ->
|
{error, command_unknown} ->
|
||||||
{io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR};
|
{io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR};
|
||||||
{error, wrong_command_arguments} ->
|
{error, wrong_command_arguments} ->
|
||||||
@ -276,24 +299,28 @@ try_call_command(Args, Auth, AccessCommands) ->
|
|||||||
Res ->
|
Res ->
|
||||||
Res
|
Res
|
||||||
catch
|
catch
|
||||||
|
throw:Error ->
|
||||||
|
{io_lib:format("~p", [Error]), ?STATUS_ERROR};
|
||||||
A:Why ->
|
A:Why ->
|
||||||
Stack = erlang:get_stacktrace(),
|
Stack = erlang:get_stacktrace(),
|
||||||
{io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR}
|
{io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} | {error, ErrorType}
|
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} | {error, ErrorType}
|
||||||
call_command([CmdString | Args], Auth, AccessCommands) ->
|
call_command([CmdString | Args], Auth, AccessCommands, Version) ->
|
||||||
CmdStringU = ejabberd_regexp:greplace(
|
CmdStringU = ejabberd_regexp:greplace(
|
||||||
list_to_binary(CmdString), <<"-">>, <<"_">>),
|
list_to_binary(CmdString), <<"-">>, <<"_">>),
|
||||||
Command = list_to_atom(binary_to_list(CmdStringU)),
|
Command = list_to_atom(binary_to_list(CmdStringU)),
|
||||||
case ejabberd_commands:get_command_format(Command, Auth) of
|
case ejabberd_commands:get_command_format(Command, Auth, Version) of
|
||||||
{error, command_unknown} ->
|
{error, command_unknown} ->
|
||||||
{error, command_unknown};
|
{error, command_unknown};
|
||||||
{ArgsFormat, ResultFormat} ->
|
{ArgsFormat, ResultFormat} ->
|
||||||
case (catch format_args(Args, ArgsFormat)) of
|
case (catch format_args(Args, ArgsFormat)) of
|
||||||
ArgsFormatted when is_list(ArgsFormatted) ->
|
ArgsFormatted when is_list(ArgsFormatted) ->
|
||||||
Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command,
|
Result = ejabberd_commands:execute_command(AccessCommands,
|
||||||
ArgsFormatted),
|
Auth, Command,
|
||||||
|
ArgsFormatted,
|
||||||
|
Version),
|
||||||
format_result(Result, ResultFormat);
|
format_result(Result, ResultFormat);
|
||||||
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
|
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
|
||||||
{NumCompa, TextCompa} =
|
{NumCompa, TextCompa} =
|
||||||
@ -404,8 +431,8 @@ make_status(ok) -> ?STATUS_SUCCESS;
|
|||||||
make_status(true) -> ?STATUS_SUCCESS;
|
make_status(true) -> ?STATUS_SUCCESS;
|
||||||
make_status(_Error) -> ?STATUS_ERROR.
|
make_status(_Error) -> ?STATUS_ERROR.
|
||||||
|
|
||||||
get_list_commands() ->
|
get_list_commands(Version) ->
|
||||||
try ejabberd_commands:list_commands() of
|
try ejabberd_commands:list_commands(Version) of
|
||||||
Commands ->
|
Commands ->
|
||||||
[tuple_command_help(Command)
|
[tuple_command_help(Command)
|
||||||
|| {N,_,_}=Command <- Commands,
|
|| {N,_,_}=Command <- Commands,
|
||||||
@ -458,10 +485,10 @@ get_list_ctls() ->
|
|||||||
-define(U2, "\e[24m").
|
-define(U2, "\e[24m").
|
||||||
-define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
|
-define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
|
||||||
|
|
||||||
print_usage() ->
|
print_usage(Version) ->
|
||||||
{MaxC, ShCode} = get_shell_info(),
|
{MaxC, ShCode} = get_shell_info(),
|
||||||
print_usage(dual, MaxC, ShCode).
|
print_usage(dual, MaxC, ShCode, Version).
|
||||||
print_usage(HelpMode, MaxC, ShCode) ->
|
print_usage(HelpMode, MaxC, ShCode, Version) ->
|
||||||
AllCommands =
|
AllCommands =
|
||||||
[
|
[
|
||||||
{"status", [], "Get ejabberd status"},
|
{"status", [], "Get ejabberd status"},
|
||||||
@ -469,11 +496,11 @@ print_usage(HelpMode, MaxC, ShCode) ->
|
|||||||
{"restart", [], "Restart ejabberd"},
|
{"restart", [], "Restart ejabberd"},
|
||||||
{"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"},
|
{"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"},
|
||||||
{"mnesia", ["[info]"], "show information of Mnesia system"}] ++
|
{"mnesia", ["[info]"], "show information of Mnesia system"}] ++
|
||||||
get_list_commands() ++
|
get_list_commands(Version) ++
|
||||||
get_list_ctls(),
|
get_list_ctls(),
|
||||||
|
|
||||||
print(
|
print(
|
||||||
["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--auth ",
|
["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--version ", ?U("api_version"), "] [--auth ",
|
||||||
?U("user"), " ", ?U("host"), " ", ?U("password"), "] ",
|
?U("user"), " ", ?U("host"), " ", ?U("password"), "] ",
|
||||||
?U("command"), " [", ?U("options"), "]\n"
|
?U("command"), " [", ?U("options"), "]\n"
|
||||||
"\n"
|
"\n"
|
||||||
@ -598,9 +625,9 @@ format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
|
|||||||
%% Print Tags
|
%% Print Tags
|
||||||
%%-----------------------------
|
%%-----------------------------
|
||||||
|
|
||||||
print_usage_tags(MaxC, ShCode) ->
|
print_usage_tags(MaxC, ShCode, Version) ->
|
||||||
print("Available tags and commands:", []),
|
print("Available tags and commands:", []),
|
||||||
TagsCommands = ejabberd_commands:get_tags_commands(),
|
TagsCommands = ejabberd_commands:get_tags_commands(Version),
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun({Tag, Commands} = _TagCommands) ->
|
fun({Tag, Commands} = _TagCommands) ->
|
||||||
print(["\n\n ", ?B(Tag), "\n "], []),
|
print(["\n\n ", ?B(Tag), "\n "], []),
|
||||||
@ -611,10 +638,10 @@ print_usage_tags(MaxC, ShCode) ->
|
|||||||
TagsCommands),
|
TagsCommands),
|
||||||
print("\n\n", []).
|
print("\n\n", []).
|
||||||
|
|
||||||
print_usage_tags(Tag, MaxC, ShCode) ->
|
print_usage_tags(Tag, MaxC, ShCode, Version) ->
|
||||||
print(["Available commands with tag ", ?B(Tag), ":", "\n"], []),
|
print(["Available commands with tag ", ?B(Tag), ":", "\n"], []),
|
||||||
HelpMode = long,
|
HelpMode = long,
|
||||||
TagsCommands = ejabberd_commands:get_tags_commands(),
|
TagsCommands = ejabberd_commands:get_tags_commands(Version),
|
||||||
CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of
|
CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of
|
||||||
{value, {Tag, CNs}} -> CNs;
|
{value, {Tag, CNs}} -> CNs;
|
||||||
false -> []
|
false -> []
|
||||||
@ -622,7 +649,7 @@ print_usage_tags(Tag, MaxC, ShCode) ->
|
|||||||
CommandsList = lists:map(
|
CommandsList = lists:map(
|
||||||
fun(NameString) ->
|
fun(NameString) ->
|
||||||
C = ejabberd_commands:get_command_definition(
|
C = ejabberd_commands:get_command_definition(
|
||||||
list_to_atom(NameString)),
|
list_to_atom(NameString), Version),
|
||||||
#ejabberd_commands{name = Name,
|
#ejabberd_commands{name = Name,
|
||||||
args = Args,
|
args = Args,
|
||||||
desc = Desc} = C,
|
desc = Desc} = C,
|
||||||
@ -673,20 +700,20 @@ print_usage_help(MaxC, ShCode) ->
|
|||||||
%%-----------------------------
|
%%-----------------------------
|
||||||
|
|
||||||
%% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok
|
%% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok
|
||||||
print_usage_commands(CmdSubString, MaxC, ShCode) ->
|
print_usage_commands2(CmdSubString, MaxC, ShCode, Version) ->
|
||||||
%% Get which command names match this substring
|
%% Get which command names match this substring
|
||||||
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()],
|
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)],
|
||||||
Cmds = filter_commands(AllCommandsNames, CmdSubString),
|
Cmds = filter_commands(AllCommandsNames, CmdSubString),
|
||||||
case Cmds of
|
case Cmds of
|
||||||
[] -> io:format("Error: not command found that match: ~p~n", [CmdSubString]);
|
[] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]);
|
||||||
_ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode)
|
_ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
print_usage_commands2(Cmds, MaxC, ShCode) ->
|
print_usage_commands3(Cmds, MaxC, ShCode, Version) ->
|
||||||
%% Then for each one print it
|
%% Then for each one print it
|
||||||
lists:mapfoldl(
|
lists:mapfoldl(
|
||||||
fun(Cmd, Remaining) ->
|
fun(Cmd, Remaining) ->
|
||||||
print_usage_command(Cmd, MaxC, ShCode),
|
print_usage_command(Cmd, MaxC, ShCode, Version),
|
||||||
case Remaining > 1 of
|
case Remaining > 1 of
|
||||||
true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []);
|
true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []);
|
||||||
false -> ok
|
false -> ok
|
||||||
@ -716,16 +743,16 @@ filter_commands_regexp(All, Glob) ->
|
|||||||
All).
|
All).
|
||||||
|
|
||||||
%% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok
|
%% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok
|
||||||
print_usage_command(Cmd, MaxC, ShCode) ->
|
print_usage_command(Cmd, MaxC, ShCode, Version) ->
|
||||||
Name = list_to_atom(Cmd),
|
Name = list_to_atom(Cmd),
|
||||||
case ejabberd_commands:get_command_definition(Name) of
|
case ejabberd_commands:get_command_definition(Name, Version) of
|
||||||
command_not_found ->
|
command_not_found ->
|
||||||
io:format("Error: command ~p not known.~n", [Cmd]);
|
io:format("Error: command ~p not known.~n", [Cmd]);
|
||||||
C ->
|
C ->
|
||||||
print_usage_command(Cmd, C, MaxC, ShCode)
|
print_usage_command2(Cmd, C, MaxC, ShCode)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
print_usage_command(Cmd, C, MaxC, ShCode) ->
|
print_usage_command2(Cmd, C, MaxC, ShCode) ->
|
||||||
#ejabberd_commands{
|
#ejabberd_commands{
|
||||||
tags = TagsAtoms,
|
tags = TagsAtoms,
|
||||||
desc = Desc,
|
desc = Desc,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,11 @@
|
|||||||
%% request_handlers:
|
%% request_handlers:
|
||||||
%% "/api": mod_http_api
|
%% "/api": mod_http_api
|
||||||
%%
|
%%
|
||||||
|
%% To use a specific API version N, add a vN element in the URL path:
|
||||||
|
%% in ejabberd_http listener
|
||||||
|
%% request_handlers:
|
||||||
|
%% "/api/v2": mod_http_api
|
||||||
|
%%
|
||||||
%% Access rights are defined with:
|
%% Access rights are defined with:
|
||||||
%% commands_admin_access: configure
|
%% commands_admin_access: configure
|
||||||
%% commands:
|
%% commands:
|
||||||
@ -76,6 +81,8 @@
|
|||||||
-include("logger.hrl").
|
-include("logger.hrl").
|
||||||
-include("ejabberd_http.hrl").
|
-include("ejabberd_http.hrl").
|
||||||
|
|
||||||
|
-define(DEFAULT_API_VERSION, 0).
|
||||||
|
|
||||||
-define(CT_PLAIN,
|
-define(CT_PLAIN,
|
||||||
{<<"Content-Type">>, <<"text/plain">>}).
|
{<<"Content-Type">>, <<"text/plain">>}).
|
||||||
|
|
||||||
@ -179,7 +186,8 @@ check_permissions2(#request{ip={IP, _Port}}, Call) ->
|
|||||||
true -> {allowed, Call, admin};
|
true -> {allowed, Call, admin};
|
||||||
_ -> unauthorized_response()
|
_ -> unauthorized_response()
|
||||||
end;
|
end;
|
||||||
_ ->
|
_E ->
|
||||||
|
?DEBUG("Unauthorized: ~p", [_E]),
|
||||||
unauthorized_response()
|
unauthorized_response()
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@ -192,10 +200,13 @@ oauth_check_token(Scope, Token) ->
|
|||||||
%% command processing
|
%% command processing
|
||||||
%% ------------------
|
%% ------------------
|
||||||
|
|
||||||
|
%process(Call, Request) ->
|
||||||
|
% ?DEBUG("~p~n~p", [Call, Request]), ok;
|
||||||
process(_, #request{method = 'POST', data = <<>>}) ->
|
process(_, #request{method = 'POST', data = <<>>}) ->
|
||||||
?DEBUG("Bad Request: no data", []),
|
?DEBUG("Bad Request: no data", []),
|
||||||
badrequest_response();
|
badrequest_response(<<"Missing POST data">>);
|
||||||
process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
|
process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
|
||||||
|
Version = get_api_version(Req),
|
||||||
try
|
try
|
||||||
Args = case jiffy:decode(Data) of
|
Args = case jiffy:decode(Data) of
|
||||||
List when is_list(List) -> List;
|
List when is_list(List) -> List;
|
||||||
@ -205,16 +216,20 @@ process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
|
|||||||
log(Call, Args, IP),
|
log(Call, Args, IP),
|
||||||
case check_permissions(Req, Call) of
|
case check_permissions(Req, Call) of
|
||||||
{allowed, Cmd, Auth} ->
|
{allowed, Cmd, Auth} ->
|
||||||
{Code, Result} = handle(Cmd, Auth, Args),
|
{Code, Result} = handle(Cmd, Auth, Args, Version),
|
||||||
json_response(Code, jiffy:encode(Result));
|
json_response(Code, jiffy:encode(Result));
|
||||||
ErrorResponse -> %% Should we reply 403 ?
|
ErrorResponse -> %% Should we reply 403 ?
|
||||||
ErrorResponse
|
ErrorResponse
|
||||||
end
|
end
|
||||||
catch _:Error ->
|
catch _:{error,{_,invalid_json}} = _Err ->
|
||||||
?DEBUG("Bad Request: ~p", [Error]),
|
?DEBUG("Bad Request: ~p", [_Err]),
|
||||||
|
badrequest_response(<<"Invalid JSON input">>);
|
||||||
|
_:_Error ->
|
||||||
|
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
|
||||||
badrequest_response()
|
badrequest_response()
|
||||||
end;
|
end;
|
||||||
process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
|
process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
|
||||||
|
Version = get_api_version(Req),
|
||||||
try
|
try
|
||||||
Args = case Data of
|
Args = case Data of
|
||||||
[{nokey, <<>>}] -> [];
|
[{nokey, <<>>}] -> [];
|
||||||
@ -223,13 +238,13 @@ process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
|
|||||||
log(Call, Args, IP),
|
log(Call, Args, IP),
|
||||||
case check_permissions(Req, Call) of
|
case check_permissions(Req, Call) of
|
||||||
{allowed, Cmd, Auth} ->
|
{allowed, Cmd, Auth} ->
|
||||||
{Code, Result} = handle(Cmd, Auth, Args),
|
{Code, Result} = handle(Cmd, Auth, Args, Version),
|
||||||
json_response(Code, jiffy:encode(Result));
|
json_response(Code, jiffy:encode(Result));
|
||||||
ErrorResponse ->
|
ErrorResponse ->
|
||||||
ErrorResponse
|
ErrorResponse
|
||||||
end
|
end
|
||||||
catch _:Error ->
|
catch _:_Error ->
|
||||||
?DEBUG("Bad Request: ~p", [Error]),
|
?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
|
||||||
badrequest_response()
|
badrequest_response()
|
||||||
end;
|
end;
|
||||||
process([], #request{method = 'OPTIONS', data = <<>>}) ->
|
process([], #request{method = 'OPTIONS', data = <<>>}) ->
|
||||||
@ -238,13 +253,28 @@ process(_Path, Request) ->
|
|||||||
?DEBUG("Bad Request: no handler ~p", [Request]),
|
?DEBUG("Bad Request: no handler ~p", [Request]),
|
||||||
badrequest_response().
|
badrequest_response().
|
||||||
|
|
||||||
|
% get API version N from last "vN" element in URL path
|
||||||
|
get_api_version(#request{path = Path}) ->
|
||||||
|
get_api_version(lists:reverse(Path));
|
||||||
|
get_api_version([<<"v", String/binary>> | Tail]) ->
|
||||||
|
case catch jlib:binary_to_integer(String) of
|
||||||
|
N when is_integer(N) ->
|
||||||
|
N;
|
||||||
|
_ ->
|
||||||
|
get_api_version(Tail)
|
||||||
|
end;
|
||||||
|
get_api_version([_Head | Tail]) ->
|
||||||
|
get_api_version(Tail);
|
||||||
|
get_api_version([]) ->
|
||||||
|
?DEFAULT_API_VERSION.
|
||||||
|
|
||||||
%% ----------------
|
%% ----------------
|
||||||
%% command handlers
|
%% command handlers
|
||||||
%% ----------------
|
%% ----------------
|
||||||
|
|
||||||
% generic ejabberd command handler
|
% generic ejabberd command handler
|
||||||
handle(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
|
handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
|
||||||
case ejabberd_commands:get_command_format(Call, Auth) of
|
case ejabberd_commands:get_command_format(Call, Auth, Version) of
|
||||||
{ArgsSpec, _} when is_list(ArgsSpec) ->
|
{ArgsSpec, _} when is_list(ArgsSpec) ->
|
||||||
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
|
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
|
||||||
Spec = lists:foldr(
|
Spec = lists:foldr(
|
||||||
@ -259,22 +289,51 @@ handle(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
|
|||||||
({Key, atom}, Acc) ->
|
({Key, atom}, Acc) ->
|
||||||
[{Key, undefined}|Acc]
|
[{Key, undefined}|Acc]
|
||||||
end, [], ArgsSpec),
|
end, [], ArgsSpec),
|
||||||
handle2(Call, Auth, match(Args2, Spec));
|
try
|
||||||
|
handle2(Call, Auth, match(Args2, Spec), Version)
|
||||||
|
catch throw:not_found ->
|
||||||
|
{404, <<"not_found">>};
|
||||||
|
throw:{not_found, Why} when is_atom(Why) ->
|
||||||
|
{404, jlib:atom_to_binary(Why)};
|
||||||
|
throw:{not_found, Msg} ->
|
||||||
|
{404, iolist_to_binary(Msg)};
|
||||||
|
throw:not_allowed ->
|
||||||
|
{401, <<"not_allowed">>};
|
||||||
|
throw:{not_allowed, Why} when is_atom(Why) ->
|
||||||
|
{401, jlib:atom_to_binary(Why)};
|
||||||
|
throw:{not_allowed, Msg} ->
|
||||||
|
{401, iolist_to_binary(Msg)};
|
||||||
|
throw:{invalid_parameter, Msg} ->
|
||||||
|
{400, iolist_to_binary(Msg)};
|
||||||
|
throw:{error, Why} when is_atom(Why) ->
|
||||||
|
{400, jlib:atom_to_binary(Why)};
|
||||||
|
throw:{error, Msg} ->
|
||||||
|
{400, iolist_to_binary(Msg)};
|
||||||
|
throw:Error when is_atom(Error) ->
|
||||||
|
{400, jlib:atom_to_binary(Error)};
|
||||||
|
throw:Msg when is_list(Msg); is_binary(Msg) ->
|
||||||
|
{400, iolist_to_binary(Msg)};
|
||||||
|
_Error ->
|
||||||
|
?ERROR_MSG("REST API Error: ~p ~p", [_Error, erlang:get_stacktrace()]),
|
||||||
|
{500, <<"internal_error">>}
|
||||||
|
end;
|
||||||
{error, Msg} ->
|
{error, Msg} ->
|
||||||
|
?ERROR_MSG("REST API Error: ~p", [Msg]),
|
||||||
{400, Msg};
|
{400, Msg};
|
||||||
_Error ->
|
_Error ->
|
||||||
|
?ERROR_MSG("REST API Error: ~p", [_Error]),
|
||||||
{400, <<"Error">>}
|
{400, <<"Error">>}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
|
handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
|
||||||
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth),
|
{ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
|
||||||
ArgsFormatted = format_args(Args, ArgsF),
|
ArgsFormatted = format_args(Args, ArgsF),
|
||||||
case ejabberd_command(Auth, Call, ArgsFormatted, 400) of
|
case ejabberd_commands:execute_command(undefined, Auth,
|
||||||
0 -> {200, <<"OK">>};
|
Call, ArgsFormatted, Version) of
|
||||||
1 -> {500, <<"500 Internal server error">>};
|
{error, Error} ->
|
||||||
400 -> {400, <<"400 Bad Request">>};
|
throw(Error);
|
||||||
404 -> {404, <<"404 Not found">>};
|
Res ->
|
||||||
Res -> format_command_result(Call, Auth, Res)
|
format_command_result(Call, Auth, Res, Version)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_elem_delete(A, L) ->
|
get_elem_delete(A, L) ->
|
||||||
@ -339,7 +398,9 @@ format_arg(undefined, binary) -> <<>>;
|
|||||||
format_arg(undefined, string) -> <<>>;
|
format_arg(undefined, string) -> <<>>;
|
||||||
format_arg(Arg, Format) ->
|
format_arg(Arg, Format) ->
|
||||||
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
|
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
|
||||||
error.
|
throw({invalid_parameter,
|
||||||
|
io_lib:format("Arg ~p is not in format ~p",
|
||||||
|
[Arg, Format])}).
|
||||||
|
|
||||||
process_unicode_codepoints(Str) ->
|
process_unicode_codepoints(Str) ->
|
||||||
iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
|
iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
|
||||||
@ -353,24 +414,14 @@ process_unicode_codepoints(Str) ->
|
|||||||
match(Args, Spec) ->
|
match(Args, Spec) ->
|
||||||
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
|
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
|
||||||
|
|
||||||
ejabberd_command(Auth, Cmd, Args, Default) ->
|
|
||||||
Access = case Auth of
|
|
||||||
admin -> [];
|
|
||||||
_ -> undefined
|
|
||||||
end,
|
|
||||||
case catch ejabberd_commands:execute_command(Access, Auth, Cmd, Args) of
|
|
||||||
{'EXIT', _} -> Default;
|
|
||||||
{error, _} -> Default;
|
|
||||||
Result -> Result
|
|
||||||
end.
|
|
||||||
|
|
||||||
format_command_result(Cmd, Auth, Result) ->
|
format_command_result(Cmd, Auth, Result, Version) ->
|
||||||
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth),
|
{_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
|
||||||
case {ResultFormat, Result} of
|
case {ResultFormat, Result} of
|
||||||
{{_, rescode}, V} when V == true; V == ok ->
|
{{_, rescode}, V} when V == true; V == ok ->
|
||||||
{200, <<"">>};
|
{200, 0};
|
||||||
{{_, rescode}, _} ->
|
{{_, rescode}, _} ->
|
||||||
{500, <<"">>};
|
{200, 1};
|
||||||
{{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
|
{{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
|
||||||
{200, iolist_to_binary(Text1)};
|
{200, iolist_to_binary(Text1)};
|
||||||
{{_, restuple}, {_, Text2}} ->
|
{{_, restuple}, {_, Text2}} ->
|
||||||
@ -421,14 +472,15 @@ format_result(404, {_Name, _}) ->
|
|||||||
"not_found".
|
"not_found".
|
||||||
|
|
||||||
unauthorized_response() ->
|
unauthorized_response() ->
|
||||||
{401, ?HEADER(?CT_XML),
|
unauthorized_response(<<"401 Unauthorized">>).
|
||||||
#xmlel{name = <<"h1">>, attrs = [],
|
unauthorized_response(Body) ->
|
||||||
children = [{xmlcdata, <<"401 Unauthorized">>}]}}.
|
json_response(401, jiffy:encode(Body)).
|
||||||
|
|
||||||
badrequest_response() ->
|
badrequest_response() ->
|
||||||
{400, ?HEADER(?CT_XML),
|
badrequest_response(<<"400 Bad Request">>).
|
||||||
#xmlel{name = <<"h1">>, attrs = [],
|
badrequest_response(Body) ->
|
||||||
children = [{xmlcdata, <<"400 Bad Request">>}]}}.
|
json_response(400, jiffy:encode(Body)).
|
||||||
|
|
||||||
json_response(Code, Body) when is_integer(Code) ->
|
json_response(Code, Body) when is_integer(Code) ->
|
||||||
{Code, ?HEADER(?CT_JSON), Body}.
|
{Code, ?HEADER(?CT_JSON), Body}.
|
||||||
|
|
||||||
|
79
test/ejabberd_admin_test.exs
Normal file
79
test/ejabberd_admin_test.exs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# ----------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# ejabberd, Copyright (C) 2002-2015 ProcessOne
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License as
|
||||||
|
# published by the Free Software Foundation; either version 2 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
defmodule EjabberdAdminTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
|
setup_all do
|
||||||
|
:mnesia.start
|
||||||
|
# For some myterious reason, :ejabberd_commands.init mays
|
||||||
|
# sometimes fails if module is not loaded before
|
||||||
|
{:module, :ejabberd_commands} = Code.ensure_loaded(:ejabberd_commands)
|
||||||
|
:ejabberd_commands.init
|
||||||
|
:ejabberd_admin.start
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Logvel can be set and retrieved" do
|
||||||
|
:ejabberd_logger.start()
|
||||||
|
|
||||||
|
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [1])
|
||||||
|
assert {1, :critical, 'Critical'} ==
|
||||||
|
:ejabberd_commands.execute_command(:get_loglevel, [])
|
||||||
|
|
||||||
|
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [2])
|
||||||
|
assert {2, :error, 'Error'} ==
|
||||||
|
:ejabberd_commands.execute_command(:get_loglevel, [])
|
||||||
|
|
||||||
|
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [3])
|
||||||
|
assert {3, :warning, 'Warning'} ==
|
||||||
|
:ejabberd_commands.execute_command(:get_loglevel, [])
|
||||||
|
|
||||||
|
assert {:wrong_loglevel, 6} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:set_loglevel, [6])
|
||||||
|
assert {3, :warning, 'Warning'} ==
|
||||||
|
:ejabberd_commands.execute_command(:get_loglevel, [])
|
||||||
|
|
||||||
|
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [4])
|
||||||
|
assert {4, :info, 'Info'} ==
|
||||||
|
:ejabberd_commands.execute_command(:get_loglevel, [])
|
||||||
|
|
||||||
|
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [5])
|
||||||
|
assert {5, :debug, 'Debug'} ==
|
||||||
|
:ejabberd_commands.execute_command(:get_loglevel, [])
|
||||||
|
|
||||||
|
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [0])
|
||||||
|
assert {0, :no_log, 'No log'} ==
|
||||||
|
:ejabberd_commands.execute_command(:get_loglevel, [])
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
test "command status works with ejabberd stopped" do
|
||||||
|
assert :ejabberd_not_running ==
|
||||||
|
elem(:ejabberd_commands.execute_command(:status, []), 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
57
test/ejabberd_auth_mock.exs
Normal file
57
test/ejabberd_auth_mock.exs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# ejabberd_auth mock
|
||||||
|
######################
|
||||||
|
|
||||||
|
defmodule EjabberdAuthMock do
|
||||||
|
|
||||||
|
@author "jsautret@process-one.net"
|
||||||
|
@agent __MODULE__
|
||||||
|
|
||||||
|
def init do
|
||||||
|
try do
|
||||||
|
Agent.stop(@agent)
|
||||||
|
catch
|
||||||
|
:exit, _e -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
|
||||||
|
|
||||||
|
mock(:ejabberd_auth, :is_user_exists,
|
||||||
|
fn (user, domain) ->
|
||||||
|
Agent.get(@agent, fn users -> Map.get(users, {user, domain}) end) != nil
|
||||||
|
end)
|
||||||
|
mock(:ejabberd_auth, :get_password_s,
|
||||||
|
fn (user, domain) ->
|
||||||
|
Agent.get(@agent, fn users -> Map.get(users, {user, domain}, "") end )
|
||||||
|
end)
|
||||||
|
mock(:ejabberd_auth, :check_password,
|
||||||
|
fn (user, domain, password) ->
|
||||||
|
Agent.get(@agent, fn users ->
|
||||||
|
Map.get(users, {user, domain}) end) == password
|
||||||
|
end)
|
||||||
|
mock(:ejabberd_auth, :set_password,
|
||||||
|
fn (user, domain, password) ->
|
||||||
|
Agent.update(@agent, fn users ->
|
||||||
|
Map.put(users, {user, domain}, password) end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_user(user, domain, password) do
|
||||||
|
Agent.update(@agent, fn users -> Map.put(users, {user, domain}, password) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# Helpers
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
# TODO refactor: Move to ejabberd_test_mock
|
||||||
|
def mock(module, function, fun) do
|
||||||
|
try do
|
||||||
|
:meck.new(module)
|
||||||
|
catch
|
||||||
|
:error, {:already_started, _pid} -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
:meck.expect(module, function, fun)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -19,39 +19,406 @@
|
|||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
defmodule EjabberdCommandsTest do
|
defmodule EjabberdCommandsTest do
|
||||||
@author "mremond@process-one.net"
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
use ExUnit.Case, async: true
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
|
# mocked callback module
|
||||||
|
@module :test_module
|
||||||
|
# Admin user
|
||||||
|
@admin "admin"
|
||||||
|
@adminpass "adminpass"
|
||||||
|
# Non admin user
|
||||||
|
@user "user"
|
||||||
|
@userpass "userpass"
|
||||||
|
# XMPP domain
|
||||||
|
@domain "domain"
|
||||||
|
|
||||||
require Record
|
require Record
|
||||||
Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl")
|
Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands,
|
||||||
|
from: "ejabberd_commands.hrl")
|
||||||
|
|
||||||
setup_all do
|
setup_all do
|
||||||
|
try do
|
||||||
|
:stringprep.start
|
||||||
|
rescue
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
:mnesia.start
|
||||||
|
EjabberdOauthMock.init
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do
|
||||||
|
:meck.unload
|
||||||
|
:meck.new(@module, [:non_strict])
|
||||||
:ejabberd_commands.init
|
:ejabberd_commands.init
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Check that we can register a command" do
|
test "API command can be registered, listed and unregistered" do
|
||||||
assert :ejabberd_commands.register_commands([user_test_command]) == :ok
|
command = ejabberd_commands name: :test, module: @module,
|
||||||
|
function: :test_command
|
||||||
|
|
||||||
|
assert :ok == :ejabberd_commands.register_commands [command]
|
||||||
commands = :ejabberd_commands.list_commands
|
commands = :ejabberd_commands.list_commands
|
||||||
assert Enum.member?(commands, {:test_user, [], "Test user"})
|
assert Enum.member? commands, {:test, [], ''}
|
||||||
|
|
||||||
|
assert :ok == :ejabberd_commands.unregister_commands [command]
|
||||||
|
commands = :ejabberd_commands.list_commands
|
||||||
|
refute Enum.member? commands, {:test, [], ''}
|
||||||
end
|
end
|
||||||
|
|
||||||
# test "Check that a user can use a user command" do
|
|
||||||
# [Command] = ets:lookup(ejabberd_commands, test_user),
|
|
||||||
# AccessCommands = ejabberd_commands:get_access_commands(undefined),
|
|
||||||
# ejabberd_commands:check_access_commands(AccessCommands, {<<"test">>,<<"localhost">>, {oauth,<<"MyToken">>}, false}, test_user, Command, []).
|
|
||||||
# end
|
|
||||||
|
|
||||||
defp user_test_command do
|
test "API command with versions can be registered, listed and unregistered" do
|
||||||
ejabberd_commands(name: :test_user, tags: [:roster],
|
command1 = ejabberd_commands name: :test, module: @module,
|
||||||
desc: "Test user",
|
function: :test_command, version: 1, desc: 'version1'
|
||||||
policy: :user,
|
command3 = ejabberd_commands name: :test, module: @module,
|
||||||
module: __MODULE__,
|
function: :test_command, version: 3, desc: 'version3'
|
||||||
function: :test_user,
|
assert :ejabberd_commands.register_commands [command1, command3]
|
||||||
args: [],
|
|
||||||
result: {:contacts, {:list, {:contact, {:tuple, [
|
version1 = {:test, [], 'version1'}
|
||||||
{:jid, :string},
|
version3 = {:test, [], 'version3'}
|
||||||
{:nick, :string}
|
|
||||||
]}}}})
|
# default version is latest one
|
||||||
|
commands = :ejabberd_commands.list_commands
|
||||||
|
refute Enum.member? commands, version1
|
||||||
|
assert Enum.member? commands, version3
|
||||||
|
|
||||||
|
# no such command in APIv0
|
||||||
|
commands = :ejabberd_commands.list_commands 0
|
||||||
|
refute Enum.member? commands, version1
|
||||||
|
refute Enum.member? commands, version3
|
||||||
|
|
||||||
|
commands = :ejabberd_commands.list_commands 1
|
||||||
|
assert Enum.member? commands, version1
|
||||||
|
refute Enum.member? commands, version3
|
||||||
|
|
||||||
|
commands = :ejabberd_commands.list_commands 2
|
||||||
|
assert Enum.member? commands, version1
|
||||||
|
refute Enum.member? commands, version3
|
||||||
|
|
||||||
|
commands = :ejabberd_commands.list_commands 3
|
||||||
|
refute Enum.member? commands, version1
|
||||||
|
assert Enum.member? commands, version3
|
||||||
|
|
||||||
|
commands = :ejabberd_commands.list_commands 4
|
||||||
|
refute Enum.member? commands, version1
|
||||||
|
assert Enum.member? commands, version3
|
||||||
|
|
||||||
|
assert :ok == :ejabberd_commands.unregister_commands [command1]
|
||||||
|
|
||||||
|
commands = :ejabberd_commands.list_commands 1
|
||||||
|
refute Enum.member? commands, version1
|
||||||
|
refute Enum.member? commands, version3
|
||||||
|
|
||||||
|
commands = :ejabberd_commands.list_commands 3
|
||||||
|
refute Enum.member? commands, version1
|
||||||
|
assert Enum.member? commands, version3
|
||||||
|
|
||||||
|
assert :ok == :ejabberd_commands.unregister_commands [command3]
|
||||||
|
|
||||||
|
commands = :ejabberd_commands.list_commands 1
|
||||||
|
refute Enum.member? commands, version1
|
||||||
|
refute Enum.member? commands, version3
|
||||||
|
|
||||||
|
commands = :ejabberd_commands.list_commands 3
|
||||||
|
refute Enum.member? commands, version1
|
||||||
|
refute Enum.member? commands, version3
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
test "API command can be registered and executed" do
|
||||||
|
# Create & register a mocked command test() -> :result
|
||||||
|
command_name = :test
|
||||||
|
function = :test_command
|
||||||
|
command = ejabberd_commands(name: command_name,
|
||||||
|
module: @module,
|
||||||
|
function: function)
|
||||||
|
:meck.expect @module, function, fn -> :result end
|
||||||
|
assert :ok == :ejabberd_commands.register_commands [command]
|
||||||
|
|
||||||
|
assert :result == :ejabberd_commands.execute_command(command_name, [])
|
||||||
|
|
||||||
|
assert :meck.validate @module
|
||||||
|
end
|
||||||
|
|
||||||
|
test "API command with versions can be registered and executed" do
|
||||||
|
command_name = :test
|
||||||
|
|
||||||
|
function1 = :test_command1
|
||||||
|
command1 = ejabberd_commands(name: command_name,
|
||||||
|
version: 1,
|
||||||
|
module: @module,
|
||||||
|
function: function1)
|
||||||
|
:meck.expect(@module, function1, fn -> :result1 end)
|
||||||
|
|
||||||
|
function3 = :test_command3
|
||||||
|
command3 = ejabberd_commands(name: command_name,
|
||||||
|
version: 3,
|
||||||
|
module: @module,
|
||||||
|
function: function3)
|
||||||
|
:meck.expect(@module, function3, fn -> :result3 end)
|
||||||
|
|
||||||
|
assert :ok == :ejabberd_commands.register_commands [command1, command3]
|
||||||
|
|
||||||
|
# default version is latest one
|
||||||
|
assert :result3 == :ejabberd_commands.execute_command(command_name, [])
|
||||||
|
# no such command in APIv0
|
||||||
|
assert :unknown_command ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(command_name, [], 0)
|
||||||
|
assert :result1 == :ejabberd_commands.execute_command(command_name, [], 1)
|
||||||
|
assert :result1 == :ejabberd_commands.execute_command(command_name, [], 2)
|
||||||
|
assert :result3 == :ejabberd_commands.execute_command(command_name, [], 3)
|
||||||
|
assert :result3 == :ejabberd_commands.execute_command(command_name, [], 4)
|
||||||
|
|
||||||
|
assert :meck.validate @module
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test "API command with user policy" do
|
||||||
|
mock_commands_config
|
||||||
|
|
||||||
|
# Register a command test(user, domain) -> {:versionN, user, domain}
|
||||||
|
# with policy=user and versions 1 & 3
|
||||||
|
command_name = :test
|
||||||
|
command1 = ejabberd_commands(name: command_name,
|
||||||
|
module: @module,
|
||||||
|
function: :test_command1,
|
||||||
|
policy: :user, version: 1)
|
||||||
|
command3 = ejabberd_commands(name: command_name,
|
||||||
|
module: @module,
|
||||||
|
function: :test_command3,
|
||||||
|
policy: :user, version: 3)
|
||||||
|
:meck.expect(@module, :test_command1,
|
||||||
|
fn(user, domain) when is_binary(user) and is_binary(domain) ->
|
||||||
|
{:version1, user, domain}
|
||||||
|
end)
|
||||||
|
:meck.expect(@module, :test_command3,
|
||||||
|
fn(user, domain) when is_binary(user) and is_binary(domain) ->
|
||||||
|
{:version3, user, domain}
|
||||||
|
end)
|
||||||
|
assert :ok == :ejabberd_commands.register_commands [command1, command3]
|
||||||
|
|
||||||
|
# A normal user must not pass user info as parameter
|
||||||
|
assert {:version1, @user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
@userpass, false},
|
||||||
|
command_name,
|
||||||
|
[], 2)
|
||||||
|
assert {:version3, @user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
@userpass, false},
|
||||||
|
command_name,
|
||||||
|
[], 3)
|
||||||
|
token = EjabberdOauthMock.get_token @user, @domain, command_name
|
||||||
|
assert {:version3, @user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
{:oauth, token}, false},
|
||||||
|
command_name,
|
||||||
|
[], 4)
|
||||||
|
# Expired oauth token
|
||||||
|
token = EjabberdOauthMock.get_token @user, @domain, command_name, 1
|
||||||
|
:timer.sleep 1500
|
||||||
|
assert {:error, :invalid_account_data} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
{:oauth, token}, false},
|
||||||
|
command_name,
|
||||||
|
[], 4)
|
||||||
|
# Wrong oauth scope
|
||||||
|
token = EjabberdOauthMock.get_token @user, @domain, :bad_command
|
||||||
|
assert {:error, :invalid_account_data} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
{:oauth, token}, false},
|
||||||
|
command_name,
|
||||||
|
[], 4)
|
||||||
|
|
||||||
|
|
||||||
|
assert :function_clause ==
|
||||||
|
catch_error :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
@userpass, false},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain], 2)
|
||||||
|
# @user is not admin
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
@userpass, true},
|
||||||
|
command_name,
|
||||||
|
[], 2)
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
@userpass, true},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain], 2)
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
{:oauth, token}, true},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain], 2)
|
||||||
|
|
||||||
|
|
||||||
|
# An admin must explicitely pass user info
|
||||||
|
assert {:version1, @user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined, :admin,
|
||||||
|
command_name, [@user, @domain], 2)
|
||||||
|
assert {:version3, @user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined, :admin,
|
||||||
|
command_name, [@user, @domain], 4)
|
||||||
|
assert {:version1, @user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain, @adminpass, true},
|
||||||
|
command_name, [@user, @domain], 1)
|
||||||
|
token = EjabberdOauthMock.get_token @admin, @domain, command_name
|
||||||
|
assert {:version3, @user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain, {:oauth, token}, true},
|
||||||
|
command_name, [@user, @domain], 3)
|
||||||
|
# Wrong @admin password
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
@adminpass<>"bad", true},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain], 3)
|
||||||
|
# @admin calling as a normal user
|
||||||
|
assert {:version3, @admin, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
@adminpass, false},
|
||||||
|
command_name, [], 5)
|
||||||
|
assert {:version3, @admin, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
{:oauth, token}, false},
|
||||||
|
command_name, [], 6)
|
||||||
|
assert :function_clause ==
|
||||||
|
catch_error :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
@adminpass, false},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain], 5)
|
||||||
|
assert :meck.validate @module
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test "API command with admin policy" do
|
||||||
|
mock_commands_config
|
||||||
|
|
||||||
|
# Register a command test(user, domain) -> {user, domain}
|
||||||
|
# with policy=admin
|
||||||
|
command_name = :test
|
||||||
|
function = :test_command
|
||||||
|
command = ejabberd_commands(name: command_name,
|
||||||
|
args: [{:user, :binary}, {:host, :binary}],
|
||||||
|
module: @module,
|
||||||
|
function: function,
|
||||||
|
policy: :admin)
|
||||||
|
:meck.expect(@module, function,
|
||||||
|
fn(user, domain) when is_binary(user) and is_binary(domain) ->
|
||||||
|
{user, domain}
|
||||||
|
end)
|
||||||
|
assert :ok == :ejabberd_commands.register_commands [command]
|
||||||
|
|
||||||
|
# A normal user cannot call the command
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
@userpass, false},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
# An admin can call the command
|
||||||
|
assert {@user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
@adminpass, true},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
# An admin can call the command with oauth token
|
||||||
|
token = EjabberdOauthMock.get_token @admin, @domain, command_name
|
||||||
|
assert {@user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
{:oauth, token}, true},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
|
||||||
|
# An admin with bad password cannot call the command
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
"bad"<>@adminpass, false},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
# An admin cannot call the command with bad oauth token
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
{:oauth, "bad"<>token}, true},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
# An admin as a normal user cannot call the command
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
@adminpass, false},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
# An admin as a normal user cannot call the command with oauth token
|
||||||
|
assert {:error, :account_unprivileged} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
{:oauth, token}, false},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
assert :meck.validate @module
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
##########################################################
|
||||||
|
# Utils
|
||||||
|
|
||||||
|
# Mock a config where only @admin user is allowed to call commands
|
||||||
|
# as admin
|
||||||
|
def mock_commands_config do
|
||||||
|
EjabberdAuthMock.init
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @userpass
|
||||||
|
EjabberdAuthMock.create_user @admin, @domain, @adminpass
|
||||||
|
|
||||||
|
:meck.new :ejabberd_config
|
||||||
|
:meck.expect(:ejabberd_config, :get_option,
|
||||||
|
fn(:commands_admin_access, _, _) -> :commands_admin_access
|
||||||
|
(:oauth_access, _, _) -> :all
|
||||||
|
(_, _, default) -> default
|
||||||
|
end)
|
||||||
|
:meck.expect(:ejabberd_config, :get_myhosts,
|
||||||
|
fn() -> [@domain] end)
|
||||||
|
:meck.new :acl
|
||||||
|
:meck.expect(:acl, :match_rule,
|
||||||
|
fn(@domain, :commands_admin_access, user) ->
|
||||||
|
case :jlib.make_jid(@admin, @domain, "") do
|
||||||
|
^user -> :allow
|
||||||
|
_ -> :deny
|
||||||
|
end
|
||||||
|
(@domain, :all, _user) ->
|
||||||
|
:allow
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -30,14 +30,14 @@
|
|||||||
# log as we are exercising hook handler recovery from that situation.
|
# log as we are exercising hook handler recovery from that situation.
|
||||||
|
|
||||||
defmodule EjabberdHooksTest do
|
defmodule EjabberdHooksTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
@author "mremond@process-one.net"
|
@author "mremond@process-one.net"
|
||||||
@host <<"domain.net">>
|
@host <<"domain.net">>
|
||||||
@self __MODULE__
|
@self __MODULE__
|
||||||
|
|
||||||
setup_all do
|
setup_all do
|
||||||
{:ok, _Pid} = :ejabberd_hooks.start_link
|
{:ok, _pid} = :ejabberd_hooks.start_link
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
30
test/ejabberd_oauth_mock.exs
Normal file
30
test/ejabberd_oauth_mock.exs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# ejabberd_oauth mock
|
||||||
|
######################
|
||||||
|
|
||||||
|
defmodule EjabberdOauthMock do
|
||||||
|
|
||||||
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
|
def init() do
|
||||||
|
:mnesia.start
|
||||||
|
:mnesia.create_table(:oauth_token,
|
||||||
|
[ram_copies: [node],
|
||||||
|
attributes: [:oauth_token, :us, :scope, :expire]])
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_token(user, domain, command, expiration \\ 3600) do
|
||||||
|
now = {megasecs, secs, _} = :os.timestamp
|
||||||
|
expire = 1000000 * megasecs + secs + expiration
|
||||||
|
:random.seed now
|
||||||
|
token = to_string :random.uniform(100000000)
|
||||||
|
|
||||||
|
{:ok, _} = :ejabberd_oauth.associate_access_token(token,
|
||||||
|
[{"resource_owner",
|
||||||
|
{:user, user, domain}},
|
||||||
|
{"scope", [to_string command]},
|
||||||
|
{"expiry_time", expire}],
|
||||||
|
:undefined)
|
||||||
|
token
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
106
test/ejabberd_sm_mock.exs
Normal file
106
test/ejabberd_sm_mock.exs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# ejabberd_sm mock
|
||||||
|
######################
|
||||||
|
|
||||||
|
defmodule EjabberdSmMock do
|
||||||
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
|
require Record
|
||||||
|
Record.defrecord :session, Record.extract(:session,
|
||||||
|
from: "ejabberd_sm.hrl")
|
||||||
|
Record.defrecord :jid, Record.extract(:jid,
|
||||||
|
from: "jlib.hrl")
|
||||||
|
|
||||||
|
@agent __MODULE__
|
||||||
|
|
||||||
|
def init do
|
||||||
|
ModLastMock.init
|
||||||
|
|
||||||
|
try do
|
||||||
|
Agent.stop(@agent)
|
||||||
|
catch
|
||||||
|
:exit, _e -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, _pid} = Agent.start_link(fn -> [] end, name: @agent)
|
||||||
|
|
||||||
|
mock(:ejabberd_sm, :get_user_resources,
|
||||||
|
fn (user, domain) -> for s <- get_sessions(user, domain), do: s.resource end)
|
||||||
|
|
||||||
|
mock(:ejabberd_sm, :route,
|
||||||
|
fn (_from, to, {:broadcast, {:exit, _reason}}) ->
|
||||||
|
user = jid(to, :user)
|
||||||
|
domain = jid(to, :server)
|
||||||
|
resource = jid(to, :resource)
|
||||||
|
disconnect_resource(user, domain, resource)
|
||||||
|
:ok
|
||||||
|
(_, _, _) -> :ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect_resource(user, domain, resource,
|
||||||
|
opts \\ [priority: 1, conn: :c2s]) do
|
||||||
|
Agent.update(@agent, fn sessions ->
|
||||||
|
session = %{user: user, domain: domain, resource: resource,
|
||||||
|
timestamp: :os.timestamp, pid: self, node: node,
|
||||||
|
auth_module: :ejabberd_auth, ip: :undefined,
|
||||||
|
priority: opts[:priority], conn: opts[:conn]}
|
||||||
|
[session | sessions]
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect_resource(user, domain, resource) do
|
||||||
|
disconnect_resource(user, domain, resource, ModLastMock.now)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect_resource(user, domain, resource, timestamp) do
|
||||||
|
Agent.update(@agent, fn sessions ->
|
||||||
|
for s <- sessions,
|
||||||
|
s.user != user or s.domain != domain or s.resource != resource, do: s
|
||||||
|
end)
|
||||||
|
ModLastMock.set_last user, domain, "", timestamp
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_sessions() do
|
||||||
|
Agent.get(@agent, fn sessions -> sessions end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_sessions(user, domain) do
|
||||||
|
Agent.get(@agent, fn sessions ->
|
||||||
|
for s <- sessions, s.user == user, s.domain == domain, do: s
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_session(user, domain, resource) do
|
||||||
|
Agent.get(@agent, fn sessions ->
|
||||||
|
for s <- sessions,
|
||||||
|
s.user == user, s.domain == domain, s.resource == resource, do: s
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_record(s) do
|
||||||
|
session(usr: {s.user, s.domain, s.ressource},
|
||||||
|
us: {s.user, s.domain},
|
||||||
|
sid: {s.timestamp, s.pid},
|
||||||
|
priority: s.priority,
|
||||||
|
info: [conn: s.conn, ip: s.ip, node: s.node,
|
||||||
|
oor: false, auth_module: s.auth_module])
|
||||||
|
end
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# Helpers
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
|
||||||
|
# TODO refactor: Move to ejabberd_test_mock
|
||||||
|
def mock(module, function, fun) do
|
||||||
|
try do
|
||||||
|
:meck.new(module)
|
||||||
|
catch
|
||||||
|
:error, {:already_started, _pid} -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
:meck.expect(module, function, fun)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
check_meck(),
|
check_meck(),
|
||||||
|
code:add_pathz(filename:join(test_dir(), "../include")),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
init_per_testcase(_TestCase, Config) ->
|
init_per_testcase(_TestCase, Config) ->
|
||||||
@ -66,6 +67,11 @@ undefined_function(Module, Func, Args) ->
|
|||||||
|
|
||||||
run_elixir_test(Func) ->
|
run_elixir_test(Func) ->
|
||||||
'Elixir.ExUnit':start([]),
|
'Elixir.ExUnit':start([]),
|
||||||
|
filelib:fold_files(test_dir(), ".*\\.exs\$", true,
|
||||||
|
fun (File, N) ->
|
||||||
|
'Elixir.Code':require_file(list_to_binary(File)),
|
||||||
|
N+1
|
||||||
|
end, 0),
|
||||||
'Elixir.Code':load_file(list_to_binary(filename:join(test_dir(), atom_to_list(Func)))),
|
'Elixir.Code':load_file(list_to_binary(filename:join(test_dir(), atom_to_list(Func)))),
|
||||||
%% I did not use map syntax, so that this file can still be build under R16
|
%% I did not use map syntax, so that this file can still be build under R16
|
||||||
ResultMap = 'Elixir.ExUnit':run(),
|
ResultMap = 'Elixir.ExUnit':run(),
|
||||||
|
699
test/mod_admin_extra_test.exs
Normal file
699
test/mod_admin_extra_test.exs
Normal file
@ -0,0 +1,699 @@
|
|||||||
|
# ----------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# ejabberd, Copyright (C) 2002-2015 ProcessOne
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License as
|
||||||
|
# published by the Free Software Foundation; either version 2 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
defmodule EjabberdModAdminExtraTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
|
@user "user"
|
||||||
|
@domain "domain"
|
||||||
|
@password "password"
|
||||||
|
@resource "resource"
|
||||||
|
|
||||||
|
require Record
|
||||||
|
Record.defrecord :jid, Record.extract(:jid,
|
||||||
|
from: "jlib.hrl")
|
||||||
|
|
||||||
|
setup_all do
|
||||||
|
try do
|
||||||
|
:stringprep.start
|
||||||
|
:mnesia.start
|
||||||
|
:p1_sha.load_nif
|
||||||
|
rescue
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
:ejabberd_commands.init
|
||||||
|
:mod_admin_extra.start(@domain, [])
|
||||||
|
:sel_application.start_app(:moka)
|
||||||
|
{:ok, _pid} = :ejabberd_hooks.start_link
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do
|
||||||
|
:meck.unload
|
||||||
|
EjabberdAuthMock.init
|
||||||
|
EjabberdSmMock.init
|
||||||
|
ModRosterMock.init(@domain, :mod_admin_extra)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
###################### Accounts
|
||||||
|
test "check_account works" do
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @password
|
||||||
|
|
||||||
|
assert :ejabberd_commands.execute_command(:check_account, [@user, @domain])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_account, [@user, "bad_domain"])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_account, ["bad_user", @domain])
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
test "check_password works" do
|
||||||
|
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @password
|
||||||
|
|
||||||
|
assert :ejabberd_commands.execute_command(:check_password,
|
||||||
|
[@user, @domain, @password])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_password,
|
||||||
|
[@user, @domain, "bad_password"])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_password,
|
||||||
|
[@user, "bad_domain", @password])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_password,
|
||||||
|
["bad_user", @domain, @password])
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
test "check_password_hash works" do
|
||||||
|
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @password
|
||||||
|
hash = "5F4DCC3B5AA765D61D8327DEB882CF99" # echo -n password|md5
|
||||||
|
|
||||||
|
assert :ejabberd_commands.execute_command(:check_password_hash,
|
||||||
|
[@user, @domain, hash, "md5"])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_password_hash,
|
||||||
|
[@user, @domain, "bad_hash", "md5"])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_password_hash,
|
||||||
|
[@user, "bad_domain", hash, "md5"])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_password_hash,
|
||||||
|
["bad_user", @domain, hash, "md5"])
|
||||||
|
|
||||||
|
hash = "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8" # echo -n password|shasum
|
||||||
|
assert :ejabberd_commands.execute_command(:check_password_hash,
|
||||||
|
[@user, @domain, hash, "sha"])
|
||||||
|
|
||||||
|
assert :unkown_hash_method ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:check_password_hash,
|
||||||
|
[@user, @domain, hash, "bad_method"])
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
test "change_password works" do
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @password
|
||||||
|
|
||||||
|
assert :ejabberd_commands.execute_command(:change_password,
|
||||||
|
[@user, @domain, "new_password"])
|
||||||
|
refute :ejabberd_commands.execute_command(:check_password,
|
||||||
|
[@user, @domain, @password])
|
||||||
|
assert :ejabberd_commands.execute_command(:check_password,
|
||||||
|
[@user, @domain, "new_password"])
|
||||||
|
assert {:not_found, 'unknown_user'} ==
|
||||||
|
catch_throw :ejabberd_commands.execute_command(:change_password,
|
||||||
|
["bad_user", @domain,
|
||||||
|
@password])
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
test "check_users_registration works" do
|
||||||
|
EjabberdAuthMock.create_user @user<>"1", @domain, @password
|
||||||
|
EjabberdAuthMock.create_user @user<>"2", @domain, @password
|
||||||
|
EjabberdAuthMock.create_user @user<>"3", @domain, @password
|
||||||
|
|
||||||
|
assert [{@user<>"0", @domain, 0},
|
||||||
|
{@user<>"1", @domain, 1},
|
||||||
|
{@user<>"2", @domain, 1},
|
||||||
|
{@user<>"3", @domain, 1}] ==
|
||||||
|
:ejabberd_commands.execute_command(:check_users_registration,
|
||||||
|
[[{@user<>"0", @domain},
|
||||||
|
{@user<>"1", @domain},
|
||||||
|
{@user<>"2", @domain},
|
||||||
|
{@user<>"3", @domain}]])
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
###################### Sessions
|
||||||
|
|
||||||
|
test "num_resources works" do
|
||||||
|
assert 0 == :ejabberd_commands.execute_command(:num_resources,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource
|
||||||
|
assert 1 == :ejabberd_commands.execute_command(:num_resources,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
|
||||||
|
assert 2 == :ejabberd_commands.execute_command(:num_resources,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
|
||||||
|
assert 2 == :ejabberd_commands.execute_command(:num_resources,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
EjabberdSmMock.disconnect_resource @user, @domain, @resource
|
||||||
|
assert 1 == :ejabberd_commands.execute_command(:num_resources,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_sm
|
||||||
|
end
|
||||||
|
|
||||||
|
test "resource_num works" do
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"3"
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
|
||||||
|
|
||||||
|
assert :bad_argument ==
|
||||||
|
elem(catch_throw(:ejabberd_commands.execute_command(:resource_num,
|
||||||
|
[@user, @domain, 0])), 0)
|
||||||
|
assert @resource<>"1" ==
|
||||||
|
:ejabberd_commands.execute_command(:resource_num, [@user, @domain, 1])
|
||||||
|
assert @resource<>"3" ==
|
||||||
|
:ejabberd_commands.execute_command(:resource_num, [@user, @domain, 3])
|
||||||
|
assert :bad_argument ==
|
||||||
|
elem(catch_throw(:ejabberd_commands.execute_command(:resource_num,
|
||||||
|
[@user, @domain, 4])), 0)
|
||||||
|
assert :meck.validate :ejabberd_sm
|
||||||
|
end
|
||||||
|
|
||||||
|
test "kick_session works" do
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"3"
|
||||||
|
|
||||||
|
assert 3 == length EjabberdSmMock.get_sessions @user, @domain
|
||||||
|
assert 1 == length EjabberdSmMock.get_session @user, @domain, @resource<>"2"
|
||||||
|
|
||||||
|
assert :ok ==
|
||||||
|
:ejabberd_commands.execute_command(:kick_session,
|
||||||
|
[@user, @domain,
|
||||||
|
@resource<>"2", "kick"])
|
||||||
|
|
||||||
|
assert 2 == length EjabberdSmMock.get_sessions @user, @domain
|
||||||
|
assert 0 == length EjabberdSmMock.get_session @user, @domain, @resource<>"2"
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_sm
|
||||||
|
end
|
||||||
|
|
||||||
|
###################### Last
|
||||||
|
|
||||||
|
test "get_last works" do
|
||||||
|
|
||||||
|
assert 'Never' ==
|
||||||
|
:ejabberd_commands.execute_command(:get_last, [@user, @domain])
|
||||||
|
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"1"
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource<>"2"
|
||||||
|
|
||||||
|
assert 'Online' ==
|
||||||
|
:ejabberd_commands.execute_command(:get_last, [@user, @domain])
|
||||||
|
|
||||||
|
EjabberdSmMock.disconnect_resource @user, @domain, @resource<>"1"
|
||||||
|
|
||||||
|
assert 'Online' ==
|
||||||
|
:ejabberd_commands.execute_command(:get_last, [@user, @domain])
|
||||||
|
|
||||||
|
now = {megasecs, secs, _microsecs} = :os.timestamp
|
||||||
|
timestamp = megasecs * 1000000 + secs
|
||||||
|
EjabberdSmMock.disconnect_resource(@user, @domain, @resource<>"2",
|
||||||
|
timestamp)
|
||||||
|
{{year, month, day}, {hour, minute, second}} = :calendar.now_to_local_time now
|
||||||
|
result = List.flatten(:io_lib.format(
|
||||||
|
"~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w ",
|
||||||
|
[year, month, day, hour, minute, second]))
|
||||||
|
assert result ==
|
||||||
|
:ejabberd_commands.execute_command(:get_last, [@user, @domain])
|
||||||
|
|
||||||
|
assert :meck.validate :mod_last
|
||||||
|
end
|
||||||
|
|
||||||
|
###################### Roster
|
||||||
|
|
||||||
|
test "add_rosteritem and delete_rosteritem work" do
|
||||||
|
# Connect user
|
||||||
|
# Add user1 & user2 to user's roster
|
||||||
|
# Remove user1 & user2 from user's roster
|
||||||
|
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource
|
||||||
|
|
||||||
|
assert [] == ModRosterMock.get_roster(@user, @domain)
|
||||||
|
|
||||||
|
assert :ok ==
|
||||||
|
:ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
|
||||||
|
@user<>"1", @domain,
|
||||||
|
"nick1",
|
||||||
|
"group1",
|
||||||
|
"both"])
|
||||||
|
# Check that user1 is the only item of the user's roster
|
||||||
|
result = ModRosterMock.get_roster(@user, @domain)
|
||||||
|
assert 1 == length result
|
||||||
|
[{{@user, @domain, jid}, opts}] = result
|
||||||
|
assert @user<>"1@"<>@domain == jid
|
||||||
|
assert "nick1" == opts.nick
|
||||||
|
assert ["group1"] == opts.groups
|
||||||
|
assert :both == opts.subs
|
||||||
|
|
||||||
|
# Check that the item roster user1 was pushed with subscription
|
||||||
|
# 'both' to user online ressource
|
||||||
|
jid = :jlib.make_jid(@user, @domain, @resource)
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
|
||||||
|
|
||||||
|
assert :ok ==
|
||||||
|
:ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
|
||||||
|
@user<>"2", @domain,
|
||||||
|
"nick2",
|
||||||
|
"group2",
|
||||||
|
"both"])
|
||||||
|
result = ModRosterMock.get_roster(@user, @domain)
|
||||||
|
assert 2 == length result
|
||||||
|
|
||||||
|
|
||||||
|
# Check that the item roster user2 was pushed with subscription
|
||||||
|
# 'both' to user online ressource
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"2", @domain, ""}, :both}}])
|
||||||
|
|
||||||
|
|
||||||
|
:ejabberd_commands.execute_command(:delete_rosteritem, [@user, @domain,
|
||||||
|
@user<>"1", @domain])
|
||||||
|
result = ModRosterMock.get_roster(@user, @domain)
|
||||||
|
assert 1 == length result
|
||||||
|
[{{@user, @domain, jid}, opts}] = result
|
||||||
|
assert @user<>"2@"<>@domain == jid
|
||||||
|
assert "nick2" == opts.nick
|
||||||
|
assert ["group2"] == opts.groups
|
||||||
|
assert :both == opts.subs
|
||||||
|
|
||||||
|
# Check that the item roster user1 was pushed with subscription
|
||||||
|
# 'none' to user online ressource
|
||||||
|
jid = :jlib.make_jid(@user, @domain, @resource)
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
|
||||||
|
|
||||||
|
:ejabberd_commands.execute_command(:delete_rosteritem, [@user, @domain,
|
||||||
|
@user<>"2", @domain])
|
||||||
|
|
||||||
|
# Check that the item roster user2 was pushed with subscription
|
||||||
|
# 'none' to user online ressource
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"2", @domain, ""}, :none}}])
|
||||||
|
|
||||||
|
# Check that nothing else was pushed to user resource
|
||||||
|
jid = jid(user: @user, server: @domain, resource: :_,
|
||||||
|
luser: @user, lserver: @domain, lresource: :_)
|
||||||
|
assert 4 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, :_, :_}}])
|
||||||
|
|
||||||
|
assert [] == ModRosterMock.get_roster(@user, @domain)
|
||||||
|
assert :meck.validate :ejabberd_sm
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get_roster works" do
|
||||||
|
assert [] == ModRosterMock.get_roster(@user, @domain)
|
||||||
|
assert [] == :ejabberd_commands.execute_command(:get_roster, [@user, @domain],
|
||||||
|
:admin)
|
||||||
|
|
||||||
|
assert :ok ==
|
||||||
|
:ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
|
||||||
|
@user<>"1", @domain,
|
||||||
|
"nick1",
|
||||||
|
"group1",
|
||||||
|
"both"])
|
||||||
|
assert [{@user<>"1@"<>@domain, "", 'both', 'none', "group1"}] ==
|
||||||
|
:ejabberd_commands.execute_command(:get_roster, [@user, @domain], :admin)
|
||||||
|
assert :ok ==
|
||||||
|
:ejabberd_commands.execute_command(:add_rosteritem, [@user, @domain,
|
||||||
|
@user<>"2", @domain,
|
||||||
|
"nick2",
|
||||||
|
"group2",
|
||||||
|
"none"])
|
||||||
|
result = :ejabberd_commands.execute_command(:get_roster, [@user, @domain], :admin)
|
||||||
|
assert 2 == length result
|
||||||
|
assert Enum.member?(result, {@user<>"1@"<>@domain, "", 'both', 'none', "group1"})
|
||||||
|
assert Enum.member?(result, {@user<>"2@"<>@domain, "", 'none', 'none', "group2"})
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
test "link_contacts & unlink_contacts work" do
|
||||||
|
# Create user1 and keep it offline
|
||||||
|
EjabberdAuthMock.create_user @user<>"1", @domain, @password
|
||||||
|
|
||||||
|
# fail if one of the users doesn't exist locally
|
||||||
|
assert 404 ==
|
||||||
|
:ejabberd_commands.execute_command(:link_contacts, [@user<>"1@"<>@domain,
|
||||||
|
"nick1",
|
||||||
|
"group1",
|
||||||
|
@user<>"2@"<>@domain,
|
||||||
|
"nick2",
|
||||||
|
"group2"])
|
||||||
|
|
||||||
|
# Create user2 and connect 2 resources
|
||||||
|
EjabberdAuthMock.create_user @user<>"2", @domain, @password
|
||||||
|
|
||||||
|
EjabberdSmMock.connect_resource @user<>"2", @domain, @resource<>"1"
|
||||||
|
EjabberdSmMock.connect_resource @user<>"2", @domain, @resource<>"2"
|
||||||
|
|
||||||
|
# Link both user1 & user2 (returns 0 if OK)
|
||||||
|
assert 0 ==
|
||||||
|
:ejabberd_commands.execute_command(:link_contacts, [@user<>"1@"<>@domain,
|
||||||
|
"nick1",
|
||||||
|
"group2",
|
||||||
|
@user<>"2@"<>@domain,
|
||||||
|
"nick2",
|
||||||
|
"group1"])
|
||||||
|
assert [{@user<>"2@"<>@domain, "", 'both', 'none', "group2"}] ==
|
||||||
|
:ejabberd_commands.execute_command(:get_roster, [@user<>"1", @domain], :admin)
|
||||||
|
|
||||||
|
assert [{@user<>"1@"<>@domain, "", 'both', 'none', "group1"}] ==
|
||||||
|
:ejabberd_commands.execute_command(:get_roster, [@user<>"2", @domain], :admin)
|
||||||
|
|
||||||
|
# Check that the item roster user1 was pushed with subscription
|
||||||
|
# 'both' to the 2 user2 online ressources
|
||||||
|
jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"1")
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
|
||||||
|
jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"2")
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
|
||||||
|
|
||||||
|
|
||||||
|
# Ulink both user1 & user2 (returns 0 if OK)
|
||||||
|
assert 0 ==
|
||||||
|
:ejabberd_commands.execute_command(:unlink_contacts, [@user<>"1@"<>@domain,
|
||||||
|
@user<>"2@"<>@domain])
|
||||||
|
assert [] ==
|
||||||
|
:ejabberd_commands.execute_command(:get_roster, [@user<>"1", @domain], :admin)
|
||||||
|
|
||||||
|
assert [] ==
|
||||||
|
:ejabberd_commands.execute_command(:get_roster, [@user<>"2", @domain], :admin)
|
||||||
|
|
||||||
|
# Check that the item roster user1 was pushed with subscription
|
||||||
|
# 'none' to the 2 user2 online ressources
|
||||||
|
jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"1")
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
|
||||||
|
jid = :jlib.make_jid(@user<>"2", @domain, @resource<>"2")
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
|
||||||
|
|
||||||
|
# Check that nothing else was pushed to user2 resources
|
||||||
|
jid = jid(user: @user<>"2", server: @domain, resource: :_,
|
||||||
|
luser: @user<>"2", lserver: @domain, lresource: :_)
|
||||||
|
assert 4 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, :_, :_}}])
|
||||||
|
|
||||||
|
# Check nothing was pushed to user1
|
||||||
|
jid = jid(user: @user<>"1", server: @domain, resource: :_,
|
||||||
|
luser: @user<>"1", lserver: @domain, lresource: :_)
|
||||||
|
refute :meck.called(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, :_, :_}}])
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_sm
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test "add_contacts and delete_contacts work" do
|
||||||
|
# Create user, user1 & user2
|
||||||
|
# Connect user & user1
|
||||||
|
# Add user0, user1 & user2 to user's roster
|
||||||
|
# Remove user0, user1 & user2 from user's roster
|
||||||
|
|
||||||
|
# user doesn't exists yet, command must fail
|
||||||
|
assert 404 ==
|
||||||
|
:ejabberd_commands.execute_command(:add_contacts, [@user, @domain,
|
||||||
|
[{@user<>"1"<>@domain,
|
||||||
|
"group1",
|
||||||
|
"nick1"},
|
||||||
|
{@user<>"2"<>@domain,
|
||||||
|
"group2",
|
||||||
|
"nick2"}]
|
||||||
|
])
|
||||||
|
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @password
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource
|
||||||
|
EjabberdAuthMock.create_user @user<>"1", @domain, @password
|
||||||
|
EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
|
||||||
|
EjabberdAuthMock.create_user @user<>"2", @domain, @password
|
||||||
|
|
||||||
|
# Add user1 & user2 in user's roster. Try also to add user0 that
|
||||||
|
# doesn't exists. Command is supposed to return number of added items.
|
||||||
|
assert 2 ==
|
||||||
|
:ejabberd_commands.execute_command(:add_contacts, [@user, @domain,
|
||||||
|
[{@user<>"0@"<>@domain,
|
||||||
|
"group0",
|
||||||
|
"nick0"},
|
||||||
|
{@user<>"1@"<>@domain,
|
||||||
|
"group1",
|
||||||
|
"nick1"},
|
||||||
|
{@user<>"2@"<>@domain,
|
||||||
|
"group2",
|
||||||
|
"nick2"}]
|
||||||
|
])
|
||||||
|
# Check that user1 & user2 are the only items in user's roster
|
||||||
|
result = ModRosterMock.get_roster(@user, @domain)
|
||||||
|
assert 2 == length result
|
||||||
|
opts1 = %{nick: "nick1", groups: ["group1"], subs: :both,
|
||||||
|
ask: :none, askmessage: ""}
|
||||||
|
assert Enum.member?(result, {{@user, @domain, @user<>"1@"<>@domain}, opts1})
|
||||||
|
opts2 = %{nick: "nick2", groups: ["group2"], subs: :both,
|
||||||
|
ask: :none, askmessage: ""}
|
||||||
|
assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
|
||||||
|
|
||||||
|
# Check that user is the only item in user1's roster
|
||||||
|
assert [{{@user<>"1", @domain, @user<>"@"<>@domain}, %{opts1|:nick => ""}}] ==
|
||||||
|
ModRosterMock.get_roster(@user<>"1", @domain)
|
||||||
|
|
||||||
|
# Check that user is the only item in user2's roster
|
||||||
|
assert [{{@user<>"2", @domain, @user<>"@"<>@domain}, %{opts2|:nick => ""}}] ==
|
||||||
|
ModRosterMock.get_roster(@user<>"2", @domain)
|
||||||
|
|
||||||
|
|
||||||
|
# Check that the roster items user1 & user2 were pushed with subscription
|
||||||
|
# 'both' to the user online ressource
|
||||||
|
jid = :jlib.make_jid(@user, @domain, @resource)
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"2", @domain, ""}, :both}}])
|
||||||
|
|
||||||
|
# Check that the roster item user was pushed with subscription
|
||||||
|
# 'both' to the user1 online ressource
|
||||||
|
jid = :jlib.make_jid(@user<>"1", @domain, @resource)
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user, @domain, ""}, :both}}])
|
||||||
|
|
||||||
|
# Check that nothing else was pushed to online resources
|
||||||
|
assert 3 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[:_, :_,
|
||||||
|
{:broadcast, {:item, :_, :_}}])
|
||||||
|
|
||||||
|
# Remove user1 & user2 from user's roster. Try also to remove
|
||||||
|
# user0 that doesn't exists. Command is supposed to return number
|
||||||
|
# of removed items.
|
||||||
|
assert 2 ==
|
||||||
|
:ejabberd_commands.execute_command(:remove_contacts, [@user, @domain,
|
||||||
|
[@user<>"0@"<>@domain,
|
||||||
|
@user<>"1@"<>@domain,
|
||||||
|
@user<>"2@"<>@domain]
|
||||||
|
])
|
||||||
|
# Check that roster of user, user1 & user2 are empty
|
||||||
|
assert [] == ModRosterMock.get_roster(@user, @domain)
|
||||||
|
assert [] == ModRosterMock.get_roster(@user<>"1", @domain)
|
||||||
|
assert [] == ModRosterMock.get_roster(@user<>"2", @domain)
|
||||||
|
|
||||||
|
# Check that the roster items user1 & user2 were pushed with subscription
|
||||||
|
# 'none' to the user online ressource
|
||||||
|
jid = :jlib.make_jid(@user, @domain, @resource)
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"2", @domain, ""}, :none}}])
|
||||||
|
|
||||||
|
# Check that the roster item user was pushed with subscription
|
||||||
|
# 'none' to the user1 online ressource
|
||||||
|
jid = :jlib.make_jid(@user<>"1", @domain, @resource)
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user, @domain, ""}, :none}}])
|
||||||
|
|
||||||
|
# Check that nothing else was pushed to online resources
|
||||||
|
assert 6 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[:_, :_,
|
||||||
|
{:broadcast, {:item, :_, :_}}])
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_sm
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
test "update_roster works" do
|
||||||
|
# user doesn't exists yet, command must fail
|
||||||
|
result =
|
||||||
|
:ejabberd_commands.execute_command(:update_roster,
|
||||||
|
[@user, @domain,
|
||||||
|
[{@user<>"1"<>@domain,
|
||||||
|
"group1",
|
||||||
|
"nick1"},
|
||||||
|
{@user<>"2"<>@domain,
|
||||||
|
"group2",
|
||||||
|
"nick2"}],
|
||||||
|
[]
|
||||||
|
])
|
||||||
|
assert :invalid_user == elem(result, 0)
|
||||||
|
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @password
|
||||||
|
EjabberdSmMock.connect_resource @user, @domain, @resource
|
||||||
|
EjabberdAuthMock.create_user @user<>"1", @domain, @password
|
||||||
|
EjabberdSmMock.connect_resource @user<>"1", @domain, @resource
|
||||||
|
EjabberdAuthMock.create_user @user<>"2", @domain, @password
|
||||||
|
EjabberdAuthMock.create_user @user<>"3", @domain, @password
|
||||||
|
|
||||||
|
assert :ok ==
|
||||||
|
:ejabberd_commands.execute_command(:update_roster,
|
||||||
|
[@user, @domain,
|
||||||
|
[{[{"username", @user<>"1"},
|
||||||
|
{"nick", "nick1"}]},
|
||||||
|
{[{"username", @user<>"2"},
|
||||||
|
{"nick", "nick2"},
|
||||||
|
{"subscription", "from"}]}],
|
||||||
|
[]])
|
||||||
|
|
||||||
|
|
||||||
|
# Check that user1 & user2 are the only items in user's roster
|
||||||
|
result = ModRosterMock.get_roster(@user, @domain)
|
||||||
|
|
||||||
|
assert 2 == length result
|
||||||
|
opts1 = %{nick: "nick1", groups: [""], subs: :both,
|
||||||
|
ask: :none, askmessage: ""}
|
||||||
|
assert Enum.member?(result, {{@user, @domain, @user<>"1@"<>@domain}, opts1})
|
||||||
|
opts2 = %{nick: "nick2", groups: [""], subs: :from,
|
||||||
|
ask: :none, askmessage: ""}
|
||||||
|
assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
|
||||||
|
|
||||||
|
# Check that the roster items user1 & user2 were pushed with subscription
|
||||||
|
# 'both' to the user online ressource
|
||||||
|
jid = :jlib.make_jid(@user, @domain, @resource)
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :both}}])
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"2", @domain, ""}, :from}}])
|
||||||
|
|
||||||
|
# Check that nothing else was pushed to online resources
|
||||||
|
assert 2 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[:_, :_,
|
||||||
|
{:broadcast, {:item, :_, :_}}])
|
||||||
|
|
||||||
|
# Add user3 & remove user1
|
||||||
|
assert :ok ==
|
||||||
|
:ejabberd_commands.execute_command(:update_roster,
|
||||||
|
[@user, @domain,
|
||||||
|
[{[{"username", @user<>"3"},
|
||||||
|
{"nick", "nick3"},
|
||||||
|
{"subscription", "to"}]}],
|
||||||
|
[{[{"username", @user<>"1"}]}]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Check that user2 & user3 are the only items in user's roster
|
||||||
|
result = ModRosterMock.get_roster(@user, @domain)
|
||||||
|
assert 2 == length result
|
||||||
|
opts2 = %{nick: "nick2", groups: [""], subs: :from,
|
||||||
|
ask: :none, askmessage: ""}
|
||||||
|
assert Enum.member?(result, {{@user, @domain, @user<>"2@"<>@domain}, opts2})
|
||||||
|
opts1 = %{nick: "nick3", groups: [""], subs: :to,
|
||||||
|
ask: :none, askmessage: ""}
|
||||||
|
assert Enum.member?(result, {{@user, @domain, @user<>"3@"<>@domain}, opts1})
|
||||||
|
|
||||||
|
# Check that the roster items user1 & user3 were pushed with subscription
|
||||||
|
# 'none' & 'to' to the user online ressource
|
||||||
|
jid = :jlib.make_jid(@user, @domain, @resource)
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"1", @domain, ""}, :none}}])
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[jid, jid,
|
||||||
|
{:broadcast, {:item, {@user<>"3", @domain, ""}, :to}}])
|
||||||
|
|
||||||
|
# Check that nothing else was pushed to online resources
|
||||||
|
assert 4 ==
|
||||||
|
:meck.num_calls(:ejabberd_sm, :route,
|
||||||
|
[:_, :_,
|
||||||
|
{:broadcast, {:item, :_, :_}}])
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_sm
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# kick_user command is defined in ejabberd_sm, move to extra?
|
||||||
|
# test "kick_user works" do
|
||||||
|
# assert 0 == :ejabberd_commands.execute_command(:num_resources,
|
||||||
|
# [@user, @domain])
|
||||||
|
# EjabberdSmMock.connect_resource(@user, @domain, @resource<>"1")
|
||||||
|
# EjabberdSmMock.connect_resource(@user, @domain, @resource<>"2")
|
||||||
|
# assert 2 ==
|
||||||
|
# :ejabberd_commands.execute_command(:kick_user, [@user, @domain])
|
||||||
|
# assert 0 == :ejabberd_commands.execute_command(:num_resources,
|
||||||
|
# [@user, @domain])
|
||||||
|
# assert :meck.validate :ejabberd_sm
|
||||||
|
# end
|
||||||
|
|
||||||
|
end
|
188
test/mod_http_api_test.exs
Normal file
188
test/mod_http_api_test.exs
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
# ----------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# ejabberd, Copyright (C) 2002-2015 ProcessOne
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License as
|
||||||
|
# published by the Free Software Foundation; either version 2 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
defmodule ModHttpApiTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
|
# Admin user
|
||||||
|
@admin "admin"
|
||||||
|
@adminpass "adminpass"
|
||||||
|
# Non admin user
|
||||||
|
@user "user"
|
||||||
|
@userpass "userpass"
|
||||||
|
# XMPP domain
|
||||||
|
@domain "domain"
|
||||||
|
# mocked command
|
||||||
|
@command "command_test"
|
||||||
|
@acommand String.to_atom(@command)
|
||||||
|
# default API version
|
||||||
|
@version 0
|
||||||
|
|
||||||
|
require Record
|
||||||
|
Record.defrecord :request, Record.extract(:request,
|
||||||
|
from: "ejabberd_http.hrl")
|
||||||
|
|
||||||
|
setup_all do
|
||||||
|
try do
|
||||||
|
:stringprep.start
|
||||||
|
rescue
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
:mod_http_api.start(@domain, [])
|
||||||
|
EjabberdOauthMock.init
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do
|
||||||
|
:meck.unload
|
||||||
|
:meck.new :ejabberd_commands
|
||||||
|
EjabberdAuthMock.init
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "HTTP GET simple command call with Basic Auth" do
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @userpass
|
||||||
|
|
||||||
|
# Mock a simple command() -> :ok
|
||||||
|
:meck.expect(:ejabberd_commands, :get_command_format,
|
||||||
|
fn (@acommand, {@user, @domain, @userpass, false}, @version) ->
|
||||||
|
{[], {:res, :rescode}}
|
||||||
|
end)
|
||||||
|
:meck.expect(:ejabberd_commands, :execute_command,
|
||||||
|
fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version) ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
#:ejabberd_logger.start
|
||||||
|
#:ejabberd_logger.set 5
|
||||||
|
|
||||||
|
# Correct Basic Auth call
|
||||||
|
req = request(method: :GET,
|
||||||
|
path: ["api", @command],
|
||||||
|
q: [nokey: ""],
|
||||||
|
# Basic auth
|
||||||
|
auth: {@user<>"@"<>@domain, @userpass},
|
||||||
|
ip: {{127,0,0,1},60000},
|
||||||
|
host: @domain)
|
||||||
|
result = :mod_http_api.process([@command], req)
|
||||||
|
assert 200 == elem(result, 0) # HTTP code
|
||||||
|
assert "0" == elem(result, 2) # command result
|
||||||
|
|
||||||
|
# Bad password
|
||||||
|
req = request(method: :GET,
|
||||||
|
path: ["api", @command],
|
||||||
|
q: [nokey: ""],
|
||||||
|
# Basic auth
|
||||||
|
auth: {@user<>"@"<>@domain, @userpass<>"bad"},
|
||||||
|
ip: {{127,0,0,1},60000},
|
||||||
|
host: @domain)
|
||||||
|
result = :mod_http_api.process([@command], req)
|
||||||
|
assert 401 == elem(result, 0) # HTTP code
|
||||||
|
|
||||||
|
# Check that the command was executed only once
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_commands, :execute_command, :_)
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
assert :meck.validate :ejabberd_commands
|
||||||
|
#assert :ok = :meck.history(:ejabberd_commands)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
test "HTTP GET simple command call with OAuth" do
|
||||||
|
EjabberdAuthMock.create_user @user, @domain, @userpass
|
||||||
|
|
||||||
|
# Mock a simple command() -> :ok
|
||||||
|
:meck.expect(:ejabberd_commands, :get_command_format,
|
||||||
|
fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
|
||||||
|
{[], {:res, :rescode}}
|
||||||
|
end)
|
||||||
|
:meck.expect(:ejabberd_commands, :execute_command,
|
||||||
|
fn (:undefined, {@user, @domain, {:oauth, _token}, false},
|
||||||
|
@acommand, [], @version) ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
#:ejabberd_logger.start
|
||||||
|
#:ejabberd_logger.set 5
|
||||||
|
|
||||||
|
# Correct OAuth call
|
||||||
|
token = EjabberdOauthMock.get_token @user, @domain, @command
|
||||||
|
req = request(method: :GET,
|
||||||
|
path: ["api", @command],
|
||||||
|
q: [nokey: ""],
|
||||||
|
# OAuth
|
||||||
|
auth: {:oauth, token, []},
|
||||||
|
ip: {{127,0,0,1},60000},
|
||||||
|
host: @domain)
|
||||||
|
result = :mod_http_api.process([@command], req)
|
||||||
|
assert 200 == elem(result, 0) # HTTP code
|
||||||
|
assert "0" == elem(result, 2) # command result
|
||||||
|
|
||||||
|
# Wrong OAuth token
|
||||||
|
req = request(method: :GET,
|
||||||
|
path: ["api", @command],
|
||||||
|
q: [nokey: ""],
|
||||||
|
# OAuth
|
||||||
|
auth: {:oauth, "bad"<>token, []},
|
||||||
|
ip: {{127,0,0,1},60000},
|
||||||
|
host: @domain)
|
||||||
|
result = :mod_http_api.process([@command], req)
|
||||||
|
assert 401 == elem(result, 0) # HTTP code
|
||||||
|
|
||||||
|
# Expired OAuth token
|
||||||
|
token = EjabberdOauthMock.get_token @user, @domain, @command, 1
|
||||||
|
:timer.sleep 1500
|
||||||
|
req = request(method: :GET,
|
||||||
|
path: ["api", @command],
|
||||||
|
q: [nokey: ""],
|
||||||
|
# OAuth
|
||||||
|
auth: {:oauth, token, []},
|
||||||
|
ip: {{127,0,0,1},60000},
|
||||||
|
host: @domain)
|
||||||
|
result = :mod_http_api.process([@command], req)
|
||||||
|
assert 401 == elem(result, 0) # HTTP code
|
||||||
|
|
||||||
|
# Wrong OAuth scope
|
||||||
|
token = EjabberdOauthMock.get_token @user, @domain, "bad_command"
|
||||||
|
:timer.sleep 1500
|
||||||
|
req = request(method: :GET,
|
||||||
|
path: ["api", @command],
|
||||||
|
q: [nokey: ""],
|
||||||
|
# OAuth
|
||||||
|
auth: {:oauth, token, []},
|
||||||
|
ip: {{127,0,0,1},60000},
|
||||||
|
host: @domain)
|
||||||
|
result = :mod_http_api.process([@command], req)
|
||||||
|
assert 401 == elem(result, 0) # HTTP code
|
||||||
|
|
||||||
|
# Check that the command was executed only once
|
||||||
|
assert 1 ==
|
||||||
|
:meck.num_calls(:ejabberd_commands, :execute_command, :_)
|
||||||
|
|
||||||
|
assert :meck.validate :ejabberd_auth
|
||||||
|
assert :meck.validate :ejabberd_commands
|
||||||
|
#assert :ok = :meck.history(:ejabberd_commands)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
65
test/mod_last_mock.exs
Normal file
65
test/mod_last_mock.exs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# mod_last mock
|
||||||
|
######################
|
||||||
|
|
||||||
|
|
||||||
|
defmodule ModLastMock do
|
||||||
|
|
||||||
|
require Record
|
||||||
|
Record.defrecord :session, Record.extract(:session,
|
||||||
|
from: "ejabberd_sm.hrl")
|
||||||
|
Record.defrecord :jid, Record.extract(:jid,
|
||||||
|
from: "jlib.hrl")
|
||||||
|
|
||||||
|
@author "jsautret@process-one.net"
|
||||||
|
@agent __MODULE__
|
||||||
|
|
||||||
|
def init do
|
||||||
|
try do
|
||||||
|
Agent.stop(@agent)
|
||||||
|
catch
|
||||||
|
:exit, _e -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
|
||||||
|
|
||||||
|
mock(:mod_last, :get_last_info,
|
||||||
|
fn (user, domain) ->
|
||||||
|
Agent.get(@agent, fn last ->
|
||||||
|
case Map.get(last, {user, domain}, :not_found) do
|
||||||
|
{ts, status} -> {:ok, ts, status}
|
||||||
|
result -> result
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_last(user, domain, status) do
|
||||||
|
set_last(user, domain, status, now)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_last(user, domain, status, timestamp) do
|
||||||
|
Agent.update(@agent, fn last ->
|
||||||
|
Map.put(last, {user, domain}, {timestamp, status})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# Helpers
|
||||||
|
####################################################################
|
||||||
|
def now() do
|
||||||
|
{megasecs, secs, _microsecs} = :os.timestamp
|
||||||
|
megasecs * 1000000 + secs
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO refactor: Move to ejabberd_test_mock
|
||||||
|
def mock(module, function, fun) do
|
||||||
|
try do
|
||||||
|
:meck.new(module)
|
||||||
|
catch
|
||||||
|
:error, {:already_started, _pid} -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
:meck.expect(module, function, fun)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
192
test/mod_roster_mock.exs
Normal file
192
test/mod_roster_mock.exs
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# mod_roster mock
|
||||||
|
######################
|
||||||
|
|
||||||
|
defmodule ModRosterMock do
|
||||||
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
|
require Record
|
||||||
|
Record.defrecord :roster, Record.extract(:roster,
|
||||||
|
from: "mod_roster.hrl")
|
||||||
|
|
||||||
|
@agent __MODULE__
|
||||||
|
|
||||||
|
def init(domain, module) do
|
||||||
|
try do
|
||||||
|
Agent.stop(@agent)
|
||||||
|
catch
|
||||||
|
:exit, _e -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, _pid} = Agent.start_link(fn -> %{} end, name: @agent)
|
||||||
|
|
||||||
|
mock_with_moka module
|
||||||
|
|
||||||
|
#:mod_roster.stop(domain)
|
||||||
|
:mod_roster.start(domain, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def mock_with_moka(module) do
|
||||||
|
try do
|
||||||
|
|
||||||
|
module_mock = :moka.start(module)
|
||||||
|
:moka.replace(module_mock, :mod_roster, :invalidate_roster_cache,
|
||||||
|
fn (_user, _server) ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
:moka.load(module_mock)
|
||||||
|
|
||||||
|
roster_mock = :moka.start(:mod_roster)
|
||||||
|
|
||||||
|
:moka.replace(roster_mock, :gen_mod, :db_type,
|
||||||
|
fn (_host, _opts) ->
|
||||||
|
{:none}
|
||||||
|
end)
|
||||||
|
|
||||||
|
:moka.replace(roster_mock, :gen_iq_handler, :add_iq_handler,
|
||||||
|
fn (_module, _host, _ns, _m, _f, _iqdisc) ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
:moka.replace(roster_mock, :gen_iq_handler, :remove_iq_handler,
|
||||||
|
fn (_module, _host, _ns) ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
:moka.replace(roster_mock, :transaction,
|
||||||
|
fn (_server, function) ->
|
||||||
|
{:atomic, function.()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
:moka.replace(roster_mock, :get_roster,
|
||||||
|
fn (user, domain) ->
|
||||||
|
to_records(get_roster(user, domain))
|
||||||
|
end)
|
||||||
|
|
||||||
|
:moka.replace(roster_mock, :update_roster_t,
|
||||||
|
fn (user, domain, {u, d, _r}, item) ->
|
||||||
|
add_roster_item(user, domain, u<>"@"<>d,
|
||||||
|
roster(item, :name),
|
||||||
|
roster(item, :subscription),
|
||||||
|
roster(item, :groups),
|
||||||
|
roster(item, :ask),
|
||||||
|
roster(item, :askmessage))
|
||||||
|
end)
|
||||||
|
|
||||||
|
:moka.replace(roster_mock, :del_roster_t,
|
||||||
|
fn (user, domain, jid) ->
|
||||||
|
remove_roster_item(user, domain, :jlib.jid_to_string(jid))
|
||||||
|
end)
|
||||||
|
|
||||||
|
:moka.load(roster_mock)
|
||||||
|
|
||||||
|
catch
|
||||||
|
{:already_started, _pid} -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def mock_with_meck do
|
||||||
|
# mock(:gen_mod, :db_type,
|
||||||
|
# fn (_server, :mod_roster) ->
|
||||||
|
# :mnesia
|
||||||
|
# end)
|
||||||
|
#
|
||||||
|
# mock(:mnesia, :transaction,
|
||||||
|
# fn (_server, function) ->
|
||||||
|
# {:atomic, function.()}
|
||||||
|
# end)
|
||||||
|
#
|
||||||
|
# mock(:mnesia, :write,
|
||||||
|
# fn (Item) ->
|
||||||
|
# throw Item
|
||||||
|
# {:atomic, :ok}
|
||||||
|
# end)
|
||||||
|
|
||||||
|
mock(:mod_roster, :transaction,
|
||||||
|
fn (_server, function) ->
|
||||||
|
{:atomic, function.()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
mock(:mod_roster, :update_roster_t,
|
||||||
|
fn (user, domain, {u, d, _r}, item) ->
|
||||||
|
add_roster_item(user, domain, u<>"@"<>d,
|
||||||
|
roster(item, :name),
|
||||||
|
roster(item, :subscription),
|
||||||
|
roster(item, :groups),
|
||||||
|
roster(item, :ask),
|
||||||
|
roster(item, :askmessage))
|
||||||
|
end)
|
||||||
|
|
||||||
|
mock(:mod_roster, :invalidate_roster_cache,
|
||||||
|
fn (_user, _server) ->
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_roster_item(user, domain, jid, nick, subs \\ :none, groups \\ [],
|
||||||
|
ask \\ :none, askmessage \\ "")
|
||||||
|
when is_binary(user) and byte_size(user) > 0
|
||||||
|
and is_binary(domain) and byte_size(domain) > 0
|
||||||
|
and is_binary(jid) and byte_size(jid) > 0
|
||||||
|
and is_binary(nick)
|
||||||
|
and is_atom(subs)
|
||||||
|
and is_list(groups)
|
||||||
|
and is_atom(ask)
|
||||||
|
and is_binary(askmessage)
|
||||||
|
do
|
||||||
|
Agent.update(@agent, fn roster ->
|
||||||
|
Map.put(roster, {user, domain, jid}, %{nick: nick,
|
||||||
|
subs: subs, groups: groups,
|
||||||
|
ask: ask, askmessage: askmessage})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_roster_item(user, domain, jid) do
|
||||||
|
Agent.update(@agent, fn roster ->
|
||||||
|
Map.delete(roster, {user, domain, jid})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_rosters() do
|
||||||
|
Agent.get(@agent, fn roster -> roster end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_roster(user, domain) do
|
||||||
|
Agent.get(@agent, fn roster ->
|
||||||
|
for {u, d, jid} <- Map.keys(roster), u == user, d == domain,
|
||||||
|
do: {{u, d, jid}, Map.fetch!(roster, {u, d, jid})}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_record({{user, domain, jid}, r}) do
|
||||||
|
roster(usj: {user, domain, jid},
|
||||||
|
us: {user, domain},
|
||||||
|
jid: :jlib.string_to_usr(jid),
|
||||||
|
subscription: r.subs,
|
||||||
|
ask: r.ask,
|
||||||
|
groups: r.groups,
|
||||||
|
askmessage: r.askmessage
|
||||||
|
)
|
||||||
|
end
|
||||||
|
def to_records(rosters) do
|
||||||
|
for item <- rosters, do: to_record(item)
|
||||||
|
end
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# Helpers
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
# TODO refactor: Move to ejabberd_test_mock
|
||||||
|
def mock(module, function, fun) do
|
||||||
|
try do
|
||||||
|
:meck.new(module, [:non_strict, :passthrough, :unstick])
|
||||||
|
catch
|
||||||
|
:error, {:already_started, _pid} -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
:meck.expect(module, function, fun)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user