24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-06-02 21:17:12 +02:00

Merge pull request #1046 from processone/commands-update

Add support for versioning in ejabberd commands
This commit is contained in:
Mickaël Rémond 2016-04-01 14:24:08 +02:00
commit ca9ac019eb
20 changed files with 2093 additions and 246 deletions

View File

@ -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',

View File

@ -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).

View File

@ -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"}}}}]}.

View File

@ -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].
list_commands(?DEFAULT_VERSION).
-spec list_commands_policy() -> [{atom(), [aterm()], string(), atom()}].
-spec list_commands(integer()) -> [{atom(), [aterm()], string()}].
%% @doc Get a list of all the available commands, arguments, description, and
%% 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].
%% @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 get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}.
-spec list_commands_policy(integer()) ->
[{atom(), [aterm()], string(), atom()}].
%% @doc Get a list of all the available commands, arguments,
%% description, and policy in a given API version.
list_commands_policy(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc, Policy} ||
#ejabberd_commands{name = Name,
args = Args,
desc = Desc,
policy = Policy} <- Commands].
-spec get_command_format(atom()) -> {[aterm()], rterm()}.
%% @doc Get the format of arguments and result of a command.
get_command_format(Name) ->
get_command_format(Name, noauth).
get_command_format(Name, noauth, ?DEFAULT_VERSION).
get_command_format(Name, Version) when is_integer(Version) ->
get_command_format(Name, noauth, Version);
get_command_format(Name, Auth) ->
get_command_format(Name, Auth, ?DEFAULT_VERSION).
get_command_format(Name, Auth) ->
-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}
%% @doc Execute a command.
execute_command(Name, Arguments) ->
execute_command([], noauth, Name, Arguments).
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
-spec execute_command([{atom(), [atom()], [any()]}] | undefined,
{binary(), binary(), binary(), boolean()} |
noauth | admin,
atom(),
[any()]
% @doc Returns all commands for a given API version
get_commands_definition(Version) ->
L = lists:reverse(
lists:sort(
mnesia:dirty_select(
ejabberd_commands,
ets:fun2ms(
fun(#ejabberd_commands{name = Name, version = V} = C)
when V =< Version ->
{Name, V, C}
end)))),
F = fun({_Name, _V, Command}, []) ->
[Command];
({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
Acc;
({_Name, _V, Command}, Acc) -> [Command | Acc]
end,
lists:foldl(F, [], L).
%% @spec (Name::atom(), Arguments) -> ResultTerm
%% where
%% Arguments = [any()]
%% @doc Execute a command.
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data |
%% no_auth_provided
execute_command(Name, Arguments) ->
execute_command(Name, Arguments, ?DEFAULT_VERSION).
-spec execute_command(atom(),
[any()],
integer() |
{binary(), binary(), binary(), boolean()} |
noauth | admin
) -> any().
%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error}
%% @spec (Name::atom(), Arguments, integer() | Auth) -> ResultTerm
%% where
%% Auth = {User::string(), Server::string(), Password::string(),
%% Admin::boolean()}
%% | noauth
%% | admin
%% Arguments = [any()]
%%
%% @doc Execute a command in a given API version
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data |
%% no_auth_provided
execute_command(Name, Arguments, Version) when is_integer(Version) ->
execute_command([], noauth, Name, Arguments, Version);
execute_command(Name, Arguments, Auth) ->
execute_command([], Auth, Name, Arguments, ?DEFAULT_VERSION).
%% @spec (AccessCommands, Auth, Name::atom(), Arguments) ->
%% ResultTerm | {error, Error}
%% where
%% 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
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
execute_command(AccessCommands, Auth, Name, Arguments) ->
execute_command(AccessCommands, Auth, Name, Arguments, ?DEFAULT_VERSION).
-spec execute_command([{atom(), [atom()], [any()]}] | undefined,
{binary(), binary(), binary(), boolean()} |
noauth | admin,
atom(),
[any()],
integer()
) -> any().
%% @spec (AccessCommands, Auth, Name::atom(), Arguments, integer()) -> ResultTerm
%% where
%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined
%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()}
%% | noauth
%% | admin
%% Arguments = [any()]
%%
%% @doc Execute a command in a given API version
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
Auth = case is_admin(Name, Auth1) of
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],

View File

@ -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,

View File

@ -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

View File

@ -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}.

33
test/README-quicktest.md Normal file
View File

@ -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])

View File

@ -0,0 +1,79 @@
# ----------------------------------------------------------------------
#
# ejabberd, Copyright (C) 2002-2015 ProcessOne
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ----------------------------------------------------------------------
defmodule EjabberdAdminTest do
use ExUnit.Case, async: false
@author "jsautret@process-one.net"
setup_all do
:mnesia.start
# For some myterious reason, :ejabberd_commands.init mays
# sometimes fails if module is not loaded before
{:module, :ejabberd_commands} = Code.ensure_loaded(:ejabberd_commands)
:ejabberd_commands.init
:ejabberd_admin.start
:ok
end
setup do
:ok
end
test "Logvel can be set and retrieved" do
:ejabberd_logger.start()
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [1])
assert {1, :critical, 'Critical'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [2])
assert {2, :error, 'Error'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [3])
assert {3, :warning, 'Warning'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert {:wrong_loglevel, 6} ==
catch_throw :ejabberd_commands.execute_command(:set_loglevel, [6])
assert {3, :warning, 'Warning'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [4])
assert {4, :info, 'Info'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [5])
assert {5, :debug, 'Debug'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
assert :lager == :ejabberd_commands.execute_command(:set_loglevel, [0])
assert {0, :no_log, 'No log'} ==
:ejabberd_commands.execute_command(:get_loglevel, [])
end
test "command status works with ejabberd stopped" do
assert :ejabberd_not_running ==
elem(:ejabberd_commands.execute_command(:status, []), 0)
end
end

View File

@ -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

View File

@ -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

View File

@ -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, [])

View File

@ -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

View File

@ -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

121
test/ejabberd_sm_mock.exs Normal file
View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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

79
test/mod_last_mock.exs Normal file
View File

@ -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

208
test/mod_roster_mock.exs Normal file
View File

@ -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