aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/README32
-rw-r--r--test/README-quicktest.md33
-rw-r--r--test/acl_test.exs446
-rw-r--r--test/announce_tests.erl76
-rw-r--r--test/carbons_tests.erl211
-rw-r--r--test/csi_tests.erl162
-rw-r--r--test/docker/README.md46
-rw-r--r--test/docker/db/mysql/initdb/mysql.sql465
-rw-r--r--test/docker/db/postgres/initdb/pg.sql470
-rw-r--r--test/docker/docker-compose.yml37
-rw-r--r--test/ejabberd_SUITE.erl2599
-rw-r--r--test/ejabberd_SUITE_data/ca.key27
-rw-r--r--test/ejabberd_SUITE_data/ca.pem21
-rw-r--r--test/ejabberd_SUITE_data/cert.pem96
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.extauth.yml5
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.ldap.yml36
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.mnesia.yml65
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.mysql.yml72
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.pgsql.yml72
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.redis.yml66
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.sqlite.yml66
-rw-r--r--test/ejabberd_SUITE_data/ejabberd.yml554
-rwxr-xr-xtest/ejabberd_SUITE_data/extauth.py33
-rwxr-xr-xtest/ejabberd_SUITE_data/gencerts.sh20
-rw-r--r--test/ejabberd_SUITE_data/macros.yml128
-rw-r--r--test/ejabberd_SUITE_data/openssl.cnf322
-rw-r--r--test/ejabberd_SUITE_data/self-signed-cert.pem47
-rw-r--r--test/ejabberd_admin_test.exs79
-rw-r--r--test/ejabberd_auth_mock.exs74
-rw-r--r--test/ejabberd_commands_mock_test.exs469
-rw-r--r--test/ejabberd_commands_test.exs106
-rw-r--r--test/ejabberd_cyrsasl_test.exs153
-rw-r--r--test/ejabberd_hooks_test.exs203
-rw-r--r--test/ejabberd_oauth_mock.exs47
-rw-r--r--test/ejabberd_sm_mock.exs121
-rw-r--r--test/elixir-config/attr_test.exs87
-rw-r--r--test/elixir-config/config_test.exs65
-rw-r--r--test/elixir-config/ejabberd_logger.exs49
-rw-r--r--test/elixir-config/shared/ejabberd.exs31
-rw-r--r--test/elixir-config/shared/ejabberd_different_from_default.exs9
-rw-r--r--test/elixir-config/shared/ejabberd_for_validation.exs18
-rw-r--r--test/elixir-config/validation_test.exs31
-rw-r--r--test/elixir_SUITE.erl95
-rw-r--r--test/example_tests.erl67
-rw-r--r--test/jid_test.exs44
-rw-r--r--test/jidprep_tests.erl62
-rw-r--r--test/ldap_srv.erl39
-rw-r--r--test/mam_tests.erl672
-rw-r--r--test/mod_admin_extra_test.exs360
-rw-r--r--test/mod_http_api_mock_test.exs275
-rw-r--r--test/mod_http_api_test.exs123
-rw-r--r--test/mod_last_mock.exs79
-rw-r--r--test/mod_roster_mock.exs225
-rw-r--r--test/muc_tests.erl1944
-rw-r--r--test/offline_tests.erl525
-rw-r--r--test/privacy_tests.erl891
-rw-r--r--test/private_tests.erl117
-rw-r--r--test/proxy65_tests.erl129
-rw-r--r--test/pubsub_tests.erl764
-rw-r--r--test/push_tests.erl234
-rw-r--r--test/replaced_tests.erl70
-rw-r--r--test/roster_tests.erl584
-rw-r--r--test/sm_tests.erl185
-rw-r--r--test/suite.erl784
-rw-r--r--test/suite.hrl45
-rw-r--r--test/test_helper.exs7
-rw-r--r--test/upload_tests.erl213
-rw-r--r--test/vcard_tests.erl149
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")]}.