diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..483ac68 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Test + +on: + pull_request: + branches: dev + push: + branches: dev + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Checkout submodules + uses: textbook/git-checkout-submodule-action@master + + - name: install python3-venv + run: sudo apt-get install python3-venv + + - name: build test image + run: docker build . -t prosody + + - name: run tests + run: cd ./tests/ && ./test.bash diff --git a/.gitignore b/.gitignore index 07f43b8..9f0f847 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -data/* \ No newline at end of file +data/* +tests/certs/ +tests/venv/ +tests/__pycache__/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4608d8c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "tests/bats/bats-support"] + path = tests/bats/bats-support + url = https://github.com/bats-core/bats-support.git +[submodule "tests/bats/bats-core"] + path = tests/bats/bats-core + url = https://github.com/bats-core/bats-core.git +[submodule "tests/bats/bats-assert"] + path = tests/bats/bats-assert + url = https://github.com/bats-core/bats-assert.git diff --git a/tests/bats/bats-assert b/tests/bats/bats-assert new file mode 160000 index 0000000..0a8dd57 --- /dev/null +++ b/tests/bats/bats-assert @@ -0,0 +1 @@ +Subproject commit 0a8dd57e2cc6d4cc064b1ed6b4e79b9f7fee096f diff --git a/tests/bats/bats-core b/tests/bats/bats-core new file mode 160000 index 0000000..8fb853a --- /dev/null +++ b/tests/bats/bats-core @@ -0,0 +1 @@ +Subproject commit 8fb853a6cbc0169958707381985f3cd59789ccb1 diff --git a/tests/bats/bats-support b/tests/bats/bats-support new file mode 160000 index 0000000..d140a65 --- /dev/null +++ b/tests/bats/bats-support @@ -0,0 +1 @@ +Subproject commit d140a65044b2d6810381935ae7f0c94c7023c8c3 diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..c04c41b --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.7' + +services: + prosody: + image: prosody + restart: unless-stopped + ports: + - "5000:5000" + - "5222:5222" + - "5223:5223" + - "5269:5269" + - "5281:5281" + environment: + DOMAIN: localhost + E2E_POLICY_WHITELIST: "admin@localhost, user1@localhost" + LOG_LEVEL: debug + PROSODY_ADMINS: "admin@localhost, admin2@localhost" + extra_hosts: + - "conference.localhost:127.0.0.1" + - "pubsub.localhost:127.0.0.1" + - "proxy.localhost:127.0.0.1" + - "upload.localhost:127.0.0.1" + volumes: + - ./certs:/usr/local/etc/prosody/certs diff --git a/tests/readme.md b/tests/readme.md new file mode 100644 index 0000000..9a6644a --- /dev/null +++ b/tests/readme.md @@ -0,0 +1,25 @@ +# Tests + +## Dependencies + +* docker +* docker-compose +* python 3 + +## Run tests + +Execute [`test.bash`](test.bash). + +## Upgrade python packages + +The following will install the newest version of packages in requirements.txt. + +``` bash +cat requirements.txt | sed 's/==.*//g' | xargs pip install -U +``` + +If updates are available --> update and create new version with: + +``` bash +pip-chill > requirements.txt +``` diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..bcd35ba --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,3 @@ +aioxmpp==0.11.0 +pip-chill==1.0.0 +pytest-asyncio==0.14.0 diff --git a/tests/test.bash b/tests/test.bash new file mode 100755 index 0000000..75048cd --- /dev/null +++ b/tests/test.bash @@ -0,0 +1,45 @@ +#!/bin/bash + +set -e + +# generate certs for testing + +generateCert() { + DOMAIN="$1" + if [[ ! -d certs/"$DOMAIN" ]] ; then + mkdir -p certs/"$DOMAIN" + cd certs/"$DOMAIN" + openssl req -x509 -newkey rsa:4096 -keyout privkey.pem -out fullchain.pem -days 365 -subj "/CN=$DOMAIN" -nodes + chmod 777 *.pem + cd ../../ + fi +} + +generateCert "localhost" +generateCert "conference.localhost" +generateCert "proxy.localhost" +generateCert "pubsub.localhost" +generateCert "upload.localhost" + +sudo docker-compose down \ +&& sudo docker-compose up -d \ +\ +&& sudo docker exec tests_prosody_1 /bin/bash -c "/entrypoint.sh register admin localhost 12345678" \ +&& sudo docker exec tests_prosody_1 /bin/bash -c "/entrypoint.sh register user1 localhost 12345678" \ +&& sudo docker exec tests_prosody_1 /bin/bash -c "/entrypoint.sh register user2 localhost 12345678" \ +&& sudo docker exec tests_prosody_1 /bin/bash -c "/entrypoint.sh register user3 localhost 12345678" \ +\ +&& python --version \ +&& python3 --version \ +&& python3 -m venv venv \ +&& source venv/bin/activate \ +&& python --version \ +&& pip --version \ +&& pip install -r requirements.txt \ +&& pytest \ +&& deactivate \ +&& sleep 5 \ +&& sudo docker-compose logs \ +&& ./bats/bats-core/bin/bats tests.bats + +sudo docker-compose down diff --git a/tests/test_prosody.py b/tests/test_prosody.py new file mode 100644 index 0000000..e6c39bc --- /dev/null +++ b/tests/test_prosody.py @@ -0,0 +1,119 @@ +import aiosasl +import aioxmpp +import aioxmpp.dispatcher +import asyncio +import pytest + +@pytest.fixture +def client(client_username, password): + + jid = aioxmpp.JID.fromstr(client_username) + + client = aioxmpp.PresenceManagedClient( + jid, + aioxmpp.make_security_layer( + password, + no_verify=True + ), + ) + return client + +@pytest.fixture +def client_with_message_dispatcher(client): + def message_received(msg): + print(msg) + print(msg.body) + assert msg.body == "Hello World!" + + # obtain an instance of the service + message_dispatcher = client.summon( + aioxmpp.dispatcher.SimpleMessageDispatcher + ) + + # register a message callback here + message_dispatcher.register_callback( + aioxmpp.MessageType.CHAT, + None, + message_received, + ) + return client + +@pytest.mark.asyncio +@pytest.mark.parametrize("client_username, password", [("admin@localhost", "12345678")]) +async def test_send_message_from_admin_to_user1(client): + recipient_jid = aioxmpp.JID.fromstr("user1@localhost") + async with client.connected() as stream: + msg = aioxmpp.Message( + to=recipient_jid, + type_=aioxmpp.MessageType.CHAT, + ) + # None is for "default language" + msg.body[None] = "Hello World!" + + await client.send(msg) + +@pytest.mark.asyncio +@pytest.mark.parametrize("client_username, password", [("admin@localhost", "12345678")]) +async def test_send_message_from_admin_to_user2(client): + recipient_jid = aioxmpp.JID.fromstr("user2@localhost") + async with client.connected() as stream: + msg = aioxmpp.Message( + to=recipient_jid, + type_=aioxmpp.MessageType.CHAT, + ) + msg.body[None] = "Hello World!" + + await client.send(msg) + +@pytest.mark.asyncio +@pytest.mark.parametrize("client_username, password", [("user1@localhost", "12345678")]) +async def test_send_message_from_user1_to_user2(client): + recipient_jid = aioxmpp.JID.fromstr("user2@localhost") + async with client.connected() as stream: + msg = aioxmpp.Message( + to=recipient_jid, + type_=aioxmpp.MessageType.CHAT, + ) + msg.body[None] = "Hello World!" + + await client.send(msg) + +@pytest.mark.asyncio +@pytest.mark.parametrize("client_username, password", [("user2@localhost", "12345678")]) +async def test_send_message_from_user2_to_user3(client): + recipient_jid = aioxmpp.JID.fromstr("user3@localhost") + async with client.connected() as stream: + msg = aioxmpp.Message( + to=recipient_jid, + type_=aioxmpp.MessageType.CHAT, + ) + msg.body[None] = "Hello World!" + + await client.send(msg) + +@pytest.mark.asyncio +@pytest.mark.parametrize("client_username, password", [("user2@localhost", "12345678")]) +async def test_send_message_from_user2_to_nonexisting(client): + recipient_jid = aioxmpp.JID.fromstr("nonexisting@localhost") + async with client.connected() as stream: + msg = aioxmpp.Message( + to=recipient_jid, + type_=aioxmpp.MessageType.CHAT, + ) + msg.body[None] = "Hello World!" + + await client.send(msg) + +@pytest.mark.asyncio +@pytest.mark.parametrize("client_username, password", [("user2@localhost", "wrong password")]) +async def test_can_not_log_in_with_wrong_password(client): + with pytest.raises(aiosasl.AuthenticationFailure): + recipient_jid = aioxmpp.JID.fromstr("nonexisting@localhost") + async with client.connected() as stream: + msg = aioxmpp.Message( + to=recipient_jid, + type_=aioxmpp.MessageType.CHAT, + ) + msg.body[None] = "Hello World!" + + await client.send(msg) diff --git a/tests/tests.bats b/tests/tests.bats new file mode 100644 index 0000000..f9f8df2 --- /dev/null +++ b/tests/tests.bats @@ -0,0 +1,95 @@ +# For tests with pipes see: https://github.com/sstephenson/bats/issues/10 + +load 'bats/bats-support/load' +load 'bats/bats-assert/load' + +# group alternation in regex because the xml properties switch around. sometimes 'type=...' comes after 'to=...' and sometimes before +@test "Should send 5 messages" { + run bash -c "sudo docker-compose logs | grep -E \"Received\[c2s\]: \" | wc -l" + assert_success + assert_output "5" +} + +@test "Should select certificate for localhost" { + run bash -c "sudo docker-compose logs | grep \"Selecting certificate /usr/local/etc/prosody/certs/localhost/fullchain.pem with key /usr/local/etc/prosody/certs/localhost/privkey.pem for localhost\" | wc -l" + assert_success + assert_output "3" +} + +@test "Should select certificate for conference.localhost" { + run bash -c "sudo docker-compose logs | grep \"Selecting certificate /usr/local/etc/prosody/certs/conference.localhost/fullchain.pem with key /usr/local/etc/prosody/certs/conference.localhost/privkey.pem for conference.localhost\" | wc -l" + assert_success + assert_output "3" +} + +@test "Should select certificate for proxy.localhost" { + run bash -c "sudo docker-compose logs | grep \"Selecting certificate /usr/local/etc/prosody/certs/proxy.localhost/fullchain.pem with key /usr/local/etc/prosody/certs/proxy.localhost/privkey.pem for proxy.localhost\" | wc -l" + assert_success + assert_output "3" +} + +@test "Should select certificate for pubsub.localhost" { + run bash -c "sudo docker-compose logs | grep \"Selecting certificate /usr/local/etc/prosody/certs/pubsub.localhost/fullchain.pem with key /usr/local/etc/prosody/certs/pubsub.localhost/privkey.pem for pubsub.localhost\" | wc -l" + assert_success + assert_output "3" +} + +@test "Should select certificate for upload.localhost" { + run bash -c "sudo docker-compose logs | grep \"Selecting certificate /usr/local/etc/prosody/certs/upload.localhost/fullchain.pem with key /usr/local/etc/prosody/certs/upload.localhost/privkey.pem for upload.localhost\" | wc -l" + assert_success + assert_output "3" +} + +@test "Should log error for user with wrong password" { + run bash -c "sudo docker-compose logs | grep \"Session closed by remote with error: undefined-condition (user intervention: authentication failed: authentication aborted by user)\"" + assert_success + assert_output +} + +@test "Should activate s2s" { + run bash -c "sudo docker-compose logs | grep -E \"Activated service 's2s' on (\[::\]:5269|\[\*\]:5269), (\[::\]:5269|\[\*\]:5269)\"" + assert_success + assert_output +} + +@test "Should activate c2s" { + run bash -c "sudo docker-compose logs | grep -E \"Activated service 'c2s' on (\[::\]:5222|\[\*\]:5222), (\[::\]:5222|\[\*\]:5222)\"" + assert_success + assert_output +} + +@test "Should activate legacy_ssl" { + run bash -c "sudo docker-compose logs | grep -E \"Activated service 'legacy_ssl' on (\[::\]:5223|\[\*\]:5223), (\[::\]:5223|\[\*\]:5223)\"" + assert_success + assert_output +} + +@test "Should activate proxy65" { + run bash -c "sudo docker-compose logs | grep -E \"Activated service 'proxy65' on (\[::\]:5000|\[\*\]:5000), (\[::\]:5000|\[\*\]:5000)\"" + assert_success + assert_output +} + +@test "Should activate http" { + run bash -c "sudo docker-compose logs | grep -E \"Activated service 'http' on (\[::\]:5280|\[\*\]:5280), (\[::\]:5280|\[\*\]:5280)\"" + assert_success + assert_output +} + +@test "Should activate https" { + run bash -c "sudo docker-compose logs | grep -E \"Activated service 'https' on (\[::\]:5281|\[\*\]:5281), (\[::\]:5281|\[\*\]:5281)\"" + assert_success + assert_output +} + +@test "Should load module cloud_notify" { + run bash -c "sudo docker-compose logs | grep \"localhost:cloud_notify.*info.*Module loaded\"" + assert_success + assert_output +} + +@test "Should show upload URL" { + run bash -c "sudo docker-compose logs | grep \"URL: - Ensure this can be reached by users\"" + assert_success + assert_output +}