aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMickaël Rémond <mickael.remond@process-one.net>2016-04-01 14:24:08 +0200
committerMickaël Rémond <mickael.remond@process-one.net>2016-04-01 14:24:08 +0200
commitca9ac019eb4a3abaade0c4c9eb31e0fe83ed875d (patch)
tree122ea1e7de3bc03f1074ec4a1feba3ddfa92c719
parentTest / Document ejabberd_commands checks (diff)
parentApply fixes and remove tests for missing methods (diff)
Merge pull request #1046 from processone/commands-update
Add support for versioning in ejabberd commands
-rw-r--r--include/ejabberd_commands.hrl5
-rw-r--r--include/logger.hrl7
-rw-r--r--rebar.config14
-rw-r--r--src/ejabberd_commands.erl293
-rw-r--r--src/ejabberd_ctl.erl141
-rw-r--r--src/mod_admin_extra.erl48
-rw-r--r--src/mod_http_api.erl169
-rw-r--r--test/README-quicktest.md33
-rw-r--r--test/ejabberd_admin_test.exs79
-rw-r--r--test/ejabberd_auth_mock.exs74
-rw-r--r--test/ejabberd_commands_mock_test.exs423
-rw-r--r--test/ejabberd_commands_test.exs5
-rw-r--r--test/ejabberd_hooks_test.exs6
-rw-r--r--test/ejabberd_oauth_mock.exs47
-rw-r--r--test/ejabberd_sm_mock.exs121
-rw-r--r--test/elixir_SUITE.erl30
-rw-r--r--test/mod_admin_extra_test.exs356
-rw-r--r--test/mod_http_api_mock_test.exs197
-rw-r--r--test/mod_last_mock.exs79
-rw-r--r--test/mod_roster_mock.exs208
20 files changed, 2091 insertions, 244 deletions
diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl
index 0742e3ba4..81be06dc3 100644
--- a/include/ejabberd_commands.hrl
+++ b/include/ejabberd_commands.hrl
@@ -31,8 +31,9 @@
tags = [] :: [atom()] | '_' | '$2',
desc = "" :: string() | '_' | '$3',
longdesc = "" :: string() | '_',
- module :: atom(),
- function :: atom(),
+ version = 0 :: integer(),
+ module :: atom() | '_',
+ function :: atom() | '_',
args = [] :: [aterm()] | '_' | '$1' | '$2',
policy = restricted :: open | restricted | admin | user,
result = {res, rescode} :: rterm() | '_' | '$2',
diff --git a/include/logger.hrl b/include/logger.hrl
index 18dc7041c..df0776863 100644
--- a/include/logger.hrl
+++ b/include/logger.hrl
@@ -34,3 +34,10 @@
-define(CRITICAL_MSG(Format, Args),
lager:critical(Format, Args)).
+
+%% Use only when trying to troubleshoot test problem with ExUnit
+-define(EXUNIT_LOG(Format, Args),
+ case lists:keyfind(logger, 1, application:loaded_applications()) of
+ false -> ok;
+ _ -> 'Elixir.Logger':bare_log(error, io_lib:format(Format, Args), [?MODULE])
+ end).
diff --git a/rebar.config b/rebar.config
index 210e623ae..f214482dd 100644
--- a/rebar.config
+++ b/rebar.config
@@ -30,10 +30,14 @@
{tag, "1.0.0"}}}},
{if_var_true, zlib, {ezlib, ".*", {git, "https://github.com/processone/ezlib",
{tag, "1.0.1"}}}},
- {if_var_true, riak, {hamcrest, ".*", {git, "https://github.com/hyperthunk/hamcrest-erlang",
- "908a24fda4a46776a5135db60ca071e3d783f9f6"}}}, % for riak_pb-2.1.0.7
{if_var_true, riak, {riakc, ".*", {git, "https://github.com/basho/riak-erlang-client",
"527722d12d0433b837cdb92a60900c2cb5df8942"}}},
+ %% Forces correct dependency for riakc and allow using newer meck version)
+ {if_var_true, riak, {hamcrest, ".*", {git, "https://github.com/hyperthunk/hamcrest-erlang",
+ "908a24fda4a46776a5135db60ca071e3d783f9f6"}}}, % for riak_pb-2.1.0.7
+ {if_var_true, riak, {protobuffs, ".*", {git, "https://github.com/basho/erlang_protobuffs",
+ "6e7fc924506e2dc166a6170e580ce1d95ebbd5bd"}}}, % for riak_pb-2.1.0.7 with correct meck dependency
+ %% Elixir support, needed to run tests
{if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir",
{tag, "v1.1.1"}}}},
%% TODO: When modules are fully migrated to new structure and mix, we will not need anymore rebar_elixir_plugin
@@ -41,8 +45,10 @@
{git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}},
{if_var_true, iconv, {iconv, ".*", {git, "https://github.com/processone/iconv",
{tag, "1.0.0"}}}},
- {if_var_true, tools, {meck, "0.8.2", {git, "https://github.com/eproxus/meck",
- {tag, "0.8.2"}}}},
+ {if_var_true, tools, {meck, "0.8.*", {git, "https://github.com/eproxus/meck",
+ {tag, "0.8.4"}}}},
+ {if_var_true, tools, {moka, ".*", {git, "https://github.com/processone/moka.git",
+ {tag, "1.0.5b"}}}},
{if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis",
{tag, "v1.0.8"}}}}]}.
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index 57285d6c6..dd14748d0 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -90,7 +90,8 @@
%%% PowFloat = math:pow(Base, Exponent),
%%% 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:
%%% <pre>-export([calc_power/2]).</pre>
%%%
@@ -201,25 +202,34 @@
%%% TODO: consider this feature:
%%% All commands are catched. If an error happens, return the restuple:
%%% {error, flattened error string}
-%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this.
-%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response.
+%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc)
+%%% need to allows this. And ejabberd_xmlrpc must be prepared to
+%%% handle such an unexpected response.
-module(ejabberd_commands).
-author('badlop@process-one.net').
+-define(DEFAULT_VERSION, 1000000).
+
-export([init/0,
list_commands/0,
+ list_commands/1,
get_command_format/1,
- get_command_format/2,
+ get_command_format/2,
+ get_command_format/3,
get_command_policy/1,
get_command_definition/1,
+ get_command_definition/2,
get_tags_commands/0,
+ get_tags_commands/1,
get_commands/0,
register_commands/1,
unregister_commands/1,
execute_command/2,
- execute_command/4,
+ execute_command/3,
+ execute_command/4,
+ execute_command/5,
opt_type/1,
get_commands_spec/0
]).
@@ -227,6 +237,7 @@
-include("ejabberd_commands.hrl").
-include("ejabberd.hrl").
-include("logger.hrl").
+-include_lib("stdlib/include/ms_transform.hrl").
-define(POLICY_ACCESS, '$policy').
@@ -261,23 +272,26 @@ get_commands_spec() ->
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
result_example = ok}].
init() ->
- ets:new(ejabberd_commands, [named_table, set, public,
- {keypos, #ejabberd_commands.name}]),
+ mnesia:delete_table(ejabberd_commands),
+ 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()).
-spec register_commands([ejabberd_commands()]) -> ok.
%% @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) ->
lists:foreach(
fun(Command) ->
- case ets:insert_new(ejabberd_commands, Command) of
- true ->
- ok;
- false ->
- ?DEBUG("This command is already defined:~n~p", [Command])
- end
+ % XXX check if command exists
+ mnesia:dirty_write(Command)
+ % ?DEBUG("This command is already defined:~n~p", [Command])
end,
Commands).
@@ -287,7 +301,7 @@ register_commands(Commands) ->
unregister_commands(Commands) ->
lists:foreach(
fun(Command) ->
- ets:delete_object(ejabberd_commands, Command)
+ mnesia:dirty_delete_object(Command)
end,
Commands).
@@ -295,47 +309,59 @@ unregister_commands(Commands) ->
%% @doc Get a list of all the available commands, arguments and description.
list_commands() ->
- Commands = ets:match(ejabberd_commands,
- #ejabberd_commands{name = '$1',
- args = '$2',
- desc = '$3',
- _ = '_'}),
- [{A, B, C} || [A, B, C] <- Commands].
-
--spec list_commands_policy() -> [{atom(), [aterm()], string(), atom()}].
-
-%% @doc Get a list of all the available commands, arguments, description, and
-%% policy.
-list_commands_policy() ->
- Commands = ets:match(ejabberd_commands,
- #ejabberd_commands{name = '$1',
- args = '$2',
- desc = '$3',
- policy = '$4',
- _ = '_'}),
- [{A, B, C, D} || [A, B, C, D] <- Commands].
-
--spec get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}.
+ list_commands(?DEFAULT_VERSION).
+
+-spec list_commands(integer()) -> [{atom(), [aterm()], string()}].
+
+%% @doc Get a list of all the available commands, arguments and
+%% description in a given API verion.
+list_commands(Version) ->
+ Commands = get_commands_definition(Version),
+ [{Name, Args, Desc} || #ejabberd_commands{name = Name,
+ args = Args,
+ desc = Desc} <- Commands].
+
+
+-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.
get_command_format(Name) ->
- get_command_format(Name, noauth).
-
-get_command_format(Name, Auth) ->
+ 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, ?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),
- Matched = ets:match(ejabberd_commands,
- #ejabberd_commands{name = Name,
- args = '$1',
- result = '$2',
- policy = '$3',
- _ = '_'}),
- case Matched of
- [] ->
- {error, command_unknown};
- [[Args, Result, user]] when Admin;
- Auth == noauth ->
+ #ejabberd_commands{args = Args,
+ result = Result,
+ policy = Policy} =
+ get_command_definition(Name, Version),
+ case Policy of
+ user when Admin;
+ Auth == noauth ->
{[{user, binary}, {server, binary} | Args], Result};
- [[Args, Result, _]] ->
+ _ ->
{Args, Result}
end.
@@ -350,50 +376,127 @@ get_command_policy(Name) ->
{error, command_not_found}
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.
get_command_definition(Name) ->
- case ets:lookup(ejabberd_commands, Name) of
- [E] -> E;
- [] -> command_not_found
+ get_command_definition(Name, ?DEFAULT_VERSION).
+
+-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.
-%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown}
+-spec get_commands_definition(integer()) -> [ejabberd_commands()].
+
+% @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([], noauth, Name, Arguments).
+ execute_command(Name, Arguments, ?DEFAULT_VERSION).
+
+-spec execute_command(atom(),
+ [any()],
+ integer() |
+ {binary(), binary(), binary(), boolean()} |
+ noauth | admin
+ ) -> any().
+
+%% @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
+%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
+%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
+%% | noauth
+%% | admin
+%% Arguments = [any()]
+%%
+%% @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()]
+ [any()],
+ integer()
) -> any().
-%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error}
+%% @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
-%% Method = atom()
%% Arguments = [any()]
-%% Error = command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
-execute_command(AccessCommands1, Auth1, Name, Arguments) ->
+%%
+%% @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
true -> admin;
false -> Auth1
end,
- case ets:lookup(ejabberd_commands, Name) of
- [Command] ->
- AccessCommands = get_access_commands(AccessCommands1),
- try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
- ok -> execute_command2(Auth, Command, Arguments)
- catch
- {error, Error} -> {error, Error}
- end;
- [] -> {error, command_unknown}
+ Command = get_command_definition(Name, Version),
+ AccessCommands = get_access_commands(AccessCommands1, Version),
+ case check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of
+ ok -> execute_command2(Auth, Command, Arguments)
end.
execute_command2(
@@ -419,26 +522,25 @@ execute_command2(Command, Arguments) ->
Module = Command#ejabberd_commands.module,
Function = Command#ejabberd_commands.function,
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
- try apply(Module, Function, Arguments) of
- Response ->
- Response
- catch
- Problem ->
- {error, Problem}
- end.
+ apply(Module, Function, Arguments).
-spec get_tags_commands() -> [{string(), [string()]}].
%% @spec () -> [{Tag::string(), [CommandName::string()]}]
%% @doc Get all the tags and associated commands.
get_tags_commands() ->
- CommandTags = ets:match(ejabberd_commands,
- #ejabberd_commands{
- name = '$1',
- tags = '$2',
- _ = '_'}),
+ get_tags_commands(?DEFAULT_VERSION).
+
+-spec get_tags_commands(integer()) -> [{string(), [string()]}].
+
+%% @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(
- fun([CommandNameAtom, CTags], D) ->
+ fun({CommandNameAtom, CTags}, D) ->
CommandName = atom_to_list(CommandNameAtom),
case CTags of
[] ->
@@ -457,7 +559,6 @@ get_tags_commands() ->
CommandTags),
orddict:to_list(Dict).
-
%% -----------------------------
%% Access verification
%% -----------------------------
@@ -491,7 +592,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
fun({Access, Commands, ArgumentRestrictions}) ->
case check_access(Command, Access, Auth) of
true ->
- check_access_command(Commands, Command, ArgumentRestrictions,
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
Method, Arguments);
false ->
false
@@ -500,7 +602,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) ->
ArgumentRestrictions = [],
case check_access(Command, Access, Auth) of
true ->
- check_access_command(Commands, Command, ArgumentRestrictions,
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
Method, Arguments);
false ->
false
@@ -563,9 +666,11 @@ check_access2(Access, User, Server) ->
deny -> false
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
- true -> check_access_arguments(Command, ArgumentRestrictions, Arguments);
+ true -> check_access_arguments(Command, ArgumentRestrictions,
+ Arguments);
false -> false
end.
@@ -589,18 +694,20 @@ tag_arguments(ArgsDefs, Args) ->
Args).
-get_access_commands(undefined) ->
- Cmds = get_commands(),
+get_access_commands(undefined, Version) ->
+ Cmds = get_commands(Version),
[{?POLICY_ACCESS, Cmds, []}];
-get_access_commands(AccessCommands) ->
+get_access_commands(AccessCommands, _Version) ->
AccessCommands.
get_commands() ->
+ get_commands(?DEFAULT_VERSION).
+get_commands(Version) ->
Opts = ejabberd_config:get_option(
commands,
fun(V) when is_list(V) -> V end,
[]),
- CommandsList = list_commands_policy(),
+ CommandsList = list_commands_policy(Version),
OpenCmds = [N || {N, _, _, open} <- CommandsList],
RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList],
AdminCmds = [N || {N, _, _, admin} <- CommandsList],
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index bf4e4675a..edec5a07e 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -48,7 +48,7 @@
-behaviour(ejabberd_config).
-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,
opt_type/1]).
@@ -57,6 +57,8 @@
-include("ejabberd.hrl").
-include("logger.hrl").
+-define(DEFAULT_VERSION, 1000000).
+
%%-----------------------------
%% Module
@@ -69,7 +71,7 @@ start() ->
[SNode3 | Args3] ->
[SNode3, 60000, Args3];
_ ->
- print_usage(),
+ print_usage(?DEFAULT_VERSION),
halt(?STATUS_USAGE)
end,
SNode1 = case string:tokens(SNode, "@") of
@@ -93,6 +95,9 @@ start() ->
[Node, Reason]),
%% TODO: show minimal start help
?STATUS_BADRPC;
+ {invalid_version, V} ->
+ print("Invalid API version number: ~p~n", [V]),
+ ?STATUS_ERROR;
S ->
S
end,
@@ -126,11 +131,17 @@ unregister_commands(CmdDescs, Module, Function) ->
%% Process
%%-----------------------------
+
-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
%% they are usable even if ejabberd is completely stopped.
-process(["status"]) ->
+process(["status"], _Version) ->
{InternalStatus, ProvidedStatus} = init:get_status(),
print("The node ~p is ~p with status: ~p~n",
[node(), InternalStatus, ProvidedStatus]),
@@ -146,24 +157,24 @@ process(["status"]) ->
?STATUS_SUCCESS
end;
-process(["stop"]) ->
+process(["stop"], _Version) ->
%%ejabberd_cover:stop(),
init:stop(),
?STATUS_SUCCESS;
-process(["restart"]) ->
+process(["restart"], _Version) ->
init:restart(),
?STATUS_SUCCESS;
-process(["mnesia"]) ->
+process(["mnesia"], _Version) ->
print("~p~n", [mnesia:system_info(all)]),
?STATUS_SUCCESS;
-process(["mnesia", "info"]) ->
+process(["mnesia", "info"], _Version) ->
mnesia:info(),
?STATUS_SUCCESS;
-process(["mnesia", Arg]) ->
+process(["mnesia", Arg], _Version) ->
case catch mnesia:system_info(list_to_atom(Arg)) of
{'EXIT', Error} -> print("Error: ~p~n", [Error]);
Return -> print("~p~n", [Return])
@@ -172,23 +183,23 @@ process(["mnesia", Arg]) ->
%% The arguments --long and --dual are not documented because they are
%% automatically selected depending in the number of columns of the shell
-process(["help" | Mode]) ->
+process(["help" | Mode], Version) ->
{MaxC, ShCode} = get_shell_info(),
case Mode of
[] ->
- print_usage(dual, MaxC, ShCode),
+ print_usage(dual, MaxC, ShCode, Version),
?STATUS_USAGE;
["--dual"] ->
- print_usage(dual, MaxC, ShCode),
+ print_usage(dual, MaxC, ShCode, Version),
?STATUS_USAGE;
["--long"] ->
- print_usage(long, MaxC, ShCode),
+ print_usage(long, MaxC, ShCode, Version),
?STATUS_USAGE;
["--tags"] ->
- print_usage_tags(MaxC, ShCode),
+ print_usage_tags(MaxC, ShCode, Version),
?STATUS_SUCCESS;
["--tags", Tag] ->
- print_usage_tags(Tag, MaxC, ShCode),
+ print_usage_tags(Tag, MaxC, ShCode, Version),
?STATUS_SUCCESS;
["help"] ->
print_usage_help(MaxC, ShCode),
@@ -196,13 +207,22 @@ process(["help" | Mode]) ->
[CmdString | _] ->
CmdStringU = ejabberd_regexp:greplace(
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
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(),
- {String, Code} = process2(Args, AccessCommands),
+ {String, Code} = process2(Args, AccessCommands, Version),
case String of
[] -> ok;
_ ->
@@ -211,18 +231,21 @@ process(Args) ->
Code.
%% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()}
-process2(["--auth", User, Server, Pass | Args], AccessCommands) ->
- process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, AccessCommands);
-process2(Args, AccessCommands) ->
- process2(Args, noauth, AccessCommands).
+process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) ->
+ process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server),
+ list_to_binary(Pass), true}, Version);
+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}
when is_list(String) ->
io:format(lists:flatten(["\n" | String]++["\n"])),
[CommandString | _] = Args,
- process(["help" | [CommandString]]),
+ process(["help" | [CommandString]], Version),
{lists:flatten(String), ?STATUS_ERROR};
{String, Code}
when is_list(String) and is_integer(Code) ->
@@ -246,29 +269,29 @@ get_accesscommands() ->
%%-----------------------------
%% @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
false when Args /= [] ->
- try_call_command(Args, Auth, AccessCommands);
+ try_call_command(Args, Auth, AccessCommands, Version);
false ->
- print_usage(),
+ print_usage(Version),
{"", ?STATUS_USAGE};
Status ->
{"", Status}
catch
exit:Why ->
- print_usage(),
+ print_usage(Version),
{io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE};
Error:Why ->
%% In this case probably ejabberd is not started, so let's show Status
- process(["status"]),
+ process(["status"], Version),
print("~n", []),
{io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE}
end.
%% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()}
-try_call_command(Args, Auth, AccessCommands) ->
- try call_command(Args, Auth, AccessCommands) of
+try_call_command(Args, Auth, AccessCommands, Version) ->
+ try call_command(Args, Auth, AccessCommands, Version) of
{error, command_unknown} ->
{io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR};
{error, wrong_command_arguments} ->
@@ -276,24 +299,28 @@ try_call_command(Args, Auth, AccessCommands) ->
Res ->
Res
catch
+ throw:Error ->
+ {io_lib:format("~p", [Error]), ?STATUS_ERROR};
A:Why ->
Stack = erlang:get_stacktrace(),
{io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR}
end.
%% @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(
list_to_binary(CmdString), <<"-">>, <<"_">>),
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};
{ArgsFormat, ResultFormat} ->
case (catch format_args(Args, ArgsFormat)) of
ArgsFormatted when is_list(ArgsFormatted) ->
- Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command,
- ArgsFormatted),
+ Result = ejabberd_commands:execute_command(AccessCommands,
+ Auth, Command,
+ ArgsFormatted,
+ Version),
format_result(Result, ResultFormat);
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
{NumCompa, TextCompa} =
@@ -404,8 +431,8 @@ make_status(ok) -> ?STATUS_SUCCESS;
make_status(true) -> ?STATUS_SUCCESS;
make_status(_Error) -> ?STATUS_ERROR.
-get_list_commands() ->
- try ejabberd_commands:list_commands() of
+get_list_commands(Version) ->
+ try ejabberd_commands:list_commands(Version) of
Commands ->
[tuple_command_help(Command)
|| {N,_,_}=Command <- Commands,
@@ -458,10 +485,10 @@ get_list_ctls() ->
-define(U2, "\e[24m").
-define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
-print_usage() ->
+print_usage(Version) ->
{MaxC, ShCode} = get_shell_info(),
- print_usage(dual, MaxC, ShCode).
-print_usage(HelpMode, MaxC, ShCode) ->
+ print_usage(dual, MaxC, ShCode, Version).
+print_usage(HelpMode, MaxC, ShCode, Version) ->
AllCommands =
[
{"status", [], "Get ejabberd status"},
@@ -469,11 +496,11 @@ print_usage(HelpMode, MaxC, ShCode) ->
{"restart", [], "Restart ejabberd"},
{"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"},
{"mnesia", ["[info]"], "show information of Mnesia system"}] ++
- get_list_commands() ++
+ get_list_commands(Version) ++
get_list_ctls(),
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("command"), " [", ?U("options"), "]\n"
"\n"
@@ -598,9 +625,9 @@ format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
%% Print Tags
%%-----------------------------
-print_usage_tags(MaxC, ShCode) ->
+print_usage_tags(MaxC, ShCode, Version) ->
print("Available tags and commands:", []),
- TagsCommands = ejabberd_commands:get_tags_commands(),
+ TagsCommands = ejabberd_commands:get_tags_commands(Version),
lists:foreach(
fun({Tag, Commands} = _TagCommands) ->
print(["\n\n ", ?B(Tag), "\n "], []),
@@ -611,10 +638,10 @@ print_usage_tags(MaxC, ShCode) ->
TagsCommands),
print("\n\n", []).
-print_usage_tags(Tag, MaxC, ShCode) ->
+print_usage_tags(Tag, MaxC, ShCode, Version) ->
print(["Available commands with tag ", ?B(Tag), ":", "\n"], []),
HelpMode = long,
- TagsCommands = ejabberd_commands:get_tags_commands(),
+ TagsCommands = ejabberd_commands:get_tags_commands(Version),
CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of
{value, {Tag, CNs}} -> CNs;
false -> []
@@ -622,7 +649,7 @@ print_usage_tags(Tag, MaxC, ShCode) ->
CommandsList = lists:map(
fun(NameString) ->
C = ejabberd_commands:get_command_definition(
- list_to_atom(NameString)),
+ list_to_atom(NameString), Version),
#ejabberd_commands{name = Name,
args = Args,
desc = Desc} = C,
@@ -673,20 +700,20 @@ print_usage_help(MaxC, ShCode) ->
%%-----------------------------
%% @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
- 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),
case Cmds of
- [] -> io:format("Error: not command found that match: ~p~n", [CmdSubString]);
- _ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode)
+ [] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]);
+ _ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version)
end.
-print_usage_commands2(Cmds, MaxC, ShCode) ->
+print_usage_commands3(Cmds, MaxC, ShCode, Version) ->
%% Then for each one print it
lists:mapfoldl(
fun(Cmd, Remaining) ->
- print_usage_command(Cmd, MaxC, ShCode),
+ print_usage_command(Cmd, MaxC, ShCode, Version),
case Remaining > 1 of
true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []);
false -> ok
@@ -716,16 +743,16 @@ filter_commands_regexp(All, Glob) ->
All).
%% @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),
- case ejabberd_commands:get_command_definition(Name) of
+ case ejabberd_commands:get_command_definition(Name, Version) of
command_not_found ->
io:format("Error: command ~p not known.~n", [Cmd]);
C ->
- print_usage_command(Cmd, C, MaxC, ShCode)
+ print_usage_command2(Cmd, C, MaxC, ShCode)
end.
-print_usage_command(Cmd, C, MaxC, ShCode) ->
+print_usage_command2(Cmd, C, MaxC, ShCode) ->
#ejabberd_commands{
tags = TagsAtoms,
desc = Desc,
diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl
index f0e567199..feff8af8a 100644
--- a/src/mod_admin_extra.erl
+++ b/src/mod_admin_extra.erl
@@ -590,36 +590,35 @@ remove_node(Node) ->
%%%
set_password(User, Host, Password) ->
- case ejabberd_auth:set_password(User, Host, Password) of
- ok ->
- ok;
- _ ->
- error
- end.
+ Fun = fun () -> ejabberd_auth:set_password(User, Host, Password) end,
+ user_action(User, Host, Fun, ok).
%% Copied some code from ejabberd_commands.erl
check_password_hash(User, Host, PasswordHash, HashMethod) ->
AccountPass = ejabberd_auth:get_password_s(User, Host),
AccountPassHash = case {AccountPass, HashMethod} of
{A, _} when is_tuple(A) -> scrammed;
- {_, "md5"} -> get_md5(AccountPass);
- {_, "sha"} -> get_sha(AccountPass);
- _ -> undefined
+ {_, <<"md5">>} -> get_md5(AccountPass);
+ {_, <<"sha">>} -> get_sha(AccountPass);
+ {_, _Method} ->
+ ?ERROR_MSG("check_password_hash called "
+ "with hash method", [_Method]),
+ undefined
end,
case AccountPassHash of
scrammed ->
- ?ERROR_MSG("Passwords are scrammed, and check_password_hash can not work.", []),
+ ?ERROR_MSG("Passwords are scrammed, and check_password_hash cannot work.", []),
throw(passwords_scrammed_command_cannot_work);
- undefined -> error;
+ undefined -> throw(unkown_hash_method);
PasswordHash -> ok;
- _ -> error
+ _ -> false
end.
get_md5(AccountPass) ->
- lists:flatten([io_lib:format("~.16B", [X])
- || X <- binary_to_list(erlang:md5(AccountPass))]).
+ iolist_to_binary([io_lib:format("~2.16.0B", [X])
+ || X <- binary_to_list(erlang:md5(AccountPass))]).
get_sha(AccountPass) ->
- lists:flatten([io_lib:format("~.16B", [X])
- || X <- binary_to_list(p1_sha:sha1(AccountPass))]).
+ iolist_to_binary([io_lib:format("~2.16.0B", [X])
+ || X <- binary_to_list(p1_sha:sha1(AccountPass))]).
num_active_users(Host, Days) ->
list_last_activity(Host, true, Days).
@@ -782,7 +781,8 @@ resource_num(User, Host, Num) ->
true ->
lists:nth(Num, Resources);
false ->
- lists:flatten(io_lib:format("Error: Wrong resource number: ~p", [Num]))
+ throw({bad_argument,
+ lists:flatten(io_lib:format("Wrong resource number: ~p", [Num]))})
end.
kick_session(User, Server, Resource, ReasonText) ->
@@ -1569,6 +1569,20 @@ decide_rip_jid({UName, UServer}, Match_list) ->
end,
Match_list).
+user_action(User, Server, Fun, OK) ->
+ case ejabberd_auth:is_user_exists(User, Server) of
+ true ->
+ case catch Fun() of
+ OK -> ok;
+ {error, Error} -> throw(Error);
+ Error ->
+ ?ERROR_MSG("Command returned: ~p", [Error]),
+ 1
+ end;
+ false ->
+ throw({not_found, "unknown_user"})
+ end.
+
%% Copied from ejabberd-2.0.0/src/acl.erl
is_regexp_match(String, RegExp) ->
case ejabberd_regexp:run(String, RegExp) of
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index c2b7d1100..c4fae2022 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -29,6 +29,11 @@
%% request_handlers:
%% "/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:
%% commands_admin_access: configure
%% commands:
@@ -76,6 +81,8 @@
-include("logger.hrl").
-include("ejabberd_http.hrl").
+-define(DEFAULT_API_VERSION, 0).
+
-define(CT_PLAIN,
{<<"Content-Type">>, <<"text/plain">>}).
@@ -181,7 +188,8 @@ check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
true -> {allowed, Call, admin};
_ -> unauthorized_response()
end;
- _ ->
+ E ->
+ ?DEBUG("Unauthorized: ~p", [E]),
unauthorized_response()
end;
check_permissions2(_Request, _Call, _Policy) ->
@@ -196,10 +204,13 @@ oauth_check_token(Scope, Token) ->
%% command processing
%% ------------------
+%process(Call, Request) ->
+% ?DEBUG("~p~n~p", [Call, Request]), ok;
process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []),
- badrequest_response();
+ badrequest_response(<<"Missing POST data">>);
process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
+ Version = get_api_version(Req),
try
Args = case jiffy:decode(Data) of
List when is_list(List) -> List;
@@ -209,33 +220,37 @@ process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) ->
log(Call, Args, IP),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args),
+ {Code, Result} = handle(Cmd, Auth, Args, Version),
json_response(Code, jiffy:encode(Result));
%% Warning: check_permission direcly formats 401 reply if not authorized
ErrorResponse ->
ErrorResponse
end
- catch _:Error ->
- ?DEBUG("Bad Request: ~p", [Error]),
- badrequest_response()
+ catch _:{error,{_,invalid_json}} = _Err ->
+ ?DEBUG("Bad Request: ~p", [_Err]),
+ badrequest_response(<<"Invalid JSON input">>);
+ _:_Error ->
+ ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
+ badrequest_response()
end;
process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) ->
+ Version = get_api_version(Req),
try
Args = case Data of
- [{nokey, <<>>}] -> [];
- _ -> Data
- end,
+ [{nokey, <<>>}] -> [];
+ _ -> Data
+ end,
log(Call, Args, IP),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args),
+ {Code, Result} = handle(Cmd, Auth, Args, Version),
json_response(Code, jiffy:encode(Result));
%% Warning: check_permission direcly formats 401 reply if not authorized
ErrorResponse ->
ErrorResponse
end
- catch _:Error ->
- ?DEBUG("Bad Request: ~p", [Error]),
+ catch _:_Error ->
+ ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]),
badrequest_response()
end;
process([], #request{method = 'OPTIONS', data = <<>>}) ->
@@ -244,13 +259,28 @@ process(_Path, Request) ->
?DEBUG("Bad Request: no handler ~p", [Request]),
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
%% ----------------
% generic ejabberd command handler
-handle(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
- case ejabberd_commands:get_command_format(Call, Auth) of
+handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
+ case ejabberd_commands:get_command_format(Call, Auth, Version) of
{ArgsSpec, _} when is_list(ArgsSpec) ->
Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args],
Spec = lists:foldr(
@@ -265,24 +295,48 @@ handle(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
({Key, atom}, Acc) ->
[{Key, undefined}|Acc]
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:{error, account_unprivileged} ->
+ {401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)};
+ 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("REST API Error: ~p", [Msg]),
{400, Msg};
_Error ->
+ ?ERROR_MSG("REST API Error: ~p", [_Error]),
{400, <<"Error">>}
end.
-handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) ->
- {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth),
+handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) ->
+ {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version),
ArgsFormatted = format_args(Args, ArgsF),
- case ejabberd_command(Auth, Call, ArgsFormatted, 400) of
- 0 -> {200, <<"OK">>};
- 1 -> {500, <<"500 Internal server error">>};
- 400 -> {400, <<"400 Bad Request">>};
- 401 -> {401, <<"401 Unauthorized">>};
- 404 -> {404, <<"404 Not found">>};
- Res -> format_command_result(Call, Auth, Res)
- end.
+ ejabberd_command(Auth, Call, ArgsFormatted, Version).
get_elem_delete(A, L) ->
case proplists:get_all_values(A, L) of
@@ -346,7 +400,9 @@ format_arg(undefined, binary) -> <<>>;
format_arg(undefined, string) -> <<>>;
format_arg(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) ->
iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
@@ -360,37 +416,37 @@ process_unicode_codepoints(Str) ->
match(Args, Spec) ->
[{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec].
-ejabberd_command(Auth, Cmd, Args, Default) ->
+ejabberd_command(Auth, Cmd, Args, Version) ->
Access = case Auth of
admin -> [];
_ -> undefined
end,
- case catch ejabberd_commands:execute_command(Access, Auth, Cmd, Args) of
- {'EXIT', _} -> Default;
- {error, account_unprivileged} -> 401;
- {error, _} -> Default;
- Result -> Result
+ case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version) of
+ {error, Error} ->
+ throw(Error);
+ Res ->
+ format_command_result(Cmd, Auth, Res, Version)
end.
-format_command_result(Cmd, Auth, Result) ->
- {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth),
+format_command_result(Cmd, Auth, Result, Version) ->
+ {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version),
case {ResultFormat, Result} of
- {{_, rescode}, V} when V == true; V == ok ->
- {200, <<"">>};
- {{_, rescode}, _} ->
- {500, <<"">>};
- {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
- {200, iolist_to_binary(Text1)};
- {{_, restuple}, {_, Text2}} ->
- {500, iolist_to_binary(Text2)};
- {{_, {list, _}}, _V} ->
- {_, L} = format_result(Result, ResultFormat),
- {200, L};
- {{_, {tuple, _}}, _V} ->
- {_, T} = format_result(Result, ResultFormat),
- {200, T};
- _ ->
- {200, {[format_result(Result, ResultFormat)]}}
+ {{_, rescode}, V} when V == true; V == ok ->
+ {200, 0};
+ {{_, rescode}, _} ->
+ {200, 1};
+ {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok ->
+ {200, iolist_to_binary(Text1)};
+ {{_, restuple}, {_, Text2}} ->
+ {500, iolist_to_binary(Text2)};
+ {{_, {list, _}}, _V} ->
+ {_, L} = format_result(Result, ResultFormat),
+ {200, L};
+ {{_, {tuple, _}}, _V} ->
+ {_, T} = format_result(Result, ResultFormat),
+ {200, T};
+ _ ->
+ {200, {[format_result(Result, ResultFormat)]}}
end.
format_result(Atom, {Name, atom}) ->
@@ -429,14 +485,15 @@ format_result(404, {_Name, _}) ->
"not_found".
unauthorized_response() ->
- {401, ?HEADER(?CT_XML),
- #xmlel{name = <<"h1">>, attrs = [],
- children = [{xmlcdata, <<"401 Unauthorized">>}]}}.
+ unauthorized_response(<<"401 Unauthorized">>).
+unauthorized_response(Body) ->
+ json_response(401, jiffy:encode(Body)).
badrequest_response() ->
- {400, ?HEADER(?CT_XML),
- #xmlel{name = <<"h1">>, attrs = [],
- children = [{xmlcdata, <<"400 Bad Request">>}]}}.
+ badrequest_response(<<"400 Bad Request">>).
+badrequest_response(Body) ->
+ json_response(400, jiffy:encode(Body)).
+
json_response(Code, Body) when is_integer(Code) ->
{Code, ?HEADER(?CT_JSON), Body}.
diff --git a/test/README-quicktest.md b/test/README-quicktest.md
new file mode 100644
index 000000000..43c71e86b
--- /dev/null
+++ b/test/README-quicktest.md
@@ -0,0 +1,33 @@
+# Elixir unit tests
+
+## Running Elixir unit tests
+
+You can run Elixir unit tests with command:
+
+make quicktest
+
+You need to have ejabberd compile with Elixir and tools enabled.
+
+## Troubleshooting test
+
+To help with troubleshooting Elixir tests, we have added a special macro in ejabberd `logger.hrl` include file: ?EXUNIT_LOG
+
+To use this, in test file:
+
+1. in `setup_all, add:
+
+ ```
+ Application.start(:logger)
+ ```
+
+2. Enable log capture for the test you want to analyse by adding
+ `capture_log` tag before test implementation:
+
+ ```
+ @tag capture_log: true
+ ```
+
+In the ejabberd code, if `logger.hrl` is included, you can code adds a
+EXUNIT_LOG macro:
+
+ ?EXUNIT_LOG("My debug log:~p ~p", [Arg1, Arg2])
diff --git a/test/ejabberd_admin_test.exs b/test/ejabberd_admin_test.exs
new file mode 100644
index 000000000..1c999314c
--- /dev/null
+++ b/test/ejabberd_admin_test.exs
@@ -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
diff --git a/test/ejabberd_auth_mock.exs b/test/ejabberd_auth_mock.exs
new file mode 100644
index 000000000..83019c8ee
--- /dev/null
+++ b/test/ejabberd_auth_mock.exs
@@ -0,0 +1,74 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2016 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 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, _authzid, 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
diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs
new file mode 100644
index 000000000..285585034
--- /dev/null
+++ b/test/ejabberd_commands_mock_test.exs
@@ -0,0 +1,423 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2016 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 EjabberdCommandsMockTest do
+ use ExUnit.Case, async: false
+
+ @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
+ Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl")
+
+ 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
+ end
+
+ test "API command can be registered, listed and unregistered" do
+ command = ejabberd_commands name: :test, module: @module,
+ function: :test_command
+
+ assert :ok == :ejabberd_commands.register_commands [command]
+ commands = :ejabberd_commands.list_commands
+ assert Enum.member? commands, {:test, [], ''}
+
+ assert :ok == :ejabberd_commands.unregister_commands [command]
+ commands = :ejabberd_commands.list_commands
+ refute Enum.member? commands, {:test, [], ''}
+ end
+
+
+ test "API command with versions can be registered, listed and unregistered" do
+ command1 = ejabberd_commands name: :test, module: @module,
+ function: :test_command, version: 1, desc: 'version1'
+ command3 = ejabberd_commands name: :test, module: @module,
+ function: :test_command, version: 3, desc: 'version3'
+ assert :ejabberd_commands.register_commands [command1, command3]
+
+ version1 = {:test, [], 'version1'}
+ version3 = {:test, [], 'version3'}
+
+ # 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
+
+
+ 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
diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs
index f415fee51..31d108214 100644
--- a/test/ejabberd_commands_test.exs
+++ b/test/ejabberd_commands_test.exs
@@ -27,6 +27,7 @@ defmodule EjabberdCommandsTest do
Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl")
setup_all do
+ :mnesia.start
:ejabberd_commands.init
end
@@ -38,7 +39,9 @@ defmodule EjabberdCommandsTest do
test "Check that admin commands are rejected with noauth credentials" do
:ok = :ejabberd_commands.register_commands([admin_test_command])
- {:error, :account_unprivileged} = :ejabberd_commands.execute_command(:undefined, :noauth, :test_admin, [])
+
+ assert catch_throw(:ejabberd_commands.execute_command(:undefined, :noauth, :test_admin, [])) == {:error, :account_unprivileged}
+
# Command executed from ejabberdctl passes anyway with access commands trick
# TODO: We should refactor to have explicit call when bypassing auth check for command-line
:ok = :ejabberd_commands.execute_command([], :noauth, :test_admin, [])
diff --git a/test/ejabberd_hooks_test.exs b/test/ejabberd_hooks_test.exs
index 6493642de..a69fbbd61 100644
--- a/test/ejabberd_hooks_test.exs
+++ b/test/ejabberd_hooks_test.exs
@@ -30,14 +30,14 @@
# log as we are exercising hook handler recovery from that situation.
defmodule EjabberdHooksTest do
- use ExUnit.Case, async: true
-
+ use ExUnit.Case, async: false
+
@author "mremond@process-one.net"
@host <<"domain.net">>
@self __MODULE__
setup_all do
- {:ok, _Pid} = :ejabberd_hooks.start_link
+ {:ok, _pid} = :ejabberd_hooks.start_link
:ok
end
diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs
new file mode 100644
index 000000000..81cfdc038
--- /dev/null
+++ b/test/ejabberd_oauth_mock.exs
@@ -0,0 +1,47 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2016 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 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
diff --git a/test/ejabberd_sm_mock.exs b/test/ejabberd_sm_mock.exs
new file mode 100644
index 000000000..53c2c750f
--- /dev/null
+++ b/test/ejabberd_sm_mock.exs
@@ -0,0 +1,121 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2016 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 EjabberdSmMock do
+ @author "jsautret@process-one.net"
+
+ require Record
+ Record.defrecord :session, Record.extract(:session, from_lib: "ejabberd/include/ejabberd_sm.hrl")
+ Record.defrecord :jid, Record.extract(:jid, from_lib: "ejabberd/include/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
diff --git a/test/elixir_SUITE.erl b/test/elixir_SUITE.erl
index 041d0603e..2d8c3c52d 100644
--- a/test/elixir_SUITE.erl
+++ b/test/elixir_SUITE.erl
@@ -19,6 +19,7 @@
init_per_suite(Config) ->
check_meck(),
+ code:add_pathz(filename:join(test_dir(), "../include")),
Config.
init_per_testcase(_TestCase, Config) ->
@@ -27,13 +28,13 @@ init_per_testcase(_TestCase, Config) ->
all() ->
case is_elixir_available() of
- true ->
- Dir = test_dir(),
- filelib:fold_files(Dir, ".*\.exs", false,
- fun(Filename, Acc) -> [list_to_atom(filename:basename(Filename)) | Acc] end,
- []);
- false ->
- []
+ true ->
+ Dir = test_dir(),
+ filelib:fold_files(Dir, ".*\.exs$", false,
+ fun(Filename, Acc) -> [list_to_atom(filename:basename(Filename)) | Acc] end,
+ []);
+ false ->
+ []
end.
check_meck() ->
@@ -56,10 +57,10 @@ is_elixir_available() ->
undefined_function(?MODULE, Func, Args) ->
case lists:suffix(".exs", atom_to_list(Func)) of
- true ->
- run_elixir_test(Func);
- false ->
- error_handler:undefined_function(?MODULE, Func, Args)
+ true ->
+ run_elixir_test(Func);
+ false ->
+ error_handler:undefined_function(?MODULE, Func, Args)
end;
undefined_function(Module, Func, Args) ->
error_handler:undefined_function(Module, Func,Args).
@@ -68,6 +69,13 @@ run_elixir_test(Func) ->
%% Elixir tests can be tagged as follow to be ignored (place before test start)
%% @tag pending: true
'Elixir.ExUnit':start([{exclude, [{pending, true}]}]),
+
+ filelib:fold_files(test_dir(), ".*mock\.exs\$", true,
+ fun (File, N) ->
+ 'Elixir.Code':load_file(list_to_binary(File)),
+ N+1
+ end, 0),
+
'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
ResultMap = 'Elixir.ExUnit':run(),
diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs
new file mode 100644
index 000000000..d104ed058
--- /dev/null
+++ b/test/mod_admin_extra_test.exs
@@ -0,0 +1,356 @@
+# ----------------------------------------------------------------------
+#
+# 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
+
+ require EjabberdAuthMock
+
+ @author "jsautret@process-one.net"
+
+ @user "user"
+ @domain "domain"
+ @password "password"
+ @resource "resource"
+
+ require Record
+ Record.defrecord :jid, Record.extract(:jid, from_lib: "ejabberd/include/jlib.hrl")
+
+ setup_all do
+ try do
+ :jid.start
+ :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 "set_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
+
+ ###################### 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
+
+# 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
diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs
new file mode 100644
index 000000000..78832d7a5
--- /dev/null
+++ b/test/mod_http_api_mock_test.exs
@@ -0,0 +1,197 @@
+# ----------------------------------------------------------------------
+#
+# 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 ModHttpApiMockTest 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_lib: "ejabberd/include/ejabberd_http.hrl")
+
+ setup_all do
+ try do
+ :jid.start
+ :mnesia.start
+ :stringprep.start
+ :ejabberd_config.start([@domain], [])
+ :ejabberd_commands.init
+ 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, :get_command_policy,
+ fn (@acommand) -> {:ok, :user} end)
+ :meck.expect(:ejabberd_commands, :get_commands,
+ fn () -> [@acommand] end)
+ :meck.expect(:ejabberd_commands, :execute_command,
+ fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version) ->
+ :ok
+ end)
+
+ :ejabberd_config.add_local_option(:commands, [[{:add_commands, [@acommand]}]])
+
+ # 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)
+
+ # history = :meck.history(:ejabberd_commands)
+
+ 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
+ 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, :get_command_policy,
+ fn (@acommand) -> {:ok, :user} end)
+ :meck.expect(:ejabberd_commands, :get_commands,
+ fn () -> [@acommand] end)
+ :meck.expect(:ejabberd_commands, :execute_command,
+ fn (:undefined, {@user, @domain, {:oauth, _token}, false},
+ @acommand, [], @version) ->
+ :ok
+ end)
+
+
+ # 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
diff --git a/test/mod_last_mock.exs b/test/mod_last_mock.exs
new file mode 100644
index 000000000..4f8da3666
--- /dev/null
+++ b/test/mod_last_mock.exs
@@ -0,0 +1,79 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2016 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 ModLastMock do
+
+ require Record
+ Record.defrecord :session, Record.extract(:session, from_lib: "ejabberd/include/ejabberd_sm.hrl")
+ Record.defrecord :jid, Record.extract(:jid, from_lib: "ejabberd/include/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
diff --git a/test/mod_roster_mock.exs b/test/mod_roster_mock.exs
new file mode 100644
index 000000000..b1251ba86
--- /dev/null
+++ b/test/mod_roster_mock.exs
@@ -0,0 +1,208 @@
+# ----------------------------------------------------------------------
+#
+# ejabberd, Copyright (C) 2002-2016 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 ModRosterMock do
+ @author "jsautret@process-one.net"
+
+ require Record
+ Record.defrecord :roster, Record.extract(:roster, from_lib: "ejabberd/include/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, :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: :jid.from_string(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