diff options
Diffstat (limited to 'test')
68 files changed, 10784 insertions, 5647 deletions
diff --git a/test/README b/test/README index 99e5eec12..de1a96aa2 100644 --- a/test/README +++ b/test/README @@ -1,11 +1,9 @@ -You need MySQL, PostgreSQL and Riak up and running. +You need MySQL, PostgreSQL and Redis up and running. MySQL should be accepting TCP connections on localhost:3306. PostgreSQL should be accepting TCP connections on localhost:5432. -Riak should be accepting TCP connections on localhost:8087. +Redis should be accepting TCP connections on localhost:6379. MySQL and PostgreSQL should grant full access to user 'ejabberd_test' with password 'ejabberd_test' on database 'ejabberd_test'. -Riak should be configured with leveldb as a database backend and -pz -should be pointed to the directory with ejabberd BEAM files. Here is a quick setup example: @@ -16,6 +14,7 @@ $ psql template1 template1=# CREATE USER ejabberd_test WITH PASSWORD 'ejabberd_test'; template1=# CREATE DATABASE ejabberd_test; template1=# GRANT ALL PRIVILEGES ON DATABASE ejabberd_test TO ejabberd_test; +$ psql ejabberd_test -f sql/pg.sql ------------------- MySQL @@ -24,27 +23,4 @@ $ mysql mysql> CREATE USER 'ejabberd_test'@'localhost' IDENTIFIED BY 'ejabberd_test'; mysql> CREATE DATABASE ejabberd_test; mysql> GRANT ALL ON ejabberd_test.* TO 'ejabberd_test'@'localhost'; - -------------------- - Riak -------------------- -$ cat /etc/riak/vm.args -... -## Map/Reduce path --pz /path/to/ejabberd/ebin -... - -For version < 2.x: - -$ cat /etc/riak/app.config: -... - {riak_kv, [ - {storage_backend, riak_kv_eleveldb_backend}, -... - -For version >= 2.x: - -$ cat /etc/riak/riak.conf: -... -storage_backend = leveldb -... +$ mysql ejabberd_test < sql/mysql.sql diff --git a/test/README-quicktest.md b/test/README-quicktest.md deleted file mode 100644 index 43c71e86b..000000000 --- a/test/README-quicktest.md +++ /dev/null @@ -1,33 +0,0 @@ -# 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/acl_test.exs b/test/acl_test.exs deleted file mode 100644 index 4bd8e6989..000000000 --- a/test/acl_test.exs +++ /dev/null @@ -1,446 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 ACLTest do - @author "mremond@process-one.net" - - use ExUnit.Case, async: false - - setup_all do - :ok = :mnesia.start - :ok = :jid.start - :stringprep.start - :ok = :ejabberd_config.start(["domain1", "domain2"], []) - :ok = :acl.start - end - - setup do - :acl.clear - end - - test "access rule match with user part ACL" do - :acl.add(:global, :basic_acl_1, {:user, "test1"}) - :acl.add(:global, :basic_acl_1, {:user, "test2"}) - :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}]) - # JID can only be passes as jid record. - # => TODO: Support passing JID as binary. - assert :acl.match_rule(:global, :basic_rule_1, :jid.from_string("test1@domain1")) == :allow - assert :acl.match_rule(:global, :basic_rule_1, :jid.from_string("test1@domain2")) == :allow - assert :acl.match_rule(:global, :basic_rule_1, :jid.from_string("test2@domain1")) == :allow - assert :acl.match_rule(:global, :basic_rule_1, :jid.from_string("test2@domain2")) == :allow - # We match on user part only for local domain. As an implicit rule remote domain are not matched - assert :acl.match_rule(:global, :basic_rule_1, :jid.from_string("test1@otherdomain")) == :deny - assert :acl.match_rule(:global, :basic_rule_1, :jid.from_string("test2@otherdomain")) == :deny - assert :acl.match_rule(:global, :basic_rule_1, :jid.from_string("test11@domain1")) == :deny - - :acl.add(:global, :basic_acl_2, {:user, {"test2", "domain1"}}) - :acl.add_access(:global, :basic_rule_2, [{:allow, [{:acl, :basic_acl_2}]}]) - assert :acl.match_rule(:global, :basic_rule_2, :jid.from_string("test2@domain1")) == :allow - assert :acl.match_rule(:global, :basic_rule_2, :jid.from_string("test2@domain2")) == :deny - assert :acl.match_rule(:global, :basic_rule_2, :jid.from_string("test2@otherdomain")) == :deny - assert :acl.match_rule(:global, :basic_rule_2, {127,0,0,1}) == :deny - end - - test "IP based ACL" do - :acl.add(:global, :ip_acl_1, {:ip, "127.0.0.0/24"}) - :acl.add_access(:global, :ip_rule_1, [{:allow, [{:acl, :ip_acl_1}]}]) - # IP must be expressed as a tuple when calling match rule - assert :acl.match_rule(:global, :ip_rule_1, {127,0,0,1}) == :allow - assert :acl.match_rule(:global, :ip_rule_1, {127,0,1,1}) == :deny - assert :acl.match_rule(:global, :ip_rule_1, :jid.from_string("test1@domain1")) == :deny - end - - test "Access rule are evaluated sequentially" do - :acl.add(:global, :user_acl_1, {:user, {"test1", "domain2"}}) - :acl.add(:global, :user_acl_2, {:user, "test1"}) - :acl.add_access(:global, :user_rule_1, [{:deny, [{:acl, :user_acl_1}]}, {:allow, [{:acl, :user_acl_2}]}]) - assert :acl.match_rule(:global, :user_rule_1, :jid.from_string("test1@domain1")) == :allow - assert :acl.match_rule(:global, :user_rule_1, :jid.from_string("test1@domain2")) == :deny - end - - # Access rules are sometimes used to provide values (i.e.: max_s2s_connections, max_user_sessions) - test "Access rules providing values" do - :acl.add(:global, :user_acl, {:user_regexp, ""}) - :acl.add(:global, :admin_acl, {:user, "admin"}) - :acl.add_access(:global, :value_rule_1, [{10, [{:acl, :admin_acl}]}, {5, [{:acl, :user_acl}]}]) - assert :acl.match_rule(:global, :value_rule_1, :jid.from_string("test1@domain1")) == 5 - assert :acl.match_rule(:global, :value_rule_1, :jid.from_string("admin@domain1")) == 10 - - # If we have no match, :deny is still the default value - # => TODO maybe we should have a match rule which allow passing custom default value ? - assert :acl.match_rule(:global, :value_rule_1, :jid.from_string("user@otherdomain")) == :deny - end - - - # At the moment IP and user rules to no go well together: There is - # no way to combine IP and user restrictions. - # => TODO we need to implement access rules that implement both and will deny the access - # if either IP or user returns deny - test "mixing IP and user access rules" do - :acl.add(:global, :user_acl_1, {:user, "test1"}) - :acl.add(:global, :ip_acl_1, {:ip, "127.0.0.0/24"}) - :acl.add_access(:global, :mixed_rule_1, [{:allow, [{:acl, :user_acl_1}]}, {:allow, [{:acl, :ip_acl_1}]}]) - assert :acl.match_rule(:global, :mixed_rule_1, :jid.from_string("test1@domain1")) == :allow - assert :acl.match_rule(:global, :mixed_rule_1, {127,0,0,1}) == :allow - - :acl.add_access(:global, :mixed_rule_2, [{:deny, [{:acl, :user_acl_1}]}, {:allow, [{:acl, :ip_acl_1}]}]) - assert :acl.match_rule(:global, :mixed_rule_2, :jid.from_string("test1@domain1")) == :deny - assert :acl.match_rule(:global, :mixed_rule_2, {127,0,0,1}) == :allow - end - - test "access_matches works with predefined access rules" do - :acl.add(:global, :user_acl_2, {:user, "user"}) - :acl.add_access(:global, :user_rule_2, [{:allow, [{:acl, :user_acl_2}]}, {:deny, [:all]}]) - - assert :acl.access_matches(:user_rule_2, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - assert :acl.access_matches(:user_rule_2, %{usr: {"user2", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches rule all always matches" do - assert :acl.access_matches(:all, %{}, :global) == :allow - assert :acl.access_matches(:all, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - end - - test "access_matches rule none never matches" do - assert :acl.access_matches(:none, %{}, :global) == :deny - assert :acl.access_matches(:none, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches with not existing rule never matches" do - assert :acl.access_matches(:bleble, %{}, :global) == :deny - assert :acl.access_matches(:bleble, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches works with inlined access rules" do - :acl.add(:global, :user_acl_3, {:user, "user"}) - - assert :acl.access_matches([{:allow, [{:acl, :user_acl_3}]}, {:deny, [:all]}], - %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - assert :acl.access_matches([{:allow, [{:acl, :user_acl_3}]}, {:deny, [:all]}], - %{usr: {"user2", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches allow to have acl rules inlined" do - assert :acl.access_matches([{:allow, [{:user, "user"}]}, {:deny, [:all]}], - %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - assert :acl.access_matches([{:allow, [{:user, "user"}]}, {:deny, [:all]}], - %{usr: {"user2", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches test have implicit deny at end" do - assert :acl.access_matches([{:allow, [{:user, "user"}]}], - %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - assert :acl.access_matches([{:allow, [{:user, "user"}]}], - %{usr: {"user2", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches requires that all subrules match" do - rules = [{:allow, [{:user, "user"}, {:ip, {{127,0,0,1}, 32}}]}] - assert :acl.access_matches(rules, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "domain1", ""}, ip: {127,0,0,2}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user2", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches rules are matched in order" do - rules = [{:allow, [{:user, "user"}]}, {:deny, [{:user, "user2"}]}, {:allow, [{:user_regexp, "user"}]}] - assert :acl.access_matches(rules, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user2", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user22", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - end - - test "access_matches rules that require ip but no one is provided don't crash" do - rules = [{:allow, [{:ip, {{127,0,0,1}, 32}}]}, - {:allow, [{:user, "user"}]}, - {:allow, [{:user, "user2"}, {:ip, {{127,0,0,1}, 32}}]}] - assert :acl.access_matches(rules, %{usr: {"user", "domain1", ""}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user2", "domain1", ""}}, :global) == :deny - end - - test "access_matches rules that require usr but no one is provided don't crash" do - rules = [{:allow, [{:ip, {{127,0,0,1}, 32}}]}, - {:allow, [{:user, "user"}]}, - {:allow, [{:user, "user2"}, {:ip, {{127,0,0,2}, 32}}]}] - assert :acl.access_matches(rules, %{ip: {127,0,0,1}}, :global) == :allow - assert :acl.access_matches(rules, %{ip: {127,0,0,2}}, :global) == :deny - end - - test "access_matches rules with all always matches" do - rules = [{:allow, [:all]}, {:deny, {:user, "user"}}] - assert :acl.access_matches(rules, %{}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - end - - test "access_matches rules with {acl, all} always matches" do - rules = [{:allow, [{:acl, :all}]}, {:deny, {:user, "user"}}] - assert :acl.access_matches(rules, %{}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :allow - end - - test "access_matches rules with none never matches" do - rules = [{:allow, [:none]}, {:deny, [:all]}] - assert :acl.access_matches(rules, %{}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches with no rules never matches" do - assert :acl.access_matches([], %{}, :global) == :deny - assert :acl.access_matches([], %{usr: {"user", "domain1", ""}, ip: {127,0,0,1}}, :global) == :deny - end - - test "access_matches ip rule accepts {ip, port}" do - rules = [{:allow, [{:ip, {{127,0,0,1}, 32}}]}] - assert :acl.access_matches(rules, %{ip: {{127,0,0,1}, 5000}}, :global) == :allow - assert :acl.access_matches(rules, %{ip: {{127,0,0,2}, 5000}}, :global) == :deny - end - - test "access_matches user rule works" do - rules = [{:allow, [{:user, "user1"}]}] - assert :acl.access_matches(rules, %{usr: {"user1", "domain1", ""}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user2", "domain1", ""}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user1", "domain3", ""}}, :global) == :deny - end - - test "access_matches 2 arg user rule works" do - rules = [{:allow, [{:user, {"user1", "server1"}}]}] - assert :acl.access_matches(rules, %{usr: {"user1", "server1", ""}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user1", "server2", ""}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user2", "server1", ""}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user2", "server2", ""}}, :global) == :deny - end - - test "access_matches server rule works" do - rules = [{:allow, [{:server, "server1"}]}] - assert :acl.access_matches(rules, %{usr: {"user", "server1", ""}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "server2", ""}}, :global) == :deny - end - - test "access_matches resource rule works" do - rules = [{:allow, [{:resource, "res1"}]}] - assert :acl.access_matches(rules, %{usr: {"user", "domain1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "domain1", "res2"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user", "domain3", "res1"}}, :global) == :allow - end - - test "access_matches user_regexp rule works" do - rules = [{:allow, [{:user_regexp, "user[0-9]"}]}] - assert :acl.access_matches(rules, %{usr: {"user1", "domain1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"userA", "domain1", "res1"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user1", "domain3", "res1"}}, :global) == :deny - end - - test "access_matches 2 arg user_regexp rule works" do - rules = [{:allow, [{:user_regexp, {"user[0-9]", "server1"}}]}] - assert :acl.access_matches(rules, %{usr: {"user1", "server1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"userA", "server1", "res1"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user1", "server2", "res1"}}, :global) == :deny - end - - test "access_matches server_regexp rule works" do - rules = [{:allow, [{:server_regexp, "server[0-9]"}]}] - assert :acl.access_matches(rules, %{usr: {"user", "server1", ""}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "serverA", ""}}, :global) == :deny - end - - test "access_matches resource_regexp rule works" do - rules = [{:allow, [{:resource_regexp, "res[0-9]"}]}] - assert :acl.access_matches(rules, %{usr: {"user", "domain1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "domain1", "resA"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user", "domain3", "res1"}}, :global) == :allow - end - - test "access_matches node_regexp rule works" do - rules = [{:allow, [{:node_regexp, {"user[0-9]", "server[0-9]"}}]}] - assert :acl.access_matches(rules, %{usr: {"user1", "server1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"userA", "server1", "res1"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user1", "serverA", "res1"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"userA", "serverA", "res1"}}, :global) == :deny - end - - test "access_matches user_glob rule works" do - rules = [{:allow, [{:user_glob, "user?"}]}] - assert :acl.access_matches(rules, %{usr: {"user1", "domain1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user11", "domain1", "res1"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user1", "domain3", "res1"}}, :global) == :deny - end - - test "access_matches 2 arg user_glob rule works" do - rules = [{:allow, [{:user_glob, {"user?", "server1"}}]}] - assert :acl.access_matches(rules, %{usr: {"user1", "server1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user11", "server1", "res1"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user1", "server2", "res1"}}, :global) == :deny - end - - test "access_matches server_glob rule works" do - rules = [{:allow, [{:server_glob, "server?"}]}] - assert :acl.access_matches(rules, %{usr: {"user", "server1", ""}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "server11", ""}}, :global) == :deny - end - - test "access_matches resource_glob rule works" do - rules = [{:allow, [{:resource_glob, "res?"}]}] - assert :acl.access_matches(rules, %{usr: {"user", "domain1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user", "domain1", "res11"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user", "domain3", "res1"}}, :global) == :allow - end - - test "access_matches node_glob rule works" do - rules = [{:allow, [{:node_glob, {"user?", "server?"}}]}] - assert :acl.access_matches(rules, %{usr: {"user1", "server1", "res1"}}, :global) == :allow - assert :acl.access_matches(rules, %{usr: {"user11", "server1", "res1"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user1", "server11", "res1"}}, :global) == :deny - assert :acl.access_matches(rules, %{usr: {"user11", "server11", "res1"}}, :global) == :deny - end - - test "transform_access_rules_config expands allow rule" do - assert :acl.transform_access_rules_config([:allow]) == [{:allow, [:all]}] - end - - test "transform_access_rules_config expands deny rule" do - assert :acl.transform_access_rules_config([:deny]) == [{:deny, [:all]}] - end - - test "transform_access_rules_config expands <integer> rule" do - assert :acl.transform_access_rules_config([100]) == [{100, [:all]}] - end - - test "transform_access_rules_config expands <shaper_name> rule" do - assert :acl.transform_access_rules_config([:fast]) == [{:fast, [:all]}] - end - - test "transform_access_rules_config expands allow: <acl_name> rule" do - assert :acl.transform_access_rules_config([{:allow, :test1}]) == [{:allow, [{:acl, :test1}]}] - end - - test "transform_access_rules_config expands deny: <acl_name> rule" do - assert :acl.transform_access_rules_config([{:deny, :test1}]) == [{:deny, [{:acl, :test1}]}] - end - - test "transform_access_rules_config expands integer: <acl_name> rule" do - assert :acl.transform_access_rules_config([{100, :test1}]) == [{100, [{:acl, :test1}]}] - end - - test "transform_access_rules_config expands <shaper_name>: <acl_name> rule" do - assert :acl.transform_access_rules_config([{:fast, :test1}]) == [{:fast, [{:acl, :test1}]}] - end - - test "transform_access_rules_config expands allow rule (no list)" do - assert :acl.transform_access_rules_config(:allow) == [{:allow, [:all]}] - end - - test "transform_access_rules_config expands deny rule (no list)" do - assert :acl.transform_access_rules_config(:deny) == [{:deny, [:all]}] - end - - test "transform_access_rules_config expands <integer> rule (no list)" do - assert :acl.transform_access_rules_config(100) == [{100, [:all]}] - end - - test "transform_access_rules_config expands <shaper_name> rule (no list)" do - assert :acl.transform_access_rules_config(:fast) == [{:fast, [:all]}] - end - - test "access_rules_validator works with <AccessName>" do - assert :acl.access_rules_validator(:my_access) == :my_access - end - - test "get_opt with access_rules_validation works with <AccessName>" do - assert :gen_mod.get_opt(:access, [access: :my_rule], &:acl.access_rules_validator/1) - == :my_rule - end - - test "get_opt with access_rules_validation perform normalization for acl rules" do - assert :gen_mod.get_opt(:access, [access: [[allow: :zed]]], &:acl.access_rules_validator/1) - == [allow: [acl: :zed]] - end - - test "get_opt with access_rules_validation perform normalization for user@server rules" do - assert :gen_mod.get_opt(:access, [access: [allow: [user: "a@b"]]], &:acl.access_rules_validator/1) - == [allow: [user: {"a", "b"}]] - end - - test "get_opt with access_rules_validation return default value with number as rule type" do - assert :gen_mod.get_opt(:access, [access: [{100, [user: "a@b"]}]], &:acl.access_rules_validator/1) - == :undefined - end - - test "get_opt with access_rules_validation return default value when invalid rule type is passed" do - assert :gen_mod.get_opt(:access, [access: [allow2: [user: "a@b"]]], &:acl.access_rules_validator/1) - == :undefined - end - - test "get_opt with access_rules_validation return default value when invalid acl is passed" do - assert :gen_mod.get_opt(:access, [access: [allow: [user2: "a@b"]]], &:acl.access_rules_validator/1) - == :undefined - end - - test "shapes_rules_validator works with <AccessName>" do - assert :acl.shaper_rules_validator(:my_access) == :my_access - end - - test "get_opt with shaper_rules_validation works with <AccessName>" do - assert :gen_mod.get_opt(:access, [access: :my_rule], &:acl.shaper_rules_validator/1) - == :my_rule - end - - test "get_opt with shaper_rules_validation perform normalization for acl rules" do - assert :gen_mod.get_opt(:access, [access: [[allow: :zed]]], &:acl.shaper_rules_validator/1) - == [allow: [acl: :zed]] - end - - test "get_opt with shaper_rules_validation perform normalization for user@server rules" do - assert :gen_mod.get_opt(:access, [access: [allow: [user: "a@b"]]], &:acl.shaper_rules_validator/1) - == [allow: [user: {"a", "b"}]] - end - - test "get_opt with shaper_rules_validation return accepts number as rule type" do - assert :gen_mod.get_opt(:access, [access: [{100, [user: "a@b"]}]], &:acl.shaper_rules_validator/1) - == [{100, [user: {"a", "b"}]}] - end - - test "get_opt with shaper_rules_validation return accepts any atom as rule type" do - assert :gen_mod.get_opt(:access, [access: [fast: [user: "a@b"]]], &:acl.shaper_rules_validator/1) - == [fast: [user: {"a", "b"}]] - end - - test "get_opt with shaper_rules_validation return default value when invalid acl is passed" do - assert :gen_mod.get_opt(:access, [access: [allow: [user2: "a@b"]]], &:acl.shaper_rules_validator/1) - == :undefined - end - - ## Checking ACL on both user pattern and IP - ## ======================================== - - # Typical example is mod_register - - # Deprecated approach - test "module can test both IP and user through two independent :acl.match_rule check (deprecated)" do - :acl.add(:global, :user_acl, {:user, {"test1", "domain1"}}) - :acl.add(:global, :ip_acl, {:ip, "127.0.0.0/24"}) - :acl.add_access(:global, :user_rule, [{:allow, [{:acl, :user_acl}]}]) - :acl.add_access(:global, :ip_rule, [{:allow, [{:acl, :ip_acl}]}]) - - # acl module in 16.03 is not able to provide a function for compound result: - assert :acl.match_rule(:global, :user_rule, :jid.from_string("test1@domain1")) == :allow - assert :acl.match_rule(:global, :ip_rule, {127,0,0,1}) == :allow - assert :acl.match_rule(:global, :user_rule, :jid.from_string("test2@domain1")) == :deny - assert :acl.match_rule(:global, :ip_rule, {127,0,1,1}) == :deny - end - -end diff --git a/test/announce_tests.erl b/test/announce_tests.erl new file mode 100644 index 000000000..1a08f317f --- /dev/null +++ b/test/announce_tests.erl @@ -0,0 +1,76 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(announce_tests). + +%% API +-compile(export_all). +-import(suite, [server_jid/1, send_recv/2, recv_message/1, disconnect/1, + send/2, wait_for_master/1, wait_for_slave/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {announce_single, [sequence], []}. + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {announce_master_slave, [sequence], + [master_slave_test(set_motd)]}. + +set_motd_master(Config) -> + ServerJID = server_jid(Config), + MotdJID = jid:replace_resource(ServerJID, <<"announce/motd">>), + Body = xmpp:mk_text(<<"motd">>), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send(Config, #message{to = MotdJID, body = Body}), + #message{from = ServerJID, body = Body} = recv_message(Config), + disconnect(Config). + +set_motd_slave(Config) -> + ServerJID = server_jid(Config), + Body = xmpp:mk_text(<<"motd">>), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + #message{from = ServerJID, body = Body} = recv_message(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("announce_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("announce_" ++ atom_to_list(T)), [parallel], + [list_to_atom("announce_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("announce_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/carbons_tests.erl b/test/carbons_tests.erl new file mode 100644 index 000000000..5142346c1 --- /dev/null +++ b/test/carbons_tests.erl @@ -0,0 +1,211 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(carbons_tests). + +%% API +-compile(export_all). +-import(suite, [is_feature_advertised/2, disconnect/1, send_recv/2, + recv_presence/1, send/2, get_event/1, recv_message/1, + my_jid/1, wait_for_slave/1, wait_for_master/1, + put_event/2]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {carbons_single, [sequence], + [single_test(feature_enabled), + single_test(unsupported_iq)]}. + +feature_enabled(Config) -> + true = is_feature_advertised(Config, ?NS_CARBONS_2), + disconnect(Config). + +unsupported_iq(Config) -> + lists:foreach( + fun({Type, SubEl}) -> + #iq{type = error} = + send_recv(Config, #iq{type = Type, sub_els = [SubEl]}) + end, [{Type, SubEl} || + Type <- [get, set], + SubEl <- [#carbons_sent{forwarded = #forwarded{}}, + #carbons_received{forwarded = #forwarded{}}, + #carbons_private{}]] ++ + [{get, SubEl} || SubEl <- [#carbons_enable{}, #carbons_disable{}]]), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {carbons_master_slave, [sequence], + [master_slave_test(send_recv), + master_slave_test(enable_disable)]}. + +send_recv_master(Config) -> + Peer = ?config(peer, Config), + prepare_master(Config), + ct:comment("Waiting for the peer to be ready"), + ready = get_event(Config), + send_messages(Config), + ct:comment("Waiting for the peer to disconnect"), + #presence{from = Peer, type = unavailable} = recv_presence(Config), + disconnect(Config). + +send_recv_slave(Config) -> + prepare_slave(Config), + ok = enable(Config), + put_event(Config, ready), + recv_carbons(Config), + disconnect(Config). + +enable_disable_master(Config) -> + prepare_master(Config), + ct:comment("Waiting for the peer to be ready"), + ready = get_event(Config), + send_messages(Config), + disconnect(Config). + +enable_disable_slave(Config) -> + Peer = ?config(peer, Config), + prepare_slave(Config), + ok = enable(Config), + ok = disable(Config), + put_event(Config, ready), + ct:comment("Waiting for the peer to disconnect"), + #presence{from = Peer, type = unavailable} = recv_presence(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("carbons_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("carbons_" ++ atom_to_list(T)), [parallel], + [list_to_atom("carbons_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("carbons_" ++ atom_to_list(T) ++ "_slave")]}. + +prepare_master(Config) -> + MyJID = my_jid(Config), + Peer = ?config(peer, Config), + #presence{from = MyJID} = send_recv(Config, #presence{priority = 10}), + wait_for_slave(Config), + ct:comment("Receiving initial presence from the peer"), + #presence{from = Peer} = recv_presence(Config), + Config. + +prepare_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = enable(Config), + wait_for_master(Config), + #presence{from = MyJID} = send_recv(Config, #presence{priority = 5}), + ct:comment("Receiving initial presence from the peer"), + #presence{from = Peer} = recv_presence(Config), + Config. + +send_messages(Config) -> + Server = ?config(server, Config), + MyJID = my_jid(Config), + JID = jid:make(p1_rand:get_string(), Server), + lists:foreach( + fun({send, #message{type = Type} = Msg}) -> + I = send(Config, Msg#message{to = JID}), + if Type /= error -> + #message{id = I, type = error} = recv_message(Config); + true -> + ok + end; + ({recv, #message{} = Msg}) -> + ejabberd_router:route( + Msg#message{from = JID, to = MyJID}), + ct:comment("Receiving message ~s", [xmpp:pp(Msg)]), + #message{} = recv_message(Config) + end, message_iterator(Config)). + +recv_carbons(Config) -> + Peer = ?config(peer, Config), + BarePeer = jid:remove_resource(Peer), + MyJID = my_jid(Config), + lists:foreach( + fun({_, #message{sub_els = [#hint{type = 'no-copy'}]}}) -> + ok; + ({_, #message{sub_els = [#carbons_private{}]}}) -> + ok; + ({_, #message{type = T}}) when T /= normal, T /= chat -> + ok; + ({Dir, #message{type = T, body = Body} = M}) + when (T == chat) or (T == normal andalso Body /= []) -> + ct:comment("Receiving carbon ~s", [xmpp:pp(M)]), + #message{from = BarePeer, to = MyJID} = CarbonMsg = + recv_message(Config), + case Dir of + send -> + #carbons_sent{forwarded = #forwarded{sub_els = [El]}} = + xmpp:get_subtag(CarbonMsg, #carbons_sent{}), + #message{body = Body} = xmpp:decode(El); + recv -> + #carbons_received{forwarded = #forwarded{sub_els = [El]}}= + xmpp:get_subtag(CarbonMsg, #carbons_received{}), + #message{body = Body} = xmpp:decode(El) + end; + (_) -> + false + end, message_iterator(Config)). + +enable(Config) -> + case send_recv( + Config, #iq{type = set, + sub_els = [#carbons_enable{}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +disable(Config) -> + case send_recv( + Config, #iq{type = set, + sub_els = [#carbons_disable{}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +message_iterator(_Config) -> + [{Dir, #message{type = Type, body = Body, sub_els = Els}} + || Dir <- [send, recv], + Type <- [error, chat, normal, groupchat, headline], + Body <- [[], xmpp:mk_text(<<"body">>)], + Els <- [[], + [#hint{type = 'no-copy'}], + [#carbons_private{}]]]. diff --git a/test/csi_tests.erl b/test/csi_tests.erl new file mode 100644 index 000000000..027cd66c0 --- /dev/null +++ b/test/csi_tests.erl @@ -0,0 +1,162 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(csi_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, wait_for_slave/1, wait_for_master/1, + send/2, send_recv/2, recv_presence/1, recv_message/1, + server_jid/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {csi_single, [sequence], + [single_test(feature_enabled)]}. + +feature_enabled(Config) -> + true = ?config(csi, Config), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {csi_master_slave, [sequence], + [master_slave_test(all)]}. + +all_master(Config) -> + Peer = ?config(peer, Config), + Presence = #presence{to = Peer}, + ChatState = #message{to = Peer, thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = active}]}, + Message = ChatState#message{body = [#text{data = <<"body">>}]}, + PepPayload = xmpp:encode(#presence{}), + PepOne = #message{ + to = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-1">>, + items = + [#ps_item{ + id = <<"pep-1">>, + sub_els = [PepPayload]}]}}]}, + PepTwo = #message{ + to = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-2">>, + items = + [#ps_item{ + id = <<"pep-2">>, + sub_els = [PepPayload]}]}}]}, + %% Wait for the slave to become inactive. + wait_for_slave(Config), + %% Should be queued (but see below): + send(Config, Presence), + %% Should replace the previous presence in the queue: + send(Config, Presence#presence{type = unavailable}), + %% The following two PEP stanzas should be queued (but see below): + send(Config, PepOne), + send(Config, PepTwo), + %% The following two PEP stanzas should replace the previous two: + send(Config, PepOne), + send(Config, PepTwo), + %% Should be queued (but see below): + send(Config, ChatState), + %% Should replace the previous chat state in the queue: + send(Config, ChatState#message{sub_els = [#chatstate{type = composing}]}), + %% Should be sent immediately, together with the queued stanzas: + send(Config, Message), + %% Wait for the slave to become active. + wait_for_slave(Config), + %% Should be delivered, as the client is active again: + send(Config, ChatState), + disconnect(Config). + +all_slave(Config) -> + Peer = ?config(peer, Config), + change_client_state(Config, inactive), + wait_for_master(Config), + #presence{from = Peer, type = unavailable, sub_els = [#delay{}]} = + recv_presence(Config), + #message{ + from = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-1">>, + items = + [#ps_item{ + id = <<"pep-1">>}]}}, + #delay{}]} = recv_message(Config), + #message{ + from = Peer, + sub_els = + [#ps_event{ + items = + #ps_items{ + node = <<"foo-2">>, + items = + [#ps_item{ + id = <<"pep-2">>}]}}, + #delay{}]} = recv_message(Config), + #message{from = Peer, thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = composing}, + #delay{}]} = recv_message(Config), + #message{from = Peer, thread = #message_thread{data = <<"1">>}, + body = [#text{data = <<"body">>}], + sub_els = [#chatstate{type = active}]} = recv_message(Config), + change_client_state(Config, active), + wait_for_master(Config), + #message{from = Peer, thread = #message_thread{data = <<"1">>}, + sub_els = [#chatstate{type = active}]} = recv_message(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("csi_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("csi_" ++ atom_to_list(T)), [parallel], + [list_to_atom("csi_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("csi_" ++ atom_to_list(T) ++ "_slave")]}. + +change_client_state(Config, NewState) -> + send(Config, #csi{type = NewState}), + send_recv(Config, #iq{type = get, to = server_jid(Config), + sub_els = [#ping{}]}). diff --git a/test/docker/README.md b/test/docker/README.md new file mode 100644 index 000000000..22324a6e0 --- /dev/null +++ b/test/docker/README.md @@ -0,0 +1,46 @@ +# Docker database images to run ejabberd tests + +## Starting databases + +You can start the Docker environment with Docker Compose, from ejabberd repository root. + +The following command will launch MySQL, PostgreSQL, Redis and keep the console +attached to it. + +``` +mkdir test/docker/db/mysql/data +mkdir test/docker/db/postgres/data +(cd test/docker; docker-compose up) +``` + +You can stop all the databases with CTRL-C. + +## Running tests + +Before running the test, you can ensure there is no running instance of Erlang common test tool. You can run the following +command, especially if all test are skipped with an `eaddrinuse` error: + +``` +pkill -9 ct_run +``` + +You can run tests with (from ejabberd repository root): + +``` +make test +``` + +## Cleaning up the test environment + +You can fully clean up the environment with: + +``` +(cd test/docker; docker-compose down) +``` + +If you want to clean the data, you can remove the data directories after the `docker-compose down` command: + +``` +rm -rf test/docker/db/mysql/data +rm -rf test/docker/db/postgres/data +``` diff --git a/test/docker/db/mysql/initdb/mysql.sql b/test/docker/db/mysql/initdb/mysql.sql new file mode 100644 index 000000000..a05f8c86c --- /dev/null +++ b/test/docker/db/mysql/initdb/mysql.sql @@ -0,0 +1,465 @@ +-- +-- ejabberd, Copyright (C) 2002-2019 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. +-- + +CREATE TABLE users ( + username varchar(191) PRIMARY KEY, + password text NOT NULL, + serverkey varchar(64) NOT NULL DEFAULT '', + salt varchar(64) NOT NULL DEFAULT '', + iterationcount integer NOT NULL DEFAULT 0, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Add support for SCRAM auth to a database created before ejabberd 16.03: +-- ALTER TABLE users ADD COLUMN serverkey varchar(64) NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN salt varchar(64) NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; + +CREATE TABLE last ( + username varchar(191) PRIMARY KEY, + seconds text NOT NULL, + state text NOT NULl +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + + +CREATE TABLE rosterusers ( + username varchar(191) NOT NULL, + jid varchar(191) NOT NULL, + nick text NOT NULL, + subscription character(1) NOT NULL, + ask character(1) NOT NULL, + askmessage text NOT NULL, + server character(1) NOT NULL, + subscribe text NOT NULL, + type text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers(username(75), jid(75)); +CREATE INDEX i_rosteru_username ON rosterusers(username); +CREATE INDEX i_rosteru_jid ON rosterusers(jid); + +CREATE TABLE rostergroups ( + username varchar(191) NOT NULL, + jid varchar(191) NOT NULL, + grp text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX pk_rosterg_user_jid ON rostergroups(username(75), jid(75)); + +CREATE TABLE sr_group ( + name varchar(191) NOT NULL, + opts text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE sr_user ( + jid varchar(191) NOT NULL, + grp varchar(191) NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_sr_user_jid_group ON sr_user(jid(75), grp(75)); +CREATE INDEX i_sr_user_jid ON sr_user(jid); +CREATE INDEX i_sr_user_grp ON sr_user(grp); + +CREATE TABLE spool ( + username varchar(191) NOT NULL, + xml mediumtext NOT NULL, + seq BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_despool USING BTREE ON spool(username); +CREATE INDEX i_spool_created_at USING BTREE ON spool(created_at); + +CREATE TABLE archive ( + username varchar(191) NOT NULL, + timestamp BIGINT UNSIGNED NOT NULL, + peer varchar(191) NOT NULL, + bare_peer varchar(191) NOT NULL, + xml mediumtext NOT NULL, + txt mediumtext, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, + kind varchar(10), + nick varchar(191), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE FULLTEXT INDEX i_text ON archive(txt); +CREATE INDEX i_username_timestamp USING BTREE ON archive(username(191), timestamp); +CREATE INDEX i_username_peer USING BTREE ON archive(username(191), peer(191)); +CREATE INDEX i_username_bare_peer USING BTREE ON archive(username(191), bare_peer(191)); +CREATE INDEX i_timestamp USING BTREE ON archive(timestamp); + +CREATE TABLE archive_prefs ( + username varchar(191) NOT NULL PRIMARY KEY, + def text NOT NULL, + always text NOT NULL, + never text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE vcard ( + username varchar(191) PRIMARY KEY, + vcard mediumtext NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE vcard_search ( + username varchar(191) NOT NULL, + lusername varchar(191) PRIMARY KEY, + fn text NOT NULL, + lfn varchar(191) NOT NULL, + family text NOT NULL, + lfamily varchar(191) NOT NULL, + given text NOT NULL, + lgiven varchar(191) NOT NULL, + middle text NOT NULL, + lmiddle varchar(191) NOT NULL, + nickname text NOT NULL, + lnickname varchar(191) NOT NULL, + bday text NOT NULL, + lbday varchar(191) NOT NULL, + ctry text NOT NULL, + lctry varchar(191) NOT NULL, + locality text NOT NULL, + llocality varchar(191) NOT NULL, + email text NOT NULL, + lemail varchar(191) NOT NULL, + orgname text NOT NULL, + lorgname varchar(191) NOT NULL, + orgunit text NOT NULL, + lorgunit varchar(191) NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_vcard_search_lfn ON vcard_search(lfn); +CREATE INDEX i_vcard_search_lfamily ON vcard_search(lfamily); +CREATE INDEX i_vcard_search_lgiven ON vcard_search(lgiven); +CREATE INDEX i_vcard_search_lmiddle ON vcard_search(lmiddle); +CREATE INDEX i_vcard_search_lnickname ON vcard_search(lnickname); +CREATE INDEX i_vcard_search_lbday ON vcard_search(lbday); +CREATE INDEX i_vcard_search_lctry ON vcard_search(lctry); +CREATE INDEX i_vcard_search_llocality ON vcard_search(llocality); +CREATE INDEX i_vcard_search_lemail ON vcard_search(lemail); +CREATE INDEX i_vcard_search_lorgname ON vcard_search(lorgname); +CREATE INDEX i_vcard_search_lorgunit ON vcard_search(lorgunit); + +CREATE TABLE privacy_default_list ( + username varchar(191) PRIMARY KEY, + name varchar(191) NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE privacy_list ( + username varchar(191) NOT NULL, + name varchar(191) NOT NULL, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_privacy_list_username USING BTREE ON privacy_list(username); +CREATE UNIQUE INDEX i_privacy_list_username_name USING BTREE ON privacy_list (username(75), name(75)); + +CREATE TABLE privacy_list_data ( + id bigint, + t character(1) NOT NULL, + value text NOT NULL, + action character(1) NOT NULL, + ord NUMERIC NOT NULL, + match_all boolean NOT NULL, + match_iq boolean NOT NULL, + match_message boolean NOT NULL, + match_presence_in boolean NOT NULL, + match_presence_out boolean NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_privacy_list_data_id ON privacy_list_data(id); + +CREATE TABLE private_storage ( + username varchar(191) NOT NULL, + namespace varchar(191) NOT NULL, + data text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_private_storage_username USING BTREE ON private_storage(username); +CREATE UNIQUE INDEX i_private_storage_username_namespace USING BTREE ON private_storage(username(75), namespace(75)); + +-- Not tested in mysql +CREATE TABLE roster_version ( + username varchar(191) PRIMARY KEY, + version text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- To update from 1.x: +-- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask; +-- UPDATE rosterusers SET askmessage = ''; +-- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; + +CREATE TABLE pubsub_node ( + host text NOT NULL, + node text NOT NULL, + parent VARCHAR(191) NOT NULL DEFAULT '', + plugin text NOT NULL, + nodeid bigint auto_increment primary key +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_node_parent ON pubsub_node(parent(120)); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node(host(71), node(120)); + +CREATE TABLE pubsub_node_option ( + nodeid bigint, + name text NOT NULL, + val text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option(nodeid); +ALTER TABLE `pubsub_node_option` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_node_owner ( + nodeid bigint, + owner text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner(nodeid); +ALTER TABLE `pubsub_node_owner` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_state ( + nodeid bigint, + jid text NOT NULL, + affiliation character(1), + subscriptions VARCHAR(191) NOT NULL DEFAULT '', + stateid bigint auto_increment primary key +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_state_jid ON pubsub_state(jid(60)); +CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state(nodeid, jid(60)); +ALTER TABLE `pubsub_state` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_item ( + nodeid bigint, + itemid text NOT NULL, + publisher text NOT NULL, + creation varchar(32) NOT NULL, + modification varchar(32) NOT NULL, + payload mediumtext NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE INDEX i_pubsub_item_itemid ON pubsub_item(itemid(36)); +CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item(nodeid, itemid(36)); +ALTER TABLE `pubsub_item` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; + +CREATE TABLE pubsub_subscription_opt ( + subid text NOT NULL, + opt_name varchar(32), + opt_value text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt(subid(32), opt_name(32)); + +CREATE TABLE muc_room ( + name text NOT NULL, + host text NOT NULL, + opts mediumtext NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_muc_room_name_host USING BTREE ON muc_room(name(75), host(75)); + +CREATE TABLE muc_registered ( + jid text NOT NULL, + host text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_muc_registered_nick USING BTREE ON muc_registered(nick(75)); +CREATE UNIQUE INDEX i_muc_registered_jid_host USING BTREE ON muc_registered(jid(75), host(75)); + +CREATE TABLE muc_online_room ( + name text NOT NULL, + host text NOT NULL, + node text NOT NULL, + pid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_muc_online_room_name_host USING BTREE ON muc_online_room(name(75), host(75)); + +CREATE TABLE muc_online_users ( + username text NOT NULL, + server text NOT NULL, + resource text NOT NULL, + name text NOT NULL, + host text NOT NULL, + node text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_muc_online_users USING BTREE ON muc_online_users(username(75), server(75), resource(75), name(75), host(75)); +CREATE INDEX i_muc_online_users_us USING BTREE ON muc_online_users(username(75), server(75)); + +CREATE TABLE muc_room_subscribers ( + room varchar(191) NOT NULL, + host varchar(191) NOT NULL, + jid varchar(191) NOT NULL, + nick text NOT NULL, + nodes text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY i_muc_room_subscribers_host_room_jid (host, room, jid) +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid); + +CREATE TABLE motd ( + username varchar(191) PRIMARY KEY, + xml text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE caps_features ( + node varchar(191) NOT NULL, + subnode varchar(191) NOT NULL, + feature text, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE INDEX i_caps_features_node_subnode ON caps_features(node(75), subnode(75)); + +CREATE TABLE sm ( + usec bigint NOT NULL, + pid text NOT NULL, + node text NOT NULL, + username varchar(191) NOT NULL, + resource varchar(191) NOT NULL, + priority text NOT NULL, + info text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_sid ON sm(usec, pid(75)); +CREATE INDEX i_node ON sm(node(75)); +CREATE INDEX i_username ON sm(username); + +CREATE TABLE oauth_token ( + token varchar(191) NOT NULL PRIMARY KEY, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE route ( + domain text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL, + local_hint text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_route ON route(domain(75), server_host(75), node(75), pid(75)); +CREATE INDEX i_route_domain ON route(domain(75)); + +CREATE TABLE bosh ( + sid text NOT NULL, + node text NOT NULL, + pid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid(75)); + +CREATE TABLE proxy65 ( + sid text NOT NULL, + pid_t text NOT NULL, + pid_i text NOT NULL, + node_t text NOT NULL, + node_i text NOT NULL, + jid_i text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid(191)); +CREATE INDEX i_proxy65_jid ON proxy65 (jid_i(191)); + +CREATE TABLE push_session ( + username text NOT NULL, + timestamp bigint NOT NULL, + service text NOT NULL, + node text NOT NULL, + xml text NOT NULL +); + +CREATE UNIQUE INDEX i_push_usn ON push_session (username(191), service(191), node(191)); +CREATE UNIQUE INDEX i_push_ut ON push_session (username(191), timestamp); + +CREATE TABLE mix_channel ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + hidden boolean NOT NULL, + hmac_key text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel(191), service(191)); +CREATE INDEX i_mix_channel_serv ON mix_channel (service(191)); + +CREATE TABLE mix_participant ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + id text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel(191), service(191), username(191), domain(191)); +CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel(191), service(191)); + +CREATE TABLE mix_subscription ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + node text NOT NULL, + jid text NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel(153), service(153), username(153), domain(153), node(153)); +CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel(191), service(191), username(191), domain(191)); +CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel(191), service(191), node(191)); +CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel(191), service(191)); + +CREATE TABLE mix_pam ( + username text NOT NULL, + channel text NOT NULL, + service text NOT NULL, + id text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username(191), channel(191), service(191)); +CREATE INDEX i_mix_pam_u ON mix_pam (username(191)); + +CREATE TABLE mqtt_pub ( + username varchar(191) NOT NULL, + resource varchar(191) NOT NULL, + topic text NOT NULL, + qos tinyint NOT NULL, + payload blob NOT NULL, + payload_format tinyint NOT NULL, + content_type text NOT NULL, + response_topic text NOT NULL, + correlation_data blob NOT NULL, + user_properties blob NOT NULL, + expiry int unsigned NOT NULL, + UNIQUE KEY i_mqtt_topic (topic(191)) +); diff --git a/test/docker/db/postgres/initdb/pg.sql b/test/docker/db/postgres/initdb/pg.sql new file mode 100644 index 000000000..eae98d3f0 --- /dev/null +++ b/test/docker/db/postgres/initdb/pg.sql @@ -0,0 +1,470 @@ +-- +-- ejabberd, Copyright (C) 2002-2019 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. +-- + +CREATE TABLE users ( + username text PRIMARY KEY, + "password" text NOT NULL, + serverkey text NOT NULL DEFAULT '', + salt text NOT NULL DEFAULT '', + iterationcount integer NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +-- Add support for SCRAM auth to a database created before ejabberd 16.03: +-- ALTER TABLE users ADD COLUMN serverkey text NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN salt text NOT NULL DEFAULT ''; +-- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; + +CREATE TABLE last ( + username text PRIMARY KEY, + seconds text NOT NULL, + state text NOT NULL +); + + +CREATE TABLE rosterusers ( + username text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + subscription character(1) NOT NULL, + ask character(1) NOT NULL, + askmessage text NOT NULL, + server character(1) NOT NULL, + subscribe text NOT NULL, + "type" text, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers USING btree (username, jid); +CREATE INDEX i_rosteru_username ON rosterusers USING btree (username); +CREATE INDEX i_rosteru_jid ON rosterusers USING btree (jid); + + +CREATE TABLE rostergroups ( + username text NOT NULL, + jid text NOT NULL, + grp text NOT NULL +); + +CREATE INDEX pk_rosterg_user_jid ON rostergroups USING btree (username, jid); + +CREATE TABLE sr_group ( + name text NOT NULL, + opts text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE TABLE sr_user ( + jid text NOT NULL, + grp text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_sr_user_jid_grp ON sr_user USING btree (jid, grp); +CREATE INDEX i_sr_user_jid ON sr_user USING btree (jid); +CREATE INDEX i_sr_user_grp ON sr_user USING btree (grp); + +CREATE TABLE spool ( + username text NOT NULL, + xml text NOT NULL, + seq SERIAL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_despool ON spool USING btree (username); + +CREATE TABLE archive ( + username text NOT NULL, + timestamp BIGINT NOT NULL, + peer text NOT NULL, + bare_peer text NOT NULL, + xml text NOT NULL, + txt text, + id SERIAL, + kind text, + nick text, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_username_timestamp ON archive USING btree (username, timestamp); +CREATE INDEX i_username_peer ON archive USING btree (username, peer); +CREATE INDEX i_username_bare_peer ON archive USING btree (username, bare_peer); +CREATE INDEX i_timestamp ON archive USING btree (timestamp); + +CREATE TABLE archive_prefs ( + username text NOT NULL PRIMARY KEY, + def text NOT NULL, + always text NOT NULL, + never text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE TABLE vcard ( + username text PRIMARY KEY, + vcard text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE TABLE vcard_search ( + username text NOT NULL, + lusername text PRIMARY KEY, + fn text NOT NULL, + lfn text NOT NULL, + family text NOT NULL, + lfamily text NOT NULL, + given text NOT NULL, + lgiven text NOT NULL, + middle text NOT NULL, + lmiddle text NOT NULL, + nickname text NOT NULL, + lnickname text NOT NULL, + bday text NOT NULL, + lbday text NOT NULL, + ctry text NOT NULL, + lctry text NOT NULL, + locality text NOT NULL, + llocality text NOT NULL, + email text NOT NULL, + lemail text NOT NULL, + orgname text NOT NULL, + lorgname text NOT NULL, + orgunit text NOT NULL, + lorgunit text NOT NULL +); + +CREATE INDEX i_vcard_search_lfn ON vcard_search(lfn); +CREATE INDEX i_vcard_search_lfamily ON vcard_search(lfamily); +CREATE INDEX i_vcard_search_lgiven ON vcard_search(lgiven); +CREATE INDEX i_vcard_search_lmiddle ON vcard_search(lmiddle); +CREATE INDEX i_vcard_search_lnickname ON vcard_search(lnickname); +CREATE INDEX i_vcard_search_lbday ON vcard_search(lbday); +CREATE INDEX i_vcard_search_lctry ON vcard_search(lctry); +CREATE INDEX i_vcard_search_llocality ON vcard_search(llocality); +CREATE INDEX i_vcard_search_lemail ON vcard_search(lemail); +CREATE INDEX i_vcard_search_lorgname ON vcard_search(lorgname); +CREATE INDEX i_vcard_search_lorgunit ON vcard_search(lorgunit); + +CREATE TABLE privacy_default_list ( + username text PRIMARY KEY, + name text NOT NULL +); + +CREATE TABLE privacy_list ( + username text NOT NULL, + name text NOT NULL, + id SERIAL UNIQUE, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_privacy_list_username ON privacy_list USING btree (username); +CREATE UNIQUE INDEX i_privacy_list_username_name ON privacy_list USING btree (username, name); + +CREATE TABLE privacy_list_data ( + id bigint REFERENCES privacy_list(id) ON DELETE CASCADE, + t character(1) NOT NULL, + value text NOT NULL, + action character(1) NOT NULL, + ord NUMERIC NOT NULL, + match_all boolean NOT NULL, + match_iq boolean NOT NULL, + match_message boolean NOT NULL, + match_presence_in boolean NOT NULL, + match_presence_out boolean NOT NULL +); + +CREATE INDEX i_privacy_list_data_id ON privacy_list_data USING btree (id); + +CREATE TABLE private_storage ( + username text NOT NULL, + namespace text NOT NULL, + data text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_private_storage_username ON private_storage USING btree (username); +CREATE UNIQUE INDEX i_private_storage_username_namespace ON private_storage USING btree (username, namespace); + + +CREATE TABLE roster_version ( + username text PRIMARY KEY, + version text NOT NULL +); + +-- To update from 0.9.8: +-- CREATE SEQUENCE spool_seq_seq; +-- ALTER TABLE spool ADD COLUMN seq integer; +-- ALTER TABLE spool ALTER COLUMN seq SET DEFAULT nextval('spool_seq_seq'); +-- UPDATE spool SET seq = DEFAULT; +-- ALTER TABLE spool ALTER COLUMN seq SET NOT NULL; + +-- To update from 1.x: +-- ALTER TABLE rosterusers ADD COLUMN askmessage text; +-- UPDATE rosterusers SET askmessage = ''; +-- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; + +CREATE TABLE pubsub_node ( + host text NOT NULL, + node text NOT NULL, + parent text NOT NULL DEFAULT '', + plugin text NOT NULL, + nodeid SERIAL UNIQUE +); +CREATE INDEX i_pubsub_node_parent ON pubsub_node USING btree (parent); +CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node USING btree (host, node); + +CREATE TABLE pubsub_node_option ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + name text NOT NULL, + val text NOT NULL +); +CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option USING btree (nodeid); + +CREATE TABLE pubsub_node_owner ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + owner text NOT NULL +); +CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner USING btree (nodeid); + +CREATE TABLE pubsub_state ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + jid text NOT NULL, + affiliation character(1), + subscriptions text NOT NULL DEFAULT '', + stateid SERIAL UNIQUE +); +CREATE INDEX i_pubsub_state_jid ON pubsub_state USING btree (jid); +CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state USING btree (nodeid, jid); + +CREATE TABLE pubsub_item ( + nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, + itemid text NOT NULL, + publisher text NOT NULL, + creation varchar(32) NOT NULL, + modification varchar(32) NOT NULL, + payload text NOT NULL DEFAULT '' +); +CREATE INDEX i_pubsub_item_itemid ON pubsub_item USING btree (itemid); +CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item USING btree (nodeid, itemid); + +CREATE TABLE pubsub_subscription_opt ( + subid text NOT NULL, + opt_name varchar(32), + opt_value text NOT NULL +); +CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt USING btree (subid, opt_name); + +CREATE TABLE muc_room ( + name text NOT NULL, + host text NOT NULL, + opts text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room USING btree (name, host); + +CREATE TABLE muc_registered ( + jid text NOT NULL, + host text NOT NULL, + nick text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_muc_registered_nick ON muc_registered USING btree (nick); +CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered USING btree (jid, host); + +CREATE TABLE muc_online_room ( + name text NOT NULL, + host text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room USING btree (name, host); + +CREATE TABLE muc_online_users ( + username text NOT NULL, + server text NOT NULL, + resource text NOT NULL, + name text NOT NULL, + host text NOT NULL, + node text NOT NULL +); + +CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users USING btree (username, server, resource, name, host); +CREATE INDEX i_muc_online_users_us ON muc_online_users USING btree (username, server); + +CREATE TABLE muc_room_subscribers ( + room text NOT NULL, + host text NOT NULL, + jid text NOT NULL, + nick text NOT NULL, + nodes text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid); +CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid); + +CREATE TABLE motd ( + username text PRIMARY KEY, + xml text, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE TABLE caps_features ( + node text NOT NULL, + subnode text NOT NULL, + feature text, + created_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE INDEX i_caps_features_node_subnode ON caps_features USING btree (node, subnode); + +CREATE TABLE sm ( + usec bigint NOT NULL, + pid text NOT NULL, + node text NOT NULL, + username text NOT NULL, + resource text NOT NULL, + priority text NOT NULL, + info text NOT NULL +); + +CREATE UNIQUE INDEX i_sm_sid ON sm USING btree (usec, pid); +CREATE INDEX i_sm_node ON sm USING btree (node); +CREATE INDEX i_sm_username ON sm USING btree (username); + +CREATE TABLE oauth_token ( + token text NOT NULL, + jid text NOT NULL, + scope text NOT NULL, + expire bigint NOT NULL +); + +CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token); + +CREATE TABLE route ( + domain text NOT NULL, + server_host text NOT NULL, + node text NOT NULL, + pid text NOT NULL, + local_hint text NOT NULL +); + +CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); +CREATE INDEX i_route_domain ON route USING btree (domain); + +CREATE TABLE bosh ( + sid text NOT NULL, + node text NOT NULL, + pid text NOT NULL +); + +CREATE UNIQUE INDEX i_bosh_sid ON bosh USING btree (sid); + +CREATE TABLE proxy65 ( + sid text NOT NULL, + pid_t text NOT NULL, + pid_i text NOT NULL, + node_t text NOT NULL, + node_i text NOT NULL, + jid_i text NOT NULL +); + +CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 USING btree (sid); +CREATE INDEX i_proxy65_jid ON proxy65 USING btree (jid_i); + +CREATE TABLE push_session ( + username text NOT NULL, + timestamp bigint NOT NULL, + service text NOT NULL, + node text NOT NULL, + xml text NOT NULL +); + +CREATE UNIQUE INDEX i_push_usn ON push_session USING btree (username, service, node); +CREATE UNIQUE INDEX i_push_ut ON push_session USING btree (username, timestamp); + +CREATE TABLE mix_channel ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + hidden boolean NOT NULL, + hmac_key text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); +CREATE INDEX i_mix_channel_serv ON mix_channel (service); + +CREATE TABLE mix_participant ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + jid text NOT NULL, + id text NOT NULL, + nick text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); +CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); + +CREATE TABLE mix_subscription ( + channel text NOT NULL, + service text NOT NULL, + username text NOT NULL, + domain text NOT NULL, + node text NOT NULL, + jid text NOT NULL +); + +CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); +CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); +CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); +CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); + +CREATE TABLE mix_pam ( + username text NOT NULL, + channel text NOT NULL, + service text NOT NULL, + id text NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, channel, service); +CREATE INDEX i_mix_pam_us ON mix_pam (username); + +CREATE TABLE mqtt_pub ( + username text NOT NULL, + resource text NOT NULL, + topic text NOT NULL, + qos smallint NOT NULL, + payload bytea NOT NULL, + payload_format smallint NOT NULL, + content_type text NOT NULL, + response_topic text NOT NULL, + correlation_data bytea NOT NULL, + user_properties bytea NOT NULL, + expiry bigint NOT NULL +); + +CREATE UNIQUE INDEX i_mqtt_topic ON mqtt_pub (topic); diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml new file mode 100644 index 000000000..f98612036 --- /dev/null +++ b/test/docker/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.7' + +services: + mysql: + image: mysql:latest + container_name: ejabberd-mysql + volumes: + - ./db/mysql/data:/var/lib/mysql + - ./db/mysql/initdb:/docker-entrypoint-initdb.d:ro + command: --default-authentication-plugin=mysql_native_password + restart: always + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: ejabberd_test + MYSQL_USER: ejabberd_test + MYSQL_PASSWORD: ejabberd_test + + postgres: + image: postgres:latest + container_name: ejabberd-postgres + volumes: + - ./db/postgres/data:/var/lib/postgresql/data + - ./db/postgres/initdb:/docker-entrypoint-initdb.d:ro + ports: + - 5432:5432 + environment: + POSTGRES_PASSWORD: ejabberd_test + POSTGRES_USER: ejabberd_test + POSTGRES_DB: ejabberd_test + + redis: + image: redis:latest + container_name: ejabberd-redis + ports: + - 6379:6379 diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl index d3e7ec668..5739fa780 100644 --- a/test/ejabberd_SUITE.erl +++ b/test/ejabberd_SUITE.erl @@ -1,31 +1,47 @@ %%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2002-2016, ProcessOne -%%% @doc -%%% -%%% @end +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 2 Jun 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> -%%%------------------------------------------------------------------- +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- -module(ejabberd_SUITE). - -compile(export_all). --import(suite, [init_config/1, connect/1, disconnect/1, - recv/0, send/2, send_recv/2, my_jid/1, server_jid/1, - pubsub_jid/1, proxy_jid/1, muc_jid/1, muc_room_jid/1, - mix_jid/1, mix_room_jid/1, get_features/2, re_register/1, - is_feature_advertised/2, subscribe_to_events/1, - is_feature_advertised/3, set_opt/3, auth_SASL/2, - wait_for_master/1, wait_for_slave/1, - make_iq_result/1, start_event_relay/0, +-import(suite, [init_config/1, connect/1, disconnect/1, recv_message/1, + recv/1, recv_presence/1, send/2, send_recv/2, my_jid/1, + server_jid/1, pubsub_jid/1, proxy_jid/1, muc_jid/1, + muc_room_jid/1, my_muc_jid/1, peer_muc_jid/1, + mix_jid/1, mix_room_jid/1, get_features/2, recv_iq/1, + re_register/1, is_feature_advertised/2, subscribe_to_events/1, + is_feature_advertised/3, set_opt/3, + auth_SASL/2, auth_SASL/3, auth_SASL/4, + wait_for_master/1, wait_for_slave/1, flush/1, + make_iq_result/1, start_event_relay/0, alt_room_jid/1, stop_event_relay/1, put_event/2, get_event/1, - bind/1, auth/1, open_session/1, zlib/1, starttls/1, - close_socket/1]). - + bind/1, auth/1, auth/2, open_session/1, open_session/2, + zlib/1, starttls/1, starttls/2, close_socket/1, init_stream/1, + auth_legacy/2, auth_legacy/3, tcp_connect/1, send_text/2, + set_roster/3, del_roster/1]). -include("suite.hrl"). suite() -> - [{timetrap, {seconds,120}}]. + [{timetrap, {seconds, 120}}]. init_per_suite(Config) -> NewConfig = init_config(Config), @@ -35,26 +51,20 @@ init_per_suite(Config) -> LDIFFile = filename:join([DataDir, "ejabberd.ldif"]), {ok, _} = file:copy(ExtAuthScript, filename:join([CWD, "extauth.py"])), {ok, _} = ldap_srv:start(LDIFFile), + inet_db:add_host({127,0,0,1}, [binary_to_list(?S2S_VHOST), + binary_to_list(?MNESIA_VHOST), + binary_to_list(?UPLOAD_VHOST)]), + inet_db:set_domain(binary_to_list(p1_rand:get_string())), + inet_db:set_lookup([file, native]), start_ejabberd(NewConfig), NewConfig. -start_ejabberd(Config) -> - case proplists:get_value(backends, Config) of - all -> - ok = application:start(ejabberd, transient); - Backends when is_list(Backends) -> - Hosts = lists:map(fun(Backend) -> Backend ++ ".localhost" end, Backends), - application:load(ejabberd), - AllHosts = Hosts ++ ["localhost"], %% We always need localhost for the generic no_db tests - application:set_env(ejabberd, hosts, AllHosts), - ok = application:start(ejabberd, transient) - end. +start_ejabberd(_) -> + {ok, _} = application:ensure_all_started(ejabberd, transient). end_per_suite(_Config) -> application:stop(ejabberd). --define(BACKENDS, [mnesia,redis,mysql,pgsql,sqlite,ldap,extauth,riak]). - init_per_group(Group, Config) -> case lists:member(Group, ?BACKENDS) of false -> @@ -67,7 +77,7 @@ init_per_group(Group, Config) -> do_init_per_group(Group, Config); Backends -> %% Skipped backends that were not explicitely enabled - case lists:member(atom_to_list(Group), Backends) of + case lists:member(Group, Backends) of true -> do_init_per_group(Group, Config); false -> @@ -78,7 +88,7 @@ init_per_group(Group, Config) -> do_init_per_group(no_db, Config) -> re_register(Config), - Config; + set_opt(persistent_room, false, Config); do_init_per_group(mnesia, Config) -> mod_muc:shutdown_rooms(?MNESIA_VHOST), set_opt(server, ?MNESIA_VHOST, Config); @@ -89,7 +99,7 @@ do_init_per_group(mysql, Config) -> case catch ejabberd_sql:sql_query(?MYSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?MYSQL_VHOST), - create_sql_tables(mysql, ?config(base_dir, Config)), + clear_sql_tables(mysql, ?config(base_dir, Config)), set_opt(server, ?MYSQL_VHOST, Config); Err -> {skip, {mysql_not_available, Err}} @@ -98,7 +108,7 @@ do_init_per_group(pgsql, Config) -> case catch ejabberd_sql:sql_query(?PGSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?PGSQL_VHOST), - create_sql_tables(pgsql, ?config(base_dir, Config)), + clear_sql_tables(pgsql, ?config(base_dir, Config)), set_opt(server, ?PGSQL_VHOST, Config); Err -> {skip, {pgsql_not_available, Err}} @@ -115,18 +125,32 @@ do_init_per_group(ldap, Config) -> set_opt(server, ?LDAP_VHOST, Config); do_init_per_group(extauth, Config) -> set_opt(server, ?EXTAUTH_VHOST, Config); -do_init_per_group(riak, Config) -> - case ejabberd_riak:is_connected() of - true -> - mod_muc:shutdown_rooms(?RIAK_VHOST), - NewConfig = set_opt(server, ?RIAK_VHOST, Config), - clear_riak_tables(NewConfig); - Err -> - {skip, {riak_not_available, Err}} - end; -do_init_per_group(_GroupName, Config) -> +do_init_per_group(s2s, Config) -> + ejabberd_config:set_option({s2s_use_starttls, ?COMMON_VHOST}, required), + ejabberd_config:set_option(ca_file, "ca.pem"), + Port = ?config(s2s_port, Config), + set_opt(server, ?COMMON_VHOST, + set_opt(xmlns, ?NS_SERVER, + set_opt(type, server, + set_opt(server_port, Port, + set_opt(stream_from, ?S2S_VHOST, + set_opt(lang, <<"">>, Config)))))); +do_init_per_group(component, Config) -> + Server = ?config(server, Config), + Port = ?config(component_port, Config), + set_opt(xmlns, ?NS_COMPONENT, + set_opt(server, <<"component.", Server/binary>>, + set_opt(type, component, + set_opt(server_port, Port, + set_opt(stream_version, undefined, + set_opt(lang, <<"">>, Config)))))); +do_init_per_group(GroupName, Config) -> Pid = start_event_relay(), - set_opt(event_relay, Pid, Config). + NewConfig = set_opt(event_relay, Pid, Config), + case GroupName of + anonymous -> set_opt(anonymous, true, NewConfig); + _ -> NewConfig + end. end_per_group(mnesia, _Config) -> ok; @@ -144,73 +168,133 @@ end_per_group(ldap, _Config) -> ok; end_per_group(extauth, _Config) -> ok; -end_per_group(riak, _Config) -> +end_per_group(component, _Config) -> ok; +end_per_group(s2s, Config) -> + Server = ?config(server, Config), + ejabberd_config:set_option({s2s_use_starttls, Server}, false); end_per_group(_GroupName, Config) -> stop_event_relay(Config), - ok. + set_opt(anonymous, false, Config). init_per_testcase(stop_ejabberd, Config) -> - open_session(bind(auth(connect(Config)))); + NewConfig = set_opt(resource, <<"">>, + set_opt(anonymous, true, Config)), + open_session(bind(auth(connect(NewConfig)))); init_per_testcase(TestCase, OrigConfig) -> - subscribe_to_events(OrigConfig), - Server = ?config(server, OrigConfig), - Resource = ?config(resource, OrigConfig), - MasterResource = ?config(master_resource, OrigConfig), - SlaveResource = ?config(slave_resource, OrigConfig), + ct:print(80, "Testcase '~p' starting", [TestCase]), Test = atom_to_list(TestCase), IsMaster = lists:suffix("_master", Test), IsSlave = lists:suffix("_slave", Test), + if IsMaster or IsSlave -> + subscribe_to_events(OrigConfig); + true -> + ok + end, + TestGroup = proplists:get_value( + name, ?config(tc_group_properties, OrigConfig)), + Server = ?config(server, OrigConfig), + Resource = case TestGroup of + anonymous -> + <<"">>; + legacy_auth -> + p1_rand:get_string(); + _ -> + ?config(resource, OrigConfig) + end, + MasterResource = ?config(master_resource, OrigConfig), + SlaveResource = ?config(slave_resource, OrigConfig), + Mode = if IsSlave -> slave; + IsMaster -> master; + true -> single + end, IsCarbons = lists:prefix("carbons_", Test), - User = if IsMaster or IsCarbons -> <<"test_master!#$%^*()`~+-;_=[]{}|\\">>; + IsReplaced = lists:prefix("replaced_", Test), + User = if IsReplaced -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; + IsCarbons and not (IsMaster or IsSlave) -> + <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; + IsMaster or IsCarbons -> <<"test_master!#$%^*()`~+-;_=[]{}|\\">>; IsSlave -> <<"test_slave!#$%^*()`~+-;_=[]{}|\\">>; true -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">> end, + Nick = if IsSlave -> ?config(slave_nick, OrigConfig); + IsMaster -> ?config(master_nick, OrigConfig); + true -> ?config(nick, OrigConfig) + end, MyResource = if IsMaster and IsCarbons -> MasterResource; IsSlave and IsCarbons -> SlaveResource; true -> Resource end, Slave = if IsCarbons -> jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, SlaveResource); + IsReplaced -> + jid:make(User, Server, Resource); true -> jid:make(<<"test_slave!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) end, Master = if IsCarbons -> jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, MasterResource); + IsReplaced -> + jid:make(User, Server, Resource); true -> jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) end, - Config = set_opt(user, User, - set_opt(slave, Slave, - set_opt(master, Master, - set_opt(resource, MyResource, OrigConfig)))), - case TestCase of - test_connect -> + Config1 = set_opt(user, User, + set_opt(slave, Slave, + set_opt(master, Master, + set_opt(resource, MyResource, + set_opt(nick, Nick, + set_opt(mode, Mode, OrigConfig)))))), + Config2 = if IsSlave -> + set_opt(peer_nick, ?config(master_nick, Config1), Config1); + IsMaster -> + set_opt(peer_nick, ?config(slave_nick, Config1), Config1); + true -> + Config1 + end, + Config = if IsSlave -> set_opt(peer, Master, Config2); + IsMaster -> set_opt(peer, Slave, Config2); + true -> Config2 + end, + case Test of + "test_connect" ++ _ -> Config; - test_auth -> + "test_legacy_auth_feature" -> + connect(Config); + "test_legacy_auth" ++ _ -> + init_stream(set_opt(stream_version, undefined, Config)); + "test_auth" ++ _ -> connect(Config); - test_starttls -> + "test_starttls" ++ _ -> connect(Config); - test_zlib -> + "test_zlib" -> + auth(connect(starttls(connect(Config)))); + "test_register" -> connect(Config); - test_register -> + "auth_md5" -> connect(Config); - auth_md5 -> + "auth_plain" -> connect(Config); - auth_plain -> - connect(Config); - test_bind -> + "auth_external" ++ _ -> + connect(Config); + "unauthenticated_" ++ _ -> + connect(Config); + "test_bind" -> auth(connect(Config)); - sm_resume -> + "sm_resume" -> auth(connect(Config)); - sm_resume_failed -> + "sm_resume_failed" -> auth(connect(Config)); - test_open_session -> + "test_open_session" -> bind(auth(connect(Config))); + "replaced" ++ _ -> + auth(connect(Config)); _ when IsMaster or IsSlave -> Password = ?config(password, Config), ejabberd_auth:try_register(User, Server, Password), open_session(bind(auth(connect(Config)))); + _ when TestGroup == s2s_tests -> + auth(connect(starttls(connect(Config)))); _ -> open_session(bind(auth(connect(Config)))) end. @@ -218,167 +302,187 @@ init_per_testcase(TestCase, OrigConfig) -> end_per_testcase(_TestCase, _Config) -> ok. +legacy_auth_tests() -> + {legacy_auth, [parallel], + [test_legacy_auth_feature, + test_legacy_auth, + test_legacy_auth_digest, + test_legacy_auth_no_resource, + test_legacy_auth_bad_jid, + test_legacy_auth_fail]}. + no_db_tests() -> - [{generic, [sequence], - [test_connect, + [{anonymous, [parallel], + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect_bad_lang, + test_connect_bad_to, + test_connect_missing_to, + test_connect, + unauthenticated_iq, + unauthenticated_message, + unauthenticated_presence, test_starttls, - test_zlib, test_auth, + test_zlib, test_bind, test_open_session, - presence, + codec_failure, + unsupported_query, + bad_nonza, + invalid_from, ping, version, time, stats, - sm, - sm_resume, - sm_resume_failed, disco]}, - {test_proxy65, [parallel], - [proxy65_master, proxy65_slave]}]. + {presence_and_s2s, [sequence], + [test_auth_fail, + presence, + s2s_dialback, + s2s_optional, + s2s_required]}, + auth_external, + auth_external_no_jid, + auth_external_no_user, + auth_external_malformed_jid, + auth_external_wrong_jid, + auth_external_wrong_server, + auth_external_invalid_cert, + jidprep_tests:single_cases(), + sm_tests:single_cases(), + sm_tests:master_slave_cases(), + muc_tests:single_cases(), + muc_tests:master_slave_cases(), + proxy65_tests:single_cases(), + proxy65_tests:master_slave_cases(), + replaced_tests:master_slave_cases(), + upload_tests:single_cases(), + carbons_tests:single_cases(), + carbons_tests:master_slave_cases()]. -db_tests(riak) -> - %% No support for mod_pubsub - [{single_user, [sequence], - [test_register, - auth_plain, - auth_md5, - presence_broadcast, - last, - roster_get, - private, - privacy, - blocking, - vcard, - test_unregister]}, - {test_muc_register, [sequence], - [muc_register_master, muc_register_slave]}, - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, - {test_flex_offline, [sequence], - [flex_offline_master, flex_offline_slave]}, - {test_offline, [sequence], - [offline_master, offline_slave]}, - {test_muc, [parallel], - [muc_master, muc_slave]}, - {test_announce, [sequence], - [announce_master, announce_slave]}, - {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]; db_tests(DB) when DB == mnesia; DB == redis -> [{single_user, [sequence], [test_register, + legacy_auth_tests(), auth_plain, auth_md5, presence_broadcast, last, - roster_get, - roster_ver, - private, - privacy, - blocking, - vcard, - pubsub, + roster_tests:single_cases(), + private_tests:single_cases(), + privacy_tests:single_cases(), + vcard_tests:single_cases(), + pubsub_tests:single_cases(), + muc_tests:single_cases(), + offline_tests:single_cases(), + mam_tests:single_cases(), + csi_tests:single_cases(), + push_tests:single_cases(), test_unregister]}, - {test_muc_register, [sequence], - [muc_register_master, muc_register_slave]}, - {test_mix, [parallel], - [mix_master, mix_slave]}, - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, - {test_flex_offline, [sequence], - [flex_offline_master, flex_offline_slave]}, - {test_offline, [sequence], - [offline_master, offline_slave]}, - {test_old_mam, [parallel], - [mam_old_master, mam_old_slave]}, - {test_new_mam, [parallel], - [mam_new_master, mam_new_slave]}, - {test_carbons, [parallel], - [carbons_master, carbons_slave]}, - {test_client_state, [parallel], - [client_state_master, client_state_slave]}, - {test_muc, [parallel], - [muc_master, muc_slave]}, - {test_muc_mam, [parallel], - [muc_mam_master, muc_mam_slave]}, - {test_announce, [sequence], - [announce_master, announce_slave]}, - {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]; -db_tests(_) -> - %% No support for carboncopy + muc_tests:master_slave_cases(), + privacy_tests:master_slave_cases(), + pubsub_tests:master_slave_cases(), + roster_tests:master_slave_cases(), + offline_tests:master_slave_cases(DB), + mam_tests:master_slave_cases(), + vcard_tests:master_slave_cases(), + announce_tests:master_slave_cases(), + csi_tests:master_slave_cases(), + push_tests:master_slave_cases()]; +db_tests(DB) -> [{single_user, [sequence], [test_register, + legacy_auth_tests(), auth_plain, auth_md5, presence_broadcast, last, - roster_get, - roster_ver, - private, - privacy, - blocking, - vcard, - pubsub, + roster_tests:single_cases(), + private_tests:single_cases(), + privacy_tests:single_cases(), + vcard_tests:single_cases(), + pubsub_tests:single_cases(), + muc_tests:single_cases(), + offline_tests:single_cases(), + mam_tests:single_cases(), + push_tests:single_cases(), test_unregister]}, - {test_muc_register, [sequence], - [muc_register_master, muc_register_slave]}, - {test_mix, [parallel], - [mix_master, mix_slave]}, - {test_roster_subscribe, [parallel], - [roster_subscribe_master, - roster_subscribe_slave]}, - {test_flex_offline, [sequence], - [flex_offline_master, flex_offline_slave]}, - {test_offline, [sequence], - [offline_master, offline_slave]}, - {test_old_mam, [parallel], - [mam_old_master, mam_old_slave]}, - {test_new_mam, [parallel], - [mam_new_master, mam_new_slave]}, - {test_muc, [parallel], - [muc_master, muc_slave]}, - {test_muc_mam, [parallel], - [muc_mam_master, muc_mam_slave]}, - {test_announce, [sequence], - [announce_master, announce_slave]}, - {test_vcard_xupdate, [parallel], - [vcard_xupdate_master, vcard_xupdate_slave]}, - {test_roster_remove, [parallel], - [roster_remove_master, - roster_remove_slave]}]. + muc_tests:master_slave_cases(), + privacy_tests:master_slave_cases(), + pubsub_tests:master_slave_cases(), + roster_tests:master_slave_cases(), + offline_tests:master_slave_cases(DB), + mam_tests:master_slave_cases(), + vcard_tests:master_slave_cases(), + announce_tests:master_slave_cases(), + push_tests:master_slave_cases()]. ldap_tests() -> [{ldap_tests, [sequence], [test_auth, + test_auth_fail, vcard_get, ldap_shared_roster_get]}]. extauth_tests() -> [{extauth_tests, [sequence], [test_auth, + test_auth_fail, test_unregister]}]. +component_tests() -> + [{component_connect, [parallel], + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect_missing_to, + test_connect, + test_auth, + test_auth_fail]}, + {component_tests, [sequence], + [test_missing_from, + test_missing_to, + test_invalid_from, + test_component_send, + bad_nonza, + codec_failure]}]. + +s2s_tests() -> + [{s2s_connect, [parallel], + [test_connect_bad_xml, + test_connect_unexpected_xml, + test_connect_unknown_ns, + test_connect_bad_xmlns, + test_connect_bad_ns_stream, + test_connect, + test_connect_s2s_starttls_required, + test_starttls, + test_connect_s2s_unauthenticated_iq, + test_auth_starttls]}, + {s2s_tests, [sequence], + [test_missing_from, + test_missing_to, + test_invalid_from, + bad_nonza, + codec_failure]}]. + groups() -> [{ldap, [sequence], ldap_tests()}, {extauth, [sequence], extauth_tests()}, {no_db, [sequence], no_db_tests()}, + {component, [sequence], component_tests()}, + {s2s, [sequence], s2s_tests()}, {mnesia, [sequence], db_tests(mnesia)}, {redis, [sequence], db_tests(redis)}, {mysql, [sequence], db_tests(mysql)}, {pgsql, [sequence], db_tests(pgsql)}, - {sqlite, [sequence], db_tests(sqlite)}, - {riak, [sequence], db_tests(riak)}]. + {sqlite, [sequence], db_tests(sqlite)}]. all() -> [{group, ldap}, @@ -389,7 +493,8 @@ all() -> {group, pgsql}, {group, sqlite}, {group, extauth}, - {group, riak}, + {group, component}, + {group, s2s}, stop_ejabberd]. stop_ejabberd(Config) -> @@ -398,13 +503,83 @@ stop_ejabberd(Config) -> ?recv1({xmlstreamend, <<"stream:stream">>}), Config. +test_connect_bad_xml(Config) -> + Config0 = tcp_connect(Config), + send_text(Config0, <<"<'/>">>), + Version = ?config(stream_version, Config0), + ?recv1(#stream_start{version = Version}), + ?recv1(#stream_error{reason = 'not-well-formed'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_unexpected_xml(Config) -> + Config0 = tcp_connect(Config), + send(Config0, #caps{}), + Version = ?config(stream_version, Config0), + ?recv1(#stream_start{version = Version}), + ?recv1(#stream_error{reason = 'invalid-xml'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_unknown_ns(Config) -> + Config0 = init_stream(set_opt(xmlns, <<"wrong">>, Config)), + ?recv1(#stream_error{reason = 'invalid-xml'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_bad_xmlns(Config) -> + NS = case ?config(type, Config) of + client -> ?NS_SERVER; + _ -> ?NS_CLIENT + end, + Config0 = init_stream(set_opt(xmlns, NS, Config)), + ?recv1(#stream_error{reason = 'invalid-namespace'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_bad_ns_stream(Config) -> + Config0 = init_stream(set_opt(ns_stream, <<"wrong">>, Config)), + ?recv1(#stream_error{reason = 'invalid-namespace'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_bad_lang(Config) -> + Lang = iolist_to_binary(lists:duplicate(36, $x)), + Config0 = init_stream(set_opt(lang, Lang, Config)), + ?recv1(#stream_error{reason = 'invalid-xml'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_bad_to(Config) -> + Config0 = init_stream(set_opt(server, <<"wrong.com">>, Config)), + ?recv1(#stream_error{reason = 'host-unknown'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + +test_connect_missing_to(Config) -> + Config0 = init_stream(set_opt(server, <<"">>, Config)), + ?recv1(#stream_error{reason = 'improper-addressing'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config0). + test_connect(Config) -> disconnect(connect(Config)). +test_connect_s2s_starttls_required(Config) -> + Config1 = connect(Config), + send(Config1, #presence{}), + ?recv1(#stream_error{reason = 'policy-violation'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config1). + +test_connect_s2s_unauthenticated_iq(Config) -> + Config1 = connect(starttls(connect(Config))), + unauthenticated_iq(Config1). + test_starttls(Config) -> case ?config(starttls, Config) of true -> - disconnect(starttls(Config)); + disconnect(connect(starttls(Config))); _ -> {skipped, 'starttls_not_available'} end. @@ -432,8 +607,8 @@ test_register(Config) -> register(Config) -> #iq{type = result, - sub_els = [#register{username = none, - password = none}]} = + sub_els = [#register{username = <<>>, + password = <<>>}]} = send_recv(Config, #iq{type = get, to = server_jid(Config), sub_els = [#register{}]}), #iq{type = result, sub_els = []} = @@ -462,6 +637,101 @@ try_unregister(Config) -> ?recv1(#stream_error{reason = conflict}), Config. +unauthenticated_presence(Config) -> + unauthenticated_packet(Config, #presence{}). + +unauthenticated_message(Config) -> + unauthenticated_packet(Config, #message{}). + +unauthenticated_iq(Config) -> + IQ = #iq{type = get, sub_els = [#disco_info{}]}, + unauthenticated_packet(Config, IQ). + +unauthenticated_packet(Config, Pkt) -> + From = my_jid(Config), + To = server_jid(Config), + send(Config, xmpp:set_from_to(Pkt, From, To)), + #stream_error{reason = 'not-authorized'} = recv(Config), + {xmlstreamend, <<"stream:stream">>} = recv(Config), + close_socket(Config). + +bad_nonza(Config) -> + %% Unsupported and invalid nonza should be silently dropped. + send(Config, #caps{}), + send(Config, #stanza_error{type = wrong}), + disconnect(Config). + +invalid_from(Config) -> + send(Config, #message{from = jid:make(p1_rand:get_string())}), + ?recv1(#stream_error{reason = 'invalid-from'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config). + +test_missing_from(Config) -> + Server = server_jid(Config), + send(Config, #message{to = Server}), + ?recv1(#stream_error{reason = 'improper-addressing'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config). + +test_missing_to(Config) -> + Server = server_jid(Config), + send(Config, #message{from = Server}), + ?recv1(#stream_error{reason = 'improper-addressing'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config). + +test_invalid_from(Config) -> + From = jid:make(p1_rand:get_string()), + To = jid:make(p1_rand:get_string()), + send(Config, #message{from = From, to = To}), + ?recv1(#stream_error{reason = 'invalid-from'}), + ?recv1({xmlstreamend, <<"stream:stream">>}), + close_socket(Config). + +test_component_send(Config) -> + To = jid:make(?COMMON_VHOST), + From = server_jid(Config), + #iq{type = result, from = To, to = From} = + send_recv(Config, #iq{type = get, to = To, from = From, + sub_els = [#ping{}]}), + disconnect(Config). + +s2s_dialback(Config) -> + Server = ?config(server, Config), + ejabberd_s2s:stop_s2s_connections(), + ejabberd_config:set_option({s2s_use_starttls, Server}, false), + ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, false), + ejabberd_config:set_option(ca_file, pkix:get_cafile()), + s2s_ping(Config). + +s2s_optional(Config) -> + Server = ?config(server, Config), + ejabberd_s2s:stop_s2s_connections(), + ejabberd_config:set_option({s2s_use_starttls, Server}, optional), + ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, optional), + ejabberd_config:set_option(ca_file, pkix:get_cafile()), + s2s_ping(Config). + +s2s_required(Config) -> + Server = ?config(server, Config), + ejabberd_s2s:stop_s2s_connections(), + gen_mod:stop_module(Server, mod_s2s_dialback), + gen_mod:stop_module(?MNESIA_VHOST, mod_s2s_dialback), + ejabberd_config:set_option({s2s_use_starttls, Server}, required), + ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, required), + ejabberd_config:set_option(ca_file, "ca.pem"), + s2s_ping(Config). + +s2s_ping(Config) -> + From = my_jid(Config), + To = jid:make(?MNESIA_VHOST), + ID = p1_rand:get_string(), + ejabberd_s2s:route(#iq{from = From, to = To, id = ID, + type = get, sub_els = [#ping{}]}), + #iq{type = result, id = ID, sub_els = []} = recv_iq(Config), + disconnect(Config). + auth_md5(Config) -> Mechs = ?config(mechs, Config), case lists:member(<<"DIGEST-MD5">>, Mechs) of @@ -482,87 +752,134 @@ auth_plain(Config) -> {skipped, 'PLAIN_not_available'} end. +auth_external(Config0) -> + Config = connect(starttls(Config0)), + disconnect(auth_SASL(<<"EXTERNAL">>, Config)). + +auth_external_no_jid(Config0) -> + Config = connect(starttls(Config0)), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShoudFail = false, + {<<"">>, <<"">>, <<"">>})). + +auth_external_no_user(Config0) -> + Config = set_opt(user, <<"">>, connect(starttls(Config0))), + disconnect(auth_SASL(<<"EXTERNAL">>, Config)). + +auth_external_malformed_jid(Config0) -> + Config = connect(starttls(Config0)), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true, + {<<"">>, <<"@">>, <<"">>})). + +auth_external_wrong_jid(Config0) -> + Config = set_opt(user, <<"wrong">>, + connect(starttls(Config0))), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true)). + +auth_external_wrong_server(Config0) -> + Config = connect(starttls(Config0)), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true, + {<<"">>, <<"wrong.com">>, <<"">>})). + +auth_external_invalid_cert(Config0) -> + Config = connect(starttls( + set_opt(certfile, "self-signed-cert.pem", Config0))), + disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true)). + +test_legacy_auth_feature(Config) -> + true = ?config(legacy_auth, Config), + disconnect(Config). + +test_legacy_auth(Config) -> + disconnect(auth_legacy(Config, _Digest = false)). + +test_legacy_auth_digest(Config) -> + disconnect(auth_legacy(Config, _Digest = true)). + +test_legacy_auth_no_resource(Config0) -> + Config = set_opt(resource, <<"">>, Config0), + disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + +test_legacy_auth_bad_jid(Config0) -> + Config = set_opt(user, <<"@">>, Config0), + disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + +test_legacy_auth_fail(Config0) -> + Config = set_opt(user, <<"wrong">>, Config0), + disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). + test_auth(Config) -> disconnect(auth(Config)). +test_auth_starttls(Config) -> + disconnect(auth(connect(starttls(Config)))). + +test_auth_fail(Config0) -> + Config = set_opt(user, <<"wrong">>, + set_opt(password, <<"wrong">>, Config0)), + disconnect(auth(Config, _ShouldFail = true)). + test_bind(Config) -> disconnect(bind(Config)). test_open_session(Config) -> - disconnect(open_session(Config)). + disconnect(open_session(Config, true)). -roster_get(Config) -> - #iq{type = result, sub_els = [#roster{items = []}]} = - send_recv(Config, #iq{type = get, sub_els = [#roster{}]}), +codec_failure(Config) -> + JID = my_jid(Config), + #iq{type = error} = + send_recv(Config, #iq{type = wrong, from = JID, to = JID}), disconnect(Config). -roster_ver(Config) -> - %% Get initial "ver" - #iq{type = result, sub_els = [#roster{ver = Ver1, items = []}]} = - send_recv(Config, #iq{type = get, - sub_els = [#roster{ver = <<"">>}]}), - %% Should receive empty IQ-result - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = get, - sub_els = [#roster{ver = Ver1}]}), - %% Attempting to subscribe to server's JID - send(Config, #presence{type = subscribe, to = server_jid(Config)}), - %% Receive a single roster push with the new "ver" - ?recv1(#iq{type = set, sub_els = [#roster{ver = Ver2}]}), - %% Requesting roster with the previous "ver". Should receive Ver2 again - #iq{type = result, sub_els = [#roster{ver = Ver2}]} = - send_recv(Config, #iq{type = get, - sub_els = [#roster{ver = Ver1}]}), - %% Now requesting roster with the newest "ver". Should receive empty IQ. - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = get, - sub_els = [#roster{ver = Ver2}]}), +unsupported_query(Config) -> + ServerJID = server_jid(Config), + #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID}), + #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID, + sub_els = [#caps{}]}), + #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID, + sub_els = [#roster_query{}, + #disco_info{}, + #privacy_query{}]}), disconnect(Config). presence(Config) -> - send(Config, #presence{}), JID = my_jid(Config), - ?recv1(#presence{from = JID, to = JID}), + #presence{from = JID, to = JID} = send_recv(Config, #presence{}), disconnect(Config). presence_broadcast(Config) -> - Feature = <<"p1:tmp:", (randoms:get_string())/binary>>, + Feature = <<"p1:tmp:", (p1_rand:get_string())/binary>>, Ver = crypto:hash(sha, ["client", $/, "bot", $/, "en", $/, "ejabberd_ct", $<, Feature, $<]), B64Ver = base64:encode(Ver), Node = <<(?EJABBERD_CT_URI)/binary, $#, B64Ver/binary>>, Server = ?config(server, Config), - ServerJID = server_jid(Config), Info = #disco_info{identities = [#identity{category = <<"client">>, type = <<"bot">>, lang = <<"en">>, name = <<"ejabberd_ct">>}], node = Node, features = [Feature]}, - Caps = #caps{hash = <<"sha-1">>, node = ?EJABBERD_CT_URI, ver = Ver}, + Caps = #caps{hash = <<"sha-1">>, node = ?EJABBERD_CT_URI, version = B64Ver}, send(Config, #presence{sub_els = [Caps]}), JID = my_jid(Config), %% We receive: %% 1) disco#info iq request for CAPS %% 2) welcome message %% 3) presence broadcast - {IQ, _, _} = ?recv3(#iq{type = get, - from = ServerJID, - sub_els = [#disco_info{node = Node}]}, - #message{type = normal}, - #presence{from = JID, to = JID}), + IQ = #iq{type = get, + from = JID, + sub_els = [#disco_info{node = Node}]} = recv_iq(Config), + #message{type = normal} = recv_message(Config), + #presence{from = JID, to = JID} = recv_presence(Config), send(Config, #iq{type = result, id = IQ#iq.id, - to = ServerJID, sub_els = [Info]}), + to = JID, sub_els = [Info]}), %% We're trying to read our feature from ejabberd database %% with exponential back-off as our IQ response may be delayed. [Feature] = lists:foldl( fun(Time, []) -> timer:sleep(Time), - mod_caps:get_features( - Server, - mod_caps:read_caps( - [xmpp_codec:encode(Caps)])); + mod_caps:get_features(Server, Caps); (_, Acc) -> Acc end, [], [0, 100, 200, 2000, 5000, 10000]), @@ -607,82 +924,6 @@ disco(Config) -> end, Items), disconnect(Config). -sm(Config) -> - Server = ?config(server, Config), - ServerJID = jid:make(<<"">>, Server, <<"">>), - %% Send messages of type 'headline' so the server discards them silently - Msg = #message{to = ServerJID, type = headline, - body = [#text{data = <<"body">>}]}, - true = ?config(sm, Config), - %% Enable the session management with resumption enabled - send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), - ?recv1(#sm_enabled{id = ID, resume = true}), - %% Initial request; 'h' should be 0. - send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), - ?recv1(#sm_a{h = 0}), - %% sending two messages and requesting again; 'h' should be 3. - send(Config, Msg), - send(Config, Msg), - send(Config, Msg), - send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), - ?recv1(#sm_a{h = 3}), - close_socket(Config), - {save_config, set_opt(sm_previd, ID, Config)}. - -sm_resume(Config) -> - {sm, SMConfig} = ?config(saved_config, Config), - ID = ?config(sm_previd, SMConfig), - Server = ?config(server, Config), - ServerJID = jid:make(<<"">>, Server, <<"">>), - MyJID = my_jid(Config), - Txt = #text{data = <<"body">>}, - Msg = #message{from = ServerJID, to = MyJID, body = [Txt]}, - %% Route message. The message should be queued by the C2S process. - ejabberd_router:route(ServerJID, MyJID, xmpp_codec:encode(Msg)), - send(Config, #sm_resume{previd = ID, h = 0, xmlns = ?NS_STREAM_MGMT_3}), - ?recv1(#sm_resumed{previd = ID, h = 3}), - ?recv1(#message{from = ServerJID, to = MyJID, body = [Txt]}), - ?recv1(#sm_r{}), - send(Config, #sm_a{h = 1, xmlns = ?NS_STREAM_MGMT_3}), - %% Send another stanza to increment the server's 'h' for sm_resume_failed. - send(Config, #presence{to = ServerJID}), - close_socket(Config), - {save_config, set_opt(sm_previd, ID, Config)}. - -sm_resume_failed(Config) -> - {sm_resume, SMConfig} = ?config(saved_config, Config), - ID = ?config(sm_previd, SMConfig), - ct:sleep(5000), % Wait for session to time out. - send(Config, #sm_resume{previd = ID, h = 1, xmlns = ?NS_STREAM_MGMT_3}), - ?recv1(#sm_failed{reason = 'item-not-found', h = 4}), - disconnect(Config). - -private(Config) -> - Conference = #bookmark_conference{name = <<"Some name">>, - autojoin = true, - jid = jid:make( - <<"some">>, - <<"some.conference.org">>, - <<>>)}, - Storage = #bookmark_storage{conference = [Conference]}, - StorageXMLOut = xmpp_codec:encode(Storage), - #iq{type = error} = - send_recv(Config, #iq{type = get, sub_els = [#private{}], - to = server_jid(Config)}), - #iq{type = result, sub_els = []} = - send_recv( - Config, #iq{type = set, - sub_els = [#private{xml_els = [StorageXMLOut]}]}), - #iq{type = result, - sub_els = [#private{xml_els = [StorageXMLIn]}]} = - send_recv( - Config, - #iq{type = get, - sub_els = [#private{xml_els = [xmpp_codec:encode( - #bookmark_storage{})]}]}), - Storage = xmpp_codec:decode(StorageXMLIn), - disconnect(Config). - last(Config) -> true = is_feature_advertised(Config, ?NS_LAST), #iq{type = result, sub_els = [#last{}]} = @@ -690,1685 +931,36 @@ last(Config) -> to = server_jid(Config)}), disconnect(Config). -privacy(Config) -> - true = is_feature_advertised(Config, ?NS_PRIVACY), - #iq{type = result, sub_els = [#privacy{}]} = - send_recv(Config, #iq{type = get, sub_els = [#privacy{}]}), - JID = <<"tybalt@example.com">>, - I1 = send(Config, - #iq{type = set, - sub_els = [#privacy{ - lists = [#privacy_list{ - name = <<"public">>, - items = - [#privacy_item{ - type = jid, - order = 3, - action = deny, - kinds = ['presence-in'], - value = JID}]}]}]}), - {Push1, _} = - ?recv2( - #iq{type = set, - sub_els = [#privacy{ - lists = [#privacy_list{ - name = <<"public">>}]}]}, - #iq{type = result, id = I1, sub_els = []}), - send(Config, make_iq_result(Push1)), - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, - sub_els = [#privacy{active = <<"public">>}]}), - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, - sub_els = [#privacy{default = <<"public">>}]}), - #iq{type = result, - sub_els = [#privacy{default = <<"public">>, - active = <<"public">>, - lists = [#privacy_list{name = <<"public">>}]}]} = - send_recv(Config, #iq{type = get, sub_els = [#privacy{}]}), - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, sub_els = [#privacy{default = none}]}), - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [#privacy{active = none}]}), - I2 = send(Config, #iq{type = set, - sub_els = [#privacy{ - lists = - [#privacy_list{ - name = <<"public">>}]}]}), - {Push2, _} = - ?recv2( - #iq{type = set, - sub_els = [#privacy{ - lists = [#privacy_list{ - name = <<"public">>}]}]}, - #iq{type = result, id = I2, sub_els = []}), - send(Config, make_iq_result(Push2)), - disconnect(Config). - -blocking(Config) -> - true = is_feature_advertised(Config, ?NS_BLOCKING), - JID = jid:make(<<"romeo">>, <<"montague.net">>, <<>>), - #iq{type = result, sub_els = [#block_list{}]} = - send_recv(Config, #iq{type = get, sub_els = [#block_list{}]}), - I1 = send(Config, #iq{type = set, - sub_els = [#block{items = [JID]}]}), - {Push1, Push2, _} = - ?recv3( - #iq{type = set, - sub_els = [#privacy{lists = [#privacy_list{}]}]}, - #iq{type = set, - sub_els = [#block{items = [JID]}]}, - #iq{type = result, id = I1, sub_els = []}), - send(Config, make_iq_result(Push1)), - send(Config, make_iq_result(Push2)), - I2 = send(Config, #iq{type = set, - sub_els = [#unblock{items = [JID]}]}), - {Push3, Push4, _} = - ?recv3( - #iq{type = set, - sub_els = [#privacy{lists = [#privacy_list{}]}]}, - #iq{type = set, - sub_els = [#unblock{items = [JID]}]}, - #iq{type = result, id = I2, sub_els = []}), - send(Config, make_iq_result(Push3)), - send(Config, make_iq_result(Push4)), - disconnect(Config). - -vcard(Config) -> - true = is_feature_advertised(Config, ?NS_VCARD), - VCard = - #vcard{fn = <<"Peter Saint-Andre">>, - n = #vcard_name{family = <<"Saint-Andre">>, - given = <<"Peter">>}, - nickname = <<"stpeter">>, - bday = <<"1966-08-06">>, - adr = [#vcard_adr{work = true, - extadd = <<"Suite 600">>, - street = <<"1899 Wynkoop Street">>, - locality = <<"Denver">>, - region = <<"CO">>, - pcode = <<"80202">>, - ctry = <<"USA">>}, - #vcard_adr{home = true, - locality = <<"Denver">>, - region = <<"CO">>, - pcode = <<"80209">>, - ctry = <<"USA">>}], - tel = [#vcard_tel{work = true,voice = true, - number = <<"303-308-3282">>}, - #vcard_tel{home = true,voice = true, - number = <<"303-555-1212">>}], - email = [#vcard_email{internet = true,pref = true, - userid = <<"stpeter@jabber.org">>}], - jabberid = <<"stpeter@jabber.org">>, - title = <<"Executive Director">>,role = <<"Patron Saint">>, - org = #vcard_org{name = <<"XMPP Standards Foundation">>}, - url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>, - desc = <<"More information about me is located on my " - "personal website: http://www.saint-andre.com/">>}, - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [VCard]}), - %% TODO: check if VCard == VCard1. - #iq{type = result, sub_els = [_VCard1]} = - send_recv(Config, #iq{type = get, sub_els = [#vcard{}]}), - disconnect(Config). - vcard_get(Config) -> true = is_feature_advertised(Config, ?NS_VCARD), %% TODO: check if VCard corresponds to LDIF data from ejabberd.ldif #iq{type = result, sub_els = [_VCard]} = - send_recv(Config, #iq{type = get, sub_els = [#vcard{}]}), + send_recv(Config, #iq{type = get, sub_els = [#vcard_temp{}]}), disconnect(Config). ldap_shared_roster_get(Config) -> - Item = #roster_item{jid = jid:from_string(<<"user2@ldap.localhost">>), name = <<"Test User 2">>, + Item = #roster_item{jid = jid:decode(<<"user2@ldap.localhost">>), name = <<"Test User 2">>, groups = [<<"group1">>], subscription = both}, - #iq{type = result, sub_els = [#roster{items = [Item]}]} = - send_recv(Config, #iq{type = get, sub_els = [#roster{}]}), - disconnect(Config). - -vcard_xupdate_master(Config) -> - Img = <<137, "PNG\r\n", 26, $\n>>, - ImgHash = p1_sha:sha(Img), - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - wait_for_slave(Config), - send(Config, #presence{}), - ?recv2(#presence{from = MyJID, type = undefined}, - #presence{from = Peer, type = undefined}), - VCard = #vcard{photo = #vcard_photo{type = <<"image/png">>, binval = Img}}, - I1 = send(Config, #iq{type = set, sub_els = [VCard]}), - ?recv2(#iq{type = result, sub_els = [], id = I1}, - #presence{from = MyJID, type = undefined, - sub_els = [#vcard_xupdate{photo = ImgHash}]}), - I2 = send(Config, #iq{type = set, sub_els = [#vcard{}]}), - ?recv3(#iq{type = result, sub_els = [], id = I2}, - #presence{from = MyJID, type = undefined, - sub_els = [#vcard_xupdate{photo = undefined}]}, - #presence{from = Peer, type = unavailable}), - disconnect(Config). - -vcard_xupdate_slave(Config) -> - Img = <<137, "PNG\r\n", 26, $\n>>, - ImgHash = p1_sha:sha(Img), - MyJID = my_jid(Config), - Peer = ?config(master, Config), - send(Config, #presence{}), - ?recv1(#presence{from = MyJID, type = undefined}), - wait_for_master(Config), - ?recv1(#presence{from = Peer, type = undefined}), - ?recv1(#presence{from = Peer, type = undefined, - sub_els = [#vcard_xupdate{photo = ImgHash}]}), - ?recv1(#presence{from = Peer, type = undefined, - sub_els = [#vcard_xupdate{photo = undefined}]}), + #iq{type = result, sub_els = [#roster_query{items = [Item]}]} = + send_recv(Config, #iq{type = get, sub_els = [#roster_query{}]}), disconnect(Config). stats(Config) -> - #iq{type = result, sub_els = [#stats{stat = Stats}]} = + #iq{type = result, sub_els = [#stats{list = Stats}]} = send_recv(Config, #iq{type = get, sub_els = [#stats{}], to = server_jid(Config)}), lists:foreach( fun(#stat{} = Stat) -> #iq{type = result, sub_els = [_|_]} = send_recv(Config, #iq{type = get, - sub_els = [#stats{stat = [Stat]}], + sub_els = [#stats{list = [Stat]}], to = server_jid(Config)}) end, Stats), disconnect(Config). -pubsub(Config) -> - Features = get_features(Config, pubsub_jid(Config)), - true = lists:member(?NS_PUBSUB, Features), - %% Publish <presence/> element within node "presence" - ItemID = randoms:get_string(), - Node = <<"presence!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>, - Item = #pubsub_item{id = ItemID, - xml_els = [xmpp_codec:encode(#presence{})]}, - #iq{type = result, - sub_els = [#pubsub{publish = #pubsub_publish{ - node = Node, - items = [#pubsub_item{id = ItemID}]}}]} = - send_recv(Config, - #iq{type = set, to = pubsub_jid(Config), - sub_els = [#pubsub{publish = #pubsub_publish{ - node = Node, - items = [Item]}}]}), - %% Subscribe to node "presence" - I1 = send(Config, - #iq{type = set, to = pubsub_jid(Config), - sub_els = [#pubsub{subscribe = #pubsub_subscribe{ - node = Node, - jid = my_jid(Config)}}]}), - ?recv2( - #message{sub_els = [#pubsub_event{}, #delay{}]}, - #iq{type = result, id = I1}), - %% Get subscriptions - true = lists:member(?PUBSUB("retrieve-subscriptions"), Features), - #iq{type = result, - sub_els = - [#pubsub{subscriptions = - {none, [#pubsub_subscription{node = Node}]}}]} = - send_recv(Config, #iq{type = get, to = pubsub_jid(Config), - sub_els = [#pubsub{subscriptions = {none, []}}]}), - %% Get affiliations - true = lists:member(?PUBSUB("retrieve-affiliations"), Features), - #iq{type = result, - sub_els = [#pubsub{ - affiliations = - [#pubsub_affiliation{node = Node, type = owner}]}]} = - send_recv(Config, #iq{type = get, to = pubsub_jid(Config), - sub_els = [#pubsub{affiliations = []}]}), - %% Fetching published items from node "presence" - #iq{type = result, - sub_els = [#pubsub{items = #pubsub_items{ - node = Node, - items = [Item]}}]} = - send_recv(Config, - #iq{type = get, to = pubsub_jid(Config), - sub_els = [#pubsub{items = #pubsub_items{node = Node}}]}), - %% Deleting the item from the node - true = lists:member(?PUBSUB("delete-items"), Features), - I2 = send(Config, - #iq{type = set, to = pubsub_jid(Config), - sub_els = [#pubsub{retract = #pubsub_retract{ - node = Node, - items = [#pubsub_item{id = ItemID}]}}]}), - ?recv2( - #iq{type = result, id = I2, sub_els = []}, - #message{sub_els = [#pubsub_event{ - items = [#pubsub_event_items{ - node = Node, - retract = [ItemID]}]}]}), - %% Unsubscribe from node "presence" - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, to = pubsub_jid(Config), - sub_els = [#pubsub{unsubscribe = #pubsub_unsubscribe{ - node = Node, - jid = my_jid(Config)}}]}), - disconnect(Config). - -mix_master(Config) -> - MIX = mix_jid(Config), - Room = mix_room_jid(Config), - MyJID = my_jid(Config), - MyBareJID = jid:remove_resource(MyJID), - true = is_feature_advertised(Config, ?NS_MIX_0, MIX), - #iq{type = result, - sub_els = - [#disco_info{ - identities = [#identity{category = <<"conference">>, - type = <<"text">>}], - xdata = [#xdata{type = result, fields = XFields}]}]} = - send_recv(Config, #iq{type = get, to = MIX, sub_els = [#disco_info{}]}), - true = lists:any( - fun(#xdata_field{var = <<"FORM_TYPE">>, - values = [?NS_MIX_SERVICEINFO_0]}) -> true; - (_) -> false - end, XFields), - %% Joining - Nodes = [?NS_MIX_NODES_MESSAGES, ?NS_MIX_NODES_PRESENCE, - ?NS_MIX_NODES_PARTICIPANTS, ?NS_MIX_NODES_SUBJECT, - ?NS_MIX_NODES_CONFIG], - I0 = send(Config, #iq{type = set, to = Room, - sub_els = [#mix_join{subscribe = Nodes}]}), - {_, #message{sub_els = - [#pubsub_event{ - items = [#pubsub_event_items{ - node = ?NS_MIX_NODES_PARTICIPANTS, - items = [#pubsub_event_item{ - id = ParticipantID, - xml_els = [PXML]}]}]}]}} = - ?recv2(#iq{type = result, id = I0, - sub_els = [#mix_join{subscribe = Nodes, jid = MyBareJID}]}, - #message{from = Room}), - #mix_participant{jid = MyBareJID} = xmpp_codec:decode(PXML), - %% Coming online - PresenceID = randoms:get_string(), - Presence = xmpp_codec:encode(#presence{}), - I1 = send( - Config, - #iq{type = set, to = Room, - sub_els = - [#pubsub{ - publish = #pubsub_publish{ - node = ?NS_MIX_NODES_PRESENCE, - items = [#pubsub_item{ - id = PresenceID, - xml_els = [Presence]}]}}]}), - ?recv2(#iq{type = result, id = I1, - sub_els = - [#pubsub{ - publish = #pubsub_publish{ - node = ?NS_MIX_NODES_PRESENCE, - items = [#pubsub_item{id = PresenceID}]}}]}, - #message{from = Room, - sub_els = - [#pubsub_event{ - items = [#pubsub_event_items{ - node = ?NS_MIX_NODES_PRESENCE, - items = [#pubsub_event_item{ - id = PresenceID, - xml_els = [Presence]}]}]}]}), - %% Coming offline - send(Config, #presence{type = unavailable, to = Room}), - %% Receiving presence retract event - #message{from = Room, - sub_els = [#pubsub_event{ - items = [#pubsub_event_items{ - node = ?NS_MIX_NODES_PRESENCE, - retract = [PresenceID]}]}]} = recv(), - %% Leaving - I2 = send(Config, #iq{type = set, to = Room, sub_els = [#mix_leave{}]}), - ?recv2(#iq{type = result, id = I2, sub_els = []}, - #message{from = Room, - sub_els = - [#pubsub_event{ - items = [#pubsub_event_items{ - node = ?NS_MIX_NODES_PARTICIPANTS, - retract = [ParticipantID]}]}]}), - disconnect(Config). - -mix_slave(Config) -> - disconnect(Config). - -roster_subscribe_master(Config) -> - send(Config, #presence{}), - ?recv1(#presence{}), - wait_for_slave(Config), - Peer = ?config(slave, Config), - LPeer = jid:remove_resource(Peer), - send(Config, #presence{type = subscribe, to = LPeer}), - Push1 = ?recv1(#iq{type = set, - sub_els = [#roster{items = [#roster_item{ - ask = subscribe, - subscription = none, - jid = LPeer}]}]}), - send(Config, make_iq_result(Push1)), - {Push2, _} = ?recv2( - #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = to, - jid = LPeer}]}]}, - #presence{type = subscribed, from = LPeer}), - send(Config, make_iq_result(Push2)), - ?recv1(#presence{type = undefined, from = Peer}), - %% BUG: ejabberd sends previous push again. Is it ok? - Push3 = ?recv1(#iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = to, - jid = LPeer}]}]}), - send(Config, make_iq_result(Push3)), - ?recv1(#presence{type = subscribe, from = LPeer}), - send(Config, #presence{type = subscribed, to = LPeer}), - Push4 = ?recv1(#iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = both, - jid = LPeer}]}]}), - send(Config, make_iq_result(Push4)), - %% Move into a group - Groups = [<<"A">>, <<"B">>], - Item = #roster_item{jid = LPeer, groups = Groups}, - I1 = send(Config, #iq{type = set, sub_els = [#roster{items = [Item]}]}), - {Push5, _} = ?recv2( - #iq{type = set, - sub_els = - [#roster{items = [#roster_item{ - jid = LPeer, - subscription = both}]}]}, - #iq{type = result, id = I1, sub_els = []}), - send(Config, make_iq_result(Push5)), - #iq{sub_els = [#roster{items = [#roster_item{groups = G1}]}]} = Push5, - Groups = lists:sort(G1), - wait_for_slave(Config), - ?recv1(#presence{type = unavailable, from = Peer}), - disconnect(Config). - -roster_subscribe_slave(Config) -> - send(Config, #presence{}), - ?recv1(#presence{}), - wait_for_master(Config), - Peer = ?config(master, Config), - LPeer = jid:remove_resource(Peer), - ?recv1(#presence{type = subscribe, from = LPeer}), - send(Config, #presence{type = subscribed, to = LPeer}), - Push1 = ?recv1(#iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = from, - jid = LPeer}]}]}), - send(Config, make_iq_result(Push1)), - send(Config, #presence{type = subscribe, to = LPeer}), - Push2 = ?recv1(#iq{type = set, - sub_els = [#roster{items = [#roster_item{ - ask = subscribe, - subscription = from, - jid = LPeer}]}]}), - send(Config, make_iq_result(Push2)), - {Push3, _} = ?recv2( - #iq{type = set, - sub_els = [#roster{items = [#roster_item{ - subscription = both, - jid = LPeer}]}]}, - #presence{type = subscribed, from = LPeer}), - send(Config, make_iq_result(Push3)), - ?recv1(#presence{type = undefined, from = Peer}), - wait_for_master(Config), - disconnect(Config). - -roster_remove_master(Config) -> - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - LPeer = jid:remove_resource(Peer), - Groups = [<<"A">>, <<"B">>], - wait_for_slave(Config), - send(Config, #presence{}), - ?recv2(#presence{from = MyJID, type = undefined}, - #presence{from = Peer, type = undefined}), - %% The peer removed us from its roster. - {Push1, Push2, _, _, _} = - ?recv5( - %% TODO: I guess this can be optimized, we don't need - %% to send transient roster push with subscription = 'to'. - #iq{type = set, - sub_els = - [#roster{items = [#roster_item{ - jid = LPeer, - subscription = to}]}]}, - #iq{type = set, - sub_els = - [#roster{items = [#roster_item{ - jid = LPeer, - subscription = none}]}]}, - #presence{type = unsubscribe, from = LPeer}, - #presence{type = unsubscribed, from = LPeer}, - #presence{type = unavailable, from = Peer}), - send(Config, make_iq_result(Push1)), - send(Config, make_iq_result(Push2)), - #iq{sub_els = [#roster{items = [#roster_item{groups = G1}]}]} = Push1, - #iq{sub_els = [#roster{items = [#roster_item{groups = G2}]}]} = Push2, - Groups = lists:sort(G1), Groups = lists:sort(G2), - disconnect(Config). - -roster_remove_slave(Config) -> - MyJID = my_jid(Config), - Peer = ?config(master, Config), - LPeer = jid:remove_resource(Peer), - send(Config, #presence{}), - ?recv1(#presence{from = MyJID, type = undefined}), - wait_for_master(Config), - ?recv1(#presence{from = Peer, type = undefined}), - %% Remove the peer from roster. - Item = #roster_item{jid = LPeer, subscription = remove}, - I = send(Config, #iq{type = set, sub_els = [#roster{items = [Item]}]}), - {Push, _, _} = ?recv3( - #iq{type = set, - sub_els = - [#roster{items = [#roster_item{ - jid = LPeer, - subscription = remove}]}]}, - #iq{type = result, id = I, sub_els = []}, - #presence{type = unavailable, from = Peer}), - send(Config, make_iq_result(Push)), - disconnect(Config). - -proxy65_master(Config) -> - Proxy = proxy_jid(Config), - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - wait_for_slave(Config), - send(Config, #presence{}), - ?recv1(#presence{from = MyJID, type = undefined}), - true = is_feature_advertised(Config, ?NS_BYTESTREAMS, Proxy), - #iq{type = result, sub_els = [#bytestreams{hosts = [StreamHost]}]} = - send_recv( - Config, - #iq{type = get, sub_els = [#bytestreams{}], to = Proxy}), - SID = randoms:get_string(), - Data = crypto:rand_bytes(1024), - put_event(Config, {StreamHost, SID, Data}), - Socks5 = socks5_connect(StreamHost, {SID, MyJID, Peer}), - wait_for_slave(Config), - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, to = Proxy, - sub_els = [#bytestreams{activate = Peer, sid = SID}]}), - socks5_send(Socks5, Data), - %%?recv1(#presence{type = unavailable, from = Peer}), - disconnect(Config). - -proxy65_slave(Config) -> - MyJID = my_jid(Config), - Peer = ?config(master, Config), - send(Config, #presence{}), - ?recv1(#presence{from = MyJID, type = undefined}), - wait_for_master(Config), - {StreamHost, SID, Data} = get_event(Config), - Socks5 = socks5_connect(StreamHost, {SID, Peer, MyJID}), - wait_for_master(Config), - socks5_recv(Socks5, Data), - disconnect(Config). - -send_messages_to_room(Config, Range) -> - MyNick = ?config(master_nick, Config), - Room = muc_room_jid(Config), - MyNickJID = jid:replace_resource(Room, MyNick), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - I = send(Config, #message{to = Room, body = [Text], - type = groupchat}), - ?recv1(#message{from = MyNickJID, id = I, - type = groupchat, - body = [Text]}) - end, Range). - -retrieve_messages_from_room_via_mam(Config, Range) -> - MyNick = ?config(master_nick, Config), - Room = muc_room_jid(Config), - MyNickJID = jid:replace_resource(Room, MyNick), - QID = randoms:get_string(), - I = send(Config, #iq{type = set, to = Room, - sub_els = [#mam_query{xmlns = ?NS_MAM_1, id = QID}]}), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{ - to = MyJID, from = Room, - sub_els = - [#mam_result{ - xmlns = ?NS_MAM_1, - queryid = QID, - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = [#message{ - from = MyNickJID, - type = groupchat, - body = [Text]}]}]}]}) - end, Range), - ?recv1(#iq{from = Room, id = I, type = result, sub_els = []}). - -muc_mam_master(Config) -> - MyJID = my_jid(Config), - MyNick = ?config(master_nick, Config), - Room = muc_room_jid(Config), - MyNickJID = jid:replace_resource(Room, MyNick), - %% Joining - send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), - %% Receive self-presence - ?recv1(#presence{from = MyNickJID}), - %% MAM feature should not be advertised at this point, - %% because MAM is not enabled so far - false = is_feature_advertised(Config, ?NS_MAM_1, Room), - %% Fill in some history - send_messages_to_room(Config, lists:seq(1, 21)), - %% We now should be able to retrieve those via MAM, even though - %% MAM is disabled. However, only last 20 messages should be received. - retrieve_messages_from_room_via_mam(Config, lists:seq(2, 21)), - %% Now enable MAM for the conference - %% Retrieve config first - #iq{type = result, sub_els = [#muc_owner{config = #xdata{} = RoomCfg}]} = - send_recv(Config, #iq{type = get, sub_els = [#muc_owner{}], - to = Room}), - %% Find the MAM field in the config and enable it - NewFields = lists:flatmap( - fun(#xdata_field{var = <<"muc#roomconfig_mam">> = Var}) -> - [#xdata_field{var = Var, values = [<<"1">>]}]; - (_) -> - [] - end, RoomCfg#xdata.fields), - NewRoomCfg = #xdata{type = submit, fields = NewFields}, - I1 = send(Config, #iq{type = set, to = Room, - sub_els = [#muc_owner{config = NewRoomCfg}]}), - ?recv2(#iq{type = result, id = I1}, - #message{from = Room, type = groupchat, - sub_els = [#muc_user{status_codes = [104]}]}), - %% Check if MAM has been enabled - true = is_feature_advertised(Config, ?NS_MAM_1, Room), - %% We now sending some messages again - send_messages_to_room(Config, lists:seq(1, 5)), - %% And retrieve them via MAM again. - retrieve_messages_from_room_via_mam(Config, lists:seq(1, 5)), - disconnect(Config). - -muc_mam_slave(Config) -> - disconnect(Config). - -muc_master(Config) -> - MyJID = my_jid(Config), - PeerJID = ?config(slave, Config), - PeerBareJID = jid:remove_resource(PeerJID), - PeerJIDStr = jid:to_string(PeerJID), - MUC = muc_jid(Config), - Room = muc_room_jid(Config), - MyNick = ?config(master_nick, Config), - MyNickJID = jid:replace_resource(Room, MyNick), - PeerNick = ?config(slave_nick, Config), - PeerNickJID = jid:replace_resource(Room, PeerNick), - Subject = ?config(room_subject, Config), - Localhost = jid:make(<<"">>, <<"localhost">>, <<"">>), - true = is_feature_advertised(Config, ?NS_MUC, MUC), - %% Joining - send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), - %% As per XEP-0045 we MUST receive stanzas in the following order: - %% 1. In-room presence from other occupants - %% 2. In-room presence from the joining entity itself (so-called "self-presence") - %% 3. Room history (if any) - %% 4. The room subject - %% 5. Live messages, presence updates, new user joins, etc. - %% As this is the newly created room, we receive only the 2nd stanza. - ?recv1(#presence{ - from = MyNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - status_codes = Codes, - items = [#muc_item{role = moderator, - jid = MyJID, - affiliation = owner}]}]}), - %% 110 -> Inform user that presence refers to itself - %% 201 -> Inform user that a new room has been created - [110, 201] = lists:sort(Codes), - %% Request the configuration - #iq{type = result, sub_els = [#muc_owner{config = #xdata{} = RoomCfg}]} = - send_recv(Config, #iq{type = get, sub_els = [#muc_owner{}], - to = Room}), - NewFields = - lists:flatmap( - fun(#xdata_field{var = Var, values = OrigVals}) -> - Vals = case Var of - <<"FORM_TYPE">> -> - OrigVals; - <<"muc#roomconfig_roomname">> -> - [<<"Test room">>]; - <<"muc#roomconfig_roomdesc">> -> - [<<"Trying to break the server">>]; - <<"muc#roomconfig_persistentroom">> -> - [<<"1">>]; - <<"members_by_default">> -> - [<<"0">>]; - <<"muc#roomconfig_allowvoicerequests">> -> - [<<"1">>]; - <<"public_list">> -> - [<<"1">>]; - <<"muc#roomconfig_publicroom">> -> - [<<"1">>]; - _ -> - [] - end, - if Vals /= [] -> - [#xdata_field{values = Vals, var = Var}]; - true -> - [] - end - end, RoomCfg#xdata.fields), - NewRoomCfg = #xdata{type = submit, fields = NewFields}, - ID = send(Config, #iq{type = set, to = Room, - sub_els = [#muc_owner{config = NewRoomCfg}]}), - ?recv2(#iq{type = result, id = ID}, - #message{from = Room, type = groupchat, - sub_els = [#muc_user{status_codes = [104]}]}), - %% Set subject - send(Config, #message{to = Room, type = groupchat, - body = [#text{data = Subject}]}), - ?recv1(#message{from = MyNickJID, type = groupchat, - body = [#text{data = Subject}]}), - %% Sending messages (and thus, populating history for our peer) - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - I = send(Config, #message{to = Room, body = [Text], - type = groupchat}), - ?recv1(#message{from = MyNickJID, id = I, - type = groupchat, - body = [Text]}) - end, lists:seq(1, 5)), - %% Inviting the peer - send(Config, #message{to = Room, type = normal, - sub_els = - [#muc_user{ - invites = - [#muc_invite{to = PeerJID}]}]}), - %% Peer is joining - ?recv1(#presence{from = PeerNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{role = visitor, - jid = PeerJID, - affiliation = none}]}]}), - %% Receiving a voice request - ?recv1(#message{from = Room, - sub_els = [#xdata{type = form, - instructions = [_], - fields = VoiceReqFs}]}), - %% Approving the voice request - ReplyVoiceReqFs = - lists:map( - fun(#xdata_field{var = Var, values = OrigVals}) -> - Vals = case {Var, OrigVals} of - {<<"FORM_TYPE">>, - [<<"http://jabber.org/protocol/muc#request">>]} -> - OrigVals; - {<<"muc#role">>, [<<"participant">>]} -> - [<<"participant">>]; - {<<"muc#jid">>, [PeerJIDStr]} -> - [PeerJIDStr]; - {<<"muc#roomnick">>, [PeerNick]} -> - [PeerNick]; - {<<"muc#request_allow">>, [<<"0">>]} -> - [<<"1">>] - end, - #xdata_field{values = Vals, var = Var} - end, VoiceReqFs), - send(Config, #message{to = Room, - sub_els = [#xdata{type = submit, - fields = ReplyVoiceReqFs}]}), - %% Peer is becoming a participant - ?recv1(#presence{from = PeerNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{role = participant, - jid = PeerJID, - affiliation = none}]}]}), - %% Receive private message from the peer - ?recv1(#message{from = PeerNickJID, body = [#text{data = Subject}]}), - %% Granting membership to the peer and localhost server - I1 = send(Config, - #iq{type = set, to = Room, - sub_els = - [#muc_admin{ - items = [#muc_item{jid = Localhost, - affiliation = member}, - #muc_item{nick = PeerNick, - jid = PeerBareJID, - affiliation = member}]}]}), - %% Peer became a member - ?recv1(#presence{from = PeerNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{affiliation = member, - jid = PeerJID, - role = participant}]}]}), - ?recv1(#message{from = Room, - sub_els = [#muc_user{ - items = [#muc_item{affiliation = member, - jid = Localhost, - role = none}]}]}), - %% BUG: We should not receive any sub_els! - ?recv1(#iq{type = result, id = I1, sub_els = [_|_]}), - %% Receive groupchat message from the peer - ?recv1(#message{type = groupchat, from = PeerNickJID, - body = [#text{data = Subject}]}), - %% Retrieving a member list - #iq{type = result, sub_els = [#muc_admin{items = MemberList}]} = - send_recv(Config, - #iq{type = get, to = Room, - sub_els = - [#muc_admin{items = [#muc_item{affiliation = member}]}]}), - [#muc_item{affiliation = member, - jid = Localhost}, - #muc_item{affiliation = member, - jid = MyBareJID}] = lists:keysort(#muc_item.jid, MemberList), - %% Kick the peer - I2 = send(Config, - #iq{type = set, to = Room, - sub_els = [#muc_admin{ - items = [#muc_item{nick = PeerNick, - role = none}]}]}), - %% Got notification the peer is kicked - %% 307 -> Inform user that he or she has been kicked from the room - ?recv1(#presence{from = PeerNickJID, type = unavailable, - sub_els = [#muc_user{ - status_codes = [307], - items = [#muc_item{affiliation = member, - jid = PeerJID, - role = none}]}]}), - %% BUG: We should not receive any sub_els! - ?recv1(#iq{type = result, id = I2, sub_els = [_|_]}), - %% Destroying the room - I3 = send(Config, - #iq{type = set, to = Room, - sub_els = [#muc_owner{ - destroy = #muc_owner_destroy{ - reason = Subject}}]}), - %% Kicked off - ?recv1(#presence{from = MyNickJID, type = unavailable, - sub_els = [#muc_user{items = [#muc_item{role = none, - affiliation = none}], - destroy = #muc_user_destroy{ - reason = Subject}}]}), - %% BUG: We should not receive any sub_els! - ?recv1(#iq{type = result, id = I3, sub_els = [_|_]}), - disconnect(Config). - -muc_slave(Config) -> - MyJID = my_jid(Config), - MyBareJID = jid:remove_resource(MyJID), - PeerJID = ?config(master, Config), - MUC = muc_jid(Config), - Room = muc_room_jid(Config), - MyNick = ?config(slave_nick, Config), - MyNickJID = jid:replace_resource(Room, MyNick), - PeerNick = ?config(master_nick, Config), - PeerNickJID = jid:replace_resource(Room, PeerNick), - Subject = ?config(room_subject, Config), - Localhost = jid:make(<<"">>, <<"localhost">>, <<"">>), - %% Receive an invite from the peer - ?recv1(#message{from = Room, type = normal, - sub_els = - [#muc_user{invites = - [#muc_invite{from = PeerJID}]}]}), - %% But before joining we discover the MUC service first - %% to check if the room is in the disco list - #iq{type = result, - sub_els = [#disco_items{items = [#disco_item{jid = Room}]}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#disco_items{}]}), - %% Now check if the peer is in the room. We check this via disco#items - #iq{type = result, - sub_els = [#disco_items{items = [#disco_item{jid = PeerNickJID, - name = PeerNick}]}]} = - send_recv(Config, #iq{type = get, to = Room, - sub_els = [#disco_items{}]}), - %% Now joining - send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), - %% First presence is from the participant, i.e. from the peer - ?recv1(#presence{ - from = PeerNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - status_codes = [], - items = [#muc_item{role = moderator, - affiliation = owner}]}]}), - %% The next is the self-presence (code 110 means it) - ?recv1(#presence{ - from = MyNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - status_codes = [110], - items = [#muc_item{role = visitor, - affiliation = none}]}]}), - %% Receive the room subject - ?recv1(#message{from = PeerNickJID, type = groupchat, - body = [#text{data = Subject}], - sub_els = [#delay{}]}), - %% Receive MUC history - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{from = PeerNickJID, - type = groupchat, - body = [Text], - sub_els = [#delay{}]}) - end, lists:seq(1, 5)), - %% Sending a voice request - VoiceReq = #xdata{ - type = submit, - fields = - [#xdata_field{ - var = <<"FORM_TYPE">>, - values = [<<"http://jabber.org/protocol/muc#request">>]}, - #xdata_field{ - var = <<"muc#role">>, - type = 'text-single', - values = [<<"participant">>]}]}, - send(Config, #message{to = Room, sub_els = [VoiceReq]}), - %% Becoming a participant - ?recv1(#presence{from = MyNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{role = participant, - affiliation = none}]}]}), - %% Sending private message to the peer - send(Config, #message{to = PeerNickJID, - body = [#text{data = Subject}]}), - %% Becoming a member - ?recv1(#presence{from = MyNickJID, - sub_els = [#vcard_xupdate{}, - #muc_user{ - items = [#muc_item{role = participant, - affiliation = member}]}]}), - %% Sending groupchat message - send(Config, #message{to = Room, type = groupchat, - body = [#text{data = Subject}]}), - %% Receive this message back - ?recv1(#message{type = groupchat, from = MyNickJID, - body = [#text{data = Subject}]}), - %% We're kicked off - %% 307 -> Inform user that he or she has been kicked from the room - ?recv1(#presence{from = MyNickJID, type = unavailable, - sub_els = [#muc_user{ - status_codes = [307], - items = [#muc_item{affiliation = member, - role = none}]}]}), - disconnect(Config). - -muc_register_nick(Config, MUC, PrevNick, Nick) -> - {Registered, PrevNickVals} = if PrevNick /= <<"">> -> - {true, [PrevNick]}; - true -> - {false, []} - end, - %% Request register form - #iq{type = result, - sub_els = [#register{registered = Registered, - xdata = #xdata{type = form, - fields = FsWithoutNick}}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#register{}]}), - %% Check if 'nick' field presents - #xdata_field{type = 'text-single', - var = <<"nick">>, - values = PrevNickVals} = - lists:keyfind(<<"nick">>, #xdata_field.var, FsWithoutNick), - X = #xdata{type = submit, - fields = [#xdata_field{var = <<"nick">>, values = [Nick]}]}, - %% Submitting form - #iq{type = result, sub_els = [_|_]} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{xdata = X}]}), - %% Check if the nick was registered - #iq{type = result, - sub_els = [#register{registered = true, - xdata = #xdata{type = form, - fields = FsWithNick}}]} = - send_recv(Config, #iq{type = get, to = MUC, - sub_els = [#register{}]}), - #xdata_field{type = 'text-single', var = <<"nick">>, - values = [Nick]} = - lists:keyfind(<<"nick">>, #xdata_field.var, FsWithNick). - -muc_register_master(Config) -> - MUC = muc_jid(Config), - %% Register nick "master1" - muc_register_nick(Config, MUC, <<"">>, <<"master1">>), - %% Unregister nick "master1" via jabber:register - #iq{type = result, sub_els = [_|_]} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{remove = true}]}), - %% Register nick "master2" - muc_register_nick(Config, MUC, <<"">>, <<"master2">>), - %% Now register nick "master" - muc_register_nick(Config, MUC, <<"master2">>, <<"master">>), - disconnect(Config). - -muc_register_slave(Config) -> - MUC = muc_jid(Config), - %% Trying to register occupied nick "master" - X = #xdata{type = submit, - fields = [#xdata_field{var = <<"nick">>, - values = [<<"master">>]}]}, - #iq{type = error} = - send_recv(Config, #iq{type = set, to = MUC, - sub_els = [#register{xdata = X}]}), - disconnect(Config). - -announce_master(Config) -> - MyJID = my_jid(Config), - ServerJID = server_jid(Config), - MotdJID = jid:replace_resource(ServerJID, <<"announce/motd">>), - MotdText = #text{data = <<"motd">>}, - send(Config, #presence{}), - ?recv1(#presence{from = MyJID}), - %% Set message of the day - send(Config, #message{to = MotdJID, body = [MotdText]}), - %% Receive this message back - ?recv1(#message{from = ServerJID, body = [MotdText]}), - disconnect(Config). - -announce_slave(Config) -> - MyJID = my_jid(Config), - ServerJID = server_jid(Config), - MotdDelJID = jid:replace_resource(ServerJID, <<"announce/motd/delete">>), - MotdText = #text{data = <<"motd">>}, - send(Config, #presence{}), - ?recv2(#presence{from = MyJID}, - #message{from = ServerJID, body = [MotdText]}), - %% Delete message of the day - send(Config, #message{to = MotdDelJID}), - disconnect(Config). - -flex_offline_master(Config) -> - Peer = ?config(slave, Config), - LPeer = jid:remove_resource(Peer), - lists:foreach( - fun(I) -> - Body = integer_to_binary(I), - send(Config, #message{to = LPeer, - body = [#text{data = Body}], - subject = [#text{data = <<"subject">>}]}) - end, lists:seq(1, 5)), - disconnect(Config). - -flex_offline_slave(Config) -> - MyJID = my_jid(Config), - MyBareJID = jid:remove_resource(MyJID), - Peer = ?config(master, Config), - Peer_s = jid:to_string(Peer), - true = is_feature_advertised(Config, ?NS_FLEX_OFFLINE), - %% Request disco#info - #iq{type = result, - sub_els = [#disco_info{ - node = ?NS_FLEX_OFFLINE, - identities = Ids, - features = Fts, - xdata = [X]}]} = - send_recv(Config, #iq{type = get, - sub_els = [#disco_info{ - node = ?NS_FLEX_OFFLINE}]}), - %% Check if we have correct identities - true = lists:any( - fun(#identity{category = <<"automation">>, - type = <<"message-list">>}) -> true; - (_) -> false - end, Ids), - %% Check if we have needed feature - true = lists:member(?NS_FLEX_OFFLINE, Fts), - %% Check xdata, the 'number_of_messages' should be 5 - #xdata{type = result, - fields = [#xdata_field{type = hidden, - var = <<"FORM_TYPE">>}, - #xdata_field{var = <<"number_of_messages">>, - values = [<<"5">>]}]} = X, - %% Fetch headers, - #iq{type = result, - sub_els = [#disco_items{ - node = ?NS_FLEX_OFFLINE, - items = DiscoItems}]} = - send_recv(Config, #iq{type = get, - sub_els = [#disco_items{ - node = ?NS_FLEX_OFFLINE}]}), - %% Check if headers are correct - Nodes = lists:sort( - lists:map( - fun(#disco_item{jid = J, name = P, node = N}) - when (J == MyBareJID) and (P == Peer_s) -> - N - end, DiscoItems)), - %% Since headers are received we can send initial presence without a risk - %% of getting offline messages flood - send(Config, #presence{}), - ?recv1(#presence{from = MyJID}), - %% Check full fetch - I0 = send(Config, #iq{type = get, sub_els = [#offline{fetch = true}]}), - lists:foreach( - fun({I, N}) -> - Text = integer_to_binary(I), - ?recv1(#message{body = Body, sub_els = SubEls}), - [#text{data = Text}] = Body, - #offline{items = [#offline_item{node = N}]} = - lists:keyfind(offline, 1, SubEls), - #delay{} = lists:keyfind(delay, 1, SubEls) - end, lists:zip(lists:seq(1, 5), Nodes)), - ?recv1(#iq{type = result, id = I0, sub_els = []}), - %% Fetch 2nd and 4th message - I1 = send(Config, - #iq{type = get, - sub_els = [#offline{ - items = [#offline_item{ - action = view, - node = lists:nth(2, Nodes)}, - #offline_item{ - action = view, - node = lists:nth(4, Nodes)}]}]}), - lists:foreach( - fun({I, N}) -> - Text = integer_to_binary(I), - ?recv1(#message{body = [#text{data = Text}], sub_els = SubEls}), - #offline{items = [#offline_item{node = N}]} = - lists:keyfind(offline, 1, SubEls) - end, lists:zip([2, 4], [lists:nth(2, Nodes), lists:nth(4, Nodes)])), - ?recv1(#iq{type = result, id = I1, sub_els = []}), - %% Delete 2nd and 4th message - #iq{type = result, sub_els = []} = - send_recv( - Config, - #iq{type = set, - sub_els = [#offline{ - items = [#offline_item{ - action = remove, - node = lists:nth(2, Nodes)}, - #offline_item{ - action = remove, - node = lists:nth(4, Nodes)}]}]}), - %% Check if messages were deleted - #iq{type = result, - sub_els = [#disco_items{ - node = ?NS_FLEX_OFFLINE, - items = RemainedItems}]} = - send_recv(Config, #iq{type = get, - sub_els = [#disco_items{ - node = ?NS_FLEX_OFFLINE}]}), - RemainedNodes = [lists:nth(1, Nodes), - lists:nth(3, Nodes), - lists:nth(5, Nodes)], - RemainedNodes = lists:sort( - lists:map( - fun(#disco_item{node = N}) -> N end, - RemainedItems)), - %% Purge everything left - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [#offline{purge = true}]}), - %% Check if there is no offline messages - #iq{type = result, - sub_els = [#disco_items{node = ?NS_FLEX_OFFLINE, items = []}]} = - send_recv(Config, #iq{type = get, - sub_els = [#disco_items{ - node = ?NS_FLEX_OFFLINE}]}), - disconnect(Config). - -offline_master(Config) -> - Peer = ?config(slave, Config), - LPeer = jid:remove_resource(Peer), - send(Config, #message{to = LPeer, - body = [#text{data = <<"body">>}], - subject = [#text{data = <<"subject">>}]}), - disconnect(Config). - -offline_slave(Config) -> - Peer = ?config(master, Config), - send(Config, #presence{}), - {_, #message{sub_els = SubEls}} = - ?recv2(#presence{}, - #message{from = Peer, - body = [#text{data = <<"body">>}], - subject = [#text{data = <<"subject">>}]}), - true = lists:keymember(delay, 1, SubEls), - disconnect(Config). - -carbons_master(Config) -> - MyJID = my_jid(Config), - MyBareJID = jid:remove_resource(MyJID), - Peer = ?config(slave, Config), - Txt = #text{data = <<"body">>}, - true = is_feature_advertised(Config, ?NS_CARBONS_2), - send(Config, #presence{priority = 10}), - ?recv1(#presence{from = MyJID}), - wait_for_slave(Config), - ?recv1(#presence{from = Peer}), - %% Enable carbons - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#carbons_enable{}]}), - %% Send a message to bare and full JID - send(Config, #message{to = MyBareJID, type = chat, body = [Txt]}), - send(Config, #message{to = MyJID, type = chat, body = [Txt]}), - send(Config, #message{to = MyBareJID, type = chat, body = [Txt], - sub_els = [#carbons_private{}]}), - send(Config, #message{to = MyJID, type = chat, body = [Txt], - sub_els = [#carbons_private{}]}), - %% Receive the messages back - ?recv4(#message{from = MyJID, to = MyBareJID, type = chat, - body = [Txt], sub_els = []}, - #message{from = MyJID, to = MyJID, type = chat, - body = [Txt], sub_els = []}, - #message{from = MyJID, to = MyBareJID, type = chat, - body = [Txt], sub_els = [#carbons_private{}]}, - #message{from = MyJID, to = MyJID, type = chat, - body = [Txt], sub_els = [#carbons_private{}]}), - %% Disable carbons - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#carbons_disable{}]}), - wait_for_slave(Config), - %% Repeat the same and leave - send(Config, #message{to = MyBareJID, type = chat, body = [Txt]}), - send(Config, #message{to = MyJID, type = chat, body = [Txt]}), - send(Config, #message{to = MyBareJID, type = chat, body = [Txt], - sub_els = [#carbons_private{}]}), - send(Config, #message{to = MyJID, type = chat, body = [Txt], - sub_els = [#carbons_private{}]}), - ?recv4(#message{from = MyJID, to = MyBareJID, type = chat, - body = [Txt], sub_els = []}, - #message{from = MyJID, to = MyJID, type = chat, - body = [Txt], sub_els = []}, - #message{from = MyJID, to = MyBareJID, type = chat, - body = [Txt], sub_els = [#carbons_private{}]}, - #message{from = MyJID, to = MyJID, type = chat, - body = [Txt], sub_els = [#carbons_private{}]}), - disconnect(Config). - -carbons_slave(Config) -> - MyJID = my_jid(Config), - MyBareJID = jid:remove_resource(MyJID), - Peer = ?config(master, Config), - Txt = #text{data = <<"body">>}, - wait_for_master(Config), - send(Config, #presence{priority = 5}), - ?recv2(#presence{from = MyJID}, #presence{from = Peer}), - %% Enable carbons - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#carbons_enable{}]}), - %% Receive messages sent by the peer - ?recv4( - #message{from = MyBareJID, to = MyJID, type = chat, - sub_els = - [#carbons_sent{ - forwarded = #forwarded{ - sub_els = - [#message{from = Peer, - to = MyBareJID, - type = chat, - body = [Txt]}]}}]}, - #message{from = MyBareJID, to = MyJID, type = chat, - sub_els = - [#carbons_sent{ - forwarded = #forwarded{ - sub_els = - [#message{from = Peer, - to = Peer, - type = chat, - body = [Txt]}]}}]}, - #message{from = MyBareJID, to = MyJID, type = chat, - sub_els = - [#carbons_received{ - forwarded = #forwarded{ - sub_els = - [#message{from = Peer, - to = MyBareJID, - type = chat, - body = [Txt]}]}}]}, - #message{from = MyBareJID, to = MyJID, type = chat, - sub_els = - [#carbons_received{ - forwarded = #forwarded{ - sub_els = - [#message{from = Peer, - to = Peer, - type = chat, - body = [Txt]}]}}]}), - %% Disable carbons - #iq{type = result, sub_els = []} = - send_recv(Config, - #iq{type = set, - sub_els = [#carbons_disable{}]}), - wait_for_master(Config), - %% Now we should receive nothing but presence unavailable from the peer - ?recv1(#presence{from = Peer, type = unavailable}), - disconnect(Config). - -mam_old_master(Config) -> - mam_master(Config, ?NS_MAM_TMP). - -mam_new_master(Config) -> - mam_master(Config, ?NS_MAM_0). - -mam_master(Config, NS) -> - true = is_feature_advertised(Config, NS), - MyJID = my_jid(Config), - BareMyJID = jid:remove_resource(MyJID), - Peer = ?config(slave, Config), - send(Config, #presence{}), - ?recv1(#presence{}), - wait_for_slave(Config), - ?recv1(#presence{from = Peer}), - #iq{type = result, sub_els = [#mam_prefs{xmlns = NS, default = roster}]} = - send_recv(Config, - #iq{type = set, - sub_els = [#mam_prefs{xmlns = NS, - default = roster, - never = [MyJID]}]}), - if NS == ?NS_MAM_TMP -> - FakeArchived = #mam_archived{id = randoms:get_string(), - by = server_jid(Config)}, - send(Config, #message{to = MyJID, - sub_els = [FakeArchived], - body = [#text{data = <<"a">>}]}), - send(Config, #message{to = BareMyJID, - sub_els = [FakeArchived], - body = [#text{data = <<"b">>}]}), - %% NOTE: The server should strip fake archived tags, - %% i.e. the sub_els received should be []. - ?recv2(#message{body = [#text{data = <<"a">>}], sub_els = []}, - #message{body = [#text{data = <<"b">>}], sub_els = []}); - true -> - ok - end, - wait_for_slave(Config), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - send(Config, - #message{to = Peer, body = [Text]}) - end, lists:seq(1, 5)), - ?recv1(#presence{type = unavailable, from = Peer}), - mam_query_all(Config, NS), - mam_query_with(Config, Peer, NS), - %% mam_query_with(Config, jid:remove_resource(Peer)), - mam_query_rsm(Config, NS), - #iq{type = result, sub_els = [#mam_prefs{xmlns = NS, default = never}]} = - send_recv(Config, #iq{type = set, - sub_els = [#mam_prefs{xmlns = NS, - default = never}]}), - disconnect(Config). - -mam_old_slave(Config) -> - mam_slave(Config, ?NS_MAM_TMP). - -mam_new_slave(Config) -> - mam_slave(Config, ?NS_MAM_0). - -mam_slave(Config, NS) -> - Peer = ?config(master, Config), - ServerJID = server_jid(Config), - wait_for_master(Config), - send(Config, #presence{}), - ?recv2(#presence{}, #presence{from = Peer}), - #iq{type = result, sub_els = [#mam_prefs{xmlns = NS, default = always}]} = - send_recv(Config, - #iq{type = set, - sub_els = [#mam_prefs{xmlns = NS, default = always}]}), - wait_for_master(Config), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{from = Peer, body = [Text], - sub_els = [#mam_archived{by = ServerJID}]}) - end, lists:seq(1, 5)), - #iq{type = result, sub_els = [#mam_prefs{xmlns = NS, default = never}]} = - send_recv(Config, #iq{type = set, - sub_els = [#mam_prefs{xmlns = NS, default = never}]}), - disconnect(Config). - -mam_query_all(Config, NS) -> - QID = randoms:get_string(), - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - Type = case NS of - ?NS_MAM_TMP -> get; - _ -> set - end, - I = send(Config, #iq{type = Type, sub_els = [#mam_query{xmlns = NS, id = QID}]}), - maybe_recv_iq_result(NS, I), - Iter = if NS == ?NS_MAM_TMP -> lists:seq(1, 5); - true -> lists:seq(1, 5) ++ lists:seq(1, 5) - end, - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{to = MyJID, - sub_els = - [#mam_result{ - queryid = QID, - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = - [#message{ - from = MyJID, to = Peer, - body = [Text]}]}]}]}) - end, Iter), - if NS == ?NS_MAM_TMP -> - ?recv1(#iq{type = result, id = I, - sub_els = [#mam_query{xmlns = NS, id = QID}]}); - true -> - ?recv1(#message{sub_els = [#mam_fin{complete = true, id = QID}]}) - end. - -mam_query_with(Config, JID, NS) -> - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - {Query, Type} = if NS == ?NS_MAM_TMP -> - {#mam_query{xmlns = NS, with = JID}, get}; - true -> - Fs = [#xdata_field{var = <<"jid">>, - values = [jid:to_string(JID)]}], - {#mam_query{xmlns = NS, - xdata = #xdata{type = submit, fields = Fs}}, set} - end, - I = send(Config, #iq{type = Type, sub_els = [Query]}), - Iter = if NS == ?NS_MAM_TMP -> lists:seq(1, 5); - true -> lists:seq(1, 5) ++ lists:seq(1, 5) - end, - maybe_recv_iq_result(NS, I), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{to = MyJID, - sub_els = - [#mam_result{ - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = - [#message{ - from = MyJID, to = Peer, - body = [Text]}]}]}]}) - end, Iter), - if NS == ?NS_MAM_TMP -> - ?recv1(#iq{type = result, id = I, - sub_els = [#mam_query{xmlns = NS}]}); - true -> - ?recv1(#message{sub_els = [#mam_fin{complete = true}]}) - end. - -maybe_recv_iq_result(?NS_MAM_0, I1) -> - ?recv1(#iq{type = result, id = I1}); -maybe_recv_iq_result(_, _) -> - ok. - -mam_query_rsm(Config, NS) -> - MyJID = my_jid(Config), - Peer = ?config(slave, Config), - Type = case NS of - ?NS_MAM_TMP -> get; - _ -> set - end, - %% Get the first 3 items out of 5 - I1 = send(Config, - #iq{type = Type, - sub_els = [#mam_query{xmlns = NS, rsm = #rsm_set{max = 3}}]}), - maybe_recv_iq_result(NS, I1), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{to = MyJID, - sub_els = - [#mam_result{ - xmlns = NS, - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = - [#message{ - from = MyJID, to = Peer, - body = [Text]}]}]}]}) - end, lists:seq(1, 3)), - if NS == ?NS_MAM_TMP -> - ?recv1(#iq{type = result, id = I1, - sub_els = [#mam_query{xmlns = NS, - rsm = #rsm_set{last = Last, count = 5}}]}); - true -> - ?recv1(#message{sub_els = [#mam_fin{ - complete = false, - rsm = #rsm_set{last = Last, count = 10}}]}) - end, - %% Get the next items starting from the `Last`. - %% Limit the response to 2 items. - I2 = send(Config, - #iq{type = Type, - sub_els = [#mam_query{xmlns = NS, - rsm = #rsm_set{max = 2, - 'after' = Last}}]}), - maybe_recv_iq_result(NS, I2), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{to = MyJID, - sub_els = - [#mam_result{ - xmlns = NS, - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = - [#message{ - from = MyJID, to = Peer, - body = [Text]}]}]}]}) - end, lists:seq(4, 5)), - if NS == ?NS_MAM_TMP -> - ?recv1(#iq{type = result, id = I2, - sub_els = [#mam_query{ - xmlns = NS, - rsm = #rsm_set{ - count = 5, - first = #rsm_first{data = First}}}]}); - true -> - ?recv1(#message{ - sub_els = [#mam_fin{ - complete = false, - rsm = #rsm_set{ - count = 10, - first = #rsm_first{data = First}}}]}) - end, - %% Paging back. Should receive 3 elements: 1, 2, 3. - I3 = send(Config, - #iq{type = Type, - sub_els = [#mam_query{xmlns = NS, - rsm = #rsm_set{max = 3, - before = First}}]}), - maybe_recv_iq_result(NS, I3), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{to = MyJID, - sub_els = - [#mam_result{ - xmlns = NS, - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = - [#message{ - from = MyJID, to = Peer, - body = [Text]}]}]}]}) - end, lists:seq(1, 3)), - if NS == ?NS_MAM_TMP -> - ?recv1(#iq{type = result, id = I3, - sub_els = [#mam_query{xmlns = NS, rsm = #rsm_set{count = 5}}]}); - true -> - ?recv1(#message{ - sub_els = [#mam_fin{complete = true, - rsm = #rsm_set{count = 10}}]}) - end, - %% Getting the item count. Should be 5 (or 10). - I4 = send(Config, - #iq{type = Type, - sub_els = [#mam_query{xmlns = NS, - rsm = #rsm_set{max = 0}}]}), - maybe_recv_iq_result(NS, I4), - if NS == ?NS_MAM_TMP -> - ?recv1(#iq{type = result, id = I4, - sub_els = [#mam_query{ - xmlns = NS, - rsm = #rsm_set{count = 5, - first = undefined, - last = undefined}}]}); - true -> - ?recv1(#message{ - sub_els = [#mam_fin{ - complete = false, - rsm = #rsm_set{count = 10, - first = undefined, - last = undefined}}]}) - end, - %% Should receive 2 last messages - I5 = send(Config, - #iq{type = Type, - sub_els = [#mam_query{xmlns = NS, - rsm = #rsm_set{max = 2, - before = none}}]}), - maybe_recv_iq_result(NS, I5), - lists:foreach( - fun(N) -> - Text = #text{data = integer_to_binary(N)}, - ?recv1(#message{to = MyJID, - sub_els = - [#mam_result{ - xmlns = NS, - sub_els = - [#forwarded{ - delay = #delay{}, - sub_els = - [#message{ - from = MyJID, to = Peer, - body = [Text]}]}]}]}) - end, lists:seq(4, 5)), - if NS == ?NS_MAM_TMP -> - ?recv1(#iq{type = result, id = I5, - sub_els = [#mam_query{xmlns = NS, rsm = #rsm_set{count = 5}}]}); - true -> - ?recv1(#message{ - sub_els = [#mam_fin{complete = false, - rsm = #rsm_set{count = 10}}]}) - end. - -client_state_master(Config) -> - true = ?config(csi, Config), - Peer = ?config(slave, Config), - Presence = #presence{to = Peer}, - ChatState = #message{to = Peer, thread = <<"1">>, - sub_els = [#chatstate{type = active}]}, - Message = ChatState#message{body = [#text{data = <<"body">>}]}, - PepPayload = xmpp_codec:encode(#presence{}), - PepOne = #message{ - to = Peer, - sub_els = - [#pubsub_event{ - items = - [#pubsub_event_items{ - node = <<"foo-1">>, - items = - [#pubsub_event_item{ - id = <<"pep-1">>, - xml_els = [PepPayload]}]}]}]}, - PepTwo = #message{ - to = Peer, - sub_els = - [#pubsub_event{ - items = - [#pubsub_event_items{ - node = <<"foo-2">>, - items = - [#pubsub_event_item{ - id = <<"pep-2">>, - xml_els = [PepPayload]}]}]}]}, - %% Wait for the slave to become inactive. - wait_for_slave(Config), - %% Should be queued (but see below): - send(Config, Presence), - %% Should replace the previous presence in the queue: - send(Config, Presence#presence{type = unavailable}), - %% The following two PEP stanzas should be queued (but see below): - send(Config, PepOne), - send(Config, PepTwo), - %% The following two PEP stanzas should replace the previous two: - send(Config, PepOne), - send(Config, PepTwo), - %% Should be queued (but see below): - send(Config, ChatState), - %% Should replace the previous chat state in the queue: - send(Config, ChatState#message{sub_els = [#chatstate{type = composing}]}), - %% Should be sent immediately, together with the queued stanzas: - send(Config, Message), - %% Wait for the slave to become active. - wait_for_slave(Config), - %% Should be delivered, as the client is active again: - send(Config, ChatState), - disconnect(Config). - -client_state_slave(Config) -> - Peer = ?config(master, Config), - change_client_state(Config, inactive), - wait_for_master(Config), - ?recv1(#presence{from = Peer, type = unavailable, - sub_els = [#delay{}]}), - #message{ - from = Peer, - sub_els = - [#pubsub_event{ - items = - [#pubsub_event_items{ - node = <<"foo-1">>, - items = - [#pubsub_event_item{ - id = <<"pep-1">>}]}]}, - #delay{}]} = recv(), - #message{ - from = Peer, - sub_els = - [#pubsub_event{ - items = - [#pubsub_event_items{ - node = <<"foo-2">>, - items = - [#pubsub_event_item{ - id = <<"pep-2">>}]}]}, - #delay{}]} = recv(), - ?recv1(#message{from = Peer, thread = <<"1">>, - sub_els = [#chatstate{type = composing}, - #delay{}]}), - ?recv1(#message{from = Peer, thread = <<"1">>, - body = [#text{data = <<"body">>}], - sub_els = [#chatstate{type = active}]}), - change_client_state(Config, active), - wait_for_master(Config), - ?recv1(#message{from = Peer, thread = <<"1">>, - sub_els = [#chatstate{type = active}]}), - disconnect(Config). - %%%=================================================================== %%% Aux functions %%%=================================================================== -change_client_state(Config, NewState) -> - send(Config, #csi{type = NewState}), - send_recv(Config, #iq{type = get, to = server_jid(Config), - sub_els = [#ping{}]}). - bookmark_conference() -> #bookmark_conference{name = <<"Some name">>, autojoin = true, @@ -2377,50 +969,56 @@ bookmark_conference() -> <<"some.conference.org">>, <<>>)}. -socks5_connect(#streamhost{host = Host, port = Port}, - {SID, JID1, JID2}) -> - Hash = p1_sha:sha([SID, jid:to_string(JID1), jid:to_string(JID2)]), - {ok, Sock} = gen_tcp:connect(binary_to_list(Host), Port, - [binary, {active, false}]), - Init = <<?VERSION_5, 1, ?AUTH_ANONYMOUS>>, - InitAck = <<?VERSION_5, ?AUTH_ANONYMOUS>>, - Req = <<?VERSION_5, ?CMD_CONNECT, 0, - ?ATYP_DOMAINNAME, 40, Hash:40/binary, 0, 0>>, - Resp = <<?VERSION_5, ?SUCCESS, 0, ?ATYP_DOMAINNAME, - 40, Hash:40/binary, 0, 0>>, - gen_tcp:send(Sock, Init), - {ok, InitAck} = gen_tcp:recv(Sock, size(InitAck)), - gen_tcp:send(Sock, Req), - {ok, Resp} = gen_tcp:recv(Sock, size(Resp)), - Sock. - -socks5_send(Sock, Data) -> - ok = gen_tcp:send(Sock, Data). - -socks5_recv(Sock, Data) -> - {ok, Data} = gen_tcp:recv(Sock, size(Data)). +'$handle_undefined_function'(F, [Config]) when is_list(Config) -> + case re:split(atom_to_list(F), "_", [{return, list}, {parts, 2}]) of + [M, T] -> + Module = list_to_atom(M ++ "_tests"), + Function = list_to_atom(T), + case erlang:function_exported(Module, Function, 1) of + true -> + Module:Function(Config); + false -> + erlang:error({undef, F}) + end; + _ -> + erlang:error({undef, F}) + end; +'$handle_undefined_function'(_, _) -> + erlang:error(undef). %%%=================================================================== %%% SQL stuff %%%=================================================================== -create_sql_tables(sqlite, _BaseDir) -> +clear_sql_tables(sqlite, _BaseDir) -> ok; -create_sql_tables(Type, BaseDir) -> +clear_sql_tables(Type, BaseDir) -> {VHost, File} = case Type of mysql -> - {?MYSQL_VHOST, "mysql.sql"}; + Path = case ejabberd_sql:use_new_schema() of + true -> + "mysql.new.sql"; + false -> + "mysql.sql" + end, + {?MYSQL_VHOST, Path}; pgsql -> - {?PGSQL_VHOST, "pg.sql"} + Path = case ejabberd_sql:use_new_schema() of + true -> + "pg.new.sql"; + false -> + "pg.sql" + end, + {?PGSQL_VHOST, Path} end, SQLFile = filename:join([BaseDir, "sql", File]), CreationQueries = read_sql_queries(SQLFile), - DropTableQueries = drop_table_queries(CreationQueries), + ClearTableQueries = clear_table_queries(CreationQueries), case ejabberd_sql:sql_transaction( - VHost, DropTableQueries ++ CreationQueries) of + VHost, ClearTableQueries) of {atomic, ok} -> ok; Err -> - ct:fail({failed_to_create_sql_tables, Type, Err}) + ct:fail({failed_to_clear_sql_tables, Type, Err}) end. read_sql_queries(File) -> @@ -2431,12 +1029,12 @@ read_sql_queries(File) -> ct:fail({open_file_failed, File, Err}) end. -drop_table_queries(Queries) -> +clear_table_queries(Queries) -> lists:foldl( fun(Query, Acc) -> case split(str:to_lower(Query)) of [<<"create">>, <<"table">>, Table|_] -> - [<<"DROP TABLE IF EXISTS ", Table/binary, ";">>|Acc]; + [<<"DELETE FROM ", Table/binary, ";">>|Acc]; _ -> Acc end @@ -2476,16 +1074,3 @@ split(Data) -> (_) -> true end, re:split(Data, <<"\s">>)). - -clear_riak_tables(Config) -> - User = ?config(user, Config), - Server = ?config(server, Config), - Room = muc_room_jid(Config), - {URoom, SRoom, _} = jid:tolower(Room), - ejabberd_auth:remove_user(User, Server), - ejabberd_auth:remove_user(<<"test_slave">>, Server), - ejabberd_auth:remove_user(<<"test_master">>, Server), - mod_muc:forget_room(Server, URoom, SRoom), - ejabberd_riak:delete(muc_registered, {{<<"test_slave">>, Server}, SRoom}), - ejabberd_riak:delete(muc_registered, {{<<"test_master">>, Server}, SRoom}), - Config. diff --git a/test/ejabberd_SUITE_data/ca.key b/test/ejabberd_SUITE_data/ca.key new file mode 100644 index 000000000..cc59087c6 --- /dev/null +++ b/test/ejabberd_SUITE_data/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA5WxWkSLK3iadpy2v57FVc7pK307aWHQqirg+q5PreRB1nLsr +oW+TaXfgB5B1/GTFStnSbmczqpkuWyi4hIB9ZzM62kWuOpZPx0+w5hHx73VWxpsr +YgaBkoQsn8BF84PfmRDNG76TOacuoLzeqnN1deWDgOGQ9a7ZesOQLuZBPF6oysfK +OpAR035fQM6XaaR8Ti6Ko53DkCzw8MiySrAHJOkgxhmX11+hUMjldWCEiRs1VL/g +rolajqe3B+wu0UdonZ/QUeVk4KRnDIAIJSKw8XmgcB4oI5cUrnDnOmv2784RgJZs +ZxuGF0e5mz5v8BqXqKiFwH/CD1inUpMA89MATQIDAQABAoIBAQCc2O1x+ixhplrg +AZ8iMp2uKe2oL5udH4Y6Im5OFSnGMdeGmHviuYo5b8gMw9m1/RrY6oQwEIRFHMaR +cgx8IfAaDu8sbLkJutu98qCJGjmiMUFrNIh7UuFgztZHPUdVjZHfbpobXrX+k2qQ +X6+HLrpeKNQ3136oSKrMgEjhl2+AGhe/uqFGw+nwCNzY3BnAJOWS8pipgV0IQ1Eo +AdJU8SoW/LToo5RTZNodPhyqLl10D1tRJ8WSAndAkvaoMRHJasYQDrmz449+QiTZ +SLRf9n/TtcKJQTaqwskV/dOdygeBUKnZQhq663TKgTWcTxF1dA5T3QxXv/7p+8Ow +9GxuxBjBAoGBAPRjb8OCLD8EAtxFXWRWBH5GWF3vGnDIq5FkPaue0uyDaw+TLgJE +AKV7Ik0IRRZkUdc/xix22Bg83L0ErOD2qLHgZuUvuXtiv+Dq/D2BIb5M3zQy8giA +vxdlE5O9i8aG647P+ACGOpYZ7a/K645HGxqOZpf8ZRmST5VzNY7qVxb9AoGBAPBS +4Bo66VMWf6BLd8RIK3DzOf0TWRRMCAwX9kCNTG22TX79imJHWB5lWQQam4yp4Cya +wo08DT3YcffURW9bJTF2q+JZHMqlEr8q9kcjIJu8uQ7X9N4JsUfCcWaBSHHBNgx/ +coved2h02NFcJmV3HuF2l/miah6p9rPJmGnvG1eRAoGBAKIEqju7OQot5peRhPDX +9fKhQERGGAldgCDLi/cTPFKAbaHNuVrXKnaKw5q+OM83gupo5UDlKS4oa08Eongi +DoSeeJjIovch6IN8Re2ghnZbED7S55KriARChlAUAW6EU/ZB+fCfDIgmeGVq6e9R +RK6+aVWphn0Feq1hy8gLo+EhAoGBAI/hvmRV4v2o2a5ZoJH2d3O/W3eGTu3U+3hq +HDfXoOuKmukt2N0wQ7SnDt1jJL/ZsOpjmZk/W9osLUeoYg3ibuknWI9CtPcqT4f+ +q8Y5ZLt5CP63EtagzO/enVA2lO3uNHLVFvpgrfLvCiSGXEKhR+7KtwBxWcGUFqzb +RJIf4qnRAoGAR+c24S4MtVuw6+UVKyLxhjB6iDTvJijdIr/+ofbeM5TQHGsYzZzP +HHNdZ5ECz5eDnaNzvAs4CCuy+75cqlUhAgzrLlCj+dJN/fYEJsD6AjWdto3Zorig +XBFM8FtXP7VRjFNwCCbdhrFOcmgbAtz3ReS6Ts6drSw7OgyeDajam1U= +-----END RSA PRIVATE KEY----- diff --git a/test/ejabberd_SUITE_data/ca.pem b/test/ejabberd_SUITE_data/ca.pem new file mode 100644 index 000000000..089238d62 --- /dev/null +++ b/test/ejabberd_SUITE_data/ca.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUUynLQejEU8NykU/YNfL1dyC7vxcwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xODA5MjQxMzE4MjRaFw00NjAy +MDkxMzE4MjRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDlbFaRIsreJp2nLa/nsVVzukrfTtpYdCqKuD6rk+t5 +EHWcuyuhb5Npd+AHkHX8ZMVK2dJuZzOqmS5bKLiEgH1nMzraRa46lk/HT7DmEfHv +dVbGmytiBoGShCyfwEXzg9+ZEM0bvpM5py6gvN6qc3V15YOA4ZD1rtl6w5Au5kE8 +XqjKx8o6kBHTfl9AzpdppHxOLoqjncOQLPDwyLJKsAck6SDGGZfXX6FQyOV1YISJ +GzVUv+CuiVqOp7cH7C7RR2idn9BR5WTgpGcMgAglIrDxeaBwHigjlxSucOc6a/bv +zhGAlmxnG4YXR7mbPm/wGpeoqIXAf8IPWKdSkwDz0wBNAgMBAAGjUzBRMB0GA1Ud +DgQWBBQGU3AZGF8ahVEnpfHB5ETAW5uIBzAfBgNVHSMEGDAWgBQGU3AZGF8ahVEn +pfHB5ETAW5uIBzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAK +jIEjOh7k1xaEMBygQob9XGLmyLgmw1GEvWx7wiDpcdHXuAH9mLC4NPNSjOXPNK2V +u4dh1KHy1z+dHJbt2apXejxtiwlcMWmPDF2EtKjstUN+KXecG7vjReArs71T9ir/ +7Xfwfg6TKD3H7efYFJaBb7d/lyneNP1Ive/rkRsGqCglkoX4ajcAm7MLkkFD8TCP +NqFc7SdA4OsaeYiUmjnyTUDbKgG0bDAXymhsUzd6Pa9kKQx+dH4GPiCoNoypCXD7 +RZSlETNGZ0vdxCjpdvT4eYxSIalG4rAU85turqPF/ovdzUzb72Sta0L5Hrf0rLa/ +um3+Xel8qI+p3kErAG2v +-----END CERTIFICATE----- diff --git a/test/ejabberd_SUITE_data/cert.pem b/test/ejabberd_SUITE_data/cert.pem index 11e18491f..7b82b3ca7 100644 --- a/test/ejabberd_SUITE_data/cert.pem +++ b/test/ejabberd_SUITE_data/cert.pem @@ -1,52 +1,54 @@ -----BEGIN CERTIFICATE----- -MIIGbDCCBVSgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTET -MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ -dHkgTHRkMB4XDTE2MDUyNDE3NDIyNVoXDTQzMTAxMDE3NDIyNVowVjELMAkGA1UE +MIIEjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET +MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMB4XDTE4MDkyNDEzMTgyNFoXDTQ2MDIwOTEzMTgyNFowWTELMAkGA1UE BhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdp -ZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAxMGYWN0aXZlMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQC+GTA1D1+yiXgLqUhJXkSj3hj5FiqlBAfJT/8OSXYifY4M4HYv -VQrqER2Fs7jdCaeoGWDvwfK/UOV0b1ROnf+T/2bXFs8EOeqjOz4xG2oexNKVrYj9 -ICYAgmSh6Hf2cZJM/YCAISje93Xl2J2w/N7oFC1ZXasPoBIZv3Fgg7hTtQIDAQAB -o4ID2DCCA9QwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5l -cmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFEynWiCoZK4tLDk3KM1wMsbrz9Ug -MB8GA1UdIwQYMBaAFND2ZsvHIjITekPKs0ywLfoNEen5MDMGA1UdHwQsMCowKKAm -oCSGImh0dHA6Ly9sb2NhbGhvc3Q6NTI4MC9kYXRhL2NybC5kZXIwNgYIKwYBBQUH -AQEEKjAoMCYGCCsGAQUFBzABhhpodHRwOi8vbG9jYWxob3N0OjUyODAvb2NzcDAL -BgNVHQ8EBAMCBeAwEwYDVR0lBAwwCgYIKwYBBQUHAwkwggLIBgNVHREEggK/MIIC -u6A4BggrBgEFBQcIBaAsDCp0ZXN0X3NpbmdsZSEjJCVeKigpYH4rLTtfPVtde318 -XEBsb2NhbGhvc3SgPwYIKwYBBQUHCAWgMwwxdGVzdF9zaW5nbGUhIyQlXiooKWB+ -Ky07Xz1bXXt9fFxAbW5lc2lhLmxvY2FsaG9zdKA+BggrBgEFBQcIBaAyDDB0ZXN0 -X3NpbmdsZSEjJCVeKigpYH4rLTtfPVtde318XEBteXNxbC5sb2NhbGhvc3SgPgYI -KwYBBQUHCAWgMgwwdGVzdF9zaW5nbGUhIyQlXiooKWB+Ky07Xz1bXXt9fFxAcGdz -cWwubG9jYWxob3N0oD8GCCsGAQUFBwgFoDMMMXRlc3Rfc2luZ2xlISMkJV4qKClg -fistO189W117fXxcQHNxbGl0ZS5sb2NhbGhvc3SgQAYIKwYBBQUHCAWgNAwydGVz -dF9zaW5nbGUhIyQlXiooKWB+Ky07Xz1bXXt9fFxAZXh0YXV0aC5sb2NhbGhvc3Sg -PQYIKwYBBQUHCAWgMQwvdGVzdF9zaW5nbGUhIyQlXiooKWB+Ky07Xz1bXXt9fFxA -bGRhcC5sb2NhbGhvc3SgPQYIKwYBBQUHCAWgMQwvdGVzdF9zaW5nbGUhIyQlXioo -KWB+Ky07Xz1bXXt9fFxAcDFkYi5sb2NhbGhvc3SgPQYIKwYBBQUHCAWgMQwvdGVz -dF9zaW5nbGUhIyQlXiooKWB+Ky07Xz1bXXt9fFxAcmlhay5sb2NhbGhvc3SgPgYI -KwYBBQUHCAWgMgwwdGVzdF9zaW5nbGUhIyQlXiooKWB+Ky07Xz1bXXt9fFxAcmVk -aXMubG9jYWxob3N0oD4GCCsGAQUFBwgFoDIMMHRlc3Rfc2luZ2xlISMkJV4qKClg -fistO189W117fXxcQG1zc3FsLmxvY2FsaG9zdDANBgkqhkiG9w0BAQUFAAOCAQEA -et4jpmpwlE+2bw+/iqCt7sfU/5nPmQ8YtgMB+32wf7DINNJgkwOdkYJpzhlMXKrh -/bn8+Ybmq6MbK0r2R91Uu858xQf8VKExQm44qaGSyL5Ug3jsAWb3GLZSaWQo37e9 -QdDeP8XijCEyr3rum19tRIdiImsRAxJqwfaE4pUSgfCEQMkvb+6//8HSf9RRPToD -o6eAg8QerEtTfxerEdW/0K1ozOrzSrQembWOu+JjvANRl+p59j+1YOWHzS/yQeZl -K3sjFoCvXPvocRnUznvT+TSdy3ORJSjwfEcP5Crim70amZZ6NeMAxfby9wwmmX0x -zkwPCSUXliXke6T88Olj7Q== +ZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnK +Fx0p+eJoNegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopE +a/2sDmwxvUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbu +lPFePw+fWM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJ +tdlqwme2AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6Ct +AvqzKtNNJzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABo4IBcjCCAW4w +CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFFvDi47v5xJKOsgQo8MP4JzY6cC/MB8GA1UdIwQY +MBaAFAZTcBkYXxqFUSel8cHkRMBbm4gHMDMGA1UdHwQsMCowKKAmoCSGImh0dHA6 +Ly9sb2NhbGhvc3Q6NTI4MC9kYXRhL2NybC5kZXIwNgYIKwYBBQUHAQEEKjAoMCYG +CCsGAQUFBzABhhpodHRwOi8vbG9jYWxob3N0OjUyODAvb2NzcDALBgNVHQ8EBAMC +BeAwJwYDVR0lBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMBBggrBgEFBQcDAjBQBgNV +HREESTBHggsqLmxvY2FsaG9zdKA4BggrBgEFBQcIBaAsDCp0ZXN0X3NpbmdsZSEj +JCVeKigpYH4rLTtfPVtde318XEBsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEB +AEW8qvdyBMOSjCwJ1G178xsxf8Adw/9QN2ftBGKCo1C3YtmP5CvipChq5FTrOvRz +XjoQxbKhlqEumkZQkfmLiM/DLbkFeNqGWpuy14lkyIPUknaLKNCJX++pXsJrPLGR +btWnlB0cb+pLIB/UkG8OIpW07pNOZxHdHoHInRMMs89kgsmhIpn5OamzPWK/bqTB +YjAPIdmdkYk9oxWfgjpJ4BG2PbGS6CnjA29j7vebuQ4ebVpFBMI9w77PY3NcuMK7 +ML6MV6ez/+nPpz+E4zRxsVxmVAbSaiFDW3G3efAybDeT5QW1x/oJm2SpsJNIGHcp +RecYNo9esOTG+Bg6wypg4WA= -----END CERTIFICATE----- -----BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC+GTA1D1+yiXgLqUhJXkSj3hj5FiqlBAfJT/8OSXYifY4M4HYv -VQrqER2Fs7jdCaeoGWDvwfK/UOV0b1ROnf+T/2bXFs8EOeqjOz4xG2oexNKVrYj9 -ICYAgmSh6Hf2cZJM/YCAISje93Xl2J2w/N7oFC1ZXasPoBIZv3Fgg7hTtQIDAQAB -AoGALddtJJ58eVVlOYqs/+RXsRyR8R9DUV/TcNx1qUBV2KNmafyHA4sCgsd10xQv -9D2rzIGyOp8OpswfSSC/t+WqB9+ezSruzMuX6IURdHZbX6aWWX6maICtPKEEkCmI -gaLxE/ojuOXnTEBTkVuVWtuFL9PsK/WGi/FIDzJbwqTWJ4ECQQDy9DrBAQM96B6u -G4XpFzBsfgJZoS+NaMdCwK+/jgcEpI6oxobK8tuGB6drp5jNSuQ905W9n8XjA6Xq -x8/GH9I5AkEAyE5g05HhMlxBWCq+P70pBDIamdHJcPQVL8+6NXkT+mTqqZxxkUy4 -nMfTh5zE6WfmqYNtrmNBDxXUyaoRSBydXQJACnFnCR7DBekxUGiMc/10LmWoMjQU -eC6Vyg/APiqbsJ5mJ2kJKDYSK4uurZjxn3lloCa1HAZ/GgfxHMtj6e86OQJAetq3 -wIwE12KGIZF1xpo6gfxJHHbzWngaVozN5OYyPq2O0CDH9xpbUK2vK8oXbCDx9J5L -s13lFV+Kd3X7y4LhcQJBAKSFg7ht33l8Sa0TdUkY6Tl1NBMCCLf+np+HYrAbQZux -2NtR6nj2YqeOpEe1ibWZm8tj3dzlTm1FCOIpa+pm114= +MIIEpgIBAAKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnKFx0p+eJo +NegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopEa/2sDmwx +vUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbulPFePw+f +WM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJtdlqwme2 +AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6CtAvqzKtNN +JzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABAoIBAQDUwGX1cHsJ5C2f +9ndwtsfJlHVZs0vPysR9CVpE0Q4TWoNVJ+0++abRB/vI4lHotHL90xZEmJXfGj1k +YZf2QHWQBI7Qj7Yg1Qdr0yUbz/IIQLCyJTA3jvEzBvc/VByveBQi9Aw0zOopqc1x +ZC1RT8bcMumEN11q8mVV/O4oXZAl+mQIbRRt6JIsRtoW8hpB1e2ipHItDMNpSnzA +6PqcddDyDDePgi5lMOaeV9un60A6pI/+uvmw16R1Io+DyYRnxds3HJ/ccI0Co1P1 +khA75QLdnoniYO+oQrq/wGvm+Uq1seh6iuj+SOWvCdB03vPmGYxPKMSW9AtX8xbJ +J9lboi3pAoGBAPBaiUYn9F+Zt9oJTHhAimZgs1ub5xVEFwVhYJtFBT3E1rQWRKuf +kiU1JRq7TB3MGaC4zGi2ql12KV3AqFhwLKG6sKtlo/IJhJfe3DgWmBVYBBifkgYs +mxmA6opgyjbjDEMn6RA+Jov5H267AsnaB4cCB1Jjra6GIdIoMvPghHZXAoGBAOR6 +7VC6E+YX5VJPCZiN0h0aBT+Hl4drYQKvZHp5N8RIBkvmcQHEJgsrUKdirFZEXW6y +WvepwI4C/Xl61y64/DZ7rum/gpAEPdzSkefKysHAiqkMRcIpjiRxTPJ547ZJycjP +E+jzcYfLwQvCW9ZiYl+KdYRbpqBFQC8aWqixFxRbAoGBAJQTsy79vpiHY7V4tRwA +50NboCR4UE3RvT0bWSFPzILZmk0oyvXRQYCa1Vk6uxJAhCl4sLZyk1MxURrpbs3N +jjG1itKNtAuRwZavPo1vnhLIPv3MkXIsWQHFYroOF4bpKszU8cmIAMeLm8nkfTtO +kASlQ02HC6HSEVQgYAPP9svRAoGBANiOnwKl7Bhpy8TQ/zJmMaG9uP23IeuL3l4y +KdVfsXjMH5OvLqtS5BAwFPkiMGBv2fMC/+/AKK8xrFiJEw3I7d0iK+6Hw1OHga8c +soh1kOpF+ecyp6fZxU1LSniFCU0M8UHw7Fke7RueBzKDHJK9m6oczTgPuoYsPSKo +IwfDGjIDAoGBAMJVkInntV8oDPT1WYpOAZ3Z0myCDZVBbjxx8kE4RSJIsFeNSiTO +nhLWCqoG11PVTUzhpYItCjp4At/dG8OQY7WWm0DJJQB38fEqA6JKWpgeWwUdkk8j +anCrNUBEuzt3UPSZ17DGCw2+J+mwsg1nevaFIXy0gN2zPtTBWtacznPL -----END RSA PRIVATE KEY----- diff --git a/test/ejabberd_SUITE_data/ejabberd.extauth.yml b/test/ejabberd_SUITE_data/ejabberd.extauth.yml new file mode 100644 index 000000000..660ddccd6 --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.extauth.yml @@ -0,0 +1,5 @@ +define_macro: + EXTAUTH_CONFIG: + queue_type: ram + extauth_program: "python extauth.py" + auth_method: external diff --git a/test/ejabberd_SUITE_data/ejabberd.ldap.yml b/test/ejabberd_SUITE_data/ejabberd.ldap.yml new file mode 100644 index 000000000..a60d227da --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.ldap.yml @@ -0,0 +1,36 @@ +define_macro: + LDAP_CONFIG: + queue_type: ram + ldap_servers: + - "localhost" + ldap_rootdn: "cn=admin,dc=localhost" + ldap_port: 1389 + ldap_password: "password" + ldap_base: "ou=users,dc=localhost" + auth_method: ldap + modules: + mod_vcard: + db_type: ldap + mod_roster: [] # mod_roster is required by mod_shared_roster + mod_shared_roster_ldap: + ldap_auth_check: off + ldap_base: "dc=localhost" + ldap_rfilter: "(objectClass=posixGroup)" + ldap_gfilter: "(&(objectClass=posixGroup)(cn=%g))" + ldap_memberattr: "memberUid" + ldap_ufilter: "(uid=%u)" + ldap_userdesc: "cn" + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.mnesia.yml b/test/ejabberd_SUITE_data/ejabberd.mnesia.yml new file mode 100644 index 000000000..14bb2bff2 --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.mnesia.yml @@ -0,0 +1,65 @@ +define_macro: + MNESIA_CONFIG: + queue_type: ram + auth_method: internal + modules: + mod_announce: + db_type: internal + access: local + mod_blocking: [] + mod_caps: + db_type: internal + mod_last: + db_type: internal + mod_muc: + db_type: internal + vcard: VCARD + mod_offline: + db_type: internal + mod_privacy: + db_type: internal + mod_private: + db_type: internal + mod_pubsub: + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: internal + mod_mam: + db_type: internal + mod_vcard: + db_type: internal + vcard: VCARD + mod_vcard_xupdate: [] + mod_client_state: + queue_presence: true + queue_chat_states: true + queue_pep: true + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.mysql.yml b/test/ejabberd_SUITE_data/ejabberd.mysql.yml new file mode 100644 index 000000000..411901976 --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.mysql.yml @@ -0,0 +1,72 @@ +define_macro: + MYSQL_CONFIG: + sql_username: MYSQL_USER + sql_type: mysql + sql_server: MYSQL_SERVER + sql_port: MYSQL_PORT + sql_pool_size: 1 + sql_password: MYSQL_PASS + sql_database: MYSQL_DB + auth_method: sql + sm_db_type: sql + modules: + mod_announce: + db_type: sql + access: local + mod_blocking: [] + mod_caps: + db_type: sql + mod_last: + db_type: sql + mod_muc: + db_type: sql + ram_db_type: sql + vcard: VCARD + mod_offline: + use_cache: true + db_type: sql + mod_privacy: + db_type: sql + mod_private: + db_type: sql + mod_pubsub: + db_type: sql + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: sql + mod_mam: + db_type: sql + mod_vcard: + db_type: sql + vcard: VCARD + mod_vcard_xupdate: [] + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + db_type: sql + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.pgsql.yml b/test/ejabberd_SUITE_data/ejabberd.pgsql.yml new file mode 100644 index 000000000..c0cd0b0d6 --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.pgsql.yml @@ -0,0 +1,72 @@ +define_macro: + PGSQL_CONFIG: + sql_username: PGSQL_USER + sql_type: pgsql + sql_server: PGSQL_SERVER + sql_port: PGSQL_PORT + sql_pool_size: 1 + sql_password: PGSQL_PASS + sql_database: PGSQL_DB + auth_method: sql + sm_db_type: sql + modules: + mod_announce: + db_type: sql + access: local + mod_blocking: [] + mod_caps: + db_type: sql + mod_last: + db_type: sql + mod_muc: + db_type: sql + ram_db_type: sql + vcard: VCARD + mod_offline: + use_cache: true + db_type: sql + mod_privacy: + db_type: sql + mod_private: + db_type: sql + mod_pubsub: + db_type: sql + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: sql + mod_mam: + db_type: sql + mod_vcard: + db_type: sql + vcard: VCARD + mod_vcard_xupdate: [] + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + db_type: sql + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.redis.yml b/test/ejabberd_SUITE_data/ejabberd.redis.yml new file mode 100644 index 000000000..7065f0ffd --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.redis.yml @@ -0,0 +1,66 @@ +define_macro: + REDIS_CONFIG: + queue_type: ram + auth_method: internal + sm_db_type: redis + modules: + mod_announce: + db_type: internal + access: local + mod_blocking: [] + mod_caps: + db_type: internal + mod_last: + db_type: internal + mod_muc: + db_type: internal + vcard: VCARD + mod_offline: + db_type: internal + mod_privacy: + db_type: internal + mod_private: + db_type: internal + mod_pubsub: + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: internal + mod_mam: + db_type: internal + mod_vcard: + db_type: internal + vcard: VCARD + mod_vcard_xupdate: [] + mod_client_state: + queue_presence: true + queue_chat_states: true + queue_pep: true + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.sqlite.yml b/test/ejabberd_SUITE_data/ejabberd.sqlite.yml new file mode 100644 index 000000000..3e22f6a2d --- /dev/null +++ b/test/ejabberd_SUITE_data/ejabberd.sqlite.yml @@ -0,0 +1,66 @@ +define_macro: + SQLITE_CONFIG: + sql_type: sqlite + sql_pool_size: 1 + auth_method: sql + sm_db_type: sql + modules: + mod_announce: + db_type: sql + access: local + mod_blocking: [] + mod_caps: + db_type: sql + mod_last: + db_type: sql + mod_muc: + db_type: sql + ram_db_type: sql + vcard: VCARD + mod_offline: + db_type: sql + mod_privacy: + db_type: sql + mod_private: + db_type: sql + mod_pubsub: + db_type: sql + access_createnode: pubsub_createnode + ignore_pep_from_offline: true + last_item_cache: false + plugins: + - "flat" + - "pep" + vcard: VCARD + mod_roster: + versioning: true + store_current_id: true + db_type: sql + mod_mam: + db_type: sql + mod_vcard: + db_type: sql + vcard: VCARD + mod_vcard_xupdate: [] + mod_adhoc: [] + mod_configure: [] + mod_disco: [] + mod_ping: [] + mod_proxy65: + port: PROXY_PORT + mod_push: + db_type: sql + include_body: false + mod_push_keepalive: [] + mod_s2s_dialback: [] + mod_stream_mgmt: + resume_timeout: 3 + mod_legacy_auth: [] + mod_register: + welcome_message: + subject: "Welcome!" + body: "Hi. +Welcome to this XMPP server." + mod_stats: [] + mod_time: [] + mod_version: [] diff --git a/test/ejabberd_SUITE_data/ejabberd.yml b/test/ejabberd_SUITE_data/ejabberd.yml index aca547d99..cea93fe90 100644 --- a/test/ejabberd_SUITE_data/ejabberd.yml +++ b/test/ejabberd_SUITE_data/ejabberd.yml @@ -1,449 +1,149 @@ -host_config: - "pgsql.localhost": - sql_username: "@@pgsql_user@@" - sql_type: pgsql - sql_server: "@@pgsql_server@@" - sql_port: @@pgsql_port@@ - sql_pool_size: 1 - sql_password: "@@pgsql_pass@@" - sql_database: "@@pgsql_db@@" - auth_method: sql - sm_db_type: sql - modules: - mod_announce: - db_type: sql - access: local - mod_blocking: [] - mod_caps: - db_type: sql - mod_last: - db_type: sql - mod_muc: - db_type: sql - mod_offline: - db_type: sql - mod_privacy: - db_type: sql - mod_private: - db_type: sql - mod_pubsub: - db_type: sql - access_createnode: pubsub_createnode - ignore_pep_from_offline: true - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" - mod_mix: [] - mod_roster: - versioning: true - store_current_id: true - db_type: sql - mod_mam: - db_type: sql - mod_vcard: - db_type: sql - mod_vcard_xupdate: - db_type: sql - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "sqlite.localhost": - sql_type: sqlite - auth_method: sql - sm_db_type: sql - modules: - mod_announce: - db_type: sql - access: local - mod_blocking: [] - mod_caps: - db_type: sql - mod_last: - db_type: sql - mod_muc: - db_type: sql - mod_offline: - db_type: sql - mod_privacy: - db_type: sql - mod_private: - db_type: sql - mod_pubsub: - db_type: sql - access_createnode: pubsub_createnode - ignore_pep_from_offline: true - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" - mod_mix: [] - mod_roster: - versioning: true - store_current_id: true - db_type: sql - mod_mam: - db_type: sql - mod_vcard: - db_type: sql - mod_vcard_xupdate: - db_type: sql - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "mysql.localhost": - sql_username: "@@mysql_user@@" - sql_type: mysql - sql_server: "@@mysql_server@@" - sql_port: @@mysql_port@@ - sql_pool_size: 1 - sql_password: "@@mysql_pass@@" - sql_database: "@@mysql_db@@" - auth_method: sql - sm_db_type: sql - modules: - mod_announce: - db_type: sql - access: local - mod_blocking: [] - mod_caps: - db_type: sql - mod_last: - db_type: sql - mod_muc: - db_type: sql - mod_offline: - db_type: sql - mod_privacy: - db_type: sql - mod_private: - db_type: sql - mod_pubsub: - db_type: sql - access_createnode: pubsub_createnode - ignore_pep_from_offline: true - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" - mod_mix: [] - mod_roster: - versioning: true - store_current_id: true - db_type: sql - mod_mam: - db_type: sql - mod_vcard: - db_type: sql - mod_vcard_xupdate: - db_type: sql - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "mnesia.localhost": - auth_method: internal - modules: - mod_announce: - db_type: internal - access: local - mod_blocking: [] - mod_caps: - db_type: internal - mod_last: - db_type: internal - mod_muc: - db_type: internal - mod_offline: - db_type: internal - mod_privacy: - db_type: internal - mod_private: - db_type: internal - mod_pubsub: - access_createnode: pubsub_createnode - ignore_pep_from_offline: true - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" - mod_mix: [] - mod_roster: - versioning: true - store_current_id: true - db_type: internal - mod_mam: - db_type: internal - mod_vcard: - db_type: internal - mod_vcard_xupdate: - db_type: internal - mod_carboncopy: [] - mod_client_state: - queue_presence: true - queue_chat_states: true - queue_pep: true - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "redis.localhost": - auth_method: internal - sm_db_type: redis - modules: - mod_announce: - db_type: internal - access: local - mod_blocking: [] - mod_caps: - db_type: internal - mod_last: - db_type: internal - mod_muc: - db_type: internal - mod_offline: - db_type: internal - mod_privacy: - db_type: internal - mod_private: - db_type: internal - mod_pubsub: - access_createnode: pubsub_createnode - ignore_pep_from_offline: true - last_item_cache: false - plugins: - - "flat" - - "hometree" - - "pep" - mod_mix: [] - mod_roster: - versioning: true - store_current_id: true - db_type: internal - mod_mam: - db_type: internal - mod_vcard: - db_type: internal - mod_vcard_xupdate: - db_type: internal - mod_carboncopy: [] - mod_client_state: - queue_presence: true - queue_chat_states: true - queue_pep: true - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "riak.localhost": - auth_method: riak - modules: - mod_announce: - db_type: riak - access: local - mod_blocking: [] - mod_caps: - db_type: riak - mod_last: - db_type: riak - mod_muc: - db_type: riak - mod_offline: - db_type: riak - mod_privacy: - db_type: riak - mod_private: - db_type: riak - mod_roster: - versioning: true - store_current_id: true - db_type: riak - mod_vcard: - db_type: riak - mod_vcard_xupdate: - db_type: riak - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "localhost": - auth_method: internal - "ldap.localhost": - ldap_servers: - - "localhost" - ldap_rootdn: "cn=admin,dc=localhost" - ldap_port: 1389 - ldap_password: "password" - ldap_base: "ou=users,dc=localhost" - auth_method: ldap - modules: - mod_vcard_ldap: [] - mod_roster: [] # mod_roster is required by mod_shared_roster - mod_shared_roster_ldap: - ldap_auth_check: off - ldap_base: "dc=localhost" - ldap_rfilter: "(objectClass=posixGroup)" - ldap_gfilter: "(&(objectClass=posixGroup)(cn=%g))" - ldap_memberattr: "memberUid" - ldap_ufilter: "(uid=%u)" - ldap_userdesc: "cn" - mod_adhoc: [] - mod_configure: [] - mod_disco: [] - mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: - subject: "Welcome!" - body: "Hi. -Welcome to this XMPP server." - mod_stats: [] - mod_time: [] - mod_version: [] - "extauth.localhost": - extauth_program: "python extauth.py" - auth_method: external -hosts: - - "localhost" - - "mnesia.localhost" - - "redis.localhost" - - "mysql.localhost" - - "pgsql.localhost" - - "extauth.localhost" - - "ldap.localhost" - - "riak.localhost" - - "sqlite.localhost" -access: - announce: - admin: allow - c2s: - blocked: deny - all: allow - c2s_shaper: - admin: none - all: normal - configure: - admin: allow - local: - local: allow - max_user_offline_messages: - admin: 5000 - all: 100 - max_user_sessions: - all: 10 - muc: - all: allow - muc_admin: - admin: allow - muc_create: - local: allow - pubsub_createnode: - local: allow - register: - all: allow - s2s_shaper: - all: fast -acl: - local: +include_config_file: + - macros.yml + - ejabberd.extauth.yml + - ejabberd.ldap.yml + - ejabberd.mnesia.yml + - ejabberd.mysql.yml + - ejabberd.pgsql.yml + - ejabberd.redis.yml + - ejabberd.sqlite.yml + +host_config: + pgsql.localhost: PGSQL_CONFIG + sqlite.localhost: SQLITE_CONFIG + mysql.localhost: MYSQL_CONFIG + mnesia.localhost: MNESIA_CONFIG + redis.localhost: REDIS_CONFIG + ldap.localhost: LDAP_CONFIG + extauth.localhost: EXTAUTH_CONFIG + localhost: + auth_method: + - internal + - anonymous + +hosts: + - localhost + - mnesia.localhost + - redis.localhost + - mysql.localhost + - pgsql.localhost + - extauth.localhost + - ldap.localhost + - sqlite.localhost + +shaper_rules: + c2s_shaper: + none: admin + normal: all + max_user_offline_messages: + infinity: all + max_user_sessions: + 10: all + s2s_shaper: + fast: all + +access_rules: + announce: + allow: admin + c2s: + deny: blocked + allow: all + configure: + allow: admin + local: + allow: local + muc: + allow: all + muc_admin: + allow: admin + muc_create: + allow: local + pubsub_createnode: + allow: local + register: + allow: all + +acl: + local: user_regexp: "" -define_macro: - CERTFILE: "cert.pem" -language: "en" -listen: - - - port: @@c2s_port@@ +language: en +listen: + - + port: C2S_PORT module: ejabberd_c2s max_stanza_size: 65536 - certfile: CERTFILE zlib: true starttls: true + tls_verify: true shaper: c2s_shaper access: c2s - resume_timeout: 3 - - - port: @@s2s_port@@ + - + port: S2S_PORT module: ejabberd_s2s_in - - - port: @@web_port@@ + - + port: WEB_PORT module: ejabberd_http - captcha: true -loglevel: @@loglevel@@ + request_handlers: + "/api": mod_http_api + "/upload": mod_http_upload + "/captcha": ejabberd_captcha + - + port: COMPONENT_PORT + module: ejabberd_service + password: PASSWORD +loglevel: LOGLEVEL max_fsm_queue: 1000 -modules: +queue_type: file +modules: mod_adhoc: [] + mod_announce: [] mod_configure: [] mod_disco: [] mod_ping: [] - mod_proxy65: [] - mod_register: - welcome_message: + mod_proxy65: + port: PROXY_PORT + vcard: VCARD + mod_muc: + vcard: VCARD + mod_muc_admin: [] + mod_carboncopy: [] + mod_jidprep: [] + mod_mam: [] + mod_last: [] + mod_register: + welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] + mod_s2s_dialback: [] + mod_legacy_auth: [] + mod_stream_mgmt: + max_ack_queue: 10 + resume_timeout: 3 mod_time: [] mod_version: [] + mod_http_upload: + docroot: PRIV_DIR + put_url: PUT_URL + get_url: GET_URL + max_size: 10000 + vcard: VCARD registration_timeout: infinity -shaper: +route_subdomains: s2s +s2s_use_starttls: false +ca_file: CAFILE +c2s_cafile: CAFILE +outgoing_s2s_port: S2S_PORT +shaper: fast: 50000 - normal: 1000 + normal: 10000 +certfiles: + - CERTFILE + +new_sql_schema: NEW_SCHEMA + +api_permissions: + "public commands": + who: all + what: "*" diff --git a/test/ejabberd_SUITE_data/extauth.py b/test/ejabberd_SUITE_data/extauth.py index 84c000144..b6a217fcc 100755 --- a/test/ejabberd_SUITE_data/extauth.py +++ b/test/ejabberd_SUITE_data/extauth.py @@ -3,20 +3,31 @@ import struct def read(): (pkt_size,) = struct.unpack('>H', sys.stdin.read(2)) - pkt = sys.stdin.read(pkt_size).split(':') - cmd = pkt[0] - args_num = len(pkt) - 1 - if cmd == 'auth' and args_num >= 3: + pkt = sys.stdin.read(pkt_size) + cmd = pkt.split(':')[0] + if cmd == 'auth': + u, s, p = pkt.split(':', 3)[1:] + if u == "wrong": + write(False) + else: + write(True) + elif cmd == 'isuser': + u, s = pkt.split(':', 2)[1:] + if u == "wrong": + write(False) + else: + write(True) + elif cmd == 'setpass': + u, s, p = pkt.split(':', 3)[1:] write(True) - elif cmd == 'isuser' and args_num == 2: + elif cmd == 'tryregister': + u, s, p = pkt.split(':', 3)[1:] write(True) - elif cmd == 'setpass' and args_num >= 3: + elif cmd == 'removeuser': + u, s = pkt.split(':', 2)[1:] write(True) - elif cmd == 'tryregister' and args_num >= 3: - write(True) - elif cmd == 'removeuser' and args_num == 2: - write(True) - elif cmd == 'removeuser3' and args_num >= 3: + elif cmd == 'removeuser3': + u, s, p = pkt.split(':', 3)[1:] write(True) else: write(False) diff --git a/test/ejabberd_SUITE_data/gencerts.sh b/test/ejabberd_SUITE_data/gencerts.sh new file mode 100755 index 000000000..6975fe422 --- /dev/null +++ b/test/ejabberd_SUITE_data/gencerts.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Update openssl.cnf if needed (in particular section [alt_names]) + +rm -rf ssl +mkdir -p ssl/newcerts +touch ssl/index.txt +echo 01 > ssl/serial +echo 1000 > ssl/crlnumber +openssl genrsa -out ca.key 2048 +openssl req -new -days 10000 -x509 -key ca.key -out ca.pem -batch +openssl genrsa -out ssl/client.key +openssl req -new -key ssl/client.key -out ssl/client.csr -config openssl.cnf -batch -subj /C=AU/ST=Some-State/O=Internet\ Widgits\ Pty\ Ltd/CN=localhost +openssl ca -keyfile ca.key -cert ca.pem -in ssl/client.csr -out ssl/client.crt -config openssl.cnf -days 10000 -batch -notext -policy policy_anything +openssl req -new -key ssl/client.key -out ssl/self-signed-client.csr -batch -subj /C=AU/ST=Some-State/O=Internet\ Widgits\ Pty\ Ltd/CN=localhost +openssl x509 -req -in ssl/self-signed-client.csr -signkey ssl/client.key -out ssl/self-signed-client.crt -days 10000 +cat ssl/client.crt > cert.pem +cat ssl/self-signed-client.crt > self-signed-cert.pem +cat ssl/client.key >> cert.pem +cat ssl/client.key >> self-signed-cert.pem +rm -rf ssl diff --git a/test/ejabberd_SUITE_data/macros.yml b/test/ejabberd_SUITE_data/macros.yml new file mode 100644 index 000000000..fdd467584 --- /dev/null +++ b/test/ejabberd_SUITE_data/macros.yml @@ -0,0 +1,128 @@ +define_macro: + CERTFILE: cert.pem + CAFILE: ca.pem + C2S_PORT: @@c2s_port@@ + S2S_PORT: @@s2s_port@@ + WEB_PORT: @@web_port@@ + COMPONENT_PORT: @@component_port@@ + PROXY_PORT: @@proxy_port@@ + PASSWORD: >- + @@password@@ + LOGLEVEL: @@loglevel@@ + PRIV_DIR: "@@priv_dir@@" + PUT_URL: "http://upload.@HOST@:@@web_port@@/upload" + GET_URL: "http://upload.@HOST@:@@web_port@@/upload" + NEW_SCHEMA: @@new_schema@@ + MYSQL_USER: "@@mysql_user@@" + MYSQL_SERVER: "@@mysql_server@@" + MYSQL_PORT: @@mysql_port@@ + MYSQL_PASS: "@@mysql_pass@@" + MYSQL_DB: "@@mysql_db@@" + PGSQL_USER: "@@pgsql_user@@" + PGSQL_SERVER: "@@pgsql_server@@" + PGSQL_PORT: @@pgsql_port@@ + PGSQL_PASS: "@@pgsql_pass@@" + PGSQL_DB: "@@pgsql_db@@" + VCARD: + version: "1.0" + fn: Full Name + n: + family: Family + given: Given + middle: Middle + prefix: Prefix + suffix: Suffix + nickname: Nickname + photo: + type: image/png + extval: https://domain.tld/photo.png + binval: >- + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA + AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== + bday: 2000-01-01 + adr: + - + home: true + work: true + postal: true + parcel: true + dom: true + intl: true + pref: true + pobox: Pobox + extadd: Extadd + street: Street + locality: Locality + region: Region + pcode: Pcode + ctry: Ctry + label: + - + home: true + work: true + postal: true + parcel: true + dom: true + intl: true + pref: true + line: + - Line1 + - Line2 + tel: + - + home: true + work: true + voice: true + fax: true + pager: true + msg: true + cell: true + video: true + bbs: true + modem: true + isdn: true + pcs: true + pref: true + number: +7-900-01-02 + email: + - + home: true + work: true + internet: true + pref: true + x400: true + userid: user@domain.tld + jabberid: user@domain.tld + mailer: Mailer + tz: TZ + geo: + lat: "12.0" + lon: "21.0" + title: Title + role: Role + logo: + type: image/png + extval: https://domain.tld/logo.png + binval: >- + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA + AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== + categories: + - Cat1 + - Cat2 + note: Note + prodid: ProdID + rev: Rev + sort_string: SortString + sound: + phonetic: Phonetic + extval: https://domain.tld/sound.ogg + binval: >- + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA + AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== + uid: UID + url: https://domain.tld + class: public + key: + type: Type + cred: Cred + desc: Desc diff --git a/test/ejabberd_SUITE_data/openssl.cnf b/test/ejabberd_SUITE_data/openssl.cnf new file mode 100644 index 000000000..594653b79 --- /dev/null +++ b/test/ejabberd_SUITE_data/openssl.cnf @@ -0,0 +1,322 @@ +# +# OpenSSL example configuration file. +# This is mostly being used for generation of certificate requests. +# + +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = . +RANDFILE = $ENV::HOME/.rnd + +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +extensions = v3_req +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] +# We can add new OIDs in here for use by 'ca' and 'req'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_default ] + +#dir = ./demoCA # Where everything is kept +dir = ssl +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +#unique_subject = no # Set to 'no' to allow creation of + # several ctificates with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = $dir/cacert.pem # The CA certificate +serial = $dir/serial # The current serial number +crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = $dir/private/cakey.pem# The private key +RANDFILE = $dir/private/.rand # private random number file + +x509_extensions = usr_cert # The extentions to add to the cert + +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options + +# Extension copying option: use with caution. +copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 365 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = sha256 # which md to use. +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match + +# For the CA policy +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = optional +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = optional +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 1024 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extentions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString. +# utf8only: only UTF8Strings. +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings +# so use this option with caution! +string_mask = nombstr + +req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = AU +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Some-State + +localityName = Locality Name (eg, city) + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Internet Widgits Pty Ltd + +# we can do this but it is not needed normally :-) +#1.organizationName = Second Organization Name (eg, company) +#1.organizationName_default = World Wide Web Pty Ltd + +organizationalUnitName = Organizational Unit Name (eg, section) +#organizationalUnitName_default = + +commonName = Common Name (eg, YOUR name) +commonName_max = 64 + +emailAddress = Email Address +emailAddress_max = 64 + +# SET-ex3 = SET extension number 3 + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +crlDistributionPoints = URI:http://localhost:5280/data/crl.der +authorityInfoAccess = OCSP;URI:http://localhost:5280/ocsp + +[ v3_req ] + +# Extensions to add to a certificate request + +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = OCSPSigning,serverAuth,clientAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = *.localhost +otherName.1 = 1.3.6.1.5.5.7.8.5;UTF8:"test_single!#$%^*()`~+-;_=[]{}|\\@localhost" + +[ v3_ca ] +crlDistributionPoints = URI:http://localhost:5280/data/crl.der + +# Extensions for a typical CA + + +# PKIX recommendation. + +subjectKeyIdentifier=hash + +authorityKeyIdentifier=keyid:always,issuer:always + +# This is what PKIX recommends but some broken software chokes on critical +# extensions. +#basicConstraints = critical,CA:true +# So we do this instead. +basicConstraints = CA:true + +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign + +# Some might want this also +# nsCertType = sslCA, emailCA + +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy + +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always,issuer:always + +[ proxy_cert_ext ] +# These extensions should be added when creating a proxy certificate + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer:always + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This really needs to be in place for it to be a proxy certificate. +proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo diff --git a/test/ejabberd_SUITE_data/self-signed-cert.pem b/test/ejabberd_SUITE_data/self-signed-cert.pem new file mode 100644 index 000000000..29fc38d36 --- /dev/null +++ b/test/ejabberd_SUITE_data/self-signed-cert.pem @@ -0,0 +1,47 @@ +-----BEGIN CERTIFICATE----- +MIIDOTCCAiECFHMoNo36Xx0BWkzS8nwvCPGnHnHRMA0GCSqGSIb3DQEBCwUAMFkx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xODA5 +MjQxMzE4MjRaFw00NjAyMDkxMzE4MjRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQI +DApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANaEDDeDGf8BH+EjO3IcB2fspkOSp7eOVWdI5oKZyhcdKfniaDXoL78GP/ND +Vk5nIGxp6q7iYjoeQBFDQ7Qg+Rv+9KM9lh4GQWZLi7KKRGv9rA5sMb1G79X/5g/I +h3A2llLygMuE1BxXhw0C9vByaJvRO24GGnXroXm8GXLG7pTxXj8Pn1jO4y1sZDGA +pX7Hc7Aa4Hq22VT5wLo++3Bl2UkOqfeozj4if5ozlQsFibXZasJntgAuAMCmHVs3 +N2LMPJREv7mzGvpT9RIfWiPHnaRyJQuZ2DS1U1muF8OgrQL6syrTTSc8MqW0d33A +12lr7ztxmN8Dh1Qv8MgrC/El3O0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAhM+Q +qt4IlM1SMb74L5GO2JKGVSUbZmaFJEZcjlrcHkw+Tfc5SMxaj7JpTPg7OGNY1L/3 +HnUDdaDRZ5xVOxUF7gTBWDAgkO7En5YfvvEYXUYUk7wwpFrqUqQpluqQIxr+Zf6l +pZFLhKIANa4wayKtZ9v4uBtRjnm9Hj7gQHeWN9sueIq7d4HO5lubYlzu1+6qeP+L +M0ciNhsUPypCwVcLPB+1Eo925QBwAhXsvPD9yKFQg1M7XxcJSy0w3DwWQsTTsEbk +8c/vIF/IhkOJHQDTKa+VSJM+hZgmx/PsyVdbWRSCAusiZpjHKhzzTCNEloGp/Vbm +5y/OeAK2TGPTg9I91w== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnKFx0p+eJo +NegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopEa/2sDmwx +vUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbulPFePw+f +WM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJtdlqwme2 +AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6CtAvqzKtNN +JzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABAoIBAQDUwGX1cHsJ5C2f +9ndwtsfJlHVZs0vPysR9CVpE0Q4TWoNVJ+0++abRB/vI4lHotHL90xZEmJXfGj1k +YZf2QHWQBI7Qj7Yg1Qdr0yUbz/IIQLCyJTA3jvEzBvc/VByveBQi9Aw0zOopqc1x +ZC1RT8bcMumEN11q8mVV/O4oXZAl+mQIbRRt6JIsRtoW8hpB1e2ipHItDMNpSnzA +6PqcddDyDDePgi5lMOaeV9un60A6pI/+uvmw16R1Io+DyYRnxds3HJ/ccI0Co1P1 +khA75QLdnoniYO+oQrq/wGvm+Uq1seh6iuj+SOWvCdB03vPmGYxPKMSW9AtX8xbJ +J9lboi3pAoGBAPBaiUYn9F+Zt9oJTHhAimZgs1ub5xVEFwVhYJtFBT3E1rQWRKuf +kiU1JRq7TB3MGaC4zGi2ql12KV3AqFhwLKG6sKtlo/IJhJfe3DgWmBVYBBifkgYs +mxmA6opgyjbjDEMn6RA+Jov5H267AsnaB4cCB1Jjra6GIdIoMvPghHZXAoGBAOR6 +7VC6E+YX5VJPCZiN0h0aBT+Hl4drYQKvZHp5N8RIBkvmcQHEJgsrUKdirFZEXW6y +WvepwI4C/Xl61y64/DZ7rum/gpAEPdzSkefKysHAiqkMRcIpjiRxTPJ547ZJycjP +E+jzcYfLwQvCW9ZiYl+KdYRbpqBFQC8aWqixFxRbAoGBAJQTsy79vpiHY7V4tRwA +50NboCR4UE3RvT0bWSFPzILZmk0oyvXRQYCa1Vk6uxJAhCl4sLZyk1MxURrpbs3N +jjG1itKNtAuRwZavPo1vnhLIPv3MkXIsWQHFYroOF4bpKszU8cmIAMeLm8nkfTtO +kASlQ02HC6HSEVQgYAPP9svRAoGBANiOnwKl7Bhpy8TQ/zJmMaG9uP23IeuL3l4y +KdVfsXjMH5OvLqtS5BAwFPkiMGBv2fMC/+/AKK8xrFiJEw3I7d0iK+6Hw1OHga8c +soh1kOpF+ecyp6fZxU1LSniFCU0M8UHw7Fke7RueBzKDHJK9m6oczTgPuoYsPSKo +IwfDGjIDAoGBAMJVkInntV8oDPT1WYpOAZ3Z0myCDZVBbjxx8kE4RSJIsFeNSiTO +nhLWCqoG11PVTUzhpYItCjp4At/dG8OQY7WWm0DJJQB38fEqA6JKWpgeWwUdkk8j +anCrNUBEuzt3UPSZ17DGCw2+J+mwsg1nevaFIXy0gN2zPtTBWtacznPL +-----END RSA PRIVATE KEY----- diff --git a/test/ejabberd_admin_test.exs b/test/ejabberd_admin_test.exs deleted file mode 100644 index 1c999314c..000000000 --- a/test/ejabberd_admin_test.exs +++ /dev/null @@ -1,79 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 deleted file mode 100644 index 83019c8ee..000000000 --- a/test/ejabberd_auth_mock.exs +++ /dev/null @@ -1,74 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 deleted file mode 100644 index 9d33d7573..000000000 --- a/test/ejabberd_commands_mock_test.exs +++ /dev/null @@ -1,469 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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. -# -# ---------------------------------------------------------------------- - -## TODO Fix next test error: add admin user ACL - -defmodule EjabberdCommandsMockTest do - use ExUnit.Case, async: false - - require EjabberdOauthMock - - @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 - :ok = :jid.start - :ok = :ejabberd_config.start(["domain1", "domain2"], []) - :ok = :acl.start - EjabberdOauthMock.init - on_exit fn -> :meck.unload end - 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 - mock_commands_config - - # 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 - mock_commands_config - - 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 {:error, :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 [:user, :admin] - - # 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 [:admin] - - # 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 - - test "Commands can perform extra check on access" do - mock_commands_config [:admin, :open] - - command_name = :test - function = :test_command - command = ejabberd_commands(name: command_name, - args: [{:user, :binary}, {:host, :binary}], - access: [:basic_rule_1], - module: @module, - function: function, - policy: :open) - :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] - -# :acl.add(:global, :basic_acl_1, {:user, @user, @host}) -# :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}]) - - assert {@user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@user, @domain, - @userpass, false}, - command_name, - [@user, @domain]) - assert {@user, @domain} == - :ejabberd_commands.execute_command(:undefined, - {@admin, @domain, - @adminpass, false}, - command_name, - [@user, @domain]) - - end - - ########################################################## - # Utils - - # Mock a config where only @admin user is allowed to call commands - # as admin - def mock_commands_config(commands \\ []) 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 - (:commands, _, _) -> [{:add_commands, commands}] - (_, _, default) -> default - end) - :meck.expect(:ejabberd_config, :get_myhosts, - fn() -> [@domain] end) - - :meck.new :acl - :meck.expect(:acl, :access_matches, - fn(:commands_admin_access, info, _scope) -> - case info do - %{usr: {@admin, @domain, _}} -> :allow - _ -> :deny - end; - (:all, _, _scope) -> - :allow - end) - end - -end diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs deleted file mode 100644 index 10b656140..000000000 --- a/test/ejabberd_commands_test.exs +++ /dev/null @@ -1,106 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 EjabberdCommandsTest do - @author "mremond@process-one.net" - - use ExUnit.Case, async: true - - require Record - Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") - - setup_all do - :mnesia.start - :stringprep.start - :ok = :ejabberd_config.start(["localhost"], []) - - :ejabberd_commands.init - :ok - end - - test "Check that we can register a command" do - :ok = :ejabberd_commands.register_commands([user_test_command]) - commands = :ejabberd_commands.list_commands - assert Enum.member?(commands, {:test_user, [], "Test user"}) - end - - test "get_exposed_commands/0 returns registered commands" do - commands = [open_test_command] - :ok = :ejabberd_commands.register_commands(commands) - :ok = :ejabberd_commands.expose_commands(commands) - exposed_commands = :ejabberd_commands.get_exposed_commands - assert Enum.member?(exposed_commands, :test_open) - end - - test "Check that admin commands are rejected with noauth credentials" do - :ok = :ejabberd_commands.register_commands([admin_test_command]) - - 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, []) - end - - # TODO Test that we can add command to list of expose commands - # This can be done with: - # ejabberd_config:add_local_option(commands, [[{add_commands, [open_cmd]}]]). - -# test "Check that a user can use a user command" do -# [Command] = ets:lookup(ejabberd_commands, test_user), -# AccessCommands = ejabberd_commands:get_access_commands(undefined), -# ejabberd_commands:check_access_commands(AccessCommands, {<<"test">>,<<"localhost">>, {oauth,<<"MyToken">>}, false}, test_user, Command, []). -# end - - defp user_test_command do - ejabberd_commands(name: :test_user, tags: [:roster], - desc: "Test user", - policy: :user, - module: __MODULE__, - function: :test_user, - args: [], - result: {:contacts, {:list, {:contact, {:tuple, [ - {:jid, :string}, - {:nick, :string} - ]}}}}) - end - - defp open_test_command do - ejabberd_commands(name: :test_open, tags: [:test], - desc: "Test open", - policy: :open, - module: __MODULE__, - function: :test_open, - args: [], - result: {:res, :rescode}) - end - - defp admin_test_command do - ejabberd_commands(name: :test_admin, tags: [:roster], - desc: "Test admin", - policy: :restricted, - module: __MODULE__, - function: :test_admin, - args: [], - result: {:res, :rescode}) - end - - def test_admin, do: :ok -end diff --git a/test/ejabberd_cyrsasl_test.exs b/test/ejabberd_cyrsasl_test.exs deleted file mode 100644 index d9b949294..000000000 --- a/test/ejabberd_cyrsasl_test.exs +++ /dev/null @@ -1,153 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 EjabberdCyrsaslTest do - @author "pawel@process-one.net" - - use ExUnit.Case, async: true - - setup_all do - :p1_sha.load_nif() - :mnesia.start - :ok = start_module(:stringprep) - :ok = start_module(:jid) - :ok = :ejabberd_config.start(["domain1"], []) - :ok = :cyrsasl.start - cyrstate = :cyrsasl.server_new("domain1", "domain1", "domain1", :ok, &get_password/1, - &check_password/3, &check_password_digest/5) - {:ok, cyrstate: cyrstate} - end - - test "Plain text (correct user and pass)", context do - step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"user1",0,"pass">>) - assert {:ok, _} = step1 - {:ok, kv} = step1 - assert kv[:authzid] == "user1", "got correct user" - end - - test "Plain text (correct user wrong pass)", context do - step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"user1",0,"badpass">>) - assert step1 == {:error, "not-authorized", "user1"}, "got error response" - end - - test "Plain text (wrong user wrong pass)", context do - step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"nouser1",0,"badpass">>) - assert step1 == {:error, "not-authorized", "nouser1"}, "got error response" - end - - test "Anonymous", context do - setup_anonymous_mocks() - step1 = :cyrsasl.server_start(context[:cyrstate], "ANONYMOUS", "domain1") - assert {:ok, _} = step1 - end - - test "Digest-MD5 (correct user and pass)", context do - assert {:continue, init_str, state1} = :cyrsasl.server_start(context[:cyrstate], "DIGEST-MD5", "") - assert [_, nonce] = Regex.run(~r/nonce="(.*?)"/, init_str) - user = "user1" - domain = "domain1" - digest_uri = "xmpp/#{domain}" - pass = "pass" - cnonce = "abcd" - nc = "00000001" - response_hash = calc_digest_sha(user, domain, pass, nc, nonce, cnonce) - response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <> - "nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <> - "charset=utf-8,algorithm=md5-sess" - assert {:continue, _calc_str, state3} = :cyrsasl.server_step(state1, response) - assert {:ok, _list} = :cyrsasl.server_step(state3, "") - end - - defp calc_digest_sha(user, domain, pass, nc, nonce, cnonce) do - digest_uri = "xmpp/#{domain}" - a0 = "#{user}:#{domain}:#{pass}" - a1 = "#{str_md5(a0)}:#{nonce}:#{cnonce}" - a2 = "AUTHENTICATE:#{digest_uri}" - hex_md5("#{hex_md5(a1)}:#{nonce}:#{nc}:#{cnonce}:auth:#{hex_md5(a2)}") - end - - defp str_md5(str) do - :erlang.md5(str) - end - - defp hex_md5(str) do - :p1_sha.to_hexlist(:erlang.md5(str)) - end - - defp setup_anonymous_mocks() do - :meck.unload - mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled, - fn (_host) -> - true - end) - mock(:ejabberd_auth, :is_user_exists, - fn (user, domain) -> - domain == "domain1" and get_password(user) != false - end) - end - - defp start_module(module) do - case apply(module, :start, []) do - :ok -> :ok - {:error, {:already_started, _}} -> :ok - other -> other - end - end - - defp get_password(user) do - if user == "user1" or user == "user2" do - {"pass", :internal} - else - :false - end - end - - defp check_password(_user, authzid, pass) do - case get_password(authzid) do - {^pass, mod} -> - {true, mod} - _ -> - false - end - end - - defp check_password_digest(_user, authzid, _pass, digest, digest_gen) do - case get_password(authzid) do - {spass, mod} -> - v = digest_gen.(spass) - if v == digest do - {true, mod} - else - false - end - _ -> - false - end - end - - defp mock(module, function, fun) do - try do - :meck.new(module, [:non_strict]) - catch - :error, {:already_started, _pid} -> :ok - end - :meck.expect(module, function, fun) - end -end diff --git a/test/ejabberd_hooks_test.exs b/test/ejabberd_hooks_test.exs deleted file mode 100644 index a69fbbd61..000000000 --- a/test/ejabberd_hooks_test.exs +++ /dev/null @@ -1,203 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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. -# -# ---------------------------------------------------------------------- - -# Notes on the tests: -# -# This test suite will print out errors in logs for tests: -# -# test "Error in run_fold is ignored" -# test "Throw in run_fold is ignored" -# test "Exit in run_fold is ignored" -# -# Those tests are not failing and we can safely ignore those errors in -# log as we are exercising hook handler recovery from that situation. - -defmodule EjabberdHooksTest do - 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 - end - - setup do - :meck.unload - :true = :ejabberd_hooks.delete_all_hooks - :ok - end - - test "An anonymous function can be added as a hook" do - hookname = :test_fun_hook - :ok = :ejabberd_hooks.add(hookname, @host, fn _ -> :ok end, 50) - [{50, :undefined, _}] = :ejabberd_hooks.get_handlers(hookname, @host) - end - - test "A module function can be added as a hook" do - hookname = :test_mod_hook - callback = :hook_callback - :ok = :ejabberd_hooks.add(hookname, @host, @self, callback, 40) - [{40, @self, _callback}] = :ejabberd_hooks.get_handlers(hookname, @host) - end - - test "An anonymous function can be removed from hook handlers" do - hookname = :test_fun_hook - anon_fun = fn _ -> :ok end - :ok = :ejabberd_hooks.add(hookname, @host, anon_fun, 50) - :ok = :ejabberd_hooks.delete(hookname, @host, anon_fun, 50) - [] = :ejabberd_hooks.get_handlers(hookname, @host) - end - - test "An module function can be removed from hook handlers" do - hookname = :test_mod_hook - callback = :hook_callback - :ok = :ejabberd_hooks.add(hookname, @host, @self, callback, 40) - :ok = :ejabberd_hooks.delete(hookname, @host, @self, callback, 40) - [] = :ejabberd_hooks.get_handlers(hookname, @host) - # TODO: Check that removed function is not call anymore - end - - test "'Run hook' call registered handler once" do - test_result = :hook_result - run_hook([], fn -> test_result end, test_result) - end - - test "'Run hook' can call registered handler with parameters" do - test_result = :hook_result_with_params - run_hook([:hook_params], fn _ -> test_result end, test_result) - end - - # TODO test "Several handlers are run in order by hook" - - test "Hook run chain is stopped when handler return 'stop'" do - # setup test - hookname = :test_mod_hook - modulename = :hook_module - mock(modulename, :hook_callback1, fn _ -> :stop end) - mock(modulename, :hook_callback2, fn _ -> :end_result end) - - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback1, 40) - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback1, 50) - - :ok = :ejabberd_hooks.run(hookname, @host, [:hook_params]) - # callback2 is never run: - [{_pid, {^modulename, _callback, [:hook_params]}, :stop}] = :meck.history(modulename) - end - - test "Run fold hooks accumulate state in correct order through handlers" do - # setup test - hookname = :test_mod_hook - modulename = :hook_module - mock(modulename, :hook_callback1, fn(list, user) -> [user|list] end) - mock(modulename, :hook_callback2, fn(list, _user) -> ["jid2"|list] end) - - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback1, 40) - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback2, 50) - - ["jid2", "jid1"] = :ejabberd_hooks.run_fold(hookname, @host, [], ["jid1"]) - end - - test "Hook run_fold are executed based on priority order, not registration order" do - # setup test - hookname = :test_mod_hook - modulename = :hook_module - mock(modulename, :hook_callback1, fn(_acc) -> :first end) - mock(modulename, :hook_callback2, fn(_acc) -> :second end) - - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback2, 50) - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback1, 40) - - :second = :ejabberd_hooks.run_fold(hookname, @host, :started, []) - # Both module have been called: - 2 = length(:meck.history(modulename)) - end - - # TODO: Test with ability to stop and return a value - test "Hook run_fold chain is stopped when handler return 'stop'" do - # setup test - hookname = :test_mod_hook - modulename = :hook_module - mock(modulename, :hook_callback1, fn(_acc) -> :stop end) - mock(modulename, :hook_callback2, fn(_acc) -> :executed end) - - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback1, 40) - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback2, 50) - - :stopped = :ejabberd_hooks.run_fold(hookname, @host, :started, []) - # Only one module has been called - [{_pid, {^modulename, :hook_callback1, [:started]}, :stop}] = :meck.history(modulename) - end - - test "Error in run_fold is ignored" do - run_fold_crash(fn(_acc) -> raise "crashed" end) - end - - test "Throw in run_fold is ignored" do - run_fold_crash(fn(_acc) -> throw :crashed end) - end - - test "Exit in run_fold is ignored" do - run_fold_crash(fn(_acc) -> exit :crashed end) - end - - # test for run hook with various number of params - def run_hook(params, fun, result) do - # setup test - hookname = :test_mod_hook - modulename = :hook_module - callback = :hook_callback - mock(modulename, callback, fun) - - # Then check - :ok = :ejabberd_hooks.add(hookname, @host, modulename, callback, 40) - :ok = :ejabberd_hooks.run(hookname, @host, params) - [{_pid, {^modulename, ^callback, ^params}, ^result}] = :meck.history(modulename) - end - - def run_fold_crash(crash_fun) do - # setup test - hookname = :test_mod_hook - modulename = :hook_module - mock(modulename, :hook_callback1, crash_fun) - mock(modulename, :hook_callback2, fn(_acc) -> :final end) - - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback1, 40) - :ok = :ejabberd_hooks.add(hookname, @host, modulename, :hook_callback2, 50) - - :final = :ejabberd_hooks.run_fold(hookname, @host, :started, []) - # Both handlers were called - 2 = length(:meck.history(modulename)) - end - - # TODO refactor: Move to ejabberd_test_mock - def mock(module, function, fun) do - try do - :meck.new(module, [:non_strict]) - catch - :error, {:already_started, _pid} -> :ok - end - - :meck.expect(module, function, fun) - end - -end diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs deleted file mode 100644 index e6a34f65e..000000000 --- a/test/ejabberd_oauth_mock.exs +++ /dev/null @@ -1,47 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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}], - []) - token - end - -end diff --git a/test/ejabberd_sm_mock.exs b/test/ejabberd_sm_mock.exs deleted file mode 100644 index 53c2c750f..000000000 --- a/test/ejabberd_sm_mock.exs +++ /dev/null @@ -1,121 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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-config/attr_test.exs b/test/elixir-config/attr_test.exs new file mode 100644 index 000000000..c5cab5bd8 --- /dev/null +++ b/test/elixir-config/attr_test.exs @@ -0,0 +1,87 @@ +defmodule Ejabberd.Config.AttrTest do + use ExUnit.Case, async: true + + alias Ejabberd.Config.Attr + + test "extract attrs from single line block" do + block = quote do + @active false + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + assert {:active, false} in block_res + end + + test "extract attrs from multi line block" do + block = quote do + @active false + @opts [http: true] + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + assert {:active, false} in block_res + assert {:opts, [http: true]} in block_res + end + + test "inserts correctly defaults attr when missing in block" do + block = quote do + @active false + @opts [http: true] + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + + assert {:active, false} in block_res + assert {:git, ""} in block_res + assert {:name, ""} in block_res + assert {:opts, [http: true]} in block_res + assert {:dependency, []} in block_res + end + + test "inserts all defaults attr when passed an empty block" do + block = quote do + end + + block_res = Attr.extract_attrs_from_block_with_defaults(block) + + assert {:active, true} in block_res + assert {:git, ""} in block_res + assert {:name, ""} in block_res + assert {:opts, []} in block_res + assert {:dependency, []} in block_res + end + + test "validates attrs and returns errors, if any" do + block = quote do + @not_supported_attr true + @active "false" + @opts [http: true] + end + + block_res = + block + |> Attr.extract_attrs_from_block_with_defaults + |> Attr.validate + + assert {:ok, {:opts, [http: true]}} in block_res + assert {:ok, {:git, ""}} in block_res + assert {:error, {:not_supported_attr, true}, :attr_not_supported} in block_res + assert {:error, {:active, "false"}, :type_not_supported} in block_res + end + + test "returns the correct type for an attribute" do + assert :boolean == Attr.get_type_for_attr(:active) + assert :string == Attr.get_type_for_attr(:git) + assert :string == Attr.get_type_for_attr(:name) + assert :list == Attr.get_type_for_attr(:opts) + assert :list == Attr.get_type_for_attr(:dependency) + end + + test "returns the correct default for an attribute" do + assert true == Attr.get_default_for_attr(:active) + assert "" == Attr.get_default_for_attr(:git) + assert "" == Attr.get_default_for_attr(:name) + assert [] == Attr.get_default_for_attr(:opts) + assert [] == Attr.get_default_for_attr(:dependency) + end +end diff --git a/test/elixir-config/config_test.exs b/test/elixir-config/config_test.exs new file mode 100644 index 000000000..c359c49c3 --- /dev/null +++ b/test/elixir-config/config_test.exs @@ -0,0 +1,65 @@ +defmodule Ejabberd.ConfigTest do + use ExUnit.Case + + alias Ejabberd.Config + alias Ejabberd.Config.Store + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "extracts successfully the module name from config file" do + assert [Ejabberd.ConfigFile] == Store.get(:module_name) + end + + test "extracts successfully general opts from config file" do + [general] = Store.get(:general) + shaper = [normal: 1000, fast: 50000, max_fsm_queue: 1000] + assert [loglevel: 4, language: "en", hosts: ["localhost"], shaper: shaper] == general + end + + test "extracts successfully listeners from config file" do + [listen] = Store.get(:listeners) + assert :ejabberd_c2s == listen.module + assert [port: 5222, max_stanza_size: 65536, shaper: :c2s_shaper, access: :c2s] == listen.attrs[:opts] + end + + test "extracts successfully modules from config file" do + [module] = Store.get(:modules) + assert :mod_adhoc == module.module + assert [] == module.attrs[:opts] + end + + test "extracts successfully hooks from config file" do + [register_hook] = Store.get(:hooks) + + assert :register_user == register_hook.hook + assert [host: "localhost"] == register_hook.opts + assert is_function(register_hook.fun) + end + + # TODO: When enalbed, this test causes the evaluation of a different config file, so + # the other tests, that uses the store, are compromised because the data is different. + # So, until a good way is found, this test should remain disabed. + # + # test "init/2 with force:true re-initializes the config store with new data" do + # config_file_path = File.cwd! <> "/ejabberd_different_from_default.exs" + # Config.init(config_file_path, true) + # + # assert [Ejabberd.ConfigFile] == Store.get(:module_name) + # assert [[loglevel: 4, language: "en", hosts: ["localhost"]]] == Store.get(:general) + # assert [] == Store.get(:modules) + # assert [] == Store.get(:listeners) + # + # Store.stop + # end +end diff --git a/test/elixir-config/ejabberd_logger.exs b/test/elixir-config/ejabberd_logger.exs new file mode 100644 index 000000000..594909289 --- /dev/null +++ b/test/elixir-config/ejabberd_logger.exs @@ -0,0 +1,49 @@ +defmodule Ejabberd.Config.EjabberdLoggerTest do + use ExUnit.Case + + import ExUnit.CaptureIO + + alias Ejabberd.Config + alias Ejabberd.Config.Store + alias Ejabberd.Config.Validation + alias Ejabberd.Config.EjabberdLogger + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd_for_validation.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "outputs correctly when attr is not supported" do + error_msg = "[ WARN ] Annotation @attr_not_supported is not supported.\n" + + [_mod_configure, mod_time] = Store.get(:modules) + fun = fn -> + mod_time + |> Validation.validate + |> EjabberdLogger.log_errors + end + + assert capture_io(fun) == error_msg + end + + test "outputs correctly when dependency is not found" do + error_msg = "[ WARN ] Module :mod_adhoc was not found, but is required as a dependency.\n" + + [mod_configure, _mod_time] = Store.get(:modules) + fun = fn -> + mod_configure + |> Validation.validate + |> EjabberdLogger.log_errors + end + + assert capture_io(fun) == error_msg + end +end diff --git a/test/elixir-config/shared/ejabberd.exs b/test/elixir-config/shared/ejabberd.exs new file mode 100644 index 000000000..5d0243bb5 --- /dev/null +++ b/test/elixir-config/shared/ejabberd.exs @@ -0,0 +1,31 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"], + shaper: shaper] + end + + defp shaper do + [normal: 1000, + fast: 50000, + max_fsm_queue: 1000] + end + + listen :ejabberd_c2s do + @opts [ + port: 5222, + max_stanza_size: 65536, + shaper: :c2s_shaper, + access: :c2s] + end + + module :mod_adhoc do + end + + hook :register_user, [host: "localhost"], fn(user, server) -> + info("User registered: #{user} on #{server}") + end +end diff --git a/test/elixir-config/shared/ejabberd_different_from_default.exs b/test/elixir-config/shared/ejabberd_different_from_default.exs new file mode 100644 index 000000000..a39409683 --- /dev/null +++ b/test/elixir-config/shared/ejabberd_different_from_default.exs @@ -0,0 +1,9 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"]] + end +end diff --git a/test/elixir-config/shared/ejabberd_for_validation.exs b/test/elixir-config/shared/ejabberd_for_validation.exs new file mode 100644 index 000000000..e47d925a9 --- /dev/null +++ b/test/elixir-config/shared/ejabberd_for_validation.exs @@ -0,0 +1,18 @@ +defmodule Ejabberd.ConfigFile do + use Ejabberd.Config + + def start do + [loglevel: 4, + language: "en", + hosts: ["localhost"]] + end + + module :mod_time do + @attr_not_supported true + end + + module :mod_configure do + @dependency [:mod_adhoc] + end + +end diff --git a/test/elixir-config/validation_test.exs b/test/elixir-config/validation_test.exs new file mode 100644 index 000000000..ca94d2705 --- /dev/null +++ b/test/elixir-config/validation_test.exs @@ -0,0 +1,31 @@ +defmodule Ejabberd.Config.ValidationTest do + use ExUnit.Case + + alias Ejabberd.Config + alias Ejabberd.Config.Store + alias Ejabberd.Config.Validation + + setup_all do + pid = Process.whereis(Ejabberd.Config.Store) + unless pid != nil and Process.alive?(pid) do + Store.start_link + + File.cd("test/elixir-config/shared") + config_file_path = File.cwd! <> "/ejabberd_for_validation.exs" + Config.init(config_file_path) + end + + {:ok, %{}} + end + + test "validates correctly the modules" do + [mod_configure, mod_time] = Store.get(:modules) + + [{:error, _mod, errors}] = Validation.validate(mod_configure) + assert %{dependency: [mod_adhoc: :not_found]} == errors + + [{:error, _mod, errors}] = Validation.validate(mod_time) + assert %{attribute: [{{:attr_not_supported, true}, :attr_not_supported}]} == errors + + end +end diff --git a/test/elixir_SUITE.erl b/test/elixir_SUITE.erl deleted file mode 100644 index aaef9151d..000000000 --- a/test/elixir_SUITE.erl +++ /dev/null @@ -1,95 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author Mickael Remond <mremond@process-one.net> -%%% @copyright (C) 2002-2016, ProcessOne -%%% @doc -%%% This is a common test wrapper to run our ejabberd tests written in -%%% Elixir from standard common test code. -%%% -%%% Example: Is run with: -%%% ./rebar skip_deps=true ct suites=elixir -%%% or from ejabber overall test suite: -%%% make quicktest -%%% @end -%%% Created : 19 Feb 2015 by Mickael Remond <mremond@process-one.net> -%%%------------------------------------------------------------------- - --module(elixir_SUITE). - --compile(export_all). - -init_per_suite(Config) -> - suite:setup_ejabberd_lib_path(Config), - check_meck(), - code:add_pathz(filename:join(test_dir(), "../include")), - Config. - -init_per_testcase(_TestCase, Config) -> - process_flag(error_handler, ?MODULE), - Config. - -all() -> - case is_elixir_available() of - true -> - Dir = test_dir(), - filelib:fold_files(Dir, ".*test\.exs$", false, - fun(Filename, Acc) -> [list_to_atom(filename:basename(Filename)) | Acc] end, - []); - false -> - [] - end. - -check_meck() -> - case catch meck:module_info(module) of - meck -> - ok; - {'EXIT',{undef, _}} -> - ct:print("meck is not available. Please make sure you configured ejabberd with --enable-elixir --enable-tools"), - ok - end. - -is_elixir_available() -> - case catch elixir:module_info() of - {'EXIT',{undef,_}} -> - ct:print("ejabberd has not been build with Elixir support, skipping Elixir tests."), - false; - ModInfo when is_list(ModInfo) -> - true - end. - -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) - end; -undefined_function(Module, Func, Args) -> - error_handler:undefined_function(Module, Func,Args). - -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}]}, {formatters, ['Elixir.ExUnit.CLIFormatter', 'Elixir.ExUnit.CTFormatter']}]), - - 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(), - case maps:find(failures, ResultMap) of - {ok, 0} -> - %% Zero failures - ok; - {ok, Failures} -> - ct:print("Tests failed in module '~s': ~.10B failures.~nSee logs for details", [Func, Failures]), - ct:fail(elixir_test_failure), - error - end. - -test_dir() -> - {ok, CWD} = file:get_cwd(), - filename:join(CWD, "../../test"). diff --git a/test/example_tests.erl b/test/example_tests.erl new file mode 100644 index 000000000..b93e0ccbb --- /dev/null +++ b/test/example_tests.erl @@ -0,0 +1,67 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(example_tests). + +%% API +-compile(export_all). +-import(suite, []). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {example_single, [sequence], + [single_test(foo)]}. + +foo(Config) -> + Config. + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {example_master_slave, [sequence], + [master_slave_test(foo)]}. + +foo_master(Config) -> + Config. + +foo_slave(Config) -> + Config. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("example_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("example_" ++ atom_to_list(T)), [parallel], + [list_to_atom("example_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("example_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/jid_test.exs b/test/jid_test.exs deleted file mode 100644 index b75a3603a..000000000 --- a/test/jid_test.exs +++ /dev/null @@ -1,44 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 JidTest do - @author "mremond@process-one.net" - - use ExUnit.Case, async: true - - require Record - Record.defrecord :jid, Record.extract(:jid, from_lib: "ejabberd/include/jlib.hrl") - - setup_all do - :stringprep.start - :jid.start - end - - test "create a jid from a binary" do - jid = :jid.from_string("test@localhost/resource") - assert jid(jid, :user) == "test" - assert jid(jid, :server) == "localhost" - assert jid(jid, :resource) == "resource" - end - - test "Check that sending a list to from_string/1 does not crash the jid process" do - {:error, :need_jid_as_binary} = :jid.from_string('test@localhost/resource') - end -end diff --git a/test/jidprep_tests.erl b/test/jidprep_tests.erl new file mode 100644 index 000000000..046f17b3c --- /dev/null +++ b/test/jidprep_tests.erl @@ -0,0 +1,62 @@ +%%%------------------------------------------------------------------- +%%% Author : Holger Weiss <holger@zedat.fu-berlin.de> +%%% Created : 11 Sep 2019 by Holger Weiss <holger@zedat.fu-berlin.de> +%%% +%%% +%%% ejabberd, Copyright (C) 2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(jidprep_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, + server_jid/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {jidprep_single, [sequence], + [single_test(feature_enabled), + single_test(normalize_jid)]}. + +feature_enabled(Config) -> + true = is_feature_advertised(Config, ?NS_JIDPREP_0), + disconnect(Config). + +normalize_jid(Config) -> + ServerJID = server_jid(Config), + OrigJID = jid:decode(<<"Romeo@Example.COM/Orchard">>), + NormJID = jid:decode(<<"romeo@example.com/Orchard">>), + Request = #jidprep{jid = OrigJID}, + #iq{type = result, sub_els = [#jidprep{jid = NormJID}]} = + send_recv(Config, #iq{type = get, to = ServerJID, + sub_els = [Request]}), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("jidprep_" ++ atom_to_list(T)). diff --git a/test/ldap_srv.erl b/test/ldap_srv.erl index 69b8a27e7..7ad53dda3 100644 --- a/test/ldap_srv.erl +++ b/test/ldap_srv.erl @@ -1,11 +1,28 @@ %%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2013-2016, Evgeniy Khramtsov -%%% @doc -%%% Simple LDAP server intended for LDAP modules testing -%%% @end +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 21 Jun 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> -%%%------------------------------------------------------------------- +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +%%% Simple LDAP server intended for LDAP modules testing + -module(ldap_srv). -behaviour(gen_server). @@ -28,7 +45,7 @@ -define(ERROR_MSG(Fmt, Args), error_logger:error_msg(Fmt, Args)). -define(TCP_SEND_TIMEOUT, 32000). --define(SERVER, ?MODULE). +-define(SERVER, ?MODULE). -record(state, {listener = make_ref() :: reference()}). @@ -54,7 +71,7 @@ init([LDIFFile]) -> case load_ldif(LDIFFile) of {ok, Tree} -> ?INFO_MSG("LDIF tree loaded, " - "ready to accept connections", []), + "ready to accept connections at ~B", [1389]), {_Pid, MRef} = spawn_monitor( fun() -> accept(ListenSocket, Tree) end @@ -105,7 +122,7 @@ accept(ListenSocket, Tree) -> process(Socket, Tree) -> case gen_tcp:recv(Socket, 0) of {ok, B} -> - case asn1rt:decode('ELDAPv3', 'LDAPMessage', B) of + case 'ELDAPv3':decode('LDAPMessage', B) of {ok, Msg} -> Replies = process_msg(Msg, Tree), Id = Msg#'LDAPMessage'.messageID, @@ -114,8 +131,8 @@ process(Socket, Tree) -> Reply = #'LDAPMessage'{messageID = Id, protocolOp = ReplyOp}, %%?DEBUG("sent:~n~p", [Reply]), - {ok, Bytes} = asn1rt:encode( - 'ELDAPv3', 'LDAPMessage', Reply), + {ok, Bytes} = 'ELDAPv3':encode( + 'LDAPMessage', Reply), gen_tcp:send(Socket, Bytes) end, Replies), process(Socket, Tree); diff --git a/test/mam_tests.erl b/test/mam_tests.erl new file mode 100644 index 000000000..75229becb --- /dev/null +++ b/test/mam_tests.erl @@ -0,0 +1,672 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 14 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(mam_tests). + +%% API +-compile(export_all). +-import(suite, [get_features/1, disconnect/1, my_jid/1, send_recv/2, + wait_for_slave/1, server_jid/1, send/2, get_features/2, + wait_for_master/1, recv_message/1, recv_iq/1, muc_room_jid/1, + muc_jid/1, is_feature_advertised/3, get_event/1, put_event/2]). + +-include("suite.hrl"). +-define(VERSIONS, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {mam_single, [sequence], + [single_test(feature_enabled), + single_test(get_set_prefs), + single_test(get_form), + single_test(fake_by)]}. + +feature_enabled(Config) -> + BareMyJID = jid:remove_resource(my_jid(Config)), + RequiredFeatures = sets:from_list(?VERSIONS), + ServerFeatures = sets:from_list(get_features(Config)), + UserFeatures = sets:from_list(get_features(Config, BareMyJID)), + MUCFeatures = get_features(Config, muc_jid(Config)), + ct:comment("Checking if all MAM server features are enabled"), + true = sets:is_subset(RequiredFeatures, ServerFeatures), + ct:comment("Checking if all MAM user features are enabled"), + true = sets:is_subset(RequiredFeatures, UserFeatures), + ct:comment("Checking if all MAM conference service features are enabled"), + true = lists:member(?NS_MAM_1, MUCFeatures), + true = lists:member(?NS_MAM_2, MUCFeatures), + clean(disconnect(Config)). + +fake_by(Config) -> + BareServerJID = server_jid(Config), + FullServerJID = jid:replace_resource(BareServerJID, p1_rand:get_string()), + FullMyJID = my_jid(Config), + BareMyJID = jid:remove_resource(FullMyJID), + Fakes = lists:flatmap( + fun(JID) -> + [#mam_archived{id = p1_rand:get_string(), by = JID}, + #stanza_id{id = p1_rand:get_string(), by = JID}] + end, [BareServerJID, FullServerJID, BareMyJID, FullMyJID]), + Body = xmpp:mk_text(<<"body">>), + ForeignJID = jid:make(p1_rand:get_string()), + Archived = #mam_archived{id = p1_rand:get_string(), by = ForeignJID}, + StanzaID = #stanza_id{id = p1_rand:get_string(), by = ForeignJID}, + #message{body = Body, sub_els = SubEls} = + send_recv(Config, #message{to = FullMyJID, + body = Body, + sub_els = [Archived, StanzaID|Fakes]}), + ct:comment("Checking if only foreign tags present"), + [ForeignJID, ForeignJID] = lists:flatmap( + fun(#mam_archived{by = By}) -> [By]; + (#stanza_id{by = By}) -> [By]; + (_) -> [] + end, SubEls), + clean(disconnect(Config)). + +get_set_prefs(Config) -> + Range = [{JID, #mam_prefs{xmlns = NS, + default = Default, + always = Always, + never = Never}} || + JID <- [undefined, server_jid(Config)], + NS <- ?VERSIONS, + Default <- [always, never, roster], + Always <- [[], [jid:decode(<<"foo@bar.baz">>)]], + Never <- [[], [jid:decode(<<"baz@bar.foo">>)]]], + lists:foreach( + fun({To, Prefs}) -> + NS = Prefs#mam_prefs.xmlns, + #iq{type = result, sub_els = [Prefs]} = + send_recv(Config, #iq{type = set, to = To, + sub_els = [Prefs]}), + #iq{type = result, sub_els = [Prefs]} = + send_recv(Config, #iq{type = get, to = To, + sub_els = [#mam_prefs{xmlns = NS}]}) + end, Range), + clean(disconnect(Config)). + +get_form(Config) -> + ServerJID = server_jid(Config), + Range = [{JID, NS} || JID <- [undefined, ServerJID], + NS <- ?VERSIONS -- [?NS_MAM_TMP]], + lists:foreach( + fun({To, NS}) -> + #iq{type = result, + sub_els = [#mam_query{xmlns = NS, + xdata = #xdata{} = X}]} = + send_recv(Config, #iq{type = get, to = To, + sub_els = [#mam_query{xmlns = NS}]}), + [NS] = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), + true = xmpp_util:has_xdata_var(<<"with">>, X), + true = xmpp_util:has_xdata_var(<<"start">>, X), + true = xmpp_util:has_xdata_var(<<"end">>, X) + end, Range), + clean(disconnect(Config)). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {mam_master_slave, [sequence], + [master_slave_test(archived_and_stanza_id), + master_slave_test(query_all), + master_slave_test(query_with), + master_slave_test(query_rsm_max), + master_slave_test(query_rsm_after), + master_slave_test(query_rsm_before), + master_slave_test(muc), + master_slave_test(mucsub), + master_slave_test(mucsub_from_muc), + master_slave_test(mucsub_from_muc_non_persistent)]}. + +archived_and_stanza_id_master(Config) -> + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + clean(disconnect(Config)). + +archived_and_stanza_id_slave(Config) -> + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + clean(disconnect(Config)). + +query_all_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_all(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_all_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_all(Config, Peer, MyJID), + clean(disconnect(Config)). + +query_with_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_with(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_with_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_with(Config, Peer, MyJID), + clean(disconnect(Config)). + +query_rsm_max_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_rsm_max(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_rsm_max_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_rsm_max(Config, Peer, MyJID), + clean(disconnect(Config)). + +query_rsm_after_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_rsm_after(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_rsm_after_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_rsm_after(Config, Peer, MyJID), + clean(disconnect(Config)). + +query_rsm_before_master(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_slave(Config), + send_messages(Config, lists:seq(1, 5)), + query_rsm_before(Config, MyJID, Peer), + clean(disconnect(Config)). + +query_rsm_before_slave(Config) -> + Peer = ?config(peer, Config), + MyJID = my_jid(Config), + ok = set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + recv_messages(Config, lists:seq(1, 5)), + query_rsm_before(Config, Peer, MyJID), + clean(disconnect(Config)). + +muc_master(Config) -> + Room = muc_room_jid(Config), + %% Joining + ok = muc_tests:join_new(Config), + %% MAM feature should not be advertised at this point, + %% because MAM is not enabled so far + false = is_feature_advertised(Config, ?NS_MAM_1, Room), + false = is_feature_advertised(Config, ?NS_MAM_2, Room), + %% Fill in some history + send_messages_to_room(Config, lists:seq(1, 21)), + %% We now should be able to retrieve those via MAM, even though + %% MAM is disabled. However, only last 20 messages should be received. + recv_messages_from_room(Config, lists:seq(2, 21)), + %% Now enable MAM for the conference + %% Retrieve config first + CfgOpts = muc_tests:get_config(Config), + %% Find the MAM field in the config + true = proplists:is_defined(mam, CfgOpts), + %% Enable MAM + [104] = muc_tests:set_config(Config, [{mam, true}]), + %% Check if MAM has been enabled + true = is_feature_advertised(Config, ?NS_MAM_1, Room), + true = is_feature_advertised(Config, ?NS_MAM_2, Room), + %% We now sending some messages again + send_messages_to_room(Config, lists:seq(1, 5)), + %% And retrieve them via MAM again. + recv_messages_from_room(Config, lists:seq(1, 5)), + put_event(Config, disconnect), + muc_tests:leave(Config), + clean(disconnect(Config)). + +muc_slave(Config) -> + disconnect = get_event(Config), + clean(disconnect(Config)). + +mucsub_master(Config) -> + Room = muc_room_jid(Config), + Peer = ?config(peer, Config), + wait_for_slave(Config), + ct:comment("Joining muc room"), + ok = muc_tests:join_new(Config), + + ct:comment("Enabling mam in room"), + CfgOpts = muc_tests:get_config(Config), + %% Find the MAM field in the config + ?match(true, proplists:is_defined(mam, CfgOpts)), + ?match(true, proplists:is_defined(allow_subscription, CfgOpts)), + %% Enable MAM + [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]), + + ct:comment("Subscribing peer to room"), + ?send_recv(#iq{to = Room, type = set, sub_els = [ + #muc_subscribe{jid = Peer, nick = <<"peer">>, + events = [?NS_MUCSUB_NODES_MESSAGES]} + ]}, #iq{type = result}), + + ct:comment("Sending messages to room"), + send_messages_to_room(Config, lists:seq(1, 5)), + + ct:comment("Retrieving messages from room mam storage"), + recv_messages_from_room(Config, lists:seq(1, 5)), + + ct:comment("Cleaning up"), + put_event(Config, ready), + ready = get_event(Config), + muc_tests:leave(Config), + clean(disconnect(Config)). + +mucsub_slave(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyJIDBare = jid:remove_resource(MyJID), + ok = set_default(Config, always), + send_recv(Config, #presence{}), + wait_for_master(Config), + + ct:comment("Receiving mucsub events"), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), + PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ + #ps_item{} = PS + ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, lists:seq(1, 5)), + + ct:comment("Retrieving personal mam archive"), + QID = p1_rand:get_string(), + I = send(Config, #iq{type = set, + sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Forw = ?match(#message{ + to = MyJID, from = MyJIDBare, + sub_els = [#mam_result{ + xmlns = ?NS_MAM_2, + queryid = QID, + sub_els = [#forwarded{ + delay = #delay{}} = Forw]}]}, + recv_message(Config), Forw), + IMsg = ?match(#message{ + to = MyJIDBare, from = Room} = IMsg, xmpp:get_subtag(Forw, #message{}), IMsg), + + PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ + #ps_item{} = PS + ]}}, xmpp:get_subtag(IMsg, #ps_event{}), PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, lists:seq(1, 5)), + RSM = ?match(#iq{from = MyJIDBare, id = I, type = result, + sub_els = [#mam_fin{xmlns = ?NS_MAM_2, + id = QID, + rsm = RSM, + complete = true}]}, recv_iq(Config), RSM), + match_rsm_count(RSM, 5), + + % Wait for master exit + ready = get_event(Config), + % Unsubscribe yourself + ?send_recv(#iq{to = Room, type = set, sub_els = [ + #muc_unsubscribe{} + ]}, #iq{type = result}), + put_event(Config, ready), + clean(disconnect(Config)). + +mucsub_from_muc_master(Config) -> + mucsub_master(Config). + +mucsub_from_muc_slave(Config) -> + Server = ?config(server, Config), + gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => true}), + Config2 = mucsub_slave(Config), + gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => false}), + Config2. + +mucsub_from_muc_non_persistent_master(Config) -> + Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}), + Config2 = mucsub_from_muc_master(Config1), + lists:keydelete(persistent_room, 1, Config2). + +mucsub_from_muc_non_persistent_slave(Config) -> + Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}), + Config2 = mucsub_from_muc_slave(Config1), + lists:keydelete(persistent_room, 1, Config2). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("mam_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("mam_" ++ atom_to_list(T)), [parallel], + [list_to_atom("mam_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("mam_" ++ atom_to_list(T) ++ "_slave")]}. + +clean(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + mod_mam:remove_user(U, S), + Config. + +set_default(Config, Default) -> + lists:foreach( + fun(NS) -> + ct:comment("Setting default preferences of '~s' to '~s'", + [NS, Default]), + #iq{type = result, + sub_els = [#mam_prefs{xmlns = NS, default = Default}]} = + send_recv(Config, #iq{type = set, + sub_els = [#mam_prefs{xmlns = NS, + default = Default}]}) + end, ?VERSIONS). + +send_messages(Config, Range) -> + Peer = ?config(peer, Config), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + send(Config, #message{to = Peer, body = Body}) + end, Range). + +recv_messages(Config, Range) -> + Peer = ?config(peer, Config), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + #message{from = Peer, body = Body} = Msg = + recv_message(Config), + #mam_archived{by = BareMyJID} = + xmpp:get_subtag(Msg, #mam_archived{}), + #stanza_id{by = BareMyJID} = + xmpp:get_subtag(Msg, #stanza_id{}) + end, Range). + +recv_archived_messages(Config, From, To, QID, Range) -> + MyJID = my_jid(Config), + lists:foreach( + fun(N) -> + ct:comment("Retreiving ~pth message in range ~p", + [N, Range]), + Body = xmpp:mk_text(integer_to_binary(N)), + #message{to = MyJID, + sub_els = + [#mam_result{ + queryid = QID, + sub_els = + [#forwarded{ + delay = #delay{}, + sub_els = [El]}]}]} = recv_message(Config), + #message{from = From, to = To, + body = Body} = xmpp:decode(El) + end, Range). + +maybe_recv_iq_result(Config, ?NS_MAM_0, I) -> + #iq{type = result, id = I} = recv_iq(Config); +maybe_recv_iq_result(_, _, _) -> + ok. + +query_iq_type(?NS_MAM_TMP) -> get; +query_iq_type(_) -> set. + +send_query(Config, #mam_query{xmlns = NS} = Query) -> + Type = query_iq_type(NS), + I = send(Config, #iq{type = Type, sub_els = [Query]}), + maybe_recv_iq_result(Config, NS, I), + I. + +recv_fin(Config, I, QueryID, NS, IsComplete) when NS == ?NS_MAM_1; NS == ?NS_MAM_2 -> + ct:comment("Receiving fin iq for namespace '~s'", [NS]), + #iq{type = result, id = I, + sub_els = [#mam_fin{xmlns = NS, + id = QueryID, + complete = Complete, + rsm = RSM}]} = recv_iq(Config), + ct:comment("Checking if complete is ~s", [IsComplete]), + ?match(IsComplete, Complete), + RSM; +recv_fin(Config, I, QueryID, ?NS_MAM_TMP = NS, _IsComplete) -> + ct:comment("Receiving fin iq for namespace '~s'", [NS]), + #iq{type = result, id = I, + sub_els = [#mam_query{xmlns = NS, + rsm = RSM, + id = QueryID}]} = recv_iq(Config), + RSM; +recv_fin(Config, _, QueryID, ?NS_MAM_0 = NS, IsComplete) -> + ct:comment("Receiving fin message for namespace '~s'", [NS]), + #message{} = FinMsg = recv_message(Config), + #mam_fin{xmlns = NS, + id = QueryID, + complete = Complete, + rsm = RSM} = xmpp:get_subtag(FinMsg, #mam_fin{xmlns = NS}), + ct:comment("Checking if complete is ~s", [IsComplete]), + ?match(IsComplete, Complete), + RSM. + +send_messages_to_room(Config, Range) -> + MyNick = ?config(master_nick, Config), + Room = muc_room_jid(Config), + MyNickJID = jid:replace_resource(Room, MyNick), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + #message{from = MyNickJID, + type = groupchat, + body = Body} = + send_recv(Config, #message{to = Room, body = Body, + type = groupchat}) + end, Range). + +recv_messages_from_room(Config, Range) -> + MyNick = ?config(master_nick, Config), + Room = muc_room_jid(Config), + MyNickJID = jid:replace_resource(Room, MyNick), + MyJID = my_jid(Config), + QID = p1_rand:get_string(), + I = send(Config, #iq{type = set, to = Room, + sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}), + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + #message{ + to = MyJID, from = Room, + sub_els = + [#mam_result{ + xmlns = ?NS_MAM_2, + queryid = QID, + sub_els = + [#forwarded{ + delay = #delay{}, + sub_els = [El]}]}]} = recv_message(Config), + #message{from = MyNickJID, + type = groupchat, + body = Body} = xmpp:decode(El) + end, Range), + #iq{from = Room, id = I, type = result, + sub_els = [#mam_fin{xmlns = ?NS_MAM_2, + id = QID, + rsm = RSM, + complete = true}]} = recv_iq(Config), + match_rsm_count(RSM, length(Range)). + +query_all(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_all(Config, From, To, NS) + end, ?VERSIONS). + +query_all(Config, From, To, NS) -> + QID = p1_rand:get_string(), + Range = lists:seq(1, 5), + ID = send_query(Config, #mam_query{xmlns = NS, id = QID}), + recv_archived_messages(Config, From, To, QID, Range), + RSM = recv_fin(Config, ID, QID, NS, _Complete = true), + match_rsm_count(RSM, 5). + +query_with(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_with(Config, From, To, NS) + end, ?VERSIONS). + +query_with(Config, From, To, NS) -> + Peer = ?config(peer, Config), + BarePeer = jid:remove_resource(Peer), + QID = p1_rand:get_string(), + Range = lists:seq(1, 5), + lists:foreach( + fun(JID) -> + ct:comment("Sending query with jid ~s", [jid:encode(JID)]), + Query = if NS == ?NS_MAM_TMP -> + #mam_query{xmlns = NS, with = JID, id = QID}; + true -> + Fs = mam_query:encode([{with, JID}]), + #mam_query{xmlns = NS, id = QID, + xdata = #xdata{type = submit, + fields = Fs}} + end, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5) + end, [Peer, BarePeer]). + +query_rsm_max(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_rsm_max(Config, From, To, NS) + end, ?VERSIONS). + +query_rsm_max(Config, From, To, NS) -> + lists:foreach( + fun(Max) -> + QID = p1_rand:get_string(), + Range = lists:sublist(lists:seq(1, Max), 5), + Query = #mam_query{xmlns = NS, id = QID, rsm = #rsm_set{max = Max}}, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + IsComplete = Max >= 5, + RSM = recv_fin(Config, ID, QID, NS, IsComplete), + match_rsm_count(RSM, 5) + end, lists:seq(0, 6)). + +query_rsm_after(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_rsm_after(Config, From, To, NS) + end, ?VERSIONS). + +query_rsm_after(Config, From, To, NS) -> + lists:foldl( + fun(Range, #rsm_first{data = After}) -> + ct:comment("Retrieving ~p messages after '~s'", + [length(Range), After]), + QID = p1_rand:get_string(), + Query = #mam_query{xmlns = NS, id = QID, + rsm = #rsm_set{'after' = After}}, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = #rsm_set{first = First} = + recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5), + First + end, #rsm_first{data = undefined}, + [lists:seq(N, 5) || N <- lists:seq(1, 6)]). + +query_rsm_before(Config, From, To) -> + lists:foreach( + fun(NS) -> + query_rsm_before(Config, From, To, NS) + end, ?VERSIONS). + +query_rsm_before(Config, From, To, NS) -> + lists:foldl( + fun(Range, Before) -> + ct:comment("Retrieving ~p messages before '~s'", + [length(Range), Before]), + QID = p1_rand:get_string(), + Query = #mam_query{xmlns = NS, id = QID, + rsm = #rsm_set{before = Before}}, + ID = send_query(Config, Query), + recv_archived_messages(Config, From, To, QID, Range), + RSM = #rsm_set{last = Last} = + recv_fin(Config, ID, QID, NS, true), + match_rsm_count(RSM, 5), + Last + end, <<"">>, lists:reverse([lists:seq(1, N) || N <- lists:seq(0, 5)])). + +match_rsm_count(#rsm_set{count = undefined}, _) -> + %% The backend doesn't support counting + ok; +match_rsm_count(#rsm_set{count = Count1}, Count2) -> + ct:comment("Checking if RSM 'count' is ~p", [Count2]), + ?match(Count2, Count1). diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs deleted file mode 100644 index 03422264f..000000000 --- a/test/mod_admin_extra_test.exs +++ /dev/null @@ -1,360 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 - require EjabberdSmMock - require ModLastMock - require ModRosterMock - - @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 - :ok = :ejabberd_config.start([@domain], []) - :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 deleted file mode 100644 index 9cba35365..000000000 --- a/test/mod_http_api_mock_test.exs +++ /dev/null @@ -1,275 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 - :meck.new(:acl, [:passthrough]) # Need to fake acl to allow oauth - 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_and_scope, - fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end) - :meck.expect(:ejabberd_commands, :get_exposed_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_and_scope, - fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) - :meck.expect(:ejabberd_commands, :get_exposed_commands, - fn () -> [@acommand] end) - :meck.expect(:ejabberd_commands, :execute_command, - fn (:undefined, {@user, @domain, {:oauth, _token}, false}, - @acommand, [], @version, _) -> - :ok - end) - - - # Correct OAuth call using specific scope - 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 - - # Correct OAuth call using specific ejabberd:user scope - token = EjabberdOauthMock.get_token @user, @domain, "ejabberd:user" - 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 twice - assert 2 == - :meck.num_calls(:ejabberd_commands, :execute_command, :_) - - assert :meck.validate :ejabberd_auth - assert :meck.validate :ejabberd_commands - #assert :ok = :meck.history(:ejabberd_commands) - end - - test "Request oauth token, resource owner password credentials" do - EjabberdAuthMock.create_user @user, @domain, @userpass - :application.set_env(:oauth2, :backend, :ejabberd_oauth) - :application.start(:oauth2) - - # 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_and_scope, - fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end) - :meck.expect(:ejabberd_commands, :get_exposed_commands, - fn () -> [@acommand] end) - :meck.expect(:ejabberd_commands, :execute_command, - fn (:undefined, {@user, @domain, {:oauth, _token}, false}, - @acommand, [], @version, _) -> - :ok - end) - - #Mock acl to allow oauth authorizations - :meck.expect(:acl, :match_rule, fn(_Server, _Access, _Jid) -> :allow end) - - - # Correct password - req = request(method: :POST, - path: ["oauth", "token"], - q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"ttl", "4000"}, {"password", @userpass}], - ip: {{127,0,0,1},60000}, - host: @domain) - result = :ejabberd_oauth.process([], req) - assert 200 = elem(result, 0) #http code - {kv} = :jiffy.decode(elem(result,2)) - assert {_, "bearer"} = List.keyfind(kv, "token_type", 0) - assert {_, @command} = List.keyfind(kv, "scope", 0) - assert {_, 4000} = List.keyfind(kv, "expires_in", 0) - {"access_token", _token} = List.keyfind(kv, "access_token", 0) - - #missing grant_type - req = request(method: :POST, - path: ["oauth", "token"], - q: [{"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass}], - ip: {{127,0,0,1},60000}, - host: @domain) - result = :ejabberd_oauth.process([], req) - assert 400 = elem(result, 0) #http code - {kv} = :jiffy.decode(elem(result,2)) - assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0) - - - # incorrect user/pass - req = request(method: :POST, - path: ["oauth", "token"], - q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass<>"aa"}], - ip: {{127,0,0,1},60000}, - host: @domain) - result = :ejabberd_oauth.process([], req) - assert 400 = elem(result, 0) #http code - {kv} = :jiffy.decode(elem(result,2)) - assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0) - - assert :meck.validate :ejabberd_auth - assert :meck.validate :ejabberd_commands - end - -end diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs deleted file mode 100644 index e2ae3d784..000000000 --- a/test/mod_http_api_test.exs +++ /dev/null @@ -1,123 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 ModHttpApiTest do - @author "mremond@process-one.net" - - use ExUnit.Case, async: true - - require Record - Record.defrecord :request, Record.extract(:request, from_lib: "ejabberd/include/ejabberd_http.hrl") - Record.defrecord :ejabberd_commands, Record.extract(:ejabberd_commands, from_lib: "ejabberd/include/ejabberd_commands.hrl") - - setup_all do - :ok = :mnesia.start - :stringprep.start - :ok = :ejabberd_config.start(["localhost"], []) - :ok = :ejabberd_commands.init - :ok = :ejabberd_commands.register_commands(cmds) - on_exit fn -> - :meck.unload - unregister_commands(cmds) end - end - - test "We can expose several commands to API at a time" do - setup_mocks() - :ejabberd_commands.expose_commands([:open_cmd, :user_cmd]) - commands = :ejabberd_commands.get_exposed_commands() - assert Enum.member?(commands, :open_cmd) - assert Enum.member?(commands, :user_cmd) - end - - test "We can call open commands without authentication" do - setup_mocks() - :ejabberd_commands.expose_commands([:open_cmd]) - request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") - {200, _, _} = :mod_http_api.process(["open_cmd"], request) - end - - # This related to the commands config file option - test "Attempting to access a command that is not exposed as HTTP API returns 403" do - setup_mocks() - :ejabberd_commands.expose_commands([]) - request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") - {403, _, _} = :mod_http_api.process(["open_cmd"], request) - end - - test "Call to user, admin or restricted commands without authentication are rejected" do - setup_mocks() - :ejabberd_commands.expose_commands([:user_cmd, :admin_cmd, :restricted]) - request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]") - {403, _, _} = :mod_http_api.process(["user_cmd"], request) - {403, _, _} = :mod_http_api.process(["admin_cmd"], request) - {403, _, _} = :mod_http_api.process(["restricted_cmd"], request) - end - - @tag pending: true - test "If admin_ip_access is enabled, we can call restricted API without authentication from that IP" do - setup_mocks() - end - - # Define a set of test commands that we expose through API - # We define one for each policy type - defp cmds do - [:open, :user, :admin, :restricted] - |> Enum.map(&({&1, String.to_atom(to_string(&1) <> "_cmd")})) - |> Enum.map(fn({cmd_type, cmd}) -> - ejabberd_commands(name: cmd, tags: [:test], - policy: cmd_type, - module: __MODULE__, - function: cmd, - args: [], - result: {:res, :rescode}) - end) - end - - def open_cmd, do: :ok - def user_cmd(_, _), do: :ok - def admin_cmd, do: :ok - def restricted_cmd, do: :ok - - defp setup_mocks() do - :meck.unload - mock(:gen_mod, :get_module_opt, - fn (_server, :mod_http_api, _admin_ip_access, _, _) -> - [{:allow, [{:ip, {{127,0,0,2}, 32}}]}] - end) - end - - defp mock(module, function, fun) do - try do - :meck.new(module) - catch - :error, {:already_started, _pid} -> :ok - end - :meck.expect(module, function, fun) - end - - defp unregister_commands(commands) do - try do - :ejabberd_commands.unregister_commands(commands) - catch - _,_ -> :ok - end - end - -end diff --git a/test/mod_last_mock.exs b/test/mod_last_mock.exs deleted file mode 100644 index 4f8da3666..000000000 --- a/test/mod_last_mock.exs +++ /dev/null @@ -1,79 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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 deleted file mode 100644 index ae990a6b1..000000000 --- a/test/mod_roster_mock.exs +++ /dev/null @@ -1,225 +0,0 @@ -# ---------------------------------------------------------------------- -# -# 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_mnesia, :invalidate_roster_cache, - fn (_user, _server) -> - :ok - end) - - :moka.load(module_mock) - - roster_mock0 = :moka.start(:mod_roster) - :moka.replace(roster_mock0, :gen_iq_handler, :add_iq_handler, - fn (_module, _host, _ns, _m, _f, _iqdisc) -> - :ok - end) - - :moka.replace(roster_mock0, :gen_iq_handler, :remove_iq_handler, - fn (_module, _host, _ns) -> - :ok - end) - :moka.replace(roster_mock0, :gen_mod, :db_mod, - fn (_host, _mod) -> - :mod_roster_mnesia - end) - :moka.replace(roster_mock0, :gen_mod, :db_mod, - fn (_host, _opts, _mod) -> - :mod_roster_mnesia - end) - :moka.replace(roster_mock0, :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_mock0, :del_roster_t, - fn (user, domain, jid) -> - remove_roster_item(user, domain, :jid.to_string(jid)) - end) - :moka.replace(roster_mock0, :get_roster, - fn (user, domain) -> - to_records(get_roster(user, domain)) - end) - - :moka.load(roster_mock0) - - roster_mock = :moka.start(:mod_roster_mnesia) - :moka.replace(roster_mock, :gen_mod, :db_type, - fn (_host, _opts) -> - {:none} - end) - - :moka.replace(roster_mock, :transaction, - fn (_server, function) -> - {:atomic, function.()} - end) - - :moka.replace(roster_mock, :update_tables, - fn () -> - :ok - 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) -> -# :mnesia -# end) -# -# mock(:mnesia, :transaction, -# fn (_server, function) -> -# {:atomic, function.()} -# end) -# -# mock(:mnesia, :write, -# fn (Item) -> -# throw Item -# {:atomic, :ok} -# end) - - mock(:mod_roster_mnesia, :init, - fn (_server, _opts) -> - :ok - end) - mock(:mod_roster_mnesia, :transaction, - fn (_server, function) -> - {:atomic, function.()} - end) - - mock(:mod_roster_mnesia, :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_mnesia, :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 diff --git a/test/muc_tests.erl b/test/muc_tests.erl new file mode 100644 index 000000000..ef57e9a7b --- /dev/null +++ b/test/muc_tests.erl @@ -0,0 +1,1944 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 15 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(muc_tests). + +%% API +-compile(export_all). +-import(suite, [recv_presence/1, send_recv/2, my_jid/1, muc_room_jid/1, + send/2, recv_message/1, recv_iq/1, muc_jid/1, + alt_room_jid/1, wait_for_slave/1, wait_for_master/1, + disconnect/1, put_event/2, get_event/1, peer_muc_jid/1, + my_muc_jid/1, get_features/2, set_opt/3]). +-include("suite.hrl"). +-include("jid.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single tests +%%%=================================================================== +single_cases() -> + {muc_single, [sequence], + [single_test(service_presence_error), + single_test(service_message_error), + single_test(service_unknown_ns_iq_error), + single_test(service_iq_set_error), + single_test(service_improper_iq_error), + single_test(service_features), + single_test(service_disco_info_node_error), + single_test(service_disco_items), + single_test(service_unique), + single_test(service_vcard), + single_test(configure_non_existent), + single_test(cancel_configure_non_existent), + single_test(service_subscriptions), + single_test(set_room_affiliation)]}. + +service_presence_error(Config) -> + Service = muc_jid(Config), + ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), + lists:foreach( + fun(To) -> + send(Config, #presence{type = error, to = To}), + lists:foreach( + fun(Type) -> + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = To}), + #stanza_error{reason = 'service-unavailable'} = + xmpp:get_error(Err) + end, [available, unavailable]) + end, [Service, ServiceResource]), + disconnect(Config). + +service_message_error(Config) -> + Service = muc_jid(Config), + send(Config, #message{type = error, to = Service}), + lists:foreach( + fun(Type) -> + #message{type = error} = Err1 = + send_recv(Config, #message{type = Type, to = Service}), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err1) + end, [chat, normal, headline, groupchat]), + ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), + send(Config, #message{type = error, to = ServiceResource}), + lists:foreach( + fun(Type) -> + #message{type = error} = Err2 = + send_recv(Config, #message{type = Type, to = ServiceResource}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err2) + end, [chat, normal, headline, groupchat]), + disconnect(Config). + +service_unknown_ns_iq_error(Config) -> + Service = muc_jid(Config), + ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), + lists:foreach( + fun(To) -> + send(Config, #iq{type = result, to = To}), + send(Config, #iq{type = error, to = To}), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err1 = + send_recv(Config, #iq{type = Type, to = To, + sub_els = [#presence{}]}), + #stanza_error{reason = 'service-unavailable'} = + xmpp:get_error(Err1) + end, [set, get]) + end, [Service, ServiceResource]), + disconnect(Config). + +service_iq_set_error(Config) -> + Service = muc_jid(Config), + lists:foreach( + fun(SubEl) -> + send(Config, #iq{type = result, to = Service, + sub_els = [SubEl]}), + #iq{type = error} = Err2 = + send_recv(Config, #iq{type = set, to = Service, + sub_els = [SubEl]}), + #stanza_error{reason = 'not-allowed'} = + xmpp:get_error(Err2) + end, [#disco_items{}, #disco_info{}, #vcard_temp{}, + #muc_unique{}, #muc_subscriptions{}]), + disconnect(Config). + +service_improper_iq_error(Config) -> + Service = muc_jid(Config), + lists:foreach( + fun(SubEl) -> + send(Config, #iq{type = result, to = Service, + sub_els = [SubEl]}), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err3 = + send_recv(Config, #iq{type = Type, to = Service, + sub_els = [SubEl]}), + #stanza_error{reason = Reason} = xmpp:get_error(Err3), + true = Reason /= 'internal-server-error' + end, [set, get]) + end, [#disco_item{jid = Service}, + #identity{category = <<"category">>, type = <<"type">>}, + #vcard_email{}, #muc_subscribe{nick = ?config(nick, Config)}]), + disconnect(Config). + +service_features(Config) -> + ServerHost = ?config(server_host, Config), + MUC = muc_jid(Config), + Features = sets:from_list(get_features(Config, MUC)), + MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of + true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; + false -> [] + end, + RequiredFeatures = sets:from_list( + [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_REGISTER, ?NS_MUC, + ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE + | MAMFeatures]), + ct:comment("Checking if all needed disco features are set"), + true = sets:is_subset(RequiredFeatures, Features), + disconnect(Config). + +service_disco_info_node_error(Config) -> + MUC = muc_jid(Config), + Node = p1_rand:get_string(), + #iq{type = error} = Err = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#disco_info{node = Node}]}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err), + disconnect(Config). + +service_disco_items(Config) -> + #jid{server = Service} = muc_jid(Config), + Rooms = lists:sort( + lists:map( + fun(I) -> + RoomName = integer_to_binary(I), + jid:make(RoomName, Service) + end, lists:seq(1, 5))), + lists:foreach( + fun(Room) -> + ok = join_new(Config, Room) + end, Rooms), + Items = disco_items(Config), + Rooms = [J || #disco_item{jid = J} <- Items], + lists:foreach( + fun(Room) -> + ok = leave(Config, Room) + end, Rooms), + [] = disco_items(Config), + disconnect(Config). + +service_vcard(Config) -> + MUC = muc_jid(Config), + ct:comment("Retreiving vCard from ~s", [jid:encode(MUC)]), + VCard = mod_muc_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = MUC, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +service_unique(Config) -> + MUC = muc_jid(Config), + ct:comment("Requesting muc unique from ~s", [jid:encode(MUC)]), + #iq{type = result, sub_els = [#muc_unique{name = Name}]} = + send_recv(Config, #iq{type = get, to = MUC, sub_els = [#muc_unique{}]}), + ct:comment("Checking if unique name is set in the response"), + <<_, _/binary>> = Name, + disconnect(Config). + +configure_non_existent(Config) -> + [_|_] = get_config(Config), + disconnect(Config). + +cancel_configure_non_existent(Config) -> + Room = muc_room_jid(Config), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{to = Room, type = set, + sub_els = [#muc_owner{config = #xdata{type = cancel}}]}), + disconnect(Config). + +service_subscriptions(Config) -> + MUC = #jid{server = Service} = muc_jid(Config), + Rooms = lists:sort( + lists:map( + fun(I) -> + RoomName = integer_to_binary(I), + jid:make(RoomName, Service) + end, lists:seq(1, 5))), + lists:foreach( + fun(Room) -> + ok = join_new(Config, Room), + [104] = set_config(Config, [{allow_subscription, true}], Room), + [] = subscribe(Config, [], Room) + end, Rooms), + #iq{type = result, sub_els = [#muc_subscriptions{list = JIDs}]} = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#muc_subscriptions{}]}), + Rooms = lists:sort([J || #muc_subscription{jid = J, events = []} <- JIDs]), + lists:foreach( + fun(Room) -> + ok = unsubscribe(Config, Room), + ok = leave(Config, Room) + end, Rooms), + disconnect(Config). + +set_room_affiliation(Config) -> + #jid{server = RoomService} = muc_jid(Config), + RoomName = <<"set_room_affiliation">>, + RoomJID = jid:make(RoomName, RoomService), + MyJID = my_jid(Config), + PeerJID = jid:remove_resource(?config(slave, Config)), + + ct:pal("joining room ~p", [RoomJID]), + ok = join_new(Config, RoomJID), + + ct:pal("setting affiliation in room ~p to 'member' for ~p", [RoomJID, PeerJID]), + ServerHost = ?config(server_host, Config), + WebPort = ct:get_config(web_port, 5280), + RequestURL = "http://" ++ ServerHost ++ ":" ++ integer_to_list(WebPort) ++ "/api/set_room_affiliation", + Headers = [{"X-Admin", "true"}], + ContentType = "application/json", + Body = jiffy:encode(#{name => RoomName, service => RoomService, jid => jid:encode(PeerJID), affiliation => member}), + {ok, {{_, 200, _}, _, _}} = httpc:request(post, {RequestURL, Headers, ContentType, Body}, [], []), + + #message{id = _, from = RoomJID, to = MyJID, sub_els = [ + #muc_user{items = [ + #muc_item{affiliation = member, role = none, jid = PeerJID}]}]} = recv_message(Config), + + ok = leave(Config, RoomJID), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {muc_master_slave, [sequence], + [master_slave_test(register), + master_slave_test(groupchat_msg), + master_slave_test(private_msg), + master_slave_test(set_subject), + master_slave_test(history), + master_slave_test(invite), + master_slave_test(invite_members_only), + master_slave_test(invite_password_protected), + master_slave_test(voice_request), + master_slave_test(change_role), + master_slave_test(kick), + master_slave_test(change_affiliation), + master_slave_test(destroy), + master_slave_test(vcard), + master_slave_test(nick_change), + master_slave_test(config_title_desc), + master_slave_test(config_public_list), + master_slave_test(config_password), + master_slave_test(config_whois), + master_slave_test(config_members_only), + master_slave_test(config_moderated), + master_slave_test(config_private_messages), + master_slave_test(config_query), + master_slave_test(config_allow_invites), + master_slave_test(config_visitor_status), + master_slave_test(config_allow_voice_requests), + master_slave_test(config_voice_request_interval), + master_slave_test(config_visitor_nickchange), + master_slave_test(join_conflict)]}. + +join_conflict_master(Config) -> + ok = join_new(Config), + put_event(Config, join), + ct:comment("Waiting for 'leave' command from the slave"), + leave = get_event(Config), + ok = leave(Config), + disconnect(Config). + +join_conflict_slave(Config) -> + NewConfig = set_opt(nick, ?config(peer_nick, Config), Config), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + ct:comment("Fail trying to join the room with conflicting nick"), + #stanza_error{reason = 'conflict'} = join(NewConfig), + put_event(Config, leave), + disconnect(NewConfig). + +groupchat_msg_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ok = master_join(Config), + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, #message{type = groupchat, to = Room, + body = Body}), + #message{type = groupchat, from = MyNickJID, + body = Body} = recv_message(Config) + end, lists:seq(1, 5)), + #muc_user{items = [#muc_item{jid = PeerJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +groupchat_msg_slave(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(master_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + {[], _, _} = slave_join(Config), + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + #message{type = groupchat, from = PeerNickJID, + body = Body} = recv_message(Config) + end, lists:seq(1, 5)), + ok = leave(Config), + disconnect(Config). + +private_msg_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = master_join(Config), + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, #message{type = chat, to = PeerNickJID, + body = Body}) + end, lists:seq(1, 5)), + #muc_user{items = [#muc_item{jid = PeerJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Fail trying to send a private message to non-existing occupant"), + send(Config, #message{type = chat, to = PeerNickJID}), + #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(ErrMsg), + ok = leave(Config), + disconnect(Config). + +private_msg_slave(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(master_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + {[], _, _} = slave_join(Config), + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + #message{type = chat, from = PeerNickJID, + body = Body} = recv_message(Config) + end, lists:seq(1, 5)), + ok = leave(Config), + disconnect(Config). + +set_subject_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + Subject1 = xmpp:mk_text(?config(room_subject, Config)), + Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>), + ok = master_join(Config), + ct:comment("Setting 1st subject"), + send(Config, #message{type = groupchat, to = Room, + subject = Subject1}), + #message{type = groupchat, from = MyNickJID, + subject = Subject1} = recv_message(Config), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Setting 2nd subject"), + send(Config, #message{type = groupchat, to = Room, + subject = Subject2}), + #message{type = groupchat, from = MyNickJID, + subject = Subject2} = recv_message(Config), + ct:comment("Asking the slave to join"), + put_event(Config, join), + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Receiving 1st subject set by the slave"), + #message{type = groupchat, from = PeerNickJID, + subject = Subject1} = recv_message(Config), + ct:comment("Disallow subject change"), + [104] = set_config(Config, [{changesubject, false}]), + ct:comment("Waiting for the slave to leave"), + #muc_user{items = [#muc_item{jid = PeerJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +set_subject_slave(Config) -> + Room = muc_room_jid(Config), + MyNickJID = my_muc_jid(Config), + PeerNick = ?config(master_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + Subject1 = xmpp:mk_text(?config(room_subject, Config)), + Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>), + {[], _, _} = slave_join(Config), + ct:comment("Receiving 1st subject set by the master"), + #message{type = groupchat, from = PeerNickJID, + subject = Subject1} = recv_message(Config), + ok = leave(Config), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {[], SubjMsg2, _} = join(Config), + ct:comment("Checking if the master has set 2nd subject during our absence"), + #message{type = groupchat, from = PeerNickJID, + subject = Subject2} = SubjMsg2, + ct:comment("Setting 1st subject"), + send(Config, #message{to = Room, type = groupchat, subject = Subject1}), + #message{type = groupchat, from = MyNickJID, + subject = Subject1} = recv_message(Config), + ct:comment("Waiting for the master to disallow subject change"), + [104] = recv_config_change_message(Config), + ct:comment("Fail trying to change the subject"), + send(Config, #message{to = Room, type = groupchat, subject = Subject2}), + #message{from = Room, type = error} = ErrMsg = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), + ok = leave(Config), + disconnect(Config). + +history_master(Config) -> + Room = muc_room_jid(Config), + ServerHost = ?config(server_host, Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + PeerNickJID = peer_muc_jid(Config), + Size = mod_muc_opt:history_size(iolist_to_binary(ServerHost)), + ok = join_new(Config), + ct:comment("Putting ~p+1 messages in the history", [Size]), + %% Only Size messages will be stored + lists:foreach( + fun(I) -> + Body = xmpp:mk_text(integer_to_binary(I)), + send(Config, #message{to = Room, type = groupchat, + body = Body}), + #message{type = groupchat, from = MyNickJID, + body = Body} = recv_message(Config) + end, lists:seq(0, Size)), + put_event(Config, join), + lists:foreach( + fun(Type) -> + recv_muc_presence(Config, PeerNickJID, Type) + end, [available, unavailable, + available, unavailable, + available, unavailable, + available, unavailable]), + ok = leave(Config), + disconnect(Config). + +history_slave(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(peer_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ServerHost = ?config(server_host, Config), + Size = mod_muc_opt:history_size(iolist_to_binary(ServerHost)), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {History, _, _} = join(Config), + ct:comment("Checking ordering of history events"), + BodyList = [binary_to_integer(xmpp:get_text(Body)) + || #message{type = groupchat, from = From, + body = Body} <- History, + From == PeerNickJID], + BodyList = lists:seq(1, Size), + ok = leave(Config), + %% If the client wishes to receive no history, it MUST set the 'maxchars' + %% attribute to a value of "0" (zero) + %% (http://xmpp.org/extensions/xep-0045.html#enter-managehistory) + ct:comment("Checking if maxchars=0 yields to no history"), + {[], _, _} = join(Config, #muc{history = #muc_history{maxchars = 0}}), + ok = leave(Config), + ct:comment("Receiving only 10 last stanzas"), + {History10, _, _} = join(Config, + #muc{history = #muc_history{maxstanzas = 10}}), + BodyList10 = [binary_to_integer(xmpp:get_text(Body)) + || #message{type = groupchat, from = From, + body = Body} <- History10, + From == PeerNickJID], + BodyList10 = lists:nthtail(Size-10, lists:seq(1, Size)), + ok = leave(Config), + #delay{stamp = TS} = xmpp:get_subtag(hd(History), #delay{}), + ct:comment("Receiving all history without the very first element"), + {HistoryWithoutFirst, _, _} = join(Config, + #muc{history = #muc_history{since = TS}}), + BodyListWithoutFirst = [binary_to_integer(xmpp:get_text(Body)) + || #message{type = groupchat, from = From, + body = Body} <- HistoryWithoutFirst, + From == PeerNickJID], + BodyListWithoutFirst = lists:nthtail(1, lists:seq(1, Size)), + ok = leave(Config), + disconnect(Config). + +invite_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + ok = join_new(Config), + wait_for_slave(Config), + %% Inviting the peer + send(Config, #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}), + #message{from = Room} = DeclineMsg = recv_message(Config), + #muc_user{decline = #muc_decline{from = PeerJID}} = + xmpp:get_subtag(DeclineMsg, #muc_user{}), + ok = leave(Config), + disconnect(Config). + +invite_slave(Config) -> + Room = muc_room_jid(Config), + wait_for_master(Config), + PeerJID = ?config(master, Config), + #message{from = Room, type = normal} = Msg = recv_message(Config), + #muc_user{invites = [#muc_invite{from = PeerJID}]} = + xmpp:get_subtag(Msg, #muc_user{}), + %% Decline invitation + send(Config, + #message{to = Room, + sub_els = [#muc_user{ + decline = #muc_decline{to = PeerJID}}]}), + disconnect(Config). + +invite_members_only_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + ok = join_new(Config), + %% Setting the room to members-only + [_|_] = set_config(Config, [{membersonly, true}]), + wait_for_slave(Config), + %% Inviting the peer + send(Config, #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}), + #message{from = Room, type = normal} = AffMsg = recv_message(Config), + #muc_user{items = [#muc_item{jid = PeerJID, affiliation = member}]} = + xmpp:get_subtag(AffMsg, #muc_user{}), + ok = leave(Config), + disconnect(Config). + +invite_members_only_slave(Config) -> + Room = muc_room_jid(Config), + wait_for_master(Config), + %% Receiving invitation + #message{from = Room, type = normal} = recv_message(Config), + disconnect(Config). + +invite_password_protected_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + Password = p1_rand:get_string(), + ok = join_new(Config), + [104] = set_config(Config, [{passwordprotectedroom, true}, + {roomsecret, Password}]), + put_event(Config, Password), + %% Inviting the peer + send(Config, #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}), + ok = leave(Config), + disconnect(Config). + +invite_password_protected_slave(Config) -> + Room = muc_room_jid(Config), + Password = get_event(Config), + %% Receiving invitation + #message{from = Room, type = normal} = Msg = recv_message(Config), + #muc_user{password = Password} = xmpp:get_subtag(Msg, #muc_user{}), + disconnect(Config). + +voice_request_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + wait_for_slave(Config), + #muc_user{ + items = [#muc_item{role = visitor, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Receiving voice request"), + #message{from = Room, type = normal} = VoiceReq = recv_message(Config), + #xdata{type = form, fields = Fs} = xmpp:get_subtag(VoiceReq, #xdata{}), + [{jid, PeerJID}, + {request_allow, false}, + {role, participant}, + {roomnick, PeerNick}] = lists:sort(muc_request:decode(Fs)), + ct:comment("Approving voice request"), + ApprovalFs = muc_request:encode([{jid, PeerJID}, {role, participant}, + {roomnick, PeerNick}, {request_allow, true}]), + send(Config, #message{to = Room, sub_els = [#xdata{type = submit, + fields = ApprovalFs}]}), + #muc_user{ + items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +voice_request_slave(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + wait_for_master(Config), + {[], _, _} = join(Config, visitor), + ct:comment("Requesting voice"), + Fs = muc_request:encode([{role, participant}]), + X = #xdata{type = submit, fields = Fs}, + send(Config, #message{to = Room, sub_els = [X]}), + ct:comment("Waiting to become a participant"), + #muc_user{ + items = [#muc_item{role = participant, + jid = MyJID, + affiliation = none}]} = + recv_muc_presence(Config, MyNickJID, available), + ok = leave(Config), + disconnect(Config). + +change_role_master(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyNick = ?config(nick, Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + ct:comment("Waiting for the slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + lists:foreach( + fun(Role) -> + ct:comment("Checking if the slave is not in the roles list"), + case get_role(Config, Role) of + [#muc_item{jid = MyJID, affiliation = owner, + role = moderator, nick = MyNick}] when Role == moderator -> + ok; + [] -> + ok + end, + Reason = p1_rand:get_string(), + put_event(Config, {Role, Reason}), + ok = set_role(Config, Role, Reason), + ct:comment("Receiving role change to ~s", [Role]), + #muc_user{ + items = [#muc_item{role = Role, + affiliation = none, + reason = Reason}]} = + recv_muc_presence(Config, PeerNickJID, available), + [#muc_item{role = Role, affiliation = none, + nick = PeerNick}|_] = get_role(Config, Role) + end, [visitor, participant, moderator]), + put_event(Config, disconnect), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +change_role_slave(Config) -> + wait_for_master(Config), + {[], _, _} = join(Config), + change_role_slave(Config, get_event(Config)). + +change_role_slave(Config, {Role, Reason}) -> + Room = muc_room_jid(Config), + MyNick = ?config(slave_nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ct:comment("Receiving role change to ~s", [Role]), + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + affiliation = none, + reason = Reason}]} = + recv_muc_presence(Config, MyNickJID, available), + true = lists:member(110, Codes), + change_role_slave(Config, get_event(Config)); +change_role_slave(Config, disconnect) -> + ok = leave(Config), + disconnect(Config). + +change_affiliation_master(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyBareJID = jid:remove_resource(MyJID), + MyNick = ?config(nick, Config), + PeerJID = ?config(slave, Config), + PeerBareJID = jid:remove_resource(PeerJID), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + ct:comment("Waiting for the slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + lists:foreach( + fun({Aff, Role, Status}) -> + ct:comment("Checking if slave is not in affiliation list"), + case get_affiliation(Config, Aff) of + [#muc_item{jid = MyBareJID, + affiliation = owner}] when Aff == owner -> + ok; + [] -> + ok + end, + Reason = p1_rand:get_string(), + put_event(Config, {Aff, Role, Status, Reason}), + ok = set_affiliation(Config, Aff, Reason), + ct:comment("Receiving affiliation change to ~s", [Aff]), + #muc_user{ + items = [#muc_item{role = Role, + affiliation = Aff, + actor = Actor, + reason = Reason}]} = + recv_muc_presence(Config, PeerNickJID, Status), + if Aff == outcast -> + ct:comment("Checking if actor is set"), + #muc_actor{nick = MyNick} = Actor; + true -> + ok + end, + Affs = get_affiliation(Config, Aff), + ct:comment("Checking if the affiliation was correctly set"), + case lists:keyfind(PeerBareJID, #muc_item.jid, Affs) of + false when Aff == none -> + ok; + #muc_item{affiliation = Aff} -> + ok + end + end, [{member, participant, available}, {none, visitor, available}, + {admin, moderator, available}, {owner, moderator, available}, + {outcast, none, unavailable}]), + ok = leave(Config), + disconnect(Config). + +change_affiliation_slave(Config) -> + wait_for_master(Config), + {[], _, _} = join(Config), + change_affiliation_slave(Config, get_event(Config)). + +change_affiliation_slave(Config, {Aff, Role, Status, Reason}) -> + Room = muc_room_jid(Config), + PeerNick = ?config(master_nick, Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ct:comment("Receiving affiliation change to ~s", [Aff]), + if Aff == outcast -> + #presence{from = Room, type = unavailable} = recv_presence(Config); + true -> + ok + end, + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + actor = Actor, + affiliation = Aff, + reason = Reason}]} = + recv_muc_presence(Config, MyNickJID, Status), + true = lists:member(110, Codes), + if Aff == outcast -> + ct:comment("Checking for status code '301' (banned)"), + true = lists:member(301, Codes), + ct:comment("Checking if actor is set"), + #muc_actor{nick = PeerNick} = Actor, + disconnect(Config); + true -> + change_affiliation_slave(Config, get_event(Config)) + end. + +kick_master(Config) -> + Room = muc_room_jid(Config), + MyNick = ?config(nick, Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + Reason = <<"Testing">>, + ok = join_new(Config), + ct:comment("Waiting for the slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + [#muc_item{role = participant, affiliation = none, + nick = PeerNick}|_] = get_role(Config, participant), + ct:comment("Kicking slave"), + ok = set_role(Config, none, Reason), + ct:comment("Receiving role change to 'none'"), + #muc_user{ + status_codes = Codes, + items = [#muc_item{role = none, + affiliation = none, + actor = #muc_actor{nick = MyNick}, + reason = Reason}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + [] = get_role(Config, participant), + ct:comment("Checking if the code is '307' (kicked)"), + true = lists:member(307, Codes), + ok = leave(Config), + disconnect(Config). + +kick_slave(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(master_nick, Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + Reason = <<"Testing">>, + wait_for_master(Config), + {[], _, _} = join(Config), + ct:comment("Receiving role change to 'none'"), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{status_codes = Codes, + items = [#muc_item{role = none, + affiliation = none, + actor = #muc_actor{nick = PeerNick}, + reason = Reason}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if codes '110' (self-presence) " + "and '307' (kicked) are present"), + true = lists:member(110, Codes), + true = lists:member(307, Codes), + disconnect(Config). + +destroy_master(Config) -> + Reason = <<"Testing">>, + Room = muc_room_jid(Config), + AltRoom = alt_room_jid(Config), + PeerJID = ?config(peer, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ok = join_new(Config), + ct:comment("Waiting for slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + jid = PeerJID, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + wait_for_slave(Config), + ok = destroy(Config, Reason), + ct:comment("Receiving destruction presence"), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{items = [#muc_item{role = none, + affiliation = none}], + destroy = #muc_destroy{jid = AltRoom, + reason = Reason}} = + recv_muc_presence(Config, MyNickJID, unavailable), + disconnect(Config). + +destroy_slave(Config) -> + Reason = <<"Testing">>, + Room = muc_room_jid(Config), + AltRoom = alt_room_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + wait_for_master(Config), + {[], _, _} = join(Config), + #stanza_error{reason = 'forbidden'} = destroy(Config, Reason), + wait_for_master(Config), + ct:comment("Receiving destruction presence"), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{items = [#muc_item{role = none, + affiliation = none}], + destroy = #muc_destroy{jid = AltRoom, + reason = Reason}} = + recv_muc_presence(Config, MyNickJID, unavailable), + disconnect(Config). + +vcard_master(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + FN = p1_rand:get_string(), + VCard = #vcard_temp{fn = FN}, + ok = join_new(Config), + ct:comment("Waiting for slave to join"), + wait_for_slave(Config), + #muc_user{items = [#muc_item{role = participant, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + #stanza_error{reason = 'item-not-found'} = get_vcard(Config), + ok = set_vcard(Config, VCard), + VCard = get_vcard(Config), + put_event(Config, VCard), + recv_muc_presence(Config, PeerNickJID, unavailable), + leave = get_event(Config), + ok = leave(Config), + disconnect(Config). + +vcard_slave(Config) -> + wait_for_master(Config), + {[], _, _} = join(Config), + [104] = recv_config_change_message(Config), + VCard = get_event(Config), + VCard = get_vcard(Config), + #stanza_error{reason = 'forbidden'} = set_vcard(Config, VCard), + ok = leave(Config), + VCard = get_vcard(Config), + put_event(Config, leave), + disconnect(Config). + +nick_change_master(Config) -> + NewNick = p1_rand:get_string(), + PeerJID = ?config(peer, Config), + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + put_event(Config, {new_nick, NewNick}), + ct:comment("Waiting for nickchange presence from the slave"), + #muc_user{status_codes = Codes, + items = [#muc_item{jid = PeerJID, + nick = NewNick}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Checking if code '303' (nick change) is set"), + true = lists:member(303, Codes), + ct:comment("Waiting for updated presence from the slave"), + PeerNewNickJID = jid:replace_resource(PeerNickJID, NewNick), + recv_muc_presence(Config, PeerNewNickJID, available), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNewNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +nick_change_slave(Config) -> + MyJID = my_jid(Config), + MyNickJID = my_muc_jid(Config), + {[], _, _} = slave_join(Config), + {new_nick, NewNick} = get_event(Config), + MyNewNickJID = jid:replace_resource(MyNickJID, NewNick), + ct:comment("Sending new presence"), + send(Config, #presence{to = MyNewNickJID}), + ct:comment("Receiving nickchange self-presence"), + #muc_user{status_codes = Codes1, + items = [#muc_item{role = participant, + jid = MyJID, + nick = NewNick}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if codes '110' (self-presence) and " + "'303' (nickchange) are present"), + lists:member(110, Codes1), + lists:member(303, Codes1), + ct:comment("Receiving self-presence update"), + #muc_user{status_codes = Codes2, + items = [#muc_item{jid = MyJID, + role = participant}]} = + recv_muc_presence(Config, MyNewNickJID, available), + ct:comment("Checking if code '110' (self-presence) is set"), + lists:member(110, Codes2), + NewConfig = set_opt(nick, NewNick, Config), + ok = leave(NewConfig), + disconnect(NewConfig). + +config_title_desc_master(Config) -> + Title = p1_rand:get_string(), + Desc = p1_rand:get_string(), + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = master_join(Config), + [104] = set_config(Config, [{roomname, Title}, {roomdesc, Desc}]), + RoomCfg = get_config(Config), + Title = proplists:get_value(roomname, RoomCfg), + Desc = proplists:get_value(roomdesc, RoomCfg), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_title_desc_slave(Config) -> + {[], _, _} = slave_join(Config), + [104] = recv_config_change_message(Config), + ok = leave(Config), + disconnect(Config). + +config_public_list_master(Config) -> + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + wait_for_slave(Config), + recv_muc_presence(Config, PeerNickJID, available), + lists:member(<<"muc_public">>, get_features(Config, Room)), + [104] = set_config(Config, [{public_list, false}, + {publicroom, false}]), + recv_muc_presence(Config, PeerNickJID, unavailable), + lists:member(<<"muc_hidden">>, get_features(Config, Room)), + wait_for_slave(Config), + ok = leave(Config), + disconnect(Config). + +config_public_list_slave(Config) -> + Room = muc_room_jid(Config), + wait_for_master(Config), + PeerNick = ?config(peer_nick, Config), + PeerNickJID = peer_muc_jid(Config), + [#disco_item{jid = Room}] = disco_items(Config), + [#disco_item{jid = PeerNickJID, + name = PeerNick}] = disco_room_items(Config), + {[], _, _} = join(Config), + [104] = recv_config_change_message(Config), + ok = leave(Config), + [] = disco_items(Config), + [] = disco_room_items(Config), + wait_for_master(Config), + disconnect(Config). + +config_password_master(Config) -> + Password = p1_rand:get_string(), + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + lists:member(<<"muc_unsecured">>, get_features(Config, Room)), + [104] = set_config(Config, [{passwordprotectedroom, true}, + {roomsecret, Password}]), + lists:member(<<"muc_passwordprotected">>, get_features(Config, Room)), + put_event(Config, Password), + recv_muc_presence(Config, PeerNickJID, available), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_password_slave(Config) -> + Password = get_event(Config), + #stanza_error{reason = 'not-authorized'} = join(Config), + #stanza_error{reason = 'not-authorized'} = + join(Config, #muc{password = p1_rand:get_string()}), + {[], _, _} = join(Config, #muc{password = Password}), + ok = leave(Config), + disconnect(Config). + +config_whois_master(Config) -> + Room = muc_room_jid(Config), + PeerNickJID = peer_muc_jid(Config), + MyNickJID = my_muc_jid(Config), + ok = master_join(Config), + lists:member(<<"muc_semianonymous">>, get_features(Config, Room)), + [172] = set_config(Config, [{whois, anyone}]), + lists:member(<<"muc_nonanonymous">>, get_features(Config, Room)), + recv_muc_presence(Config, PeerNickJID, unavailable), + recv_muc_presence(Config, PeerNickJID, available), + send(Config, #presence{to = Room}), + recv_muc_presence(Config, MyNickJID, available), + [173] = set_config(Config, [{whois, moderators}]), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_whois_slave(Config) -> + PeerJID = ?config(peer, Config), + PeerNickJID = peer_muc_jid(Config), + {[], _, _} = slave_join(Config), + ct:comment("Checking if the room becomes non-anonymous (code '172')"), + [172] = recv_config_change_message(Config), + ct:comment("Re-joining in order to check status codes"), + ok = leave(Config), + {[], _, Codes} = join(Config), + ct:comment("Checking if code '100' (non-anonymous) present"), + true = lists:member(100, Codes), + ct:comment("Receiving presence from peer with JID exposed"), + #muc_user{items = [#muc_item{jid = PeerJID}]} = + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Waiting for the room to become anonymous again (code '173')"), + [173] = recv_config_change_message(Config), + ok = leave(Config), + disconnect(Config). + +config_members_only_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + lists:member(<<"muc_open">>, get_features(Config, Room)), + [104] = set_config(Config, [{membersonly, true}]), + #muc_user{status_codes = Codes, + items = [#muc_item{jid = PeerJID, + affiliation = none, + role = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Checking if code '322' (non-member) is set"), + true = lists:member(322, Codes), + lists:member(<<"muc_membersonly">>, get_features(Config, Room)), + ct:comment("Waiting for slave to fail joining the room"), + set_member = get_event(Config), + ok = set_affiliation(Config, member, p1_rand:get_string()), + #message{from = Room, type = normal} = Msg = recv_message(Config), + #muc_user{items = [#muc_item{jid = PeerBareJID, + affiliation = member}]} = + xmpp:get_subtag(Msg, #muc_user{}), + ct:comment("Asking peer to join"), + put_event(Config, join), + ct:comment("Waiting for peer to join"), + recv_muc_presence(Config, PeerNickJID, available), + ok = set_affiliation(Config, none, p1_rand:get_string()), + ct:comment("Waiting for peer to be kicked"), + #muc_user{status_codes = NewCodes, + items = [#muc_item{affiliation = none, + role = none}]} = + recv_muc_presence(Config, PeerNickJID, unavailable), + ct:comment("Checking if code '321' (became non-member in " + "members-only room) is set"), + true = lists:member(321, NewCodes), + ok = leave(Config), + disconnect(Config). + +config_members_only_slave(Config) -> + Room = muc_room_jid(Config), + MyJID = my_jid(Config), + MyNickJID = my_muc_jid(Config), + {[], _, _} = slave_join(Config), + [104] = recv_config_change_message(Config), + ct:comment("Getting kicked because the room has become members-only"), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{status_codes = Codes, + items = [#muc_item{jid = MyJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if the code '110' (self-presence) " + "and '322' (non-member) is set"), + true = lists:member(110, Codes), + true = lists:member(322, Codes), + ct:comment("Fail trying to join members-only room"), + #stanza_error{reason = 'registration-required'} = join(Config), + ct:comment("Asking the peer to set us member"), + put_event(Config, set_member), + ct:comment("Waiting for the peer to ask for join"), + join = get_event(Config), + {[], _, _} = join(Config, participant, member), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{status_codes = NewCodes, + items = [#muc_item{jid = MyJID, + role = none, + affiliation = none}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if the code '110' (self-presence) " + "and '321' (became non-member in members-only room) is set"), + true = lists:member(110, NewCodes), + true = lists:member(321, NewCodes), + disconnect(Config). + +config_moderated_master(Config) -> + Room = muc_room_jid(Config), + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + lists:member(<<"muc_moderated">>, get_features(Config, Room)), + ok = set_role(Config, visitor, p1_rand:get_string()), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + set_unmoderated = get_event(Config), + [104] = set_config(Config, [{moderatedroom, false}]), + #message{from = PeerNickJID, type = groupchat} = recv_message(Config), + recv_muc_presence(Config, PeerNickJID, unavailable), + lists:member(<<"muc_unmoderated">>, get_features(Config, Room)), + ok = leave(Config), + disconnect(Config). + +config_moderated_slave(Config) -> + Room = muc_room_jid(Config), + MyNickJID = my_muc_jid(Config), + {[], _, _} = slave_join(Config), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, MyNickJID, available), + send(Config, #message{to = Room, type = groupchat}), + ErrMsg = #message{from = Room, type = error} = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), + put_event(Config, set_unmoderated), + [104] = recv_config_change_message(Config), + send(Config, #message{to = Room, type = groupchat}), + #message{from = MyNickJID, type = groupchat} = recv_message(Config), + ok = leave(Config), + disconnect(Config). + +config_private_messages_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + ct:comment("Waiting for a private message from the slave"), + #message{from = PeerNickJID, type = chat} = recv_message(Config), + ok = set_role(Config, visitor, <<>>), + ct:comment("Waiting for the peer to become a visitor"), + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Waiting for a private message from the slave"), + #message{from = PeerNickJID, type = chat} = recv_message(Config), + [104] = set_config(Config, [{allow_private_messages_from_visitors, moderators}]), + ct:comment("Waiting for a private message from the slave"), + #message{from = PeerNickJID, type = chat} = recv_message(Config), + [104] = set_config(Config, [{allow_private_messages_from_visitors, nobody}]), + wait_for_slave(Config), + [104] = set_config(Config, [{allow_private_messages_from_visitors, anyone}, + {allow_private_messages, false}]), + ct:comment("Fail trying to send a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), + ok = set_role(Config, participant, <<>>), + ct:comment("Waiting for the peer to become a participant"), + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Waiting for the peer to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_private_messages_slave(Config) -> + MyNickJID = my_muc_jid(Config), + PeerNickJID = peer_muc_jid(Config), + {[], _, _} = slave_join(Config), + ct:comment("Sending a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + ct:comment("Waiting to become a visitor"), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Sending a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + [104] = recv_config_change_message(Config), + ct:comment("Sending a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + [104] = recv_config_change_message(Config), + ct:comment("Fail trying to send a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + #message{from = PeerNickJID, type = error} = ErrMsg1 = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg1), + wait_for_master(Config), + [104] = recv_config_change_message(Config), + ct:comment("Waiting to become a participant again"), + #muc_user{items = [#muc_item{role = participant}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Fail trying to send a private message"), + send(Config, #message{to = PeerNickJID, type = chat}), + #message{from = PeerNickJID, type = error} = ErrMsg2 = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg2), + ok = leave(Config), + disconnect(Config). + +config_query_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + ok = join_new(Config), + wait_for_slave(Config), + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Receiving IQ query from the slave"), + #iq{type = get, from = PeerNickJID, id = I, + sub_els = [#ping{}]} = recv_iq(Config), + send(Config, #iq{type = result, to = PeerNickJID, id = I}), + [104] = set_config(Config, [{allow_query_users, false}]), + ct:comment("Fail trying to send IQ"), + #iq{type = error, from = PeerNickJID} = Err = + send_recv(Config, #iq{type = get, to = PeerNickJID, + sub_els = [#ping{}]}), + #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_query_slave(Config) -> + PeerNickJID = peer_muc_jid(Config), + wait_for_master(Config), + ct:comment("Checking if IQ queries are denied from non-occupants"), + #iq{type = error, from = PeerNickJID} = Err1 = + send_recv(Config, #iq{type = get, to = PeerNickJID, + sub_els = [#ping{}]}), + #stanza_error{reason = 'not-acceptable'} = xmpp:get_error(Err1), + {[], _, _} = join(Config), + ct:comment("Sending IQ to the master"), + #iq{type = result, from = PeerNickJID, sub_els = []} = + send_recv(Config, #iq{to = PeerNickJID, type = get, sub_els = [#ping{}]}), + [104] = recv_config_change_message(Config), + ct:comment("Fail trying to send IQ"), + #iq{type = error, from = PeerNickJID} = Err2 = + send_recv(Config, #iq{type = get, to = PeerNickJID, + sub_els = [#ping{}]}), + #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err2), + ok = leave(Config), + disconnect(Config). + +config_allow_invites_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + PeerNickJID = peer_muc_jid(Config), + ok = master_join(Config), + [104] = set_config(Config, [{allowinvites, true}]), + ct:comment("Receiving an invitation from the slave"), + #message{from = Room, type = normal} = recv_message(Config), + [104] = set_config(Config, [{allowinvites, false}]), + send_invitation = get_event(Config), + ct:comment("Sending an invitation"), + send(Config, #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_allow_invites_slave(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + InviteMsg = #message{to = Room, type = normal, + sub_els = + [#muc_user{ + invites = + [#muc_invite{to = PeerJID}]}]}, + {[], _, _} = slave_join(Config), + [104] = recv_config_change_message(Config), + ct:comment("Sending an invitation"), + send(Config, InviteMsg), + [104] = recv_config_change_message(Config), + ct:comment("Fail sending an invitation"), + send(Config, InviteMsg), + #message{from = Room, type = error} = Err = recv_message(Config), + #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), + ct:comment("Checking if the master is still able to send invitations"), + put_event(Config, send_invitation), + #message{from = Room, type = normal} = recv_message(Config), + ok = leave(Config), + disconnect(Config). + +config_visitor_status_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + Status = xmpp:mk_text(p1_rand:get_string()), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + ct:comment("Asking the slave to join as a visitor"), + put_event(Config, {join, Status}), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + ct:comment("Receiving status change from the visitor"), + #presence{from = PeerNickJID, status = Status} = recv_presence(Config), + [104] = set_config(Config, [{allow_visitor_status, false}]), + ct:comment("Receiving status change with <status/> stripped"), + #presence{from = PeerNickJID, status = []} = recv_presence(Config), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_visitor_status_slave(Config) -> + Room = muc_room_jid(Config), + MyNickJID = my_muc_jid(Config), + ct:comment("Waiting for 'join' command from the master"), + {join, Status} = get_event(Config), + {[], _, _} = join(Config, visitor, none), + ct:comment("Sending status change"), + send(Config, #presence{to = Room, status = Status}), + #presence{from = MyNickJID, status = Status} = recv_presence(Config), + [104] = recv_config_change_message(Config), + ct:comment("Sending status change again"), + send(Config, #presence{to = Room, status = Status}), + #presence{from = MyNickJID, status = []} = recv_presence(Config), + ok = leave(Config), + disconnect(Config). + +config_allow_voice_requests_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + ct:comment("Asking the slave to join as a visitor"), + put_event(Config, join), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + [104] = set_config(Config, [{allow_voice_requests, false}]), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_allow_voice_requests_slave(Config) -> + Room = muc_room_jid(Config), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {[], _, _} = join(Config, visitor), + [104] = recv_config_change_message(Config), + ct:comment("Fail sending voice request"), + Fs = muc_request:encode([{role, participant}]), + X = #xdata{type = submit, fields = Fs}, + send(Config, #message{to = Room, sub_els = [X]}), + #message{from = Room, type = error} = Err = recv_message(Config), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err), + ok = leave(Config), + disconnect(Config). + +config_voice_request_interval_master(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(peer, Config), + PeerNick = ?config(peer_nick, Config), + PeerNickJID = peer_muc_jid(Config), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + ct:comment("Asking the slave to join as a visitor"), + put_event(Config, join), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + [104] = set_config(Config, [{voice_request_min_interval, 5}]), + ct:comment("Receiving a voice request from slave"), + #message{from = Room, type = normal} = recv_message(Config), + ct:comment("Deny voice request at first"), + Fs = muc_request:encode([{jid, PeerJID}, {role, participant}, + {roomnick, PeerNick}, {request_allow, false}]), + send(Config, #message{to = Room, sub_els = [#xdata{type = submit, + fields = Fs}]}), + put_event(Config, denied), + ct:comment("Waiting for repeated voice request from the slave"), + #message{from = Room, type = normal} = recv_message(Config), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_voice_request_interval_slave(Config) -> + Room = muc_room_jid(Config), + Fs = muc_request:encode([{role, participant}]), + X = #xdata{type = submit, fields = Fs}, + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {[], _, _} = join(Config, visitor), + [104] = recv_config_change_message(Config), + ct:comment("Sending voice request"), + send(Config, #message{to = Room, sub_els = [X]}), + ct:comment("Waiting for the master to deny our voice request"), + denied = get_event(Config), + ct:comment("Requesting voice again"), + send(Config, #message{to = Room, sub_els = [X]}), + ct:comment("Receving voice request error because we're sending to fast"), + #message{from = Room, type = error} = Err = recv_message(Config), + #stanza_error{reason = 'resource-constraint'} = xmpp:get_error(Err), + ct:comment("Waiting for 5 seconds"), + timer:sleep(timer:seconds(5)), + ct:comment("Repeating again"), + send(Config, #message{to = Room, sub_els = [X]}), + ok = leave(Config), + disconnect(Config). + +config_visitor_nickchange_master(Config) -> + PeerNickJID = peer_muc_jid(Config), + ok = join_new(Config), + [104] = set_config(Config, [{members_by_default, false}]), + ct:comment("Asking the slave to join as a visitor"), + put_event(Config, join), + ct:comment("Waiting for the slave to join"), + #muc_user{items = [#muc_item{role = visitor}]} = + recv_muc_presence(Config, PeerNickJID, available), + [104] = set_config(Config, [{allow_visitor_nickchange, false}]), + ct:comment("Waiting for the slave to leave"), + recv_muc_presence(Config, PeerNickJID, unavailable), + ok = leave(Config), + disconnect(Config). + +config_visitor_nickchange_slave(Config) -> + NewNick = p1_rand:get_string(), + MyNickJID = my_muc_jid(Config), + MyNewNickJID = jid:replace_resource(MyNickJID, NewNick), + ct:comment("Waiting for 'join' command from the master"), + join = get_event(Config), + {[], _, _} = join(Config, visitor), + [104] = recv_config_change_message(Config), + ct:comment("Fail trying to change nickname"), + send(Config, #presence{to = MyNewNickJID}), + #presence{from = MyNewNickJID, type = error} = Err = recv_presence(Config), + #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), + ok = leave(Config), + disconnect(Config). + +register_master(Config) -> + MUC = muc_jid(Config), + %% Register nick "master1" + register_nick(Config, MUC, <<"">>, <<"master1">>), + %% Unregister nick "master1" via jabber:register + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, to = MUC, + sub_els = [#register{remove = true}]}), + %% Register nick "master2" + register_nick(Config, MUC, <<"">>, <<"master2">>), + %% Now register nick "master" + register_nick(Config, MUC, <<"master2">>, <<"master">>), + %% Wait for slave to fail trying to register nick "master" + wait_for_slave(Config), + wait_for_slave(Config), + %% Now register empty ("") nick, which means we're unregistering + register_nick(Config, MUC, <<"master">>, <<"">>), + disconnect(Config). + +register_slave(Config) -> + MUC = muc_jid(Config), + wait_for_master(Config), + %% Trying to register occupied nick "master" + Fs = muc_register:encode([{roomnick, <<"master">>}]), + X = #xdata{type = submit, fields = Fs}, + #iq{type = error} = + send_recv(Config, #iq{type = set, to = MUC, + sub_els = [#register{xdata = X}]}), + wait_for_master(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("muc_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("muc_" ++ atom_to_list(T)), [parallel], + [list_to_atom("muc_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("muc_" ++ atom_to_list(T) ++ "_slave")]}. + +recv_muc_presence(Config, From, Type) -> + Pres = #presence{from = From, type = Type} = recv_presence(Config), + xmpp:get_subtag(Pres, #muc_user{}). + +join_new(Config) -> + join_new(Config, muc_room_jid(Config)). + +join_new(Config, Room) -> + MyJID = my_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + ct:comment("Joining new room ~p", [Room]), + send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), + #presence{from = Room, type = available} = recv_presence(Config), + %% As per XEP-0045 we MUST receive stanzas in the following order: + %% 1. In-room presence from other occupants + %% 2. In-room presence from the joining entity itself (so-called "self-presence") + %% 3. Room history (if any) + %% 4. The room subject + %% 5. Live messages, presence updates, new user joins, etc. + %% As this is the newly created room, we receive only the 2nd and 4th stanza. + #muc_user{ + status_codes = Codes, + items = [#muc_item{role = moderator, + jid = MyJID, + affiliation = owner}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Checking if codes '110' (self-presence) and " + "'201' (new room) is set"), + true = lists:member(110, Codes), + true = lists:member(201, Codes), + ct:comment("Receiving empty room subject"), + #message{from = Room, type = groupchat, body = [], + subject = [#text{data = <<>>}]} = recv_message(Config), + case ?config(persistent_room, Config) of + true -> + [104] = set_config(Config, [{persistentroom, true}], Room), + ok; + false -> + ok + end. + +recv_history_and_subject(Config) -> + ct:comment("Receiving room history and/or subject"), + recv_history_and_subject(Config, []). + +recv_history_and_subject(Config, History) -> + Room = muc_room_jid(Config), + #message{type = groupchat, subject = Subj, + body = Body, thread = Thread} = Msg = recv_message(Config), + case xmpp:get_subtag(Msg, #delay{}) of + #delay{from = Room} -> + recv_history_and_subject(Config, [Msg|History]); + false when Subj /= [], Body == [], Thread == undefined -> + {lists:reverse(History), Msg} + end. + +join(Config) -> + join(Config, participant, none, #muc{}). + +join(Config, Role) when is_atom(Role) -> + join(Config, Role, none, #muc{}); +join(Config, #muc{} = SubEl) -> + join(Config, participant, none, SubEl). + +join(Config, Role, Aff) when is_atom(Role), is_atom(Aff) -> + join(Config, Role, Aff, #muc{}); +join(Config, Role, #muc{} = SubEl) when is_atom(Role) -> + join(Config, Role, none, SubEl). + +join(Config, Role, Aff, SubEl) -> + ct:comment("Joining existing room as ~s/~s", [Aff, Role]), + MyJID = my_jid(Config), + Room = muc_room_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + PeerNick = ?config(peer_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + send(Config, #presence{to = MyNickJID, sub_els = [SubEl]}), + case recv_presence(Config) of + #presence{type = error, from = MyNickJID} = Err -> + xmpp:get_subtag(Err, #stanza_error{}); + #presence{from = Room, type = available} -> + case recv_presence(Config) of + #presence{type = available, from = PeerNickJID} = Pres -> + #muc_user{items = [#muc_item{role = moderator, + affiliation = owner}]} = + xmpp:get_subtag(Pres, #muc_user{}), + ct:comment("Receiving initial self-presence"), + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + jid = MyJID, + affiliation = Aff}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + {History, Subj} = recv_history_and_subject(Config), + {History, Subj, Codes}; + #presence{type = available, from = MyNickJID} = Pres -> + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + jid = MyJID, + affiliation = Aff}]} = + xmpp:get_subtag(Pres, #muc_user{}), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + {History, Subj} = recv_history_and_subject(Config), + {empty, History, Subj, Codes} + end + end. + +leave(Config) -> + leave(Config, muc_room_jid(Config)). + +leave(Config, Room) -> + MyJID = my_jid(Config), + MyNick = ?config(nick, Config), + MyNickJID = jid:replace_resource(Room, MyNick), + Mode = ?config(mode, Config), + IsPersistent = ?config(persistent_room, Config), + if Mode /= slave, IsPersistent -> + [104] = set_config(Config, [{persistentroom, false}], Room); + true -> + ok + end, + ct:comment("Leaving the room"), + send(Config, #presence{to = MyNickJID, type = unavailable}), + #presence{from = Room, type = unavailable} = recv_presence(Config), + #muc_user{ + status_codes = Codes, + items = [#muc_item{role = none, jid = MyJID}]} = + recv_muc_presence(Config, MyNickJID, unavailable), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + ok. + +get_config(Config) -> + ct:comment("Get room config"), + Room = muc_room_jid(Config), + case send_recv(Config, + #iq{type = get, to = Room, + sub_els = [#muc_owner{}]}) of + #iq{type = result, + sub_els = [#muc_owner{config = #xdata{type = form} = X}]} -> + muc_roomconfig:decode(X#xdata.fields); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +set_config(Config, RoomConfig) -> + set_config(Config, RoomConfig, muc_room_jid(Config)). + +set_config(Config, RoomConfig, Room) -> + ct:comment("Set room config: ~p", [RoomConfig]), + Fs = case RoomConfig of + [] -> []; + _ -> muc_roomconfig:encode(RoomConfig) + end, + case send_recv(Config, + #iq{type = set, to = Room, + sub_els = [#muc_owner{config = #xdata{type = submit, + fields = Fs}}]}) of + #iq{type = result, sub_els = []} -> + #presence{from = Room, type = available} = recv_presence(Config), + #message{from = Room, type = groupchat} = Msg = recv_message(Config), + #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), + lists:sort(Codes); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +create_persistent(Config) -> + [_|_] = get_config(Config), + [] = set_config(Config, [{persistentroom, true}], false), + ok. + +destroy(Config) -> + destroy(Config, <<>>). + +destroy(Config, Reason) -> + Room = muc_room_jid(Config), + AltRoom = alt_room_jid(Config), + ct:comment("Destroying a room"), + case send_recv(Config, + #iq{type = set, to = Room, + sub_els = [#muc_owner{destroy = #muc_destroy{ + reason = Reason, + jid = AltRoom}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +disco_items(Config) -> + MUC = muc_jid(Config), + ct:comment("Performing disco#items request to ~s", [jid:encode(MUC)]), + #iq{type = result, from = MUC, sub_els = [DiscoItems]} = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#disco_items{}]}), + lists:keysort(#disco_item.jid, DiscoItems#disco_items.items). + +disco_room_items(Config) -> + Room = muc_room_jid(Config), + #iq{type = result, from = Room, sub_els = [DiscoItems]} = + send_recv(Config, #iq{type = get, to = Room, + sub_els = [#disco_items{}]}), + DiscoItems#disco_items.items. + +get_affiliations(Config, Aff) -> + Room = muc_room_jid(Config), + case send_recv(Config, + #iq{type = get, to = Room, + sub_els = [#muc_admin{items = [#muc_item{affiliation = Aff}]}]}) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + Items; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +master_join(Config) -> + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerNick = ?config(slave_nick, Config), + PeerNickJID = jid:replace_resource(Room, PeerNick), + ok = join_new(Config), + wait_for_slave(Config), + #muc_user{items = [#muc_item{jid = PeerJID, + role = participant, + affiliation = none}]} = + recv_muc_presence(Config, PeerNickJID, available), + ok. + +slave_join(Config) -> + wait_for_master(Config), + join(Config). + +set_role(Config, Role, Reason) -> + ct:comment("Changing role to ~s", [Role]), + Room = muc_room_jid(Config), + PeerNick = ?config(slave_nick, Config), + case send_recv( + Config, + #iq{type = set, to = Room, + sub_els = + [#muc_admin{ + items = [#muc_item{role = Role, + reason = Reason, + nick = PeerNick}]}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +get_role(Config, Role) -> + ct:comment("Requesting list for role '~s'", [Role]), + Room = muc_room_jid(Config), + case send_recv( + Config, + #iq{type = get, to = Room, + sub_els = [#muc_admin{ + items = [#muc_item{role = Role}]}]}) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + lists:keysort(#muc_item.affiliation, Items); + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +set_affiliation(Config, Aff, Reason) -> + ct:comment("Changing affiliation to ~s", [Aff]), + Room = muc_room_jid(Config), + PeerJID = ?config(slave, Config), + PeerBareJID = jid:remove_resource(PeerJID), + case send_recv( + Config, + #iq{type = set, to = Room, + sub_els = + [#muc_admin{ + items = [#muc_item{affiliation = Aff, + reason = Reason, + jid = PeerBareJID}]}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +get_affiliation(Config, Aff) -> + ct:comment("Requesting list for affiliation '~s'", [Aff]), + Room = muc_room_jid(Config), + case send_recv( + Config, + #iq{type = get, to = Room, + sub_els = [#muc_admin{ + items = [#muc_item{affiliation = Aff}]}]}) of + #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> + Items; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +set_vcard(Config, VCard) -> + Room = muc_room_jid(Config), + ct:comment("Setting vCard for ~s", [jid:encode(Room)]), + case send_recv(Config, #iq{type = set, to = Room, + sub_els = [VCard]}) of + #iq{type = result, sub_els = []} -> + [104] = recv_config_change_message(Config), + ok; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +get_vcard(Config) -> + Room = muc_room_jid(Config), + ct:comment("Retreiving vCard from ~s", [jid:encode(Room)]), + case send_recv(Config, #iq{type = get, to = Room, + sub_els = [#vcard_temp{}]}) of + #iq{type = result, sub_els = [VCard]} -> + VCard; + #iq{type = error} = Err -> + xmpp:get_subtag(Err, #stanza_error{}) + end. + +recv_config_change_message(Config) -> + ct:comment("Receiving configuration change notification message"), + Room = muc_room_jid(Config), + #presence{from = Room, type = available} = recv_presence(Config), + #message{type = groupchat, from = Room} = Msg = recv_message(Config), + #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), + lists:sort(Codes). + +register_nick(Config, MUC, PrevNick, Nick) -> + PrevRegistered = if PrevNick /= <<"">> -> true; + true -> false + end, + NewRegistered = if Nick /= <<"">> -> true; + true -> false + end, + ct:comment("Requesting registration form"), + #iq{type = result, + sub_els = [#register{registered = PrevRegistered, + xdata = #xdata{type = form, + fields = FsWithoutNick}}]} = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#register{}]}), + ct:comment("Checking if previous nick is registered"), + PrevNick = proplists:get_value( + roomnick, muc_register:decode(FsWithoutNick)), + X = #xdata{type = submit, fields = muc_register:encode([{roomnick, Nick}])}, + ct:comment("Submitting registration form"), + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, to = MUC, + sub_els = [#register{xdata = X}]}), + ct:comment("Checking if new nick was registered"), + #iq{type = result, + sub_els = [#register{registered = NewRegistered, + xdata = #xdata{type = form, + fields = FsWithNick}}]} = + send_recv(Config, #iq{type = get, to = MUC, + sub_els = [#register{}]}), + Nick = proplists:get_value( + roomnick, muc_register:decode(FsWithNick)). + +subscribe(Config, Events, Room) -> + MyNick = ?config(nick, Config), + case send_recv(Config, + #iq{type = set, to = Room, + sub_els = [#muc_subscribe{nick = MyNick, + events = Events}]}) of + #iq{type = result, sub_els = [#muc_subscribe{events = ResEvents}]} -> + lists:sort(ResEvents); + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +unsubscribe(Config, Room) -> + case send_recv(Config, #iq{type = set, to = Room, + sub_els = [#muc_unsubscribe{}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. diff --git a/test/offline_tests.erl b/test/offline_tests.erl new file mode 100644 index 000000000..81a3f99c5 --- /dev/null +++ b/test/offline_tests.erl @@ -0,0 +1,525 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 7 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(offline_tests). + +%% API +-compile(export_all). +-import(suite, [send/2, disconnect/1, my_jid/1, send_recv/2, recv_message/1, + get_features/1, recv/1, get_event/1, server_jid/1, + wait_for_master/1, wait_for_slave/1, + connect/1, open_session/1, bind/1, auth/1]). +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +single_cases() -> + {offline_single, [sequence], + [single_test(feature_enabled), + single_test(check_identity), + single_test(send_non_existent), + single_test(view_non_existent), + single_test(remove_non_existent), + single_test(view_non_integer), + single_test(remove_non_integer), + single_test(malformed_iq), + single_test(wrong_user), + single_test(unsupported_iq)]}. + +feature_enabled(Config) -> + Features = get_features(Config), + ct:comment("Checking if offline features are set"), + true = lists:member(?NS_FEATURE_MSGOFFLINE, Features), + true = lists:member(?NS_FLEX_OFFLINE, Features), + disconnect(Config). + +check_identity(Config) -> + #iq{type = result, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE, + identities = Ids}]} = + send_recv(Config, #iq{type = get, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE}]}), + true = lists:any( + fun(#identity{category = <<"automation">>, + type = <<"message-list">>}) -> true; + (_) -> false + end, Ids), + disconnect(Config). + +send_non_existent(Config) -> + Server = ?config(server, Config), + To = jid:make(<<"non-existent">>, Server), + #message{type = error} = Err = send_recv(Config, #message{to = To}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err), + disconnect(Config). + +view_non_existent(Config) -> + #stanza_error{reason = 'item-not-found'} = view(Config, [rand_string()], false), + disconnect(Config). + +remove_non_existent(Config) -> + ok = remove(Config, [rand_string()]), + disconnect(Config). + +view_non_integer(Config) -> + #stanza_error{reason = 'item-not-found'} = view(Config, [<<"foo">>], false), + disconnect(Config). + +remove_non_integer(Config) -> + #stanza_error{reason = 'item-not-found'} = remove(Config, [<<"foo">>]), + disconnect(Config). + +malformed_iq(Config) -> + Item = #offline_item{node = rand_string()}, + Range = [{Type, SubEl} || Type <- [set, get], + SubEl <- [#offline{items = [], _ = false}, + #offline{items = [Item], _ = true}]] + ++ [{set, #offline{items = [], fetch = true, purge = false}}, + {set, #offline{items = [Item], fetch = true, purge = false}}, + {get, #offline{items = [], fetch = false, purge = true}}, + {get, #offline{items = [Item], fetch = false, purge = true}}], + lists:foreach( + fun({Type, SubEl}) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, sub_els = [SubEl]}), + #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err) + end, Range), + disconnect(Config). + +wrong_user(Config) -> + Server = ?config(server, Config), + To = jid:make(<<"foo">>, Server), + Item = #offline_item{node = rand_string()}, + Range = [{Type, Items, Purge, Fetch} || + Type <- [set, get], + Items <- [[], [Item]], + Purge <- [false, true], + Fetch <- [false, true]], + lists:foreach( + fun({Type, Items, Purge, Fetch}) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, to = To, + sub_els = [#offline{items = Items, + purge = Purge, + fetch = Fetch}]}), + #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err) + end, Range), + disconnect(Config). + +unsupported_iq(Config) -> + Item = #offline_item{node = rand_string()}, + lists:foreach( + fun(Type) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, sub_els = [Item]}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, [set, get]), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases(DB) -> + {offline_master_slave, [sequence], + [master_slave_test(flex), + master_slave_test(send_all), + master_slave_test(from_mam), + master_slave_test(mucsub_mam)]}. + +flex_master(Config) -> + send_messages(Config, 5), + disconnect(Config). + +flex_slave(Config) -> + wait_for_master(Config), + peer_down = get_event(Config), + 5 = get_number(Config), + Nodes = get_nodes(Config), + %% Since headers are received we can send initial presence without a risk + %% of getting offline messages flood + #presence{} = send_recv(Config, #presence{}), + ct:comment("Checking fetch"), + Nodes = fetch(Config, lists:seq(1, 5)), + ct:comment("Fetching 2nd and 4th message"), + [2, 4] = view(Config, [lists:nth(2, Nodes), lists:nth(4, Nodes)]), + ct:comment("Deleting 2nd and 4th message"), + ok = remove(Config, [lists:nth(2, Nodes), lists:nth(4, Nodes)]), + ct:comment("Checking if messages were deleted"), + [1, 3, 5] = view(Config, [lists:nth(1, Nodes), + lists:nth(3, Nodes), + lists:nth(5, Nodes)]), + ct:comment("Purging everything left"), + ok = purge(Config), + ct:comment("Checking if there are no offline messages"), + 0 = get_number(Config), + clean(disconnect(Config)). + +from_mam_master(Config) -> + C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}), + C3 = send_all_master(C2), + lists:keydelete(mam_enabled, 1, C3). + +from_mam_slave(Config) -> + Server = ?config(server, Config), + gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => true}), + ok = mam_tests:set_default(Config, always), + C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}), + C3 = send_all_slave(C2), + gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => false}), + C4 = lists:keydelete(mam_enabled, 1, C3), + mam_tests:clean(C4). + +mucsub_mam_master(Config) -> + Room = suite:muc_room_jid(Config), + Peer = ?config(peer, Config), + wait_for_slave(Config), + ct:comment("Joining muc room"), + ok = muc_tests:join_new(Config), + + ct:comment("Enabling mam in room"), + CfgOpts = muc_tests:get_config(Config), + %% Find the MAM field in the config + ?match(true, proplists:is_defined(mam, CfgOpts)), + ?match(true, proplists:is_defined(allow_subscription, CfgOpts)), + %% Enable MAM + [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]), + + ct:comment("Subscribing peer to room"), + ?send_recv(#iq{to = Room, type = set, sub_els = [ + #muc_subscribe{jid = Peer, nick = <<"peer">>, + events = [?NS_MUCSUB_NODES_MESSAGES]} + ]}, #iq{type = result}), + + ?match(#message{type = groupchat}, + send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"1">>)})), + ?match(#message{type = groupchat}, + send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"2">>), + sub_els = [#hint{type = 'no-store'}]})), + ?match(#message{type = groupchat}, + send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"3">>)})), + + ct:comment("Cleaning up"), + suite:put_event(Config, ready), + ready = get_event(Config), + muc_tests:leave(Config), + mam_tests:clean(clean(disconnect(Config))). + +mucsub_mam_slave(Config) -> + Server = ?config(server, Config), + gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => true}), + gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => true}), + + Room = suite:muc_room_jid(Config), + MyJID = my_jid(Config), + MyJIDBare = jid:remove_resource(MyJID), + ok = mam_tests:set_default(Config, always), + #presence{} = send_recv(Config, #presence{}), + send(Config, #presence{type = unavailable}), + + wait_for_master(Config), + ready = get_event(Config), + ct:sleep(100), + + ct:comment("Receiving offline messages"), + + ?match(#presence{}, suite:send_recv(Config, #presence{})), + + lists:foreach( + fun(N) -> + Body = xmpp:mk_text(integer_to_binary(N)), + Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), + PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ + #ps_item{} = PS + ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS), + ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) + end, [1, 3]), + + % Unsubscribe yourself + ?send_recv(#iq{to = Room, type = set, sub_els = [ + #muc_unsubscribe{} + ]}, #iq{type = result}), + suite:put_event(Config, ready), + mam_tests:clean(clean(disconnect(Config))), + gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => false}), + gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => false}). + +send_all_master(Config) -> + wait_for_slave(Config), + Peer = ?config(peer, Config), + BarePeer = jid:remove_resource(Peer), + {Deliver, Errors} = message_iterator(Config), + N = lists:foldl( + fun(#message{type = error} = Msg, Acc) -> + send(Config, Msg#message{to = BarePeer}), + Acc; + (Msg, Acc) -> + I = send(Config, Msg#message{to = BarePeer}), + case {xmpp:get_subtag(Msg, #offline{}), xmpp:get_subtag(Msg, #xevent{})} of + {#offline{}, _} -> + ok; + {_, #xevent{offline = true, id = undefined}} -> + ct:comment("Receiving event-reply for:~n~s", + [xmpp:pp(Msg)]), + #message{} = Reply = recv_message(Config), + #xevent{id = I} = xmpp:get_subtag(Reply, #xevent{}); + _ -> + ok + end, + Acc + 1 + end, 0, Deliver), + lists:foreach( + fun(#message{type = headline} = Msg) -> + send(Config, Msg#message{to = BarePeer}); + (Msg) -> + #message{type = error} = Err = + send_recv(Config, Msg#message{to = BarePeer}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, Errors), + ok = wait_for_complete(Config, N), + disconnect(Config). + +send_all_slave(Config) -> + ServerJID = server_jid(Config), + Peer = ?config(peer, Config), + #presence{} = send_recv(Config, #presence{}), + send(Config, #presence{type = unavailable}), + wait_for_master(Config), + peer_down = get_event(Config), + #presence{} = send_recv(Config, #presence{}), + {Deliver, _Errors} = message_iterator(Config), + lists:foreach( + fun(#message{type = error}) -> + ok; + (#message{type = Type, body = Body, subject = Subject} = Msg) -> + ct:comment("Receiving message:~n~s", [xmpp:pp(Msg)]), + #message{from = Peer, + type = Type, + body = Body, + subject = Subject} = RecvMsg = recv_message(Config), + ct:comment("Checking if delay tag is correctly set"), + #delay{from = ServerJID} = xmpp:get_subtag(RecvMsg, #delay{}) + end, Deliver), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("offline_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("offline_" ++ atom_to_list(T)), [parallel], + [list_to_atom("offline_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("offline_" ++ atom_to_list(T) ++ "_slave")]}. + +clean(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + mod_offline:remove_user(U, S), + Config. + +send_messages(Config, Num) -> + send_messages(Config, Num, normal, []). + +send_messages(Config, Num, Type, SubEls) -> + wait_for_slave(Config), + Peer = ?config(peer, Config), + BarePeer = jid:remove_resource(Peer), + lists:foreach( + fun(I) -> + Body = integer_to_binary(I), + send(Config, + #message{to = BarePeer, + type = Type, + body = [#text{data = Body}], + subject = [#text{data = <<"subject">>}], + sub_els = SubEls}) + end, lists:seq(1, Num)), + ct:comment("Waiting for all messages to be delivered to offline spool"), + ok = wait_for_complete(Config, Num). + +recv_messages(Config, Num) -> + wait_for_master(Config), + peer_down = get_event(Config), + Peer = ?config(peer, Config), + #presence{} = send_recv(Config, #presence{}), + lists:foreach( + fun(I) -> + Text = integer_to_binary(I), + #message{sub_els = SubEls, + from = Peer, + body = [#text{data = Text}], + subject = [#text{data = <<"subject">>}]} = + recv_message(Config), + true = lists:keymember(delay, 1, SubEls) + end, lists:seq(1, Num)), + clean(disconnect(Config)). + +get_number(Config) -> + ct:comment("Getting offline message number"), + #iq{type = result, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE, + xdata = [X]}]} = + send_recv(Config, #iq{type = get, + sub_els = [#disco_info{ + node = ?NS_FLEX_OFFLINE}]}), + Form = flex_offline:decode(X#xdata.fields), + proplists:get_value(number_of_messages, Form). + +get_nodes(Config) -> + MyJID = my_jid(Config), + MyBareJID = jid:remove_resource(MyJID), + Peer = ?config(peer, Config), + Peer_s = jid:encode(Peer), + ct:comment("Getting headers"), + #iq{type = result, + sub_els = [#disco_items{ + node = ?NS_FLEX_OFFLINE, + items = DiscoItems}]} = + send_recv(Config, #iq{type = get, + sub_els = [#disco_items{ + node = ?NS_FLEX_OFFLINE}]}), + ct:comment("Checking if headers are correct"), + lists:sort( + lists:map( + fun(#disco_item{jid = J, name = P, node = N}) + when (J == MyBareJID) and (P == Peer_s) -> + N + end, DiscoItems)). + +fetch(Config, Range) -> + ID = send(Config, #iq{type = get, sub_els = [#offline{fetch = true}]}), + Nodes = lists:map( + fun(I) -> + Text = integer_to_binary(I), + #message{body = Body, sub_els = SubEls} = recv(Config), + [#text{data = Text}] = Body, + #offline{items = [#offline_item{node = Node}]} = + lists:keyfind(offline, 1, SubEls), + #delay{} = lists:keyfind(delay, 1, SubEls), + Node + end, Range), + #iq{id = ID, type = result, sub_els = []} = recv(Config), + Nodes. + +view(Config, Nodes) -> + view(Config, Nodes, true). + +view(Config, Nodes, NeedReceive) -> + Items = lists:map( + fun(Node) -> + #offline_item{action = view, node = Node} + end, Nodes), + I = send(Config, + #iq{type = get, sub_els = [#offline{items = Items}]}), + Range = if NeedReceive -> + lists:map( + fun(Node) -> + #message{body = [#text{data = Text}], + sub_els = SubEls} = recv(Config), + #offline{items = [#offline_item{node = Node}]} = + lists:keyfind(offline, 1, SubEls), + binary_to_integer(Text) + end, Nodes); + true -> + [] + end, + case recv(Config) of + #iq{id = I, type = result, sub_els = []} -> Range; + #iq{id = I, type = error} = Err -> xmpp:get_error(Err) + end. + +remove(Config, Nodes) -> + Items = lists:map( + fun(Node) -> + #offline_item{action = remove, node = Node} + end, Nodes), + case send_recv(Config, #iq{type = set, + sub_els = [#offline{items = Items}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +purge(Config) -> + case send_recv(Config, #iq{type = set, + sub_els = [#offline{purge = true}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +wait_for_complete(_Config, 0) -> + ok; +wait_for_complete(Config, N) -> + {U, S, _} = jid:tolower(?config(peer, Config)), + lists:foldl( + fun(_Time, ok) -> + ok; + (Time, Acc) -> + timer:sleep(Time), + case mod_offline:count_offline_messages(U, S) of + N -> ok; + _ -> Acc + end + end, error, [0, 100, 200, 2000, 5000, 10000]). + +message_iterator(Config) -> + ServerJID = server_jid(Config), + ChatStates = [[#chatstate{type = composing}]], + Offline = [[#offline{}]], + Hints = [[#hint{type = T}] || T <- [store, 'no-store']], + XEvent = [[#xevent{id = ID, offline = OfflineFlag}] + || ID <- [undefined, rand_string()], + OfflineFlag <- [false, true]], + Delay = [[#delay{stamp = p1_time_compat:timestamp(), from = ServerJID}]], + AllEls = [Els1 ++ Els2 || Els1 <- [[]] ++ ChatStates ++ Delay ++ Hints ++ Offline, + Els2 <- [[]] ++ XEvent], + All = [#message{type = Type, body = Body, subject = Subject, sub_els = Els} + || %%Type <- [chat], + Type <- [error, chat, normal, groupchat, headline], + Body <- [[], xmpp:mk_text(<<"body">>)], + Subject <- [[], xmpp:mk_text(<<"subject">>)], + Els <- AllEls], + MamEnabled = ?config(mam_enabled, Config) == true, + lists:partition( + fun(#message{type = error}) -> true; + (#message{type = groupchat}) -> false; + (#message{sub_els = [#hint{type = store}|_]}) when MamEnabled -> true; + (#message{sub_els = [#offline{}|_]}) when not MamEnabled -> false; + (#message{sub_els = [_, #xevent{id = I}]}) when I /= undefined, not MamEnabled -> false; + (#message{sub_els = [#xevent{id = I}]}) when I /= undefined, not MamEnabled -> false; + (#message{sub_els = [#hint{type = store}|_]}) -> true; + (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; + (#message{body = [], subject = []}) -> false; + (#message{type = Type}) -> (Type == chat) or (Type == normal); + (_) -> false + end, All). + +rand_string() -> + integer_to_binary(p1_rand:uniform((1 bsl 31)-1)). diff --git a/test/privacy_tests.erl b/test/privacy_tests.erl new file mode 100644 index 000000000..f27a08b82 --- /dev/null +++ b/test/privacy_tests.erl @@ -0,0 +1,891 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 18 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(privacy_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, send_recv/2, get_event/1, put_event/2, + recv_iq/1, recv_presence/1, recv_message/1, recv/1, + send/2, my_jid/1, server_jid/1, get_features/1, + set_roster/3, del_roster/1, get_roster/1]). +-include("suite.hrl"). +-include("mod_roster.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single cases +%%%=================================================================== +single_cases() -> + {privacy_single, [sequence], + [single_test(feature_enabled), + single_test(set_get_list), + single_test(get_list_non_existent), + single_test(get_empty_lists), + single_test(set_default), + single_test(del_default), + single_test(set_default_non_existent), + single_test(set_active), + single_test(del_active), + single_test(set_active_non_existent), + single_test(remove_list), + single_test(remove_default_list), + single_test(remove_active_list), + single_test(remove_list_non_existent), + single_test(allow_local_server), + single_test(malformed_iq_query), + single_test(malformed_get), + single_test(malformed_set), + single_test(malformed_type_value), + single_test(set_get_block)]}. + +feature_enabled(Config) -> + Features = get_features(Config), + true = lists:member(?NS_PRIVACY, Features), + true = lists:member(?NS_BLOCKING, Features), + disconnect(Config). + +set_get_list(Config) -> + ListName = <<"set-get-list">>, + Items = [#privacy_item{order = 0, action = deny, + type = jid, value = <<"user@jabber.org">>, + iq = true}, + #privacy_item{order = 1, action = allow, + type = group, value = <<"group">>, + message = true}, + #privacy_item{order = 2, action = allow, + type = subscription, value = <<"both">>, + presence_in = true}, + #privacy_item{order = 3, action = deny, + type = subscription, value = <<"from">>, + presence_out = true}, + #privacy_item{order = 4, action = deny, + type = subscription, value = <<"to">>, + iq = true, message = true}, + #privacy_item{order = 5, action = deny, + type = subscription, value = <<"none">>, + _ = true}, + #privacy_item{order = 6, action = deny}], + ok = set_items(Config, ListName, Items), + #privacy_list{name = ListName, items = Items1} = get_list(Config, ListName), + Items = lists:keysort(#privacy_item.order, Items1), + del_privacy(disconnect(Config)). + +get_list_non_existent(Config) -> + ListName = <<"get-list-non-existent">>, + #stanza_error{reason = 'item-not-found'} = get_list(Config, ListName), + disconnect(Config). + +get_empty_lists(Config) -> + #privacy_query{default = none, + active = none, + lists = []} = get_lists(Config), + disconnect(Config). + +set_default(Config) -> + ListName = <<"set-default">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_default(Config, ListName), + #privacy_query{default = ListName} = get_lists(Config), + del_privacy(disconnect(Config)). + +del_default(Config) -> + ListName = <<"del-default">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_default(Config, ListName), + #privacy_query{default = ListName} = get_lists(Config), + ok = set_default(Config, none), + #privacy_query{default = none} = get_lists(Config), + del_privacy(disconnect(Config)). + +set_default_non_existent(Config) -> + ListName = <<"set-default-non-existent">>, + #stanza_error{reason = 'item-not-found'} = set_default(Config, ListName), + disconnect(Config). + +set_active(Config) -> + ListName = <<"set-active">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + #privacy_query{active = ListName} = get_lists(Config), + del_privacy(disconnect(Config)). + +del_active(Config) -> + ListName = <<"del-active">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + #privacy_query{active = ListName} = get_lists(Config), + ok = set_active(Config, none), + #privacy_query{active = none} = get_lists(Config), + del_privacy(disconnect(Config)). + +set_active_non_existent(Config) -> + ListName = <<"set-active-non-existent">>, + #stanza_error{reason = 'item-not-found'} = set_active(Config, ListName), + disconnect(Config). + +remove_list(Config) -> + ListName = <<"remove-list">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = del_list(Config, ListName), + #privacy_query{lists = []} = get_lists(Config), + del_privacy(disconnect(Config)). + +remove_active_list(Config) -> + ListName = <<"remove-active-list">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + #stanza_error{reason = 'conflict'} = del_list(Config, ListName), + del_privacy(disconnect(Config)). + +remove_default_list(Config) -> + ListName = <<"remove-default-list">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_default(Config, ListName), + #stanza_error{reason = 'conflict'} = del_list(Config, ListName), + del_privacy(disconnect(Config)). + +remove_list_non_existent(Config) -> + ListName = <<"remove-list-non-existent">>, + #stanza_error{reason = 'item-not-found'} = del_list(Config, ListName), + disconnect(Config). + +allow_local_server(Config) -> + ListName = <<"allow-local-server">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + %% Whatever privacy rules are set, we should always communicate + %% with our home server + server_send_iqs(Config), + server_recv_iqs(Config), + send_stanzas_to_server_resource(Config), + del_privacy(disconnect(Config)). + +malformed_iq_query(Config) -> + lists:foreach( + fun(Type) -> + #iq{type = error} = + send_recv(Config, + #iq{type = Type, + sub_els = [#privacy_list{name = <<"foo">>}]}) + end, [get, set]), + disconnect(Config). + +malformed_get(Config) -> + JID = jid:make(p1_rand:get_string()), + Item = #block_item{jid = JID}, + lists:foreach( + fun(SubEl) -> + #iq{type = error} = + send_recv(Config, #iq{type = get, sub_els = [SubEl]}) + end, [#privacy_query{active = none}, + #privacy_query{default = none}, + #privacy_query{lists = [#privacy_list{name = <<"1">>}, + #privacy_list{name = <<"2">>}]}, + #block{items = [Item]}, #unblock{items = [Item]}, + #block{}, #unblock{}]), + disconnect(Config). + +malformed_set(Config) -> + lists:foreach( + fun(SubEl) -> + #iq{type = error} = + send_recv(Config, #iq{type = set, sub_els = [SubEl]}) + end, [#privacy_query{active = none, default = none}, + #privacy_query{lists = [#privacy_list{name = <<"1">>}, + #privacy_list{name = <<"2">>}]}, + #block{}, + #block_list{}, + #block_list{ + items = [#block_item{ + jid = jid:make(p1_rand:get_string())}]}]), + disconnect(Config). + +malformed_type_value(Config) -> + Item = #privacy_item{order = 0, action = deny}, + #stanza_error{reason = 'bad-request'} = + set_items(Config, <<"malformed-jid">>, + [Item#privacy_item{type = jid, value = <<"@bad">>}]), + #stanza_error{reason = 'bad-request'} = + set_items(Config, <<"malformed-group">>, + [Item#privacy_item{type = group, value = <<"">>}]), + #stanza_error{reason = 'bad-request'} = + set_items(Config, <<"malformed-subscription">>, + [Item#privacy_item{type = subscription, value = <<"bad">>}]), + disconnect(Config). + +set_get_block(Config) -> + J1 = jid:make(p1_rand:get_string(), p1_rand:get_string()), + J2 = jid:make(p1_rand:get_string(), p1_rand:get_string()), + {ok, ListName} = set_block(Config, [J1, J2]), + JIDs = get_block(Config), + JIDs = lists:sort([J1, J2]), + {ok, ListName} = set_unblock(Config, [J2, J1]), + [] = get_block(Config), + del_privacy(disconnect(Config)). + +%%%=================================================================== +%%% Master-slave cases +%%%=================================================================== +master_slave_cases() -> + {privacy_master_slave, [sequence], + [master_slave_test(deny_bare_jid), + master_slave_test(deny_full_jid), + master_slave_test(deny_server_bare_jid), + master_slave_test(deny_server_full_jid), + master_slave_test(deny_group), + master_slave_test(deny_sub_both), + master_slave_test(deny_sub_from), + master_slave_test(deny_sub_to), + master_slave_test(deny_sub_none), + master_slave_test(deny_all), + master_slave_test(deny_offline), + master_slave_test(block), + master_slave_test(unblock), + master_slave_test(unblock_all)]}. + +deny_bare_jid_master(Config) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + deny_master(Config, {jid, jid:encode(PeerBareJID)}). + +deny_bare_jid_slave(Config) -> + deny_slave(Config). + +deny_full_jid_master(Config) -> + PeerJID = ?config(peer, Config), + deny_master(Config, {jid, jid:encode(PeerJID)}). + +deny_full_jid_slave(Config) -> + deny_slave(Config). + +deny_server_bare_jid_master(Config) -> + {_, Server, _} = jid:tolower(?config(peer, Config)), + deny_master(Config, {jid, Server}). + +deny_server_bare_jid_slave(Config) -> + deny_slave(Config). + +deny_server_full_jid_master(Config) -> + {_, Server, Resource} = jid:tolower(?config(peer, Config)), + deny_master(Config, {jid, jid:encode({<<"">>, Server, Resource})}). + +deny_server_full_jid_slave(Config) -> + deny_slave(Config). + +deny_group_master(Config) -> + Group = p1_rand:get_string(), + deny_master(Config, {group, Group}). + +deny_group_slave(Config) -> + deny_slave(Config). + +deny_sub_both_master(Config) -> + deny_master(Config, {subscription, <<"both">>}). + +deny_sub_both_slave(Config) -> + deny_slave(Config, 2). + +deny_sub_from_master(Config) -> + deny_master(Config, {subscription, <<"from">>}). + +deny_sub_from_slave(Config) -> + deny_slave(Config, 1). + +deny_sub_to_master(Config) -> + deny_master(Config, {subscription, <<"to">>}). + +deny_sub_to_slave(Config) -> + deny_slave(Config, 2). + +deny_sub_none_master(Config) -> + deny_master(Config, {subscription, <<"none">>}). + +deny_sub_none_slave(Config) -> + deny_slave(Config). + +deny_all_master(Config) -> + deny_master(Config, {undefined, <<"">>}). + +deny_all_slave(Config) -> + deny_slave(Config). + +deny_master(Config, {Type, Value}) -> + Sub = if Type == subscription -> + erlang:binary_to_atom(Value, utf8); + true -> + both + end, + Groups = if Type == group -> [Value]; + true -> [] + end, + set_roster(Config, Sub, Groups), + lists:foreach( + fun(Opts) -> + ct:pal("Set list for ~s, ~s, ~w", [Type, Value, Opts]), + ListName = p1_rand:get_string(), + Item = #privacy_item{order = 0, + action = deny, + iq = proplists:get_bool(iq, Opts), + message = proplists:get_bool(message, Opts), + presence_in = proplists:get_bool(presence_in, Opts), + presence_out = proplists:get_bool(presence_out, Opts), + type = Type, + value = Value}, + ok = set_items(Config, ListName, [Item]), + ok = set_active(Config, ListName), + put_event(Config, Opts), + case is_presence_in_blocked(Opts) of + true -> ok; + false -> recv_presences(Config) + end, + case is_iq_in_blocked(Opts) of + true -> ok; + false -> recv_iqs(Config) + end, + case is_message_in_blocked(Opts) of + true -> ok; + false -> recv_messages(Config) + end, + ct:comment("Waiting for 'send' command from the slave"), + send = get_event(Config), + case is_presence_out_blocked(Opts) of + true -> check_presence_blocked(Config, 'not-acceptable'); + false -> ok + end, + case is_iq_out_blocked(Opts) of + true -> check_iq_blocked(Config, 'not-acceptable'); + false -> send_iqs(Config) + end, + case is_message_out_blocked(Opts) of + true -> check_message_blocked(Config, 'not-acceptable'); + false -> send_messages(Config) + end, + case is_other_blocked(Opts) of + true -> + check_other_blocked(Config, 'not-acceptable', Value); + false -> ok + end, + ct:comment("Waiting for slave to finish processing our stanzas"), + done = get_event(Config) + end, + [[iq], [message], [presence_in], [presence_out], + [iq, message, presence_in, presence_out], []]), + put_event(Config, disconnect), + clean_up(disconnect(Config)). + +deny_slave(Config) -> + deny_slave(Config, 0). + +deny_slave(Config, RosterPushesCount) -> + set_roster(Config, both, []), + deny_slave(Config, RosterPushesCount, get_event(Config)). + +deny_slave(Config, RosterPushesCount, disconnect) -> + recv_roster_pushes(Config, RosterPushesCount), + clean_up(disconnect(Config)); +deny_slave(Config, RosterPushesCount, Opts) -> + send_presences(Config), + case is_iq_in_blocked(Opts) of + true -> check_iq_blocked(Config, 'service-unavailable'); + false -> send_iqs(Config) + end, + case is_message_in_blocked(Opts) of + true -> check_message_blocked(Config, 'service-unavailable'); + false -> send_messages(Config) + end, + put_event(Config, send), + case is_iq_out_blocked(Opts) of + true -> ok; + false -> recv_iqs(Config) + end, + case is_message_out_blocked(Opts) of + true -> ok; + false -> recv_messages(Config) + end, + put_event(Config, done), + deny_slave(Config, RosterPushesCount, get_event(Config)). + +deny_offline_master(Config) -> + set_roster(Config, both, []), + ListName = <<"deny-offline">>, + Item = #privacy_item{order = 0, action = deny}, + ok = set_items(Config, ListName, [Item]), + ok = set_default(Config, ListName), + NewConfig = disconnect(Config), + put_event(NewConfig, send), + ct:comment("Waiting for the slave to finish"), + done = get_event(NewConfig), + clean_up(NewConfig). + +deny_offline_slave(Config) -> + set_roster(Config, both, []), + ct:comment("Waiting for 'send' command from the master"), + send = get_event(Config), + send_presences(Config), + check_iq_blocked(Config, 'service-unavailable'), + check_message_blocked(Config, 'service-unavailable'), + put_event(Config, done), + clean_up(disconnect(Config)). + +block_master(Config) -> + PeerJID = ?config(peer, Config), + set_roster(Config, both, []), + {ok, _} = set_block(Config, [PeerJID]), + check_presence_blocked(Config, 'not-acceptable'), + check_iq_blocked(Config, 'not-acceptable'), + check_message_blocked(Config, 'not-acceptable'), + check_other_blocked(Config, 'not-acceptable', other), + %% We should always be able to communicate with our home server + server_send_iqs(Config), + server_recv_iqs(Config), + send_stanzas_to_server_resource(Config), + put_event(Config, send), + done = get_event(Config), + clean_up(disconnect(Config)). + +block_slave(Config) -> + set_roster(Config, both, []), + ct:comment("Waiting for 'send' command from master"), + send = get_event(Config), + send_presences(Config), + check_iq_blocked(Config, 'service-unavailable'), + check_message_blocked(Config, 'service-unavailable'), + put_event(Config, done), + clean_up(disconnect(Config)). + +unblock_master(Config) -> + PeerJID = ?config(peer, Config), + set_roster(Config, both, []), + {ok, ListName} = set_block(Config, [PeerJID]), + {ok, ListName} = set_unblock(Config, [PeerJID]), + put_event(Config, send), + recv_presences(Config), + recv_iqs(Config), + recv_messages(Config), + clean_up(disconnect(Config)). + +unblock_slave(Config) -> + set_roster(Config, both, []), + ct:comment("Waiting for 'send' command from master"), + send = get_event(Config), + send_presences(Config), + send_iqs(Config), + send_messages(Config), + clean_up(disconnect(Config)). + +unblock_all_master(Config) -> + PeerJID = ?config(peer, Config), + set_roster(Config, both, []), + {ok, ListName} = set_block(Config, [PeerJID]), + {ok, ListName} = set_unblock(Config, []), + put_event(Config, send), + recv_presences(Config), + recv_iqs(Config), + recv_messages(Config), + clean_up(disconnect(Config)). + +unblock_all_slave(Config) -> + set_roster(Config, both, []), + ct:comment("Waiting for 'send' command from master"), + send = get_event(Config), + send_presences(Config), + send_iqs(Config), + send_messages(Config), + clean_up(disconnect(Config)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("privacy_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("privacy_" ++ atom_to_list(T)), [parallel], + [list_to_atom("privacy_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("privacy_" ++ atom_to_list(T) ++ "_slave")]}. + +set_items(Config, Name, Items) -> + ct:comment("Setting privacy list ~s with items = ~p", [Name, Items]), + case send_recv( + Config, + #iq{type = set, sub_els = [#privacy_query{ + lists = [#privacy_list{ + name = Name, + items = Items}]}]}) of + #iq{type = result, sub_els = []} -> + ct:comment("Receiving privacy list push"), + #iq{type = set, id = ID, + sub_els = [#privacy_query{lists = [#privacy_list{ + name = Name}]}]} = + recv_iq(Config), + send(Config, #iq{type = result, id = ID}), + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_list(Config, Name) -> + ct:comment("Requesting privacy list ~s", [Name]), + case send_recv(Config, + #iq{type = get, + sub_els = [#privacy_query{ + lists = [#privacy_list{name = Name}]}]}) of + #iq{type = result, sub_els = [#privacy_query{lists = [List]}]} -> + List; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_lists(Config) -> + ct:comment("Requesting privacy lists"), + case send_recv(Config, #iq{type = get, sub_els = [#privacy_query{}]}) of + #iq{type = result, sub_els = [SubEl]} -> + SubEl; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +del_list(Config, Name) -> + case send_recv( + Config, + #iq{type = set, sub_els = [#privacy_query{ + lists = [#privacy_list{ + name = Name}]}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_active(Config, Name) -> + ct:comment("Setting active privacy list ~s", [Name]), + case send_recv( + Config, + #iq{type = set, sub_els = [#privacy_query{active = Name}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_default(Config, Name) -> + ct:comment("Setting default privacy list ~s", [Name]), + case send_recv( + Config, + #iq{type = set, sub_els = [#privacy_query{default = Name}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_block(Config) -> + case send_recv(Config, #iq{type = get, sub_els = [#block_list{}]}) of + #iq{type = result, sub_els = [#block_list{items = Items}]} -> + lists:sort([JID || #block_item{jid = JID} <- Items]); + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_block(Config, JIDs) -> + Items = [#block_item{jid = JID} || JID <- JIDs], + case send_recv(Config, #iq{type = set, + sub_els = [#block{items = Items}]}) of + #iq{type = result, sub_els = []} -> + {#iq{id = I1, sub_els = [#block{items = Items1}]}, + #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = + ?recv2(#iq{type = set, sub_els = [#block{}]}, + #iq{type = set, sub_els = [#privacy_query{}]}), + send(Config, #iq{type = result, id = I1}), + send(Config, #iq{type = result, id = I2}), + ct:comment("Checking if all JIDs present in the push"), + true = lists:sort(Items) == lists:sort(Items1), + ct:comment("Getting name of the corresponding privacy list"), + [#privacy_list{name = Name}] = Lists, + {ok, Name}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +set_unblock(Config, JIDs) -> + ct:comment("Unblocking ~p", [JIDs]), + Items = [#block_item{jid = JID} || JID <- JIDs], + case send_recv(Config, #iq{type = set, + sub_els = [#unblock{items = Items}]}) of + #iq{type = result, sub_els = []} -> + {#iq{id = I1, sub_els = [#unblock{items = Items1}]}, + #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = + ?recv2(#iq{type = set, sub_els = [#unblock{}]}, + #iq{type = set, sub_els = [#privacy_query{}]}), + send(Config, #iq{type = result, id = I1}), + send(Config, #iq{type = result, id = I2}), + ct:comment("Checking if all JIDs present in the push"), + true = lists:sort(Items) == lists:sort(Items1), + ct:comment("Getting name of the corresponding privacy list"), + [#privacy_list{name = Name}] = Lists, + {ok, Name}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +del_privacy(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + ct:comment("Removing all privacy data"), + mod_privacy:remove_user(U, S), + Config. + +clean_up(Config) -> + del_privacy(del_roster(Config)). + +check_iq_blocked(Config, Reason) -> + PeerJID = ?config(peer, Config), + ct:comment("Checking if all IQs are blocked"), + lists:foreach( + fun(Type) -> + send(Config, #iq{type = Type, to = PeerJID}) + end, [error, result]), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, to = PeerJID, + sub_els = [#ping{}]}), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, [set, get]). + +check_message_blocked(Config, Reason) -> + PeerJID = ?config(peer, Config), + ct:comment("Checking if all messages are blocked"), + %% TODO: do something with headline and groupchat. + %% The hack from 64d96778b452aad72349b21d2ac94e744617b07a + %% screws this up. + lists:foreach( + fun(Type) -> + send(Config, #message{type = Type, to = PeerJID}) + end, [error]), + lists:foreach( + fun(Type) -> + #message{type = error} = Err = + send_recv(Config, #message{type = Type, to = PeerJID}), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, [chat, normal]). + +check_presence_blocked(Config, Reason) -> + PeerJID = ?config(peer, Config), + ct:comment("Checking if all presences are blocked"), + lists:foreach( + fun(Type) -> + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = PeerJID}), + #stanza_error{reason = Reason} = xmpp:get_error(Err) + end, [available, unavailable]). + +recv_roster_pushes(_Config, 0) -> + ok; +recv_roster_pushes(Config, Count) -> + receive + #iq{type = set, sub_els = [#roster_query{}]} -> + recv_roster_pushes(Config, Count - 1) + end. + +recv_err_and_roster_pushes(Config, Count) -> + recv_roster_pushes(Config, Count), + recv_presence(Config). + +check_other_blocked(Config, Reason, Subscription) -> + PeerJID = ?config(peer, Config), + ct:comment("Checking if subscriptions and presence-errors are blocked"), + send(Config, #presence{type = error, to = PeerJID}), + {ErrorFor, PushFor} = case Subscription of + <<"both">> -> + {[subscribe, subscribed], + [unsubscribe, unsubscribed]}; + <<"from">> -> + {[subscribe, subscribed, unsubscribe], + [subscribe, unsubscribe, unsubscribed]}; + <<"to">> -> + {[unsubscribe], + [subscribed, unsubscribe, unsubscribed]}; + <<"none">> -> + {[subscribe, subscribed, unsubscribe, unsubscribed], + [subscribe, unsubscribe]}; + _ -> + {[subscribe, subscribed, unsubscribe, unsubscribed], + [unsubscribe, unsubscribed]} + end, + lists:foreach( + fun(Type) -> + send(Config, #presence{type = Type, to = PeerJID}), + Count = case lists:member(Type, PushFor) of true -> 1; _ -> 0 end, + case lists:member(Type, ErrorFor) of + true -> + Err = recv_err_and_roster_pushes(Config, Count), + #stanza_error{reason = Reason} = xmpp:get_error(Err); + _ -> + recv_roster_pushes(Config, Count) + end + end, [subscribe, subscribed, unsubscribe, unsubscribed]). + +send_presences(Config) -> + PeerJID = ?config(peer, Config), + ct:comment("Sending all types of presences to the peer"), + lists:foreach( + fun(Type) -> + send(Config, #presence{type = Type, to = PeerJID}) + end, [available, unavailable]). + +send_iqs(Config) -> + PeerJID = ?config(peer, Config), + ct:comment("Sending all types of IQs to the peer"), + lists:foreach( + fun(Type) -> + send(Config, #iq{type = Type, to = PeerJID}) + end, [set, get, error, result]). + +send_messages(Config) -> + PeerJID = ?config(peer, Config), + ct:comment("Sending all types of messages to the peer"), + lists:foreach( + fun(Type) -> + send(Config, #message{type = Type, to = PeerJID}) + end, [chat, error, groupchat, headline, normal]). + +recv_presences(Config) -> + PeerJID = ?config(peer, Config), + lists:foreach( + fun(Type) -> + #presence{type = Type, from = PeerJID} = + recv_presence(Config) + end, [available, unavailable]). + +recv_iqs(Config) -> + PeerJID = ?config(peer, Config), + lists:foreach( + fun(Type) -> + #iq{type = Type, from = PeerJID} = recv_iq(Config) + end, [set, get, error, result]). + +recv_messages(Config) -> + PeerJID = ?config(peer, Config), + lists:foreach( + fun(Type) -> + #message{type = Type, from = PeerJID} = recv_message(Config) + end, [chat, error, groupchat, headline, normal]). + +match_all(Opts) -> + IQ = proplists:get_bool(iq, Opts), + Message = proplists:get_bool(message, Opts), + PresenceIn = proplists:get_bool(presence_in, Opts), + PresenceOut = proplists:get_bool(presence_out, Opts), + not (IQ or Message or PresenceIn or PresenceOut). + +is_message_in_blocked(Opts) -> + proplists:get_bool(message, Opts) or match_all(Opts). + +is_message_out_blocked(Opts) -> + match_all(Opts). + +is_iq_in_blocked(Opts) -> + proplists:get_bool(iq, Opts) or match_all(Opts). + +is_iq_out_blocked(Opts) -> + match_all(Opts). + +is_presence_in_blocked(Opts) -> + proplists:get_bool(presence_in, Opts) or match_all(Opts). + +is_presence_out_blocked(Opts) -> + proplists:get_bool(presence_out, Opts) or match_all(Opts). + +is_other_blocked(Opts) -> + %% 'other' means subscriptions and presence-errors + match_all(Opts). + +server_send_iqs(Config) -> + ServerJID = server_jid(Config), + MyJID = my_jid(Config), + ct:comment("Sending IQs from ~s to ~s", + [jid:encode(ServerJID), jid:encode(MyJID)]), + lists:foreach( + fun(Type) -> + ejabberd_router:route( + #iq{from = ServerJID, to = MyJID, type = Type}) + end, [error, result]), + lists:foreach( + fun(Type) -> + ejabberd_local:route_iq( + #iq{from = ServerJID, to = MyJID, type = Type}, + fun(#iq{type = result, sub_els = []}) -> ok; + (IQ) -> ct:fail({unexpected_iq_result, IQ}) + end) + end, [set, get]). + +server_recv_iqs(Config) -> + ServerJID = server_jid(Config), + ct:comment("Receiving IQs from ~s", [jid:encode(ServerJID)]), + lists:foreach( + fun(Type) -> + #iq{type = Type, from = ServerJID} = recv_iq(Config) + end, [error, result]), + lists:foreach( + fun(Type) -> + #iq{type = Type, from = ServerJID, id = I} = recv_iq(Config), + send(Config, #iq{to = ServerJID, type = result, id = I}) + end, [set, get]). + +send_stanzas_to_server_resource(Config) -> + ServerJID = server_jid(Config), + ServerJIDResource = jid:replace_resource(ServerJID, <<"resource">>), + %% All stanzas sent should be handled by local_send_to_resource_hook + %% and should be bounced with item-not-found error + ct:comment("Sending IQs to ~s", [jid:encode(ServerJIDResource)]), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err = + send_recv(Config, #iq{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, [set, get]), + ct:comment("Sending messages to ~s", [jid:encode(ServerJIDResource)]), + lists:foreach( + fun(Type) -> + #message{type = error} = Err = + send_recv(Config, #message{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, [normal, chat, groupchat, headline]), + ct:comment("Sending presences to ~s", [jid:encode(ServerJIDResource)]), + lists:foreach( + fun(Type) -> + #presence{type = error} = Err = + send_recv(Config, #presence{type = Type, to = ServerJIDResource}), + #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) + end, [available, unavailable]). diff --git a/test/private_tests.erl b/test/private_tests.erl new file mode 100644 index 000000000..5ae832b36 --- /dev/null +++ b/test/private_tests.erl @@ -0,0 +1,117 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 23 Nov 2018 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%------------------------------------------------------------------- +-module(private_tests). + +%% API +-compile(export_all). +-import(suite, [my_jid/1, server_jid/1, is_feature_advertised/3, + send_recv/2, disconnect/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {private_single, [sequence], + [single_test(test_features), + single_test(test_no_namespace), + single_test(test_set_get), + single_test(test_published)]}. + +test_features(Config) -> + Server = jid:encode(server_jid(Config)), + MyJID = my_jid(Config), + case gen_mod:is_loaded(Server, mod_pubsub) of + true -> + true = is_feature_advertised(Config, ?NS_BOOKMARKS_CONVERSION_0, + jid:remove_resource(MyJID)); + false -> + ok + end, + disconnect(Config). + +test_no_namespace(Config) -> + WrongEl = #xmlel{name = <<"wrong">>}, + #iq{type = error} = + send_recv(Config, #iq{type = get, + sub_els = [#private{sub_els = [WrongEl]}]}), + disconnect(Config). + +test_set_get(Config) -> + Storage = bookmark_storage(), + StorageXMLOut = xmpp:encode(Storage), + #iq{type = result, sub_els = []} = + send_recv( + Config, #iq{type = set, + sub_els = [#private{sub_els = [StorageXMLOut]}]}), + #iq{type = result, + sub_els = [#private{sub_els = [StorageXMLIn]}]} = + send_recv( + Config, + #iq{type = get, + sub_els = [#private{sub_els = [xmpp:encode( + #bookmark_storage{})]}]}), + Storage = xmpp:decode(StorageXMLIn), + disconnect(Config). + +test_published(Config) -> + Server = jid:encode(server_jid(Config)), + case gen_mod:is_loaded(Server, mod_pubsub) of + true -> + Storage = bookmark_storage(), + Node = xmpp:get_ns(Storage), + #iq{type = result, + sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} = + send_recv( + Config, + #iq{type = get, + sub_els = [#pubsub{items = #ps_items{node = Node}}]}), + [#ps_item{sub_els = [StorageXMLIn]}] = Items, + Storage = xmpp:decode(StorageXMLIn), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{type = set, + sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}); + false -> + ok + end, + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("private_" ++ atom_to_list(T)). + +conference_bookmark() -> + #bookmark_conference{ + name = <<"Some name">>, + autojoin = true, + jid = jid:make(<<"some">>, <<"some.conference.org">>)}. + +bookmark_storage() -> + #bookmark_storage{conference = [conference_bookmark()]}. diff --git a/test/proxy65_tests.erl b/test/proxy65_tests.erl new file mode 100644 index 000000000..7685952d9 --- /dev/null +++ b/test/proxy65_tests.erl @@ -0,0 +1,129 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(proxy65_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, is_feature_advertised/3, proxy_jid/1, + my_jid/1, wait_for_slave/1, wait_for_master/1, + send_recv/2, put_event/2, get_event/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {proxy65_single, [sequence], + [single_test(feature_enabled), + single_test(service_vcard)]}. + +feature_enabled(Config) -> + true = is_feature_advertised(Config, ?NS_BYTESTREAMS, proxy_jid(Config)), + disconnect(Config). + +service_vcard(Config) -> + JID = proxy_jid(Config), + ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), + VCard = mod_proxy65_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {proxy65_master_slave, [sequence], + [master_slave_test(all)]}. + +all_master(Config) -> + Proxy = proxy_jid(Config), + MyJID = my_jid(Config), + Peer = ?config(slave, Config), + wait_for_slave(Config), + #presence{} = send_recv(Config, #presence{}), + #iq{type = result, sub_els = [#bytestreams{hosts = [StreamHost]}]} = + send_recv( + Config, + #iq{type = get, sub_els = [#bytestreams{}], to = Proxy}), + SID = p1_rand:get_string(), + Data = p1_rand:bytes(1024), + put_event(Config, {StreamHost, SID, Data}), + Socks5 = socks5_connect(StreamHost, {SID, MyJID, Peer}), + wait_for_slave(Config), + #iq{type = result, sub_els = []} = + send_recv(Config, + #iq{type = set, to = Proxy, + sub_els = [#bytestreams{activate = Peer, sid = SID}]}), + socks5_send(Socks5, Data), + disconnect(Config). + +all_slave(Config) -> + MyJID = my_jid(Config), + Peer = ?config(master, Config), + #presence{} = send_recv(Config, #presence{}), + wait_for_master(Config), + {StreamHost, SID, Data} = get_event(Config), + Socks5 = socks5_connect(StreamHost, {SID, Peer, MyJID}), + wait_for_master(Config), + socks5_recv(Socks5, Data), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("proxy65_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("proxy65_" ++ atom_to_list(T)), [parallel], + [list_to_atom("proxy65_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("proxy65_" ++ atom_to_list(T) ++ "_slave")]}. + +socks5_connect(#streamhost{host = Host, port = Port}, + {SID, JID1, JID2}) -> + Hash = p1_sha:sha([SID, jid:encode(JID1), jid:encode(JID2)]), + {ok, Sock} = gen_tcp:connect(binary_to_list(Host), Port, + [binary, {active, false}]), + Init = <<?VERSION_5, 1, ?AUTH_ANONYMOUS>>, + InitAck = <<?VERSION_5, ?AUTH_ANONYMOUS>>, + Req = <<?VERSION_5, ?CMD_CONNECT, 0, + ?ATYP_DOMAINNAME, 40, Hash:40/binary, 0, 0>>, + Resp = <<?VERSION_5, ?SUCCESS, 0, ?ATYP_DOMAINNAME, + 40, Hash:40/binary, 0, 0>>, + gen_tcp:send(Sock, Init), + {ok, InitAck} = gen_tcp:recv(Sock, size(InitAck)), + gen_tcp:send(Sock, Req), + {ok, Resp} = gen_tcp:recv(Sock, size(Resp)), + Sock. + +socks5_send(Sock, Data) -> + ok = gen_tcp:send(Sock, Data). + +socks5_recv(Sock, Data) -> + {ok, Data} = gen_tcp:recv(Sock, size(Data)). diff --git a/test/pubsub_tests.erl b/test/pubsub_tests.erl new file mode 100644 index 000000000..ac64185ff --- /dev/null +++ b/test/pubsub_tests.erl @@ -0,0 +1,764 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(pubsub_tests). + +%% API +-compile(export_all). +-import(suite, [pubsub_jid/1, send_recv/2, get_features/2, disconnect/1, + put_event/2, get_event/1, wait_for_master/1, wait_for_slave/1, + recv_message/1, my_jid/1, send/2, recv_presence/1, recv/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {pubsub_single, [sequence], + [single_test(test_features), + single_test(test_vcard), + single_test(test_create), + single_test(test_configure), + single_test(test_delete), + single_test(test_get_affiliations), + single_test(test_get_subscriptions), + single_test(test_create_instant), + single_test(test_default), + single_test(test_create_configure), + single_test(test_publish), + single_test(test_auto_create), + single_test(test_get_items), + single_test(test_delete_item), + single_test(test_purge), + single_test(test_subscribe), + single_test(test_subscribe_max_item_1), + single_test(test_unsubscribe)]}. + +test_features(Config) -> + PJID = pubsub_jid(Config), + AllFeatures = sets:from_list(get_features(Config, PJID)), + NeededFeatures = sets:from_list( + [?NS_PUBSUB, + ?PUBSUB("access-open"), + ?PUBSUB("access-authorize"), + ?PUBSUB("create-nodes"), + ?PUBSUB("instant-nodes"), + ?PUBSUB("config-node"), + ?PUBSUB("retrieve-default"), + ?PUBSUB("create-and-configure"), + ?PUBSUB("publish"), + ?PUBSUB("auto-create"), + ?PUBSUB("retrieve-items"), + ?PUBSUB("delete-items"), + ?PUBSUB("subscribe"), + ?PUBSUB("retrieve-affiliations"), + ?PUBSUB("modify-affiliations"), + ?PUBSUB("retrieve-subscriptions"), + ?PUBSUB("manage-subscriptions"), + ?PUBSUB("purge-nodes"), + ?PUBSUB("delete-nodes")]), + true = sets:is_subset(NeededFeatures, AllFeatures), + disconnect(Config). + +test_vcard(Config) -> + JID = pubsub_jid(Config), + ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), + VCard = mod_pubsub_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +test_create(Config) -> + Node = ?config(pubsub_node, Config), + Node = create_node(Config, Node), + disconnect(Config). + +test_create_instant(Config) -> + Node = create_node(Config, <<>>), + delete_node(Config, Node), + disconnect(Config). + +test_configure(Config) -> + Node = ?config(pubsub_node, Config), + NodeTitle = ?config(pubsub_node_title, Config), + NodeConfig = get_node_config(Config, Node), + MyNodeConfig = set_opts(NodeConfig, + [{title, NodeTitle}]), + set_node_config(Config, Node, MyNodeConfig), + NewNodeConfig = get_node_config(Config, Node), + NodeTitle = proplists:get_value(title, NewNodeConfig, <<>>), + disconnect(Config). + +test_default(Config) -> + get_default_node_config(Config), + disconnect(Config). + +test_create_configure(Config) -> + NodeTitle = ?config(pubsub_node_title, Config), + DefaultNodeConfig = get_default_node_config(Config), + CustomNodeConfig = set_opts(DefaultNodeConfig, + [{title, NodeTitle}]), + Node = create_node(Config, <<>>, CustomNodeConfig), + NodeConfig = get_node_config(Config, Node), + NodeTitle = proplists:get_value(title, NodeConfig, <<>>), + delete_node(Config, Node), + disconnect(Config). + +test_publish(Config) -> + Node = create_node(Config, <<>>), + publish_item(Config, Node), + delete_node(Config, Node), + disconnect(Config). + +test_auto_create(Config) -> + Node = p1_rand:get_string(), + publish_item(Config, Node), + delete_node(Config, Node), + disconnect(Config). + +test_get_items(Config) -> + Node = create_node(Config, <<>>), + ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)], + ItemsOut = get_items(Config, Node), + true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)] + == [I || #ps_item{id = I} <- lists:sort(ItemsOut)], + delete_node(Config, Node), + disconnect(Config). + +test_delete_item(Config) -> + Node = create_node(Config, <<>>), + #ps_item{id = I} = publish_item(Config, Node), + [#ps_item{id = I}] = get_items(Config, Node), + delete_item(Config, Node, I), + [] = get_items(Config, Node), + delete_node(Config, Node), + disconnect(Config). + +test_subscribe(Config) -> + Node = create_node(Config, <<>>), + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + [#ps_subscription{node = Node}] = get_subscriptions(Config), + delete_node(Config, Node), + disconnect(Config). + +test_subscribe_max_item_1(Config) -> + DefaultNodeConfig = get_default_node_config(Config), + CustomNodeConfig = set_opts(DefaultNodeConfig, + [{max_items, 1}]), + Node = create_node(Config, <<>>, CustomNodeConfig), + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + [#ps_subscription{node = Node}] = get_subscriptions(Config), + delete_node(Config, Node), + disconnect(Config). + +test_unsubscribe(Config) -> + Node = create_node(Config, <<>>), + subscribe_node(Config, Node), + [#ps_subscription{node = Node}] = get_subscriptions(Config), + unsubscribe_node(Config, Node), + [] = get_subscriptions(Config), + delete_node(Config, Node), + disconnect(Config). + +test_get_affiliations(Config) -> + Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]), + Affs = get_affiliations(Config), + Nodes = lists:sort([Node || #ps_affiliation{node = Node, + type = owner} <- Affs]), + [delete_node(Config, Node) || Node <- Nodes], + disconnect(Config). + +test_get_subscriptions(Config) -> + Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]), + [subscribe_node(Config, Node) || Node <- Nodes], + Subs = get_subscriptions(Config), + Nodes = lists:sort([Node || #ps_subscription{node = Node} <- Subs]), + [delete_node(Config, Node) || Node <- Nodes], + disconnect(Config). + +test_purge(Config) -> + Node = create_node(Config, <<>>), + ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)], + ItemsOut = get_items(Config, Node), + true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)] + == [I || #ps_item{id = I} <- lists:sort(ItemsOut)], + purge_node(Config, Node), + [] = get_items(Config, Node), + delete_node(Config, Node), + disconnect(Config). + +test_delete(Config) -> + Node = ?config(pubsub_node, Config), + delete_node(Config, Node), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {pubsub_master_slave, [sequence], + [master_slave_test(publish), + master_slave_test(subscriptions), + master_slave_test(affiliations), + master_slave_test(authorize)]}. + +publish_master(Config) -> + Node = create_node(Config, <<>>), + put_event(Config, Node), + ready = get_event(Config), + #ps_item{id = ID} = publish_item(Config, Node), + #ps_item{id = ID} = get_event(Config), + delete_node(Config, Node), + disconnect(Config). + +publish_slave(Config) -> + Node = get_event(Config), + subscribe_node(Config, Node), + put_event(Config, ready), + #message{ + sub_els = + [#ps_event{ + items = #ps_items{node = Node, + items = [Item]}}]} = recv_message(Config), + put_event(Config, Item), + disconnect(Config). + +subscriptions_master(Config) -> + Peer = ?config(slave, Config), + Node = ?config(pubsub_node, Config), + Node = create_node(Config, Node), + [] = get_subscriptions(Config, Node), + wait_for_slave(Config), + lists:foreach( + fun(Type) -> + ok = set_subscriptions(Config, Node, [{Peer, Type}]), + #ps_item{} = publish_item(Config, Node), + case get_subscriptions(Config, Node) of + [] when Type == none; Type == pending -> + ok; + [#ps_subscription{jid = Peer, type = Type}] -> + ok + end + end, [subscribed, unconfigured, pending, none]), + delete_node(Config, Node), + disconnect(Config). + +subscriptions_slave(Config) -> + wait_for_master(Config), + MyJID = my_jid(Config), + Node = ?config(pubsub_node, Config), + lists:foreach( + fun(subscribed = Type) -> + ?recv2(#message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID, + type = Type}}]}, + #message{sub_els = [#ps_event{}]}); + (Type) -> + #message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID, + type = Type}}]} = + recv_message(Config) + end, [subscribed, unconfigured, pending, none]), + disconnect(Config). + +affiliations_master(Config) -> + Peer = ?config(slave, Config), + BarePeer = jid:remove_resource(Peer), + lists:foreach( + fun(Aff) -> + Node = <<(atom_to_binary(Aff, utf8))/binary, + $-, (p1_rand:get_string())/binary>>, + create_node(Config, Node, default_node_config(Config)), + #ps_item{id = I} = publish_item(Config, Node), + ok = set_affiliations(Config, Node, [{Peer, Aff}]), + Affs = get_affiliations(Config, Node), + case lists:keyfind(BarePeer, #ps_affiliation.jid, Affs) of + false when Aff == none -> + ok; + #ps_affiliation{type = Aff} -> + ok + end, + put_event(Config, {Aff, Node, I}), + wait_for_slave(Config), + delete_node(Config, Node) + end, [outcast, none, member, publish_only, publisher, owner]), + put_event(Config, disconnect), + disconnect(Config). + +affiliations_slave(Config) -> + affiliations_slave(Config, get_event(Config)). + +affiliations_slave(Config, {outcast, Node, ItemID}) -> + #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node), + #stanza_error{} = unsubscribe_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_items(Config, Node), + #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), + #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {none, Node, ItemID}) -> + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + ok = unsubscribe_node(Config, Node), + %% This violates the affiliation char from section 4.1 + [_|_] = get_items(Config, Node), + #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), + #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {member, Node, ItemID}) -> + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + ok = unsubscribe_node(Config, Node), + [_|_] = get_items(Config, Node), + #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), + #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {publish_only, Node, ItemID}) -> + #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node), + #stanza_error{} = unsubscribe_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_items(Config, Node), + #ps_item{id = _MyItemID} = publish_item(Config, Node), + %% BUG: This should be fixed + %% ?match(ok, delete_item(Config, Node, MyItemID)), + #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {publisher, Node, _ItemID}) -> + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + ok = unsubscribe_node(Config, Node), + [_|_] = get_items(Config, Node), + #ps_item{id = MyItemID} = publish_item(Config, Node), + ok = delete_item(Config, Node, MyItemID), + %% BUG: this should be fixed + %% #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), + #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), + #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_node_config(Config, Node, default_node_config(Config)), + #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), + #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), + #stanza_error{reason = 'forbidden'} = + set_affiliations(Config, Node, [{?config(master, Config), outcast}, + {my_jid(Config), owner}]), + #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, {owner, Node, ItemID}) -> + MyJID = my_jid(Config), + Peer = ?config(master, Config), + #ps_subscription{type = subscribed} = subscribe_node(Config, Node), + ok = unsubscribe_node(Config, Node), + [_|_] = get_items(Config, Node), + #ps_item{id = MyItemID} = publish_item(Config, Node), + ok = delete_item(Config, Node, MyItemID), + ok = delete_item(Config, Node, ItemID), + ok = purge_node(Config, Node), + [_|_] = get_node_config(Config, Node), + ok = set_node_config(Config, Node, default_node_config(Config)), + ok = set_subscriptions(Config, Node, []), + [] = get_subscriptions(Config, Node), + ok = set_affiliations(Config, Node, [{Peer, outcast}, {MyJID, owner}]), + [_, _] = get_affiliations(Config, Node), + ok = delete_node(Config, Node), + wait_for_master(Config), + affiliations_slave(Config, get_event(Config)); +affiliations_slave(Config, disconnect) -> + disconnect(Config). + +authorize_master(Config) -> + send(Config, #presence{}), + #presence{} = recv_presence(Config), + Peer = ?config(slave, Config), + PJID = pubsub_jid(Config), + NodeConfig = set_opts(default_node_config(Config), + [{access_model, authorize}]), + Node = ?config(pubsub_node, Config), + Node = create_node(Config, Node, NodeConfig), + wait_for_slave(Config), + #message{sub_els = [#xdata{fields = F1}]} = recv_message(Config), + C1 = pubsub_subscribe_authorization:decode(F1), + Node = proplists:get_value(node, C1), + Peer = proplists:get_value(subscriber_jid, C1), + %% Deny it at first + Deny = #xdata{type = submit, + fields = pubsub_subscribe_authorization:encode( + [{node, Node}, + {subscriber_jid, Peer}, + {allow, false}])}, + send(Config, #message{to = PJID, sub_els = [Deny]}), + %% We should not have any subscriptions + [] = get_subscriptions(Config, Node), + wait_for_slave(Config), + #message{sub_els = [#xdata{fields = F2}]} = recv_message(Config), + C2 = pubsub_subscribe_authorization:decode(F2), + Node = proplists:get_value(node, C2), + Peer = proplists:get_value(subscriber_jid, C2), + %% Now we accept is as the peer is very insisting ;) + Approve = #xdata{type = submit, + fields = pubsub_subscribe_authorization:encode( + [{node, Node}, + {subscriber_jid, Peer}, + {allow, true}])}, + send(Config, #message{to = PJID, sub_els = [Approve]}), + wait_for_slave(Config), + delete_node(Config, Node), + disconnect(Config). + +authorize_slave(Config) -> + Node = ?config(pubsub_node, Config), + MyJID = my_jid(Config), + wait_for_master(Config), + #ps_subscription{type = pending} = subscribe_node(Config, Node), + %% We're denied at first + #message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{type = none, + jid = MyJID}}]} = + recv_message(Config), + wait_for_master(Config), + #ps_subscription{type = pending} = subscribe_node(Config, Node), + %% Now much better! + #message{ + sub_els = + [#ps_event{ + subscription = #ps_subscription{type = subscribed, + jid = MyJID}}]} = + recv_message(Config), + wait_for_master(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("pubsub_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("pubsub_" ++ atom_to_list(T)), [parallel], + [list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_slave")]}. + +set_opts(Config, Options) -> + lists:foldl( + fun({Opt, Val}, Acc) -> + lists:keystore(Opt, 1, Acc, {Opt, Val}) + end, Config, Options). + +create_node(Config, Node) -> + create_node(Config, Node, undefined). + +create_node(Config, Node, Options) -> + PJID = pubsub_jid(Config), + NodeConfig = if is_list(Options) -> + #xdata{type = submit, + fields = pubsub_node_config:encode(Options)}; + true -> + undefined + end, + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{create = Node, + configure = {<<>>, NodeConfig}}]}) of + #iq{type = result, sub_els = [#pubsub{create = NewNode}]} -> + NewNode; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +delete_node(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +purge_node(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{purge = Node}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_default_node_config(Config) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub_owner{default = {<<>>, undefined}}]}) of + #iq{type = result, + sub_els = [#pubsub_owner{default = {<<>>, NodeConfig}}]} -> + pubsub_node_config:decode(NodeConfig#xdata.fields); + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_node_config(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub_owner{configure = {Node, undefined}}]}) of + #iq{type = result, + sub_els = [#pubsub_owner{configure = {Node, NodeConfig}}]} -> + pubsub_node_config:decode(NodeConfig#xdata.fields); + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +set_node_config(Config, Node, Options) -> + PJID = pubsub_jid(Config), + NodeConfig = #xdata{type = submit, + fields = pubsub_node_config:encode(Options)}, + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{configure = + {Node, NodeConfig}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +publish_item(Config, Node) -> + PJID = pubsub_jid(Config), + ItemID = p1_rand:get_string(), + Item = #ps_item{id = ItemID, sub_els = [xmpp:encode(#presence{id = ItemID})]}, + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{publish = #ps_publish{ + node = Node, + items = [Item]}}]}) of + #iq{type = result, + sub_els = [#pubsub{publish = #ps_publish{ + node = Node, + items = [#ps_item{id = ItemID}]}}]} -> + Item; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_items(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub{items = #ps_items{node = Node}}]}) of + #iq{type = result, + sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} -> + Items; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +delete_item(Config, Node, I) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{retract = + #ps_retract{ + node = Node, + items = [#ps_item{id = I}]}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +subscribe_node(Config, Node) -> + PJID = pubsub_jid(Config), + MyJID = my_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{subscribe = #ps_subscribe{ + node = Node, + jid = MyJID}}]}) of + #iq{type = result, + sub_els = [#pubsub{ + subscription = #ps_subscription{ + node = Node, + jid = MyJID} = Sub}]} -> + Sub; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +unsubscribe_node(Config, Node) -> + PJID = pubsub_jid(Config), + MyJID = my_jid(Config), + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub{ + unsubscribe = #ps_unsubscribe{ + node = Node, + jid = MyJID}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_affiliations(Config) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub{affiliations = {<<>>, []}}]}) of + #iq{type = result, + sub_els = [#pubsub{affiliations = {<<>>, Affs}}]} -> + Affs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_affiliations(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub_owner{affiliations = {Node, []}}]}) of + #iq{type = result, + sub_els = [#pubsub_owner{affiliations = {Node, Affs}}]} -> + Affs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +set_affiliations(Config, Node, JTs) -> + PJID = pubsub_jid(Config), + Affs = [#ps_affiliation{jid = J, type = T} || {J, T} <- JTs], + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{affiliations = + {Node, Affs}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_subscriptions(Config) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub{subscriptions = {<<>>, []}}]}) of + #iq{type = result, sub_els = [#pubsub{subscriptions = {<<>>, Subs}}]} -> + Subs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +get_subscriptions(Config, Node) -> + PJID = pubsub_jid(Config), + case send_recv(Config, + #iq{type = get, to = PJID, + sub_els = [#pubsub_owner{subscriptions = {Node, []}}]}) of + #iq{type = result, + sub_els = [#pubsub_owner{subscriptions = {Node, Subs}}]} -> + Subs; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +set_subscriptions(Config, Node, JTs) -> + PJID = pubsub_jid(Config), + Subs = [#ps_subscription{jid = J, type = T} || {J, T} <- JTs], + case send_recv(Config, + #iq{type = set, to = PJID, + sub_els = [#pubsub_owner{subscriptions = + {Node, Subs}}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = IQ -> + xmpp:get_subtag(IQ, #stanza_error{}) + end. + +default_node_config(Config) -> + [{title, ?config(pubsub_node_title, Config)}, + {notify_delete, false}, + {send_last_published_item, never}]. diff --git a/test/push_tests.erl b/test/push_tests.erl new file mode 100644 index 000000000..436f94d55 --- /dev/null +++ b/test/push_tests.erl @@ -0,0 +1,234 @@ +%%%------------------------------------------------------------------- +%%% Author : Holger Weiss <holger@zedat.fu-berlin.de> +%%% Created : 15 Jul 2017 by Holger Weiss <holger@zedat.fu-berlin.de> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(push_tests). + +%% API +-compile(export_all). +-import(suite, [close_socket/1, connect/1, disconnect/1, get_event/1, + get_features/2, make_iq_result/1, my_jid/1, put_event/2, recv/1, + recv_iq/1, recv_message/1, self_presence/2, send/2, send_recv/2, + server_jid/1]). + +-include("suite.hrl"). + +-define(PUSH_NODE, <<"d3v1c3">>). +-define(PUSH_XDATA_FIELDS, + [#xdata_field{var = <<"FORM_TYPE">>, + values = [?NS_PUBSUB_PUBLISH_OPTIONS]}, + #xdata_field{var = <<"secret">>, + values = [<<"c0nf1d3nt14l">>]}]). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {push_single, [sequence], + [single_test(feature_enabled), + single_test(unsupported_iq)]}. + +feature_enabled(Config) -> + BareMyJID = jid:remove_resource(my_jid(Config)), + Features = get_features(Config, BareMyJID), + true = lists:member(?NS_PUSH_0, Features), + disconnect(Config). + +unsupported_iq(Config) -> + PushJID = my_jid(Config), + lists:foreach( + fun(SubEl) -> + #iq{type = error} = + send_recv(Config, #iq{type = get, sub_els = [SubEl]}) + end, [#push_enable{jid = PushJID}, #push_disable{jid = PushJID}]), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {push_master_slave, [sequence], + [master_slave_test(sm), + master_slave_test(offline), + master_slave_test(mam)]}. + +sm_master(Config) -> + ct:comment("Waiting for the slave to close the socket"), + peer_down = get_event(Config), + ct:comment("Waiting a bit in order to test the keepalive feature"), + ct:sleep(5000), % Without mod_push_keepalive, the session would time out. + ct:comment("Sending message to the slave"), + send_test_message(Config), + ct:comment("Handling push notification"), + handle_notification(Config), + ct:comment("Receiving bounced message from the slave"), + #message{type = error} = recv_message(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +sm_slave(Config) -> + ct:comment("Enabling push notifications"), + ok = enable_push(Config), + ct:comment("Enabling stream management"), + ok = enable_sm(Config), + ct:comment("Closing the socket"), + close_socket(Config). + +offline_master(Config) -> + ct:comment("Waiting for the slave to be ready"), + ready = get_event(Config), + ct:comment("Sending message to the slave"), + send_test_message(Config), % No push notification, slave is online. + ct:comment("Waiting for the slave to disconnect"), + peer_down = get_event(Config), + ct:comment("Sending message to offline storage"), + send_test_message(Config), + ct:comment("Handling push notification for offline message"), + handle_notification(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +offline_slave(Config) -> + ct:comment("Re-enabling push notifications"), + ok = enable_push(Config), + ct:comment("Letting the master know that we're ready"), + put_event(Config, ready), + ct:comment("Receiving message from the master"), + recv_test_message(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +mam_master(Config) -> + ct:comment("Waiting for the slave to be ready"), + ready = get_event(Config), + ct:comment("Sending message to the slave"), + send_test_message(Config), + ct:comment("Handling push notification for MAM message"), + handle_notification(Config), + ct:comment("Closing the connection"), + disconnect(Config). + +mam_slave(Config) -> + self_presence(Config, available), + ct:comment("Receiving message from offline storage"), + recv_test_message(Config), + %% Don't re-enable push notifications, otherwise the notification would be + %% suppressed while the slave is online. + ct:comment("Enabling MAM"), + ok = enable_mam(Config), + ct:comment("Letting the master know that we're ready"), + put_event(Config, ready), + ct:comment("Receiving message from the master"), + recv_test_message(Config), + ct:comment("Waiting for the master to disconnect"), + peer_down = get_event(Config), + ct:comment("Disabling push notifications"), + ok = disable_push(Config), + ct:comment("Closing the connection and cleaning up"), + clean(disconnect(Config)). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("push_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("push_" ++ atom_to_list(T)), [parallel], + [list_to_atom("push_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("push_" ++ atom_to_list(T) ++ "_slave")]}. + +enable_sm(Config) -> + send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3, resume = true}), + case recv(Config) of + #sm_enabled{resume = true} -> + ok; + #sm_failed{reason = Reason} -> + Reason + end. + +enable_mam(Config) -> + case send_recv( + Config, #iq{type = set, sub_els = [#mam_prefs{xmlns = ?NS_MAM_1, + default = always}]}) of + #iq{type = result} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +enable_push(Config) -> + %% Usually, the push JID would be a server JID (such as push.example.com). + %% We specify the peer's full user JID instead, so the push notifications + %% will be sent to the peer. + PushJID = ?config(peer, Config), + XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, + case send_recv( + Config, #iq{type = set, + sub_els = [#push_enable{jid = PushJID, + node = ?PUSH_NODE, + xdata = XData}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +disable_push(Config) -> + PushJID = ?config(peer, Config), + case send_recv( + Config, #iq{type = set, + sub_els = [#push_disable{jid = PushJID, + node = ?PUSH_NODE}]}) of + #iq{type = result, sub_els = []} -> + ok; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +send_test_message(Config) -> + Peer = ?config(peer, Config), + Msg = #message{to = Peer, body = [#text{data = <<"test">>}]}, + send(Config, Msg). + +recv_test_message(Config) -> + Peer = ?config(peer, Config), + #message{from = Peer, + body = [#text{data = <<"test">>}]} = recv_message(Config). + +handle_notification(Config) -> + From = server_jid(Config), + Item = #ps_item{sub_els = [xmpp:encode(#push_notification{})]}, + Publish = #ps_publish{node = ?PUSH_NODE, items = [Item]}, + XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, + PubSub = #pubsub{publish = Publish, publish_options = XData}, + IQ = #iq{type = set, from = From, sub_els = [PubSub]} = recv_iq(Config), + send(Config, make_iq_result(IQ)). + +clean(Config) -> + {U, S, _} = jid:tolower(my_jid(Config)), + mod_push:remove_user(U, S), + mod_mam:remove_user(U, S), + Config. diff --git a/test/replaced_tests.erl b/test/replaced_tests.erl new file mode 100644 index 000000000..884852c3e --- /dev/null +++ b/test/replaced_tests.erl @@ -0,0 +1,70 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(replaced_tests). + +%% API +-compile(export_all). +-import(suite, [bind/1, wait_for_slave/1, wait_for_master/1, recv/1, + close_socket/1, disconnect/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {replaced_single, [sequence], []}. + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {replaced_master_slave, [sequence], + [master_slave_test(conflict)]}. + +conflict_master(Config0) -> + Config = bind(Config0), + wait_for_slave(Config), + #stream_error{reason = conflict} = recv(Config), + {xmlstreamend, <<"stream:stream">>} = recv(Config), + close_socket(Config). + +conflict_slave(Config0) -> + wait_for_master(Config0), + Config = bind(Config0), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("replaced_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("replaced_" ++ atom_to_list(T)), [parallel], + [list_to_atom("replaced_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("replaced_" ++ atom_to_list(T) ++ "_slave")]}. diff --git a/test/roster_tests.erl b/test/roster_tests.erl new file mode 100644 index 000000000..38420abb7 --- /dev/null +++ b/test/roster_tests.erl @@ -0,0 +1,584 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 22 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(roster_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, recv_iq/1, send/2, disconnect/1, del_roster/1, + del_roster/2, make_iq_result/1, wait_for_slave/1, + wait_for_master/1, recv_presence/1, self_presence/2, + put_event/2, get_event/1, match_failure/2, get_roster/1]). +-include("suite.hrl"). +-include("mod_roster.hrl"). + +-record(state, {subscription = none :: none | from | to | both, + peer_available = false, + pending_in = false :: boolean(), + pending_out = false :: boolean()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_TestCase, Config) -> + Config. + +stop(_TestCase, Config) -> + Config. + +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {roster_single, [sequence], + [single_test(feature_enabled), + single_test(iq_set_many_items), + single_test(iq_set_duplicated_groups), + single_test(iq_get_item), + single_test(iq_unexpected_element), + single_test(iq_set_ask), + single_test(set_item), + single_test(version)]}. + +feature_enabled(Config) -> + ct:comment("Checking if roster versioning stream feature is set"), + true = ?config(rosterver, Config), + disconnect(Config). + +set_item(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + Item = #roster_item{jid = JID}, + {V1, Item} = set_items(Config, [Item]), + {V1, [Item]} = get_items(Config), + ItemWithGroups = Item#roster_item{groups = [<<"G1">>, <<"G2">>]}, + {V2, ItemWithGroups} = set_items(Config, [ItemWithGroups]), + {V2, [ItemWithGroups]} = get_items(Config), + {V3, Item} = set_items(Config, [Item]), + {V3, [Item]} = get_items(Config), + ItemWithName = Item#roster_item{name = <<"some name">>}, + {V4, ItemWithName} = set_items(Config, [ItemWithName]), + {V4, [ItemWithName]} = get_items(Config), + ItemRemoved = Item#roster_item{subscription = remove}, + {V5, ItemRemoved} = set_items(Config, [ItemRemoved]), + {V5, []} = get_items(Config), + del_roster(disconnect(Config), JID). + +iq_set_many_items(Config) -> + J1 = jid:decode(<<"nurse1@example.com">>), + J2 = jid:decode(<<"nurse2@example.com">>), + ct:comment("Trying to send roster-set with many <item/> elements"), + Items = [#roster_item{jid = J1}, #roster_item{jid = J2}], + #stanza_error{reason = 'bad-request'} = set_items(Config, Items), + disconnect(Config). + +iq_set_duplicated_groups(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + G = p1_rand:get_string(), + ct:comment("Trying to send roster-set with duplicated groups"), + Item = #roster_item{jid = JID, groups = [G, G]}, + #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), + disconnect(Config). + +iq_set_ask(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + ct:comment("Trying to send roster-set with 'ask' included"), + Item = #roster_item{jid = JID, ask = subscribe}, + #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), + disconnect(Config). + +iq_get_item(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + ct:comment("Trying to send roster-get with <item/> element"), + #iq{type = error} = Err3 = + send_recv(Config, #iq{type = get, + sub_els = [#roster_query{ + items = [#roster_item{jid = JID}]}]}), + #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err3), + disconnect(Config). + +iq_unexpected_element(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + ct:comment("Trying to send IQs with unexpected element"), + lists:foreach( + fun(Type) -> + #iq{type = error} = Err4 = + send_recv(Config, #iq{type = Type, + sub_els = [#roster_item{jid = JID}]}), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err4) + end, [get, set]), + disconnect(Config). + +version(Config) -> + JID = jid:decode(<<"nurse@example.com">>), + ct:comment("Requesting roster"), + {InitialVersion, _} = get_items(Config, <<"">>), + ct:comment("Requesting roster with initial version"), + {empty, []} = get_items(Config, InitialVersion), + ct:comment("Adding JID to the roster"), + {NewVersion, _} = set_items(Config, [#roster_item{jid = JID}]), + ct:comment("Requesting roster with initial version"), + {NewVersion, _} = get_items(Config, InitialVersion), + ct:comment("Requesting roster with new version"), + {empty, []} = get_items(Config, NewVersion), + del_roster(disconnect(Config), JID). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {roster_master_slave, [sequence], + [master_slave_test(subscribe)]}. + +subscribe_master(Config) -> + Actions = actions(), + process_subscriptions_master(Config, Actions), + del_roster(disconnect(Config)). + +subscribe_slave(Config) -> + process_subscriptions_slave(Config), + del_roster(disconnect(Config)). + +process_subscriptions_master(Config, Actions) -> + EnumeratedActions = lists:zip(lists:seq(1, length(Actions)), Actions), + self_presence(Config, available), + Peer = ?config(peer, Config), + lists:foldl( + fun({N, {Dir, Type}}, State) -> + if Dir == out -> put_event(Config, {N, in, Type}); + Dir == in -> put_event(Config, {N, out, Type}) + end, + Roster = get_roster(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), pp(Roster)]), + check_roster(Roster, Config, State), + wait_for_slave(Config), + Id = mk_id(N, Dir, Type), + NewState = transition(Id, Config, Dir, Type, State), + wait_for_slave(Config), + send_recv(Config, #iq{type = get, to = Peer, id = Id, + sub_els = [#ping{}]}), + check_roster_item(Config, NewState), + NewState + end, #state{}, EnumeratedActions), + put_event(Config, done), + wait_for_slave(Config), + Config. + +process_subscriptions_slave(Config) -> + self_presence(Config, available), + process_subscriptions_slave(Config, get_event(Config), #state{}). + +process_subscriptions_slave(Config, done, _State) -> + wait_for_master(Config), + Config; +process_subscriptions_slave(Config, {N, Dir, Type}, State) -> + Roster = get_roster(Config), + ct:pal("Performing ~s-~s (#~p) " + "in state:~n~s~nwith roster:~n~s", + [Dir, Type, N, pp(State), pp(Roster)]), + check_roster(Roster, Config, State), + wait_for_master(Config), + NewState = transition(mk_id(N, Dir, Type), Config, Dir, Type, State), + wait_for_master(Config), + send(Config, xmpp:make_iq_result(recv_iq(Config))), + check_roster_item(Config, NewState), + process_subscriptions_slave(Config, get_event(Config), NewState). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("roster_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("roster_" ++ atom_to_list(T)), [parallel], + [list_to_atom("roster_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("roster_" ++ atom_to_list(T) ++ "_slave")]}. + +get_items(Config) -> + get_items(Config, <<"">>). + +get_items(Config, Version) -> + case send_recv(Config, #iq{type = get, + sub_els = [#roster_query{ver = Version}]}) of + #iq{type = result, + sub_els = [#roster_query{ver = NewVersion, items = Items}]} -> + {NewVersion, Items}; + #iq{type = result, sub_els = []} -> + {empty, []}; + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +get_item(Config, JID) -> + case get_items(Config) of + {_Ver, Items} when is_list(Items) -> + lists:keyfind(JID, #roster_item.jid, Items); + _ -> + false + end. + +set_items(Config, Items) -> + case send_recv(Config, #iq{type = set, + sub_els = [#roster_query{items = Items}]}) of + #iq{type = result, sub_els = []} -> + recv_push(Config); + #iq{type = error} = Err -> + xmpp:get_error(Err) + end. + +recv_push(Config) -> + ct:comment("Receiving roster push"), + Push = #iq{type = set, + sub_els = [#roster_query{ver = Ver, items = [PushItem]}]} + = recv_iq(Config), + send(Config, make_iq_result(Push)), + {Ver, PushItem}. + +recv_push(Config, Subscription, Ask) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + Match = #roster_item{jid = PeerBareJID, + subscription = Subscription, + ask = Ask, + groups = [], + name = <<"">>}, + ct:comment("Receiving roster push"), + Push = #iq{type = set, sub_els = [#roster_query{items = [Item]}]} = + recv_iq(Config), + case Item of + Match -> send(Config, make_iq_result(Push)); + _ -> match_failure(Item, Match) + end. + +recv_presence(Config, Type) -> + PeerJID = ?config(peer, Config), + case recv_presence(Config) of + #presence{from = PeerJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerJID, type = Type}) + end. + +recv_subscription(Config, Type) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + case recv_presence(Config) of + #presence{from = PeerBareJID, type = Type} -> ok; + Pres -> match_failure(Pres, #presence{from = PeerBareJID, type = Type}) + end. + +pp(Term) -> + io_lib_pretty:print(Term, fun pp/2). + +pp(state, N) -> + Fs = record_info(fields, state), + try N = length(Fs), Fs + catch _:_ -> no end; +pp(roster, N) -> + Fs = record_info(fields, roster), + try N = length(Fs), Fs + catch _:_ -> no end; +pp(_, _) -> no. + +mk_id(N, Dir, Type) -> + list_to_binary([integer_to_list(N), $-, atom_to_list(Dir), + $-, atom_to_list(Type)]). + +check_roster([], _Config, _State) -> + ok; +check_roster([Roster], _Config, State) -> + case {Roster#roster.subscription == State#state.subscription, + Roster#roster.ask, State#state.pending_in, State#state.pending_out} of + {true, both, true, true} -> ok; + {true, in, true, false} -> ok; + {true, out, false, true} -> ok; + {true, none, false, false} -> ok; + _ -> + ct:fail({roster_mismatch, State, Roster}) + end. + +check_roster_item(Config, State) -> + Peer = jid:remove_resource(?config(peer, Config)), + RosterItem = case get_item(Config, Peer) of + false -> #roster_item{}; + Item -> Item + end, + case {RosterItem#roster_item.subscription == State#state.subscription, + RosterItem#roster_item.ask, State#state.pending_out} of + {true, subscribe, true} -> ok; + {true, undefined, false} -> ok; + _ -> ct:fail({roster_item_mismatch, State, RosterItem}) + end. + +%% RFC6121, A.2.1 +transition(Id, Config, out, subscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{id = Id, to = PeerBareJID, type = subscribe}), + case {Sub, Out, In} of + {none, false, _} -> + recv_push(Config, none, subscribe), + State#state{pending_out = true}; + {none, true, false} -> + %% BUG: we should not receive roster push here + recv_push(Config, none, subscribe), + State; + {from, false, false} -> + recv_push(Config, from, subscribe), + State#state{pending_out = true}; + _ -> + State + end; +%% RFC6121, A.2.2 +transition(Id, Config, out, unsubscribe, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{id = Id, to = PeerBareJID, type = unsubscribe}), + case {Sub, Out, In} of + {none, true, _} -> + recv_push(Config, none, undefined), + State#state{pending_out = false}; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + State#state{pending_out = false}; + {both, false, false} -> + recv_push(Config, from, undefined), + recv_presence(Config, unavailable), + State#state{subscription = from, peer_available = false}; + _ -> + State + end; +%% RFC6121, A.2.3 +transition(Id, Config, out, subscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{id = Id, to = PeerBareJID, type = subscribed}), + case {Sub, Out, In} of + {none, false, true} -> + recv_push(Config, from, undefined), + State#state{subscription = from, pending_in = false}; + {none, true, true} -> + recv_push(Config, from, subscribe), + State#state{subscription = from, pending_in = false}; + {to, false, true} -> + recv_push(Config, both, undefined), + State#state{subscription = both, pending_in = false}; + {to, false, _} -> + %% BUG: we should not transition to 'both' state + recv_push(Config, both, undefined), + State#state{subscription = both}; + _ -> + State + end; +%% RFC6121, A.2.4 +transition(Id, Config, out, unsubscribed, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + send(Config, #presence{id = Id, to = PeerBareJID, type = unsubscribed}), + case {Sub, Out, In} of + {none, false, true} -> + State#state{subscription = none, pending_in = false}; + {none, true, true} -> + recv_push(Config, none, subscribe), + State#state{subscription = none, pending_in = false}; + {to, _, true} -> + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + State#state{subscription = to}; + _ -> + State + end; +%% RFC6121, A.3.1 +transition(_, Config, in, subscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, false, false} -> + recv_subscription(Config, Type), + State#state{pending_in = true}; + {none, true, false} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{pending_in = true}; + {to, false, false} -> + %% BUG: we should not receive roster push in this state! + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = true}; + _ -> + State + end; +%% RFC6121, A.3.2 +transition(_, Config, in, unsubscribe = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, _, true} -> + State#state{pending_in = false}; + {to, _, true} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{pending_in = false}; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none}; + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, Type), + State#state{subscription = none}; + {both, _, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + State#state{subscription = to}; + _ -> + State + end; +%% RFC6121, A.3.3 +transition(_, Config, in, subscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, _} -> + recv_push(Config, to, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = to, pending_out = false, peer_available = true}; + {from, true, _} -> + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + {from, false, _} -> + %% BUG: we should not transition to 'both' in this state + recv_push(Config, both, undefined), + recv_subscription(Config, Type), + recv_presence(Config, available), + State#state{subscription = both, pending_out = false, peer_available = true}; + _ -> + State + end; +%% RFC6121, A.3.4 +transition(_, Config, in, unsubscribed = Type, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, true} -> + %% BUG: we should receive roster push in this state! + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, true, false} -> + recv_push(Config, none, undefined), + recv_subscription(Config, Type), + State#state{subscription = none, pending_out = false}; + {none, false, false} -> + State; + {to, false, _} -> + recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, Type), + State#state{subscription = none, peer_available = false}; + {from, true, false} -> + recv_push(Config, from, undefined), + recv_subscription(Config, Type), + State#state{subscription = from, pending_out = false}; + {both, _, _} -> + recv_push(Config, from, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, Type), + State#state{subscription = from, peer_available = false}; + _ -> + State + end; +%% Outgoing roster remove +transition(Id, Config, out, remove, + #state{subscription = Sub, pending_in = In, pending_out = Out}) -> + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + Item = #roster_item{jid = PeerBareJID, subscription = remove}, + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, id = Id, + sub_els = [#roster_query{items = [Item]}]}), + recv_push(Config, remove, undefined), + case {Sub, Out, In} of + {to, _, _} -> + recv_presence(Config, unavailable); + {both, _, _} -> + recv_presence(Config, unavailable); + _ -> + ok + end, + #state{}; +%% Incoming roster remove +transition(_, Config, in, remove, + #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> + case {Sub, Out, In} of + {none, true, _} -> + ok; + {from, false, _} -> + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribe); + {from, true, _} -> + recv_push(Config, none, subscribe), + recv_subscription(Config, unsubscribe); + {to, false, _} -> + %% BUG: we should receive push here + %% recv_push(Config, none, undefined), + recv_presence(Config, unavailable), + recv_subscription(Config, unsubscribed); + {both, _, _} -> + recv_presence(Config, unavailable), + recv_push(Config, to, undefined), + recv_subscription(Config, unsubscribe), + recv_push(Config, none, undefined), + recv_subscription(Config, unsubscribed); + _ -> + ok + end, + State#state{subscription = none}. + +actions() -> + States = [{Dir, Type} || Dir <- [out, in], + Type <- [subscribe, subscribed, + unsubscribe, unsubscribed, + remove]], + Actions = lists:flatten([[X, Y] || X <- States, Y <- States]), + remove_dups(Actions, []). + +remove_dups([X|T], [X,X|_] = Acc) -> + remove_dups(T, Acc); +remove_dups([X|T], Acc) -> + remove_dups(T, [X|Acc]); +remove_dups([], Acc) -> + lists:reverse(Acc). diff --git a/test/sm_tests.erl b/test/sm_tests.erl new file mode 100644 index 000000000..3eb2bf214 --- /dev/null +++ b/test/sm_tests.erl @@ -0,0 +1,185 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(sm_tests). + +%% API +-compile(export_all). +-import(suite, [send/2, recv/1, close_socket/1, set_opt/3, my_jid/1, + recv_message/1, disconnect/1, send_recv/2, + put_event/2, get_event/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {sm_single, [sequence], + [single_test(feature_enabled), + single_test(enable), + single_test(resume), + single_test(resume_failed)]}. + +feature_enabled(Config) -> + true = ?config(sm, Config), + disconnect(Config). + +enable(Config) -> + Server = ?config(server, Config), + ServerJID = jid:make(<<"">>, Server, <<"">>), + ct:comment("Send messages of type 'headline' so the server discards them silently"), + Msg = #message{to = ServerJID, type = headline, + body = [#text{data = <<"body">>}]}, + ct:comment("Enable the session management with resumption enabled"), + send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), + #sm_enabled{id = ID, resume = true} = recv(Config), + ct:comment("Initial request; 'h' should be 0"), + send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), + #sm_a{h = 0} = recv(Config), + ct:comment("Sending two messages and requesting again; 'h' should be 3"), + send(Config, Msg), + send(Config, Msg), + send(Config, Msg), + send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), + #sm_a{h = 3} = recv(Config), + ct:comment("Closing socket"), + close_socket(Config), + {save_config, set_opt(sm_previd, ID, Config)}. + +resume(Config) -> + {_, SMConfig} = ?config(saved_config, Config), + ID = ?config(sm_previd, SMConfig), + Server = ?config(server, Config), + ServerJID = jid:make(<<"">>, Server, <<"">>), + MyJID = my_jid(Config), + Txt = #text{data = <<"body">>}, + Msg = #message{from = ServerJID, to = MyJID, body = [Txt]}, + ct:comment("Route message. The message should be queued by the C2S process"), + ejabberd_router:route(Msg), + ct:comment("Resuming the session"), + send(Config, #sm_resume{previd = ID, h = 0, xmlns = ?NS_STREAM_MGMT_3}), + #sm_resumed{previd = ID, h = 3} = recv(Config), + ct:comment("Receiving unacknowledged stanza"), + #message{from = ServerJID, to = MyJID, body = [Txt]} = recv_message(Config), + #sm_r{} = recv(Config), + send(Config, #sm_a{h = 1, xmlns = ?NS_STREAM_MGMT_3}), + ct:comment("Checking if the server counts stanzas correctly"), + send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), + #sm_a{h = 3} = recv(Config), + ct:comment("Send another stanza to increment the server's 'h' for sm_resume_failed"), + send(Config, #presence{to = ServerJID}), + ct:comment("Closing socket"), + close_socket(Config), + {save_config, set_opt(sm_previd, ID, Config)}. + +resume_failed(Config) -> + {_, SMConfig} = ?config(saved_config, Config), + ID = ?config(sm_previd, SMConfig), + ct:comment("Waiting for the session to time out"), + ct:sleep(5000), + ct:comment("Trying to resume timed out session"), + send(Config, #sm_resume{previd = ID, h = 1, xmlns = ?NS_STREAM_MGMT_3}), + #sm_failed{reason = 'item-not-found', h = 4} = recv(Config), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {sm_master_slave, [sequence], + [master_slave_test(queue_limit), + master_slave_test(queue_limit_detached)]}. + +queue_limit_master(Config) -> + ct:comment("Waiting for 'send' command from the peer"), + send = get_event(Config), + send_recv_messages(Config), + ct:comment("Waiting for peer to disconnect"), + peer_down = get_event(Config), + disconnect(Config). + +queue_limit_slave(Config) -> + ct:comment("Enable the session management without resumption"), + send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3}), + #sm_enabled{resume = false} = recv(Config), + put_event(Config, send), + ct:comment("Receiving all messages"), + lists:foreach( + fun(I) -> + ID = integer_to_binary(I), + Body = xmpp:mk_text(ID), + #message{id = ID, body = Body} = recv_message(Config) + end, lists:seq(1, 11)), + ct:comment("Receiving request ACK"), + #sm_r{} = recv(Config), + ct:comment("Receiving policy-violation stream error"), + #stream_error{reason = 'policy-violation'} = recv(Config), + {xmlstreamend, <<"stream:stream">>} = recv(Config), + ct:comment("Closing socket"), + close_socket(Config). + +queue_limit_detached_master(Config) -> + ct:comment("Waiting for the peer to disconnect"), + peer_down = get_event(Config), + send_recv_messages(Config), + disconnect(Config). + +queue_limit_detached_slave(Config) -> + #presence{} = send_recv(Config, #presence{}), + ct:comment("Enable the session management with resumption enabled"), + send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), + #sm_enabled{resume = true} = recv(Config), + ct:comment("Closing socket"), + close_socket(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("sm_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("sm_" ++ atom_to_list(T)), [parallel], + [list_to_atom("sm_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("sm_" ++ atom_to_list(T) ++ "_slave")]}. + +send_recv_messages(Config) -> + PeerJID = ?config(peer, Config), + Msg = #message{to = PeerJID}, + ct:comment("Sending messages to peer"), + lists:foreach( + fun(I) -> + ID = integer_to_binary(I), + send(Config, Msg#message{id = ID, body = xmpp:mk_text(ID)}) + end, lists:seq(1, 11)), + ct:comment("Receiving bounced messages from the peer"), + lists:foreach( + fun(I) -> + ID = integer_to_binary(I), + Err = #message{id = ID, type = error} = recv_message(Config), + #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) + end, lists:seq(1, 11)). diff --git a/test/suite.erl b/test/suite.erl index c5593c4cf..a82f06efb 100644 --- a/test/suite.erl +++ b/test/suite.erl @@ -1,11 +1,26 @@ %%%------------------------------------------------------------------- -%%% @author Evgeniy Khramtsov <> -%%% @copyright (C) 2013-2016, Evgeniy Khramtsov -%%% @doc +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 27 Jun 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% -%%% @end -%%% Created : 27 Jun 2013 by Evgeniy Khramtsov <> -%%%------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + -module(suite). %% API @@ -13,6 +28,7 @@ -include("suite.hrl"). -include_lib("kernel/include/file.hrl"). +-include("mod_roster.hrl"). %%%=================================================================== %%% API @@ -22,56 +38,120 @@ init_config(Config) -> PrivDir = proplists:get_value(priv_dir, Config), [_, _|Tail] = lists:reverse(filename:split(DataDir)), BaseDir = filename:join(lists:reverse(Tail)), - ConfigPathTpl = filename:join([DataDir, "ejabberd.yml"]), + MacrosPathTpl = filename:join([DataDir, "macros.yml"]), + ConfigPath = filename:join([DataDir, "ejabberd.yml"]), LogPath = filename:join([PrivDir, "ejabberd.log"]), SASLPath = filename:join([PrivDir, "sasl.log"]), MnesiaDir = filename:join([PrivDir, "mnesia"]), CertFile = filename:join([DataDir, "cert.pem"]), + SelfSignedCertFile = filename:join([DataDir, "self-signed-cert.pem"]), + CAFile = filename:join([DataDir, "ca.pem"]), {ok, CWD} = file:get_cwd(), {ok, _} = file:copy(CertFile, filename:join([CWD, "cert.pem"])), - {ok, CfgContentTpl} = file:read_file(ConfigPathTpl), - CfgContent = process_config_tpl(CfgContentTpl, [ - {c2s_port, 5222}, - {loglevel, 4}, - {s2s_port, 5269}, - {web_port, 5280}, - {mysql_server, <<"localhost">>}, - {mysql_port, 3306}, - {mysql_db, <<"ejabberd_test">>}, - {mysql_user, <<"ejabberd_test">>}, - {mysql_pass, <<"ejabberd_test">>}, - {pgsql_server, <<"localhost">>}, - {pgsql_port, 5432}, - {pgsql_db, <<"ejabberd_test">>}, - {pgsql_user, <<"ejabberd_test">>}, - {pgsql_pass, <<"ejabberd_test">>} - ]), - ConfigPath = filename:join([CWD, "ejabberd.yml"]), - ok = file:write_file(ConfigPath, CfgContent), + {ok, _} = file:copy(SelfSignedCertFile, + filename:join([CWD, "self-signed-cert.pem"])), + {ok, _} = file:copy(CAFile, filename:join([CWD, "ca.pem"])), + {ok, MacrosContentTpl} = file:read_file(MacrosPathTpl), + Password = <<"password!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>, + Backends = get_config_backends(), + MacrosContent = process_config_tpl( + MacrosContentTpl, + [{c2s_port, 5222}, + {loglevel, 4}, + {new_schema, false}, + {s2s_port, 5269}, + {component_port, 5270}, + {web_port, 5280}, + {proxy_port, 7777}, + {password, Password}, + {mysql_server, <<"localhost">>}, + {mysql_port, 3306}, + {mysql_db, <<"ejabberd_test">>}, + {mysql_user, <<"ejabberd_test">>}, + {mysql_pass, <<"ejabberd_test">>}, + {pgsql_server, <<"localhost">>}, + {pgsql_port, 5432}, + {pgsql_db, <<"ejabberd_test">>}, + {pgsql_user, <<"ejabberd_test">>}, + {pgsql_pass, <<"ejabberd_test">>}, + {priv_dir, PrivDir}]), + MacrosPath = filename:join([CWD, "macros.yml"]), + ok = file:write_file(MacrosPath, MacrosContent), + copy_backend_configs(DataDir, CWD, Backends), setup_ejabberd_lib_path(Config), - ok = application:load(sasl), - ok = application:load(mnesia), - ok = application:load(ejabberd), + case application:load(sasl) of + ok -> ok; + {error, {already_loaded, _}} -> ok + end, + case application:load(mnesia) of + ok -> ok; + {error, {already_loaded, _}} -> ok + end, + case application:load(ejabberd) of + ok -> ok; + {error, {already_loaded, _}} -> ok + end, application:set_env(ejabberd, config, ConfigPath), application:set_env(ejabberd, log_path, LogPath), application:set_env(sasl, sasl_error_logger, {file, SASLPath}), application:set_env(mnesia, dir, MnesiaDir), [{server_port, ct:get_config(c2s_port, 5222)}, {server_host, "localhost"}, + {component_port, ct:get_config(component_port, 5270)}, + {s2s_port, ct:get_config(s2s_port, 5269)}, {server, ?COMMON_VHOST}, {user, <<"test_single!#$%^*()`~+-;_=[]{}|\\">>}, + {nick, <<"nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {master_nick, <<"master_nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {slave_nick, <<"slave_nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {room_subject, <<"hello, world!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {certfile, CertFile}, + {persistent_room, true}, + {anonymous, false}, + {type, client}, + {xmlns, ?NS_CLIENT}, + {ns_stream, ?NS_STREAM}, + {stream_version, {1, 0}}, + {stream_id, <<"">>}, + {stream_from, <<"">>}, + {db_xmlns, <<"">>}, + {mechs, []}, + {rosterver, false}, + {lang, <<"en">>}, {base_dir, BaseDir}, + {receiver, undefined}, + {pubsub_node, <<"node!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, + {pubsub_node_title, <<"title!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {resource, <<"resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {master_resource, <<"master_resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {slave_resource, <<"slave_resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, - {password, <<"password!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, - {backends, get_config_backends()} + {password, Password}, + {backends, Backends} |Config]. +copy_backend_configs(DataDir, CWD, Backends) -> + Files = filelib:wildcard(filename:join([DataDir, "ejabberd.*.yml"])), + lists:foreach( + fun(Src) -> + File = filename:basename(Src), + case string:tokens(File, ".") of + ["ejabberd", SBackend, "yml"] -> + Backend = list_to_atom(SBackend), + Macro = list_to_atom(string:to_upper(SBackend) ++ "_CONFIG"), + Dst = filename:join([CWD, File]), + case lists:member(Backend, Backends) of + true -> + {ok, _} = file:copy(Src, Dst); + false -> + ok = file:write_file( + Dst, fast_yaml:encode( + [{define_macro, [{Macro, []}]}])) + end; + _ -> + ok + end + end, Files). + find_top_dir(Dir) -> case file:read_file_info(filename:join([Dir, ebin])) of {ok, #file_info{type = directory}} -> @@ -93,146 +173,335 @@ setup_ejabberd_lib_path(Config) -> ok end. -%% Read environment variable CT_DB=riak,mysql to limit the backends to test. +%% Read environment variable CT_DB=mysql to limit the backends to test. %% You can thus limit the backend you want to test with: -%% CT_BACKENDS=riak,mysql rebar ct suites=ejabberd +%% CT_BACKENDS=mysql rebar ct suites=ejabberd get_config_backends() -> - case os:getenv("CT_BACKENDS") of - false -> all; - String -> - Backends0 = string:tokens(String, ","), - lists:map(fun(Backend) -> string:strip(Backend, both, $ ) end, Backends0) - end. + EnvBackends = case os:getenv("CT_BACKENDS") of + false -> ?BACKENDS; + String -> + Backends0 = string:tokens(String, ","), + lists:map( + fun(Backend) -> + list_to_atom(string:strip(Backend, both, $ )) + end, Backends0) + end, + application:load(ejabberd), + EnabledBackends = application:get_env(ejabberd, enabled_backends, EnvBackends), + misc:intersection(EnvBackends, [mnesia, ldap, extauth|EnabledBackends]). process_config_tpl(Content, []) -> Content; process_config_tpl(Content, [{Name, DefaultValue} | Rest]) -> Val = case ct:get_config(Name, DefaultValue) of - V1 when is_integer(V1) -> - integer_to_binary(V1); - V2 when is_atom(V2) -> - atom_to_binary(V2, latin1); - V3 -> - V3 + V when is_integer(V) -> + integer_to_binary(V); + V when is_atom(V) -> + atom_to_binary(V, latin1); + V -> + iolist_to_binary(V) end, - NewContent = binary:replace(Content, <<"@@",(atom_to_binary(Name, latin1))/binary, "@@">>, Val), + NewContent = binary:replace(Content, + <<"@@",(atom_to_binary(Name,latin1))/binary, "@@">>, + Val, [global]), process_config_tpl(NewContent, Rest). +stream_header(Config) -> + To = case ?config(server, Config) of + <<"">> -> undefined; + Server -> jid:make(Server) + end, + From = case ?config(stream_from, Config) of + <<"">> -> undefined; + Frm -> jid:make(Frm) + end, + #stream_start{to = To, + from = From, + lang = ?config(lang, Config), + version = ?config(stream_version, Config), + xmlns = ?config(xmlns, Config), + db_xmlns = ?config(db_xmlns, Config), + stream_xmlns = ?config(ns_stream, Config)}. connect(Config) -> - {ok, Sock} = ejabberd_socket:connect( - ?config(server_host, Config), - ?config(server_port, Config), - [binary, {packet, 0}, {active, false}]), - init_stream(set_opt(socket, Sock, Config)). + NewConfig = init_stream(Config), + case ?config(type, NewConfig) of + client -> process_stream_features(NewConfig); + server -> process_stream_features(NewConfig); + component -> NewConfig + end. + +tcp_connect(Config) -> + case ?config(receiver, Config) of + undefined -> + Owner = self(), + NS = case ?config(type, Config) of + client -> ?NS_CLIENT; + server -> ?NS_SERVER; + component -> ?NS_COMPONENT + end, + Server = ?config(server_host, Config), + Port = ?config(server_port, Config), + ReceiverPid = spawn(fun() -> + start_receiver(NS, Owner, Server, Port) + end), + set_opt(receiver, ReceiverPid, Config); + _ -> + Config + end. init_stream(Config) -> - ok = send_text(Config, io_lib:format(?STREAM_HEADER, - [?config(server, Config)])), - {xmlstreamstart, <<"stream:stream">>, Attrs} = recv(), - <<"jabber:client">> = fxml:get_attr_s(<<"xmlns">>, Attrs), - <<"1.0">> = fxml:get_attr_s(<<"version">>, Attrs), - #stream_features{sub_els = Fs} = recv(), - Mechs = lists:flatmap( - fun(#sasl_mechanisms{list = Ms}) -> - Ms; - (_) -> - [] - end, Fs), - lists:foldl( - fun(#feature_register{}, Acc) -> - set_opt(register, true, Acc); - (#starttls{}, Acc) -> - set_opt(starttls, true, Acc); - (#compression{methods = Ms}, Acc) -> - set_opt(compression, Ms, Acc); - (_, Acc) -> - Acc - end, set_opt(mechs, Mechs, Config), Fs). + Version = ?config(stream_version, Config), + NewConfig = tcp_connect(Config), + send(NewConfig, stream_header(NewConfig)), + XMLNS = case ?config(type, Config) of + client -> ?NS_CLIENT; + component -> ?NS_COMPONENT; + server -> ?NS_SERVER + end, + receive + #stream_start{id = ID, xmlns = XMLNS, version = Version} -> + set_opt(stream_id, ID, NewConfig) + end. + +process_stream_features(Config) -> + receive + #stream_features{sub_els = Fs} -> + Mechs = lists:flatmap( + fun(#sasl_mechanisms{list = Ms}) -> + Ms; + (_) -> + [] + end, Fs), + lists:foldl( + fun(#feature_register{}, Acc) -> + set_opt(register, true, Acc); + (#starttls{}, Acc) -> + set_opt(starttls, true, Acc); + (#legacy_auth_feature{}, Acc) -> + set_opt(legacy_auth, true, Acc); + (#compression{methods = Ms}, Acc) -> + set_opt(compression, Ms, Acc); + (_, Acc) -> + Acc + end, set_opt(mechs, Mechs, Config), Fs) + end. disconnect(Config) -> - Socket = ?config(socket, Config), - ok = ejabberd_socket:send(Socket, ?STREAM_TRAILER), - {xmlstreamend, <<"stream:stream">>} = recv(), - ejabberd_socket:close(Socket), - Config. + ct:comment("Disconnecting"), + try + send_text(Config, ?STREAM_TRAILER) + catch exit:normal -> + ok + end, + receive {xmlstreamend, <<"stream:stream">>} -> ok end, + flush(Config), + ok = recv_call(Config, close), + ct:comment("Disconnected"), + set_opt(receiver, undefined, Config). close_socket(Config) -> - Socket = ?config(socket, Config), - ejabberd_socket:close(Socket), + ok = recv_call(Config, close), Config. starttls(Config) -> + starttls(Config, false). + +starttls(Config, ShouldFail) -> send(Config, #starttls{}), - #starttls_proceed{} = recv(), - TLSSocket = ejabberd_socket:starttls( - ?config(socket, Config), - [{certfile, ?config(certfile, Config)}, - connect]), - init_stream(set_opt(socket, TLSSocket, Config)). + receive + #starttls_proceed{} when ShouldFail -> + ct:fail(starttls_should_have_failed); + #starttls_failure{} when ShouldFail -> + Config; + #starttls_failure{} -> + ct:fail(starttls_failed); + #starttls_proceed{} -> + ok = recv_call(Config, {starttls, ?config(certfile, Config)}), + Config + end. zlib(Config) -> send(Config, #compress{methods = [<<"zlib">>]}), - #compressed{} = recv(), - ZlibSocket = ejabberd_socket:compress(?config(socket, Config)), - init_stream(set_opt(socket, ZlibSocket, Config)). + receive #compressed{} -> ok end, + ok = recv_call(Config, compress), + process_stream_features(init_stream(Config)). auth(Config) -> + auth(Config, false). + +auth(Config, ShouldFail) -> + Type = ?config(type, Config), + IsAnonymous = ?config(anonymous, Config), Mechs = ?config(mechs, Config), HaveMD5 = lists:member(<<"DIGEST-MD5">>, Mechs), HavePLAIN = lists:member(<<"PLAIN">>, Mechs), - if HavePLAIN -> - auth_SASL(<<"PLAIN">>, Config); + HaveExternal = lists:member(<<"EXTERNAL">>, Mechs), + HaveAnonymous = lists:member(<<"ANONYMOUS">>, Mechs), + if HaveAnonymous and IsAnonymous -> + auth_SASL(<<"ANONYMOUS">>, Config, ShouldFail); + HavePLAIN -> + auth_SASL(<<"PLAIN">>, Config, ShouldFail); HaveMD5 -> - auth_SASL(<<"DIGEST-MD5">>, Config); + auth_SASL(<<"DIGEST-MD5">>, Config, ShouldFail); + HaveExternal -> + auth_SASL(<<"EXTERNAL">>, Config, ShouldFail); + Type == client -> + auth_legacy(Config, false, ShouldFail); + Type == component -> + auth_component(Config, ShouldFail); true -> - ct:fail(no_sasl_mechanisms_available) + ct:fail(no_known_sasl_mechanism_available) end. bind(Config) -> - #iq{type = result, sub_els = [#bind{}]} = - send_recv( - Config, - #iq{type = set, - sub_els = [#bind{resource = ?config(resource, Config)}]}), - Config. + U = ?config(user, Config), + S = ?config(server, Config), + R = ?config(resource, Config), + case ?config(type, Config) of + client -> + #iq{type = result, sub_els = [#bind{jid = JID}]} = + send_recv( + Config, #iq{type = set, sub_els = [#bind{resource = R}]}), + case ?config(anonymous, Config) of + false -> + {U, S, R} = jid:tolower(JID), + Config; + true -> + {User, S, Resource} = jid:tolower(JID), + set_opt(user, User, set_opt(resource, Resource, Config)) + end; + component -> + Config + end. open_session(Config) -> - #iq{type = result, sub_els = []} = - send_recv(Config, #iq{type = set, sub_els = [#session{}]}), + open_session(Config, false). + +open_session(Config, Force) -> + if Force -> + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [#xmpp_session{}]}); + true -> + ok + end, Config. +auth_legacy(Config, IsDigest) -> + auth_legacy(Config, IsDigest, false). + +auth_legacy(Config, IsDigest, ShouldFail) -> + ServerJID = server_jid(Config), + U = ?config(user, Config), + R = ?config(resource, Config), + P = ?config(password, Config), + #iq{type = result, + from = ServerJID, + sub_els = [#legacy_auth{username = <<"">>, + password = <<"">>, + resource = <<"">>} = Auth]} = + send_recv(Config, + #iq{to = ServerJID, type = get, + sub_els = [#legacy_auth{}]}), + Res = case Auth#legacy_auth.digest of + <<"">> when IsDigest -> + StreamID = ?config(stream_id, Config), + D = p1_sha:sha(<<StreamID/binary, P/binary>>), + send_recv(Config, #iq{to = ServerJID, type = set, + sub_els = [#legacy_auth{username = U, + resource = R, + digest = D}]}); + _ when not IsDigest -> + send_recv(Config, #iq{to = ServerJID, type = set, + sub_els = [#legacy_auth{username = U, + resource = R, + password = P}]}) + end, + case Res of + #iq{from = ServerJID, type = result, sub_els = []} -> + if ShouldFail -> + ct:fail(legacy_auth_should_have_failed); + true -> + Config + end; + #iq{from = ServerJID, type = error} -> + if ShouldFail -> + Config; + true -> + ct:fail(legacy_auth_failed) + end + end. + +auth_component(Config, ShouldFail) -> + StreamID = ?config(stream_id, Config), + Password = ?config(password, Config), + Digest = p1_sha:sha(<<StreamID/binary, Password/binary>>), + send(Config, #handshake{data = Digest}), + receive + #handshake{} when ShouldFail -> + ct:fail(component_auth_should_have_failed); + #handshake{} -> + Config; + #stream_error{reason = 'not-authorized'} when ShouldFail -> + Config; + #stream_error{reason = 'not-authorized'} -> + ct:fail(component_auth_failed) + end. + auth_SASL(Mech, Config) -> - {Response, SASL} = sasl_new(Mech, - ?config(user, Config), - ?config(server, Config), - ?config(password, Config)), + auth_SASL(Mech, Config, false). + +auth_SASL(Mech, Config, ShouldFail) -> + Creds = {?config(user, Config), + ?config(server, Config), + ?config(password, Config)}, + auth_SASL(Mech, Config, ShouldFail, Creds). + +auth_SASL(Mech, Config, ShouldFail, Creds) -> + {Response, SASL} = sasl_new(Mech, Creds), send(Config, #sasl_auth{mechanism = Mech, text = Response}), - wait_auth_SASL_result(set_opt(sasl, SASL, Config)). + wait_auth_SASL_result(set_opt(sasl, SASL, Config), ShouldFail). -wait_auth_SASL_result(Config) -> - case recv() of +wait_auth_SASL_result(Config, ShouldFail) -> + receive + #sasl_success{} when ShouldFail -> + ct:fail(sasl_auth_should_have_failed); #sasl_success{} -> - ejabberd_socket:reset_stream(?config(socket, Config)), - send_text(Config, - io_lib:format(?STREAM_HEADER, - [?config(server, Config)])), - {xmlstreamstart, <<"stream:stream">>, Attrs} = recv(), - <<"jabber:client">> = fxml:get_attr_s(<<"xmlns">>, Attrs), - <<"1.0">> = fxml:get_attr_s(<<"version">>, Attrs), - #stream_features{sub_els = Fs} = recv(), - lists:foldl( - fun(#feature_sm{}, ConfigAcc) -> - set_opt(sm, true, ConfigAcc); - (#feature_csi{}, ConfigAcc) -> - set_opt(csi, true, ConfigAcc); - (_, ConfigAcc) -> - ConfigAcc - end, Config, Fs); + ok = recv_call(Config, reset_stream), + send(Config, stream_header(Config)), + Type = ?config(type, Config), + NS = if Type == client -> ?NS_CLIENT; + Type == server -> ?NS_SERVER + end, + Config2 = receive #stream_start{id = ID, xmlns = NS, version = {1,0}} -> + set_opt(stream_id, ID, Config) + end, + receive #stream_features{sub_els = Fs} -> + if Type == client -> + #xmpp_session{optional = true} = + lists:keyfind(xmpp_session, 1, Fs); + true -> + ok + end, + lists:foldl( + fun(#feature_sm{}, ConfigAcc) -> + set_opt(sm, true, ConfigAcc); + (#feature_csi{}, ConfigAcc) -> + set_opt(csi, true, ConfigAcc); + (#rosterver_feature{}, ConfigAcc) -> + set_opt(rosterver, true, ConfigAcc); + (#compression{methods = Ms}, ConfigAcc) -> + set_opt(compression, Ms, ConfigAcc); + (_, ConfigAcc) -> + ConfigAcc + end, Config2, Fs) + end; #sasl_challenge{text = ClientIn} -> {Response, SASL} = (?config(sasl, Config))(ClientIn), send(Config, #sasl_response{text = Response}), - wait_auth_SASL_result(set_opt(sasl, SASL, Config)); + wait_auth_SASL_result(set_opt(sasl, SASL, Config), ShouldFail); + #sasl_failure{} when ShouldFail -> + Config; #sasl_failure{} -> ct:fail(sasl_auth_failed) end. @@ -241,39 +510,54 @@ re_register(Config) -> User = ?config(user, Config), Server = ?config(server, Config), Pass = ?config(password, Config), - {atomic, ok} = ejabberd_auth:try_register(User, Server, Pass), - ok. + ok = ejabberd_auth:try_register(User, Server, Pass). match_failure(Received, [Match]) when is_list(Match)-> ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~s", [Received, Match]); match_failure(Received, Matches) -> ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~p", [Received, Matches]). -recv() -> +recv(_Config) -> receive - {'$gen_event', {xmlstreamelement, El}} -> - Pkt = xmpp_codec:decode(fix_ns(El)), - ct:pal("recv: ~p ->~n~s", [El, xmpp_codec:pp(Pkt)]), - Pkt; - {'$gen_event', Event} -> - Event + {fail, El, Why} -> + ct:fail("recv failed: ~p->~n~s", + [El, xmpp:format_error(Why)]); + Event -> + Event + end. + +recv_iq(_Config) -> + receive #iq{} = IQ -> IQ end. + +recv_presence(_Config) -> + receive #presence{} = Pres -> Pres end. + +recv_message(_Config) -> + receive #message{} = Msg -> Msg end. + +decode_stream_element(NS, El) -> + decode(El, NS, []). + +format_element(El) -> + case erlang:function_exported(ct, log, 5) of + true -> ejabberd_web_admin:pretty_print_xml(El); + false -> io_lib:format("~p~n", [El]) end. -fix_ns(#xmlel{name = Tag, attrs = Attrs} = El) - when Tag == <<"stream:features">>; Tag == <<"stream:error">> -> - NewAttrs = [{<<"xmlns">>, <<"http://etherx.jabber.org/streams">>} - |lists:keydelete(<<"xmlns">>, 1, Attrs)], - El#xmlel{attrs = NewAttrs}; -fix_ns(#xmlel{name = Tag, attrs = Attrs} = El) - when Tag == <<"message">>; Tag == <<"iq">>; Tag == <<"presence">> -> - NewAttrs = [{<<"xmlns">>, <<"jabber:client">>} - |lists:keydelete(<<"xmlns">>, 1, Attrs)], - El#xmlel{attrs = NewAttrs}; -fix_ns(El) -> - El. +decode(El, NS, Opts) -> + try + Pkt = xmpp:decode(El, NS, Opts), + ct:pal("RECV:~n~s~n~s", + [format_element(El), xmpp:pp(Pkt)]), + Pkt + catch _:{xmpp_codec, Why} -> + ct:pal("recv failed: ~p->~n~s", + [El, xmpp:format_error(Why)]), + erlang:error({xmpp_codec, Why}) + end. send_text(Config, Text) -> - ejabberd_socket:send(?config(socket, Config), Text). + recv_call(Config, {send_text, Text}). send(State, Pkt) -> {NewID, NewPkt} = case Pkt of @@ -289,22 +573,39 @@ send(State, Pkt) -> _ -> {undefined, Pkt} end, - El = xmpp_codec:encode(NewPkt), - ct:pal("sent: ~p <-~n~s", [El, xmpp_codec:pp(NewPkt)]), - ok = send_text(State, fxml:element_to_binary(El)), + El = xmpp:encode(NewPkt), + ct:pal("SENT:~n~s~n~s", + [format_element(El), xmpp:pp(NewPkt)]), + Data = case NewPkt of + #stream_start{} -> fxml:element_to_header(El); + _ -> fxml:element_to_binary(El) + end, + ok = send_text(State, Data), NewID. -send_recv(State, IQ) -> +send_recv(State, #message{} = Msg) -> + ID = send(State, Msg), + receive #message{id = ID} = Result -> Result end; +send_recv(State, #presence{} = Pres) -> + ID = send(State, Pres), + receive #presence{id = ID} = Result -> Result end; +send_recv(State, #iq{} = IQ) -> ID = send(State, IQ), - #iq{id = ID} = recv(). + receive #iq{id = ID} = Result -> Result end. -sasl_new(<<"PLAIN">>, User, Server, Password) -> +sasl_new(<<"PLAIN">>, {User, Server, Password}) -> {<<User/binary, $@, Server/binary, 0, User/binary, 0, Password/binary>>, fun (_) -> {error, <<"Invalid SASL challenge">>} end}; -sasl_new(<<"DIGEST-MD5">>, User, Server, Password) -> +sasl_new(<<"EXTERNAL">>, {User, Server, _Password}) -> + {jid:encode(jid:make(User, Server)), + fun(_) -> ct:fail(sasl_challenge_is_not_expected) end}; +sasl_new(<<"ANONYMOUS">>, _) -> + {<<"">>, + fun(_) -> ct:fail(sasl_challenge_is_not_expected) end}; +sasl_new(<<"DIGEST-MD5">>, {User, Server, Password}) -> {<<"">>, fun (ServerIn) -> - case cyrsasl_digest:parse(ServerIn) of + case xmpp_sasl_digest:parse(ServerIn) of bad -> {error, <<"Invalid SASL challenge">>}; KeyVals -> Nonce = fxml:get_attr_s(<<"nonce">>, KeyVals), @@ -330,7 +631,7 @@ sasl_new(<<"DIGEST-MD5">>, User, Server, Password) -> MyResponse/binary, "\"">>, {Resp, fun (ServerIn2) -> - case cyrsasl_digest:parse(ServerIn2) of + case xmpp_sasl_digest:parse(ServerIn2) of bad -> {error, <<"Invalid SASL challenge">>}; _KeyVals2 -> {<<"">>, @@ -387,6 +688,10 @@ proxy_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"proxy.", Server/binary>>, <<>>). +upload_jid(Config) -> + Server = ?config(server, Config), + jid:make(<<>>, <<"upload.", Server/binary>>, <<>>). + muc_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"conference.", Server/binary>>, <<>>). @@ -395,6 +700,20 @@ muc_room_jid(Config) -> Server = ?config(server, Config), jid:make(<<"test">>, <<"conference.", Server/binary>>, <<>>). +my_muc_jid(Config) -> + Nick = ?config(nick, Config), + RoomJID = muc_room_jid(Config), + jid:replace_resource(RoomJID, Nick). + +peer_muc_jid(Config) -> + PeerNick = ?config(peer_nick, Config), + RoomJID = muc_room_jid(Config), + jid:replace_resource(RoomJID, PeerNick). + +alt_room_jid(Config) -> + Server = ?config(server, Config), + jid:make(<<"alt">>, <<"conference.", Server/binary>>, <<>>). + mix_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"mix.", Server/binary>>, <<>>). @@ -404,10 +723,10 @@ mix_room_jid(Config) -> jid:make(<<"test">>, <<"mix.", Server/binary>>, <<>>). id() -> - id(undefined). + id(<<>>). -id(undefined) -> - randoms:get_string(); +id(<<>>) -> + p1_rand:get_string(); id(ID) -> ID. @@ -415,6 +734,7 @@ get_features(Config) -> get_features(Config, server_jid(Config)). get_features(Config, To) -> + ct:comment("Getting features of ~s", [jid:encode(To)]), #iq{type = result, sub_els = [#disco_info{features = Features}]} = send_recv(Config, #iq{type = get, sub_els = [#disco_info{}], to = To}), Features. @@ -430,16 +750,130 @@ set_opt(Opt, Val, Config) -> [{Opt, Val}|lists:keydelete(Opt, 1, Config)]. wait_for_master(Config) -> - put_event(Config, slave_ready), - master_ready = get_event(Config). + put_event(Config, peer_ready), + case get_event(Config) of + peer_ready -> + ok; + Other -> + suite:match_failure(Other, peer_ready) + end. wait_for_slave(Config) -> - put_event(Config, master_ready), - slave_ready = get_event(Config). + put_event(Config, peer_ready), + case get_event(Config) of + peer_ready -> + ok; + Other -> + suite:match_failure(Other, peer_ready) + end. make_iq_result(#iq{from = From} = IQ) -> IQ#iq{type = result, to = From, from = undefined, sub_els = []}. +self_presence(Config, Type) -> + MyJID = my_jid(Config), + ct:comment("Sending self-presence"), + #presence{type = Type, from = MyJID} = + send_recv(Config, #presence{type = Type}). + +set_roster(Config, Subscription, Groups) -> + MyJID = my_jid(Config), + {U, S, _} = jid:tolower(MyJID), + PeerJID = ?config(peer, Config), + PeerBareJID = jid:remove_resource(PeerJID), + PeerLJID = jid:tolower(PeerBareJID), + ct:comment("Adding ~s to roster with subscription '~s' in groups ~p", + [jid:encode(PeerBareJID), Subscription, Groups]), + {atomic, _} = mod_roster:set_roster(#roster{usj = {U, S, PeerLJID}, + us = {U, S}, + jid = PeerLJID, + subscription = Subscription, + groups = Groups}), + Config. + +del_roster(Config) -> + del_roster(Config, ?config(peer, Config)). + +del_roster(Config, PeerJID) -> + MyJID = my_jid(Config), + {U, S, _} = jid:tolower(MyJID), + PeerBareJID = jid:remove_resource(PeerJID), + PeerLJID = jid:tolower(PeerBareJID), + ct:comment("Removing ~s from roster", [jid:encode(PeerBareJID)]), + {atomic, _} = mod_roster:del_roster(U, S, PeerLJID), + Config. + +get_roster(Config) -> + {LUser, LServer, _} = jid:tolower(my_jid(Config)), + mod_roster:get_roster(LUser, LServer). + +recv_call(Config, Msg) -> + Receiver = ?config(receiver, Config), + Ref = make_ref(), + Receiver ! {Ref, Msg}, + receive + {Ref, Reply} -> + Reply + end. + +start_receiver(NS, Owner, Server, Port) -> + MRef = erlang:monitor(process, Owner), + {ok, Socket} = xmpp_socket:connect( + Server, Port, + [binary, {packet, 0}, {active, false}], infinity), + receiver(NS, Owner, Socket, MRef). + +receiver(NS, Owner, Socket, MRef) -> + receive + {Ref, reset_stream} -> + Socket1 = xmpp_socket:reset_stream(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, Socket1, MRef); + {Ref, {starttls, Certfile}} -> + {ok, TLSSocket} = xmpp_socket:starttls( + Socket, + [{certfile, Certfile}, connect]), + Owner ! {Ref, ok}, + receiver(NS, Owner, TLSSocket, MRef); + {Ref, compress} -> + {ok, ZlibSocket} = xmpp_socket:compress(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, ZlibSocket, MRef); + {Ref, {send_text, Text}} -> + Ret = xmpp_socket:send(Socket, Text), + Owner ! {Ref, Ret}, + receiver(NS, Owner, Socket, MRef); + {Ref, close} -> + xmpp_socket:close(Socket), + Owner ! {Ref, ok}, + receiver(NS, Owner, Socket, MRef); + {'$gen_event', {xmlstreamelement, El}} -> + Owner ! decode_stream_element(NS, El), + receiver(NS, Owner, Socket, MRef); + {'$gen_event', {xmlstreamstart, Name, Attrs}} -> + Owner ! decode(#xmlel{name = Name, attrs = Attrs}, <<>>, []), + receiver(NS, Owner, Socket, MRef); + {'$gen_event', Event} -> + Owner ! Event, + receiver(NS, Owner, Socket, MRef); + {'DOWN', MRef, process, Owner, _} -> + ok; + {tcp, _, Data} -> + case xmpp_socket:recv(Socket, Data) of + {ok, Socket1} -> + receiver(NS, Owner, Socket1, MRef); + {error, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef) + end; + {tcp_error, _, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef); + {tcp_closed, _} -> + Owner ! closed, + receiver(NS, Owner, Socket, MRef) + end. + %%%=================================================================== %%% Clients puts and gets events via this relay. %%%=================================================================== @@ -456,6 +890,7 @@ event_relay() -> event_relay(Events, Subscribers) -> receive {subscribe, From} -> + erlang:monitor(process, From), From ! {ok, self()}, lists:foreach( fun(Event) -> From ! {event, Event, self()} @@ -469,7 +904,19 @@ event_relay(Events, Subscribers) -> (_) -> ok end, Subscribers), - event_relay([Event|Events], Subscribers) + event_relay([Event|Events], Subscribers); + {'DOWN', _MRef, process, Pid, _Info} -> + case lists:member(Pid, Subscribers) of + true -> + NewSubscribers = lists:delete(Pid, Subscribers), + lists:foreach( + fun(Subscriber) -> + Subscriber ! {event, peer_down, self()} + end, NewSubscribers), + event_relay(Events, NewSubscribers); + false -> + event_relay(Events, Subscribers) + end end. subscribe_to_events(Config) -> @@ -494,3 +941,12 @@ get_event(Config) -> {event, Event, Relay} -> Event end. + +flush(Config) -> + receive + {event, peer_down, _} -> flush(Config); + closed -> flush(Config); + Msg -> ct:fail({unexpected_msg, Msg}) + after 0 -> + ok + end. diff --git a/test/suite.hrl b/test/suite.hrl index fb6b4f3ac..194c48eb5 100644 --- a/test/suite.hrl +++ b/test/suite.hrl @@ -1,16 +1,9 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("fast_xml/include/fxml.hrl"). -include("ns.hrl"). --include("ejabberd.hrl"). -include("mod_proxy65.hrl"). -include("xmpp_codec.hrl"). --define(STREAM_HEADER, - <<"<?xml version='1.0'?><stream:stream " - "xmlns:stream='http://etherx.jabber.org/stream" - "s' xmlns='jabber:client' to='~s' version='1.0" - "'>">>). - -define(STREAM_TRAILER, <<"</stream:stream>">>). -define(PUBSUB(Node), <<(?NS_PUBSUB)/binary, "#", Node>>). @@ -19,7 +12,7 @@ -define(recv1(P1), P1 = (fun() -> - V = recv(), + V = suite:recv(Config), case V of P1 -> V; _ -> suite:match_failure([V], [??P1]) @@ -28,7 +21,7 @@ -define(recv2(P1, P2), (fun() -> - case {R1 = recv(), R2 = recv()} of + case {R1 = suite:recv(Config), R2 = suite:recv(Config)} of {P1, P2} -> {R1, R2}; {P2, P1} -> {R2, R1}; {P1, V1} -> suite:match_failure([V1], [P2]); @@ -41,7 +34,7 @@ -define(recv3(P1, P2, P3), (fun() -> - case R3 = recv() of + case R3 = suite:recv(Config) of P1 -> insert(R3, 1, ?recv2(P2, P3)); P2 -> insert(R3, 2, ?recv2(P1, P3)); P3 -> insert(R3, 3, ?recv2(P1, P2)); @@ -51,7 +44,7 @@ -define(recv4(P1, P2, P3, P4), (fun() -> - case R4 = recv() of + case R4 = suite:recv(Config) of P1 -> insert(R4, 1, ?recv3(P2, P3, P4)); P2 -> insert(R4, 2, ?recv3(P1, P3, P4)); P3 -> insert(R4, 3, ?recv3(P1, P2, P4)); @@ -62,7 +55,7 @@ -define(recv5(P1, P2, P3, P4, P5), (fun() -> - case R5 = recv() of + case R5 = suite:recv(Config) of P1 -> insert(R5, 1, ?recv4(P2, P3, P4, P5)); P2 -> insert(R5, 2, ?recv4(P1, P3, P4, P5)); P3 -> insert(R5, 3, ?recv4(P1, P2, P4, P5)); @@ -72,6 +65,29 @@ end end)()). +-define(match(Pattern, Result), + (fun() -> + case Result of + Pattern -> + ok; + Mismatch -> + suite:match_failure([Mismatch], [??Pattern]) + end + end)()). + +-define(match(Pattern, Result, PatternRes), + (fun() -> + case Result of + Pattern -> + PatternRes; + Mismatch -> + suite:match_failure([Mismatch], [??Pattern]) + end + end)()). + +-define(send_recv(Send, Recv), + ?match(Recv, suite:send_recv(Config, Send))). + -define(COMMON_VHOST, <<"localhost">>). -define(MNESIA_VHOST, <<"mnesia.localhost">>). -define(REDIS_VHOST, <<"redis.localhost">>). @@ -80,7 +96,10 @@ -define(SQLITE_VHOST, <<"sqlite.localhost">>). -define(LDAP_VHOST, <<"ldap.localhost">>). -define(EXTAUTH_VHOST, <<"extauth.localhost">>). --define(RIAK_VHOST, <<"riak.localhost">>). +-define(S2S_VHOST, <<"s2s.localhost">>). +-define(UPLOAD_VHOST, <<"upload.localhost">>). + +-define(BACKENDS, [mnesia, redis, mysql, pgsql, sqlite, ldap, extauth]). insert(Val, N, Tuple) -> L = tuple_to_list(Tuple), diff --git a/test/test_helper.exs b/test/test_helper.exs deleted file mode 100644 index 454f2338a..000000000 --- a/test/test_helper.exs +++ /dev/null @@ -1,7 +0,0 @@ -Code.require_file "ejabberd_auth_mock.exs", __DIR__ -Code.require_file "ejabberd_oauth_mock.exs", __DIR__ -Code.require_file "ejabberd_sm_mock.exs", __DIR__ -Code.require_file "mod_last_mock.exs", __DIR__ -Code.require_file "mod_roster_mock.exs", __DIR__ - -ExUnit.start diff --git a/test/upload_tests.erl b/test/upload_tests.erl new file mode 100644 index 000000000..9d4b86d6a --- /dev/null +++ b/test/upload_tests.erl @@ -0,0 +1,213 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 17 May 2018 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%------------------------------------------------------------------- +-module(upload_tests). + +%% API +-compile(export_all). +-import(suite, [disconnect/1, is_feature_advertised/3, upload_jid/1, + my_jid/1, wait_for_slave/1, wait_for_master/1, + send_recv/2, put_event/2, get_event/1]). + +-include("suite.hrl"). +-define(CONTENT_TYPE, "image/png"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {upload_single, [sequence], + [single_test(feature_enabled), + single_test(service_vcard), + single_test(get_max_size), + single_test(slot_request), + single_test(put_get_request), + single_test(max_size_exceed)]}. + +feature_enabled(Config) -> + lists:foreach( + fun(NS) -> + true = is_feature_advertised(Config, NS, upload_jid(Config)) + end, namespaces()), + disconnect(Config). + +service_vcard(Config) -> + Upload = upload_jid(Config), + ct:comment("Retreiving vCard from ~s", [jid:encode(Upload)]), + VCard = mod_http_upload_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = Upload, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +get_max_size(Config) -> + Xs = get_disco_info_xdata(Config), + lists:foreach( + fun(NS) -> + get_max_size(Config, Xs, NS) + end, namespaces()), + disconnect(Config). + +get_max_size(_, _, ?NS_HTTP_UPLOAD_OLD) -> + %% This old spec didn't specify 'max-file-size' attribute + ok; +get_max_size(Config, Xs, NS) -> + Xs = get_disco_info_xdata(Config), + get_size(NS, Config, Xs). + +slot_request(Config) -> + lists:foreach( + fun(NS) -> + slot_request(Config, NS) + end, namespaces()), + disconnect(Config). + +put_get_request(Config) -> + lists:foreach( + fun(NS) -> + {GetURL, PutURL, _Filename, Size} = slot_request(Config, NS), + Data = p1_rand:bytes(Size), + put_request(Config, PutURL, Data), + get_request(Config, GetURL, Data) + end, namespaces()), + disconnect(Config). + +max_size_exceed(Config) -> + lists:foreach( + fun(NS) -> + max_size_exceed(Config, NS) + end, namespaces()), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("upload_" ++ atom_to_list(T)). + +get_disco_info_xdata(Config) -> + To = upload_jid(Config), + #iq{type = result, sub_els = [#disco_info{xdata = Xs}]} = + send_recv(Config, + #iq{type = get, sub_els = [#disco_info{}], to = To}), + Xs. + +get_size(NS, Config, [X|Xs]) -> + case xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X) of + [NS] -> + [Size] = xmpp_util:get_xdata_values(<<"max-file-size">>, X), + true = erlang:binary_to_integer(Size) > 0, + Size; + _ -> + get_size(NS, Config, Xs) + end; +get_size(NS, _Config, []) -> + ct:fail({disco_info_xdata_failed, NS}). + +slot_request(Config, NS) -> + To = upload_jid(Config), + Filename = filename(), + Size = p1_rand:uniform(1, 1024), + case NS of + ?NS_HTTP_UPLOAD_0 -> + #iq{type = result, + sub_els = [#upload_slot_0{get = GetURL, + put = PutURL, + xmlns = NS}]} = + send_recv(Config, + #iq{type = get, to = To, + sub_els = [#upload_request_0{ + filename = Filename, + size = Size, + 'content-type' = <<?CONTENT_TYPE>>, + xmlns = NS}]}), + {GetURL, PutURL, Filename, Size}; + _ -> + #iq{type = result, + sub_els = [#upload_slot{get = GetURL, + put = PutURL, + xmlns = NS}]} = + send_recv(Config, + #iq{type = get, to = To, + sub_els = [#upload_request{ + filename = Filename, + size = Size, + 'content-type' = <<?CONTENT_TYPE>>, + xmlns = NS}]}), + {GetURL, PutURL, Filename, Size} + end. + +put_request(_Config, URL0, Data) -> + ct:comment("Putting ~B bytes to ~s", [size(Data), URL0]), + URL = binary_to_list(URL0), + {ok, {{"HTTP/1.1", 201, _}, _, _}} = + httpc:request(put, {URL, [], ?CONTENT_TYPE, Data}, [], []). + +get_request(_Config, URL0, Data) -> + ct:comment("Getting ~B bytes from ~s", [size(Data), URL0]), + URL = binary_to_list(URL0), + {ok, {{"HTTP/1.1", 200, _}, _, Body}} = + httpc:request(get, {URL, []}, [], [{body_format, binary}]), + ct:comment("Checking returned body"), + Body = Data. + +max_size_exceed(Config, NS) -> + To = upload_jid(Config), + Filename = filename(), + Size = 1000000000, + IQErr = + case NS of + ?NS_HTTP_UPLOAD_0 -> + #iq{type = error} = + send_recv(Config, + #iq{type = get, to = To, + sub_els = [#upload_request_0{ + filename = Filename, + size = Size, + 'content-type' = <<?CONTENT_TYPE>>, + xmlns = NS}]}); + _ -> + #iq{type = error} = + send_recv(Config, + #iq{type = get, to = To, + sub_els = [#upload_request{ + filename = Filename, + size = Size, + 'content-type' = <<?CONTENT_TYPE>>, + xmlns = NS}]}) + end, + check_size_error(IQErr, Size, NS). + +check_size_error(IQErr, Size, NS) -> + Err = xmpp:get_error(IQErr), + FileTooLarge = xmpp:get_subtag(Err, #upload_file_too_large{xmlns = NS}), + #stanza_error{reason = 'not-acceptable'} = Err, + #upload_file_too_large{'max-file-size' = MaxSize} = FileTooLarge, + true = Size > MaxSize. + +namespaces() -> + [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD, ?NS_HTTP_UPLOAD_OLD]. + +filename() -> + <<(p1_rand:get_string())/binary, ".png">>. diff --git a/test/vcard_tests.erl b/test/vcard_tests.erl new file mode 100644 index 000000000..530429590 --- /dev/null +++ b/test/vcard_tests.erl @@ -0,0 +1,149 @@ +%%%------------------------------------------------------------------- +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2019 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. +%%% +%%%---------------------------------------------------------------------- + +-module(vcard_tests). + +%% API +-compile(export_all). +-import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, + is_feature_advertised/3, server_jid/1, + my_jid/1, wait_for_slave/1, wait_for_master/1, + recv_presence/1, recv/1]). + +-include("suite.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%%=================================================================== +%%% Single user tests +%%%=================================================================== +single_cases() -> + {vcard_single, [sequence], + [single_test(feature_enabled), + single_test(get_set), + single_test(service_vcard)]}. + +feature_enabled(Config) -> + BareMyJID = jid:remove_resource(my_jid(Config)), + true = is_feature_advertised(Config, ?NS_VCARD), + true = is_feature_advertised(Config, ?NS_VCARD, BareMyJID), + disconnect(Config). + +get_set(Config) -> + VCard = + #vcard_temp{fn = <<"Peter Saint-Andre">>, + n = #vcard_name{family = <<"Saint-Andre">>, + given = <<"Peter">>}, + nickname = <<"stpeter">>, + bday = <<"1966-08-06">>, + adr = [#vcard_adr{work = true, + extadd = <<"Suite 600">>, + street = <<"1899 Wynkoop Street">>, + locality = <<"Denver">>, + region = <<"CO">>, + pcode = <<"80202">>, + ctry = <<"USA">>}, + #vcard_adr{home = true, + locality = <<"Denver">>, + region = <<"CO">>, + pcode = <<"80209">>, + ctry = <<"USA">>}], + tel = [#vcard_tel{work = true,voice = true, + number = <<"303-308-3282">>}, + #vcard_tel{home = true,voice = true, + number = <<"303-555-1212">>}], + email = [#vcard_email{internet = true,pref = true, + userid = <<"stpeter@jabber.org">>}], + jabberid = <<"stpeter@jabber.org">>, + title = <<"Executive Director">>,role = <<"Patron Saint">>, + org = #vcard_org{name = <<"XMPP Standards Foundation">>}, + url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>, + desc = <<"More information about me is located on my " + "personal website: http://www.saint-andre.com/">>}, + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [VCard]}), + %% TODO: check if VCard == VCard1. + #iq{type = result, sub_els = [_VCard1]} = + send_recv(Config, #iq{type = get, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +service_vcard(Config) -> + JID = server_jid(Config), + ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), + VCard = mod_vcard_opt:vcard(?config(server, Config)), + #iq{type = result, sub_els = [VCard]} = + send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), + disconnect(Config). + +%%%=================================================================== +%%% Master-slave tests +%%%=================================================================== +master_slave_cases() -> + {vcard_master_slave, [sequence], []}. + %%[master_slave_test(xupdate)]}. + +xupdate_master(Config) -> + Img = <<137, "PNG\r\n", 26, $\n>>, + ImgHash = p1_sha:sha(Img), + MyJID = my_jid(Config), + Peer = ?config(slave, Config), + wait_for_slave(Config), + #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), + #presence{from = Peer, type = available} = recv_presence(Config), + VCard = #vcard_temp{photo = #vcard_photo{type = <<"image/png">>, binval = Img}}, + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [VCard]}), + #presence{from = MyJID, type = available, + sub_els = [#vcard_xupdate{hash = ImgHash}]} = recv_presence(Config), + #iq{type = result, sub_els = []} = + send_recv(Config, #iq{type = set, sub_els = [#vcard_temp{}]}), + ?recv2(#presence{from = MyJID, type = available, + sub_els = [#vcard_xupdate{hash = undefined}]}, + #presence{from = Peer, type = unavailable}), + disconnect(Config). + +xupdate_slave(Config) -> + Img = <<137, "PNG\r\n", 26, $\n>>, + ImgHash = p1_sha:sha(Img), + MyJID = my_jid(Config), + Peer = ?config(master, Config), + #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), + wait_for_master(Config), + #presence{from = Peer, type = available} = recv_presence(Config), + #presence{from = Peer, type = available, + sub_els = [#vcard_xupdate{hash = ImgHash}]} = recv_presence(Config), + #presence{from = Peer, type = available, + sub_els = [#vcard_xupdate{hash = undefined}]} = recv_presence(Config), + disconnect(Config). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +single_test(T) -> + list_to_atom("vcard_" ++ atom_to_list(T)). + +master_slave_test(T) -> + {list_to_atom("vcard_" ++ atom_to_list(T)), [parallel], + [list_to_atom("vcard_" ++ atom_to_list(T) ++ "_master"), + list_to_atom("vcard_" ++ atom_to_list(T) ++ "_slave")]}. |