From 5f371d18d401444a3c9ffd2283b6a8a268343b07 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Wed, 25 Nov 2020 16:07:27 +0100 Subject: [PATCH] Add tests for PTGbot Add tests for most commands in PTGbot, checking that they result in the desired outcome. With this patch all track commands are tested, as well as most admin commands. Change-Id: I0a9907da6946db224a9efd2c740f275dfad0f00e --- .stestr.conf | 3 + .zuul.yaml | 9 + ptgbot/bot.py | 2 +- ptgbot/tests/__init__.py | 0 ptgbot/tests/test_message_process.py | 399 +++++++++++++++++++++++++++ requirements.txt | 1 + test-requirements.txt | 1 + tox.ini | 10 +- 8 files changed, 420 insertions(+), 5 deletions(-) create mode 100644 .stestr.conf create mode 100644 .zuul.yaml create mode 100644 ptgbot/tests/__init__.py create mode 100644 ptgbot/tests/test_message_process.py diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..cf14020 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./ptgbot/tests +top_path=./ diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 0000000..e48694f --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,9 @@ +- project: + check: + jobs: + - tox-pep8 + - tox-py38 + gate: + jobs: + - tox-pep8 + - tox-py38 diff --git a/ptgbot/bot.py b/ptgbot/bot.py index 605c3a3..4f4063f 100644 --- a/ptgbot/bot.py +++ b/ptgbot/bot.py @@ -469,7 +469,7 @@ class PTGBot(SASL, SSL, irc.bot.SingleServerIRCBot): return getattr(self.data, command + '_tracks')(words[1:]) else: - self.send(chan, "%s: unknown command '%s'" % (nick, command)) + self.send(chan, "Unknown command '%s'" % command) return def notify(self, track, adverb, params): diff --git a/ptgbot/tests/__init__.py b/ptgbot/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ptgbot/tests/test_message_process.py b/ptgbot/tests/test_message_process.py new file mode 100644 index 0000000..02a94c9 --- /dev/null +++ b/ptgbot/tests/test_message_process.py @@ -0,0 +1,399 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +test_message_process +-------------------- +Check that IRC messages are processed correctly +""" + +from irc.client import Event +import copy +import testtools +from unittest import mock + +from ptgbot.bot import DOC_URL, PTGBot +from ptgbot.db import PTGDataBase + + +class TestProcessMessage(testtools.TestCase): + + def setUp(self): + super(TestProcessMessage, self).setUp() + self.db = PTGDataBase( + {'db_filename': 'base.json'}, + write_to_disk=False + ) + self.bot = PTGBot('', '', '', '', '#channel', self.db) + self.bot.identify_msg_cap = True + + def test_ignored_messages(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+hey ptgbot wazzzup']) + + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + self.bot.on_pubmsg('', msg) + self.assertFalse(mock_send.called) + + def test_help(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#help']) + + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + self.bot.on_pubmsg('', msg) + mock_send.assert_called_with('#channel', "See doc at: " + DOC_URL) + + def test_invalidtrack(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#svift now Looking at me']) + + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + self.bot.on_pubmsg('', msg) + mock_send.assert_any_call( + '#channel', + "johndoe: unknown track 'svift'" + ) + + def test_now(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift now Looking at me']) + + self.bot.on_pubmsg('', msg) + self.assertEquals( + self.db.data['now']['swift'], + "Looking at me" + ) + + def test_next(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift next Looking at you']) + + self.bot.on_pubmsg('', msg) + self.assertEquals( + self.db.data['next']['swift'], + ["Looking at you"] + ) + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift next Looking at us']) + + self.bot.on_pubmsg('', msg) + self.assertEquals( + self.db.data['next']['swift'], + ["Looking at you", "Looking at us"] + ) + + def test_now_clears_next(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift next Looking at you']) + + self.bot.on_pubmsg('', msg) + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift now Looking at me']) + + self.bot.on_pubmsg('', msg) + self.assertFalse('swift' in self.db.data['next']) + + def test_etherpad(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift etherpad https://etherpad.opendev.org/swift']) + + self.bot.on_pubmsg('', msg) + self.assertEquals( + self.db.data['etherpads']['swift'], + "https://etherpad.opendev.org/swift" + ) + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift etherpad auto']) + + self.bot.on_pubmsg('', msg) + self.assertFalse('swift' in self.db.data['etherpads']) + + def test_url(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift url https://meetpad.opendev.org/swift']) + + self.bot.on_pubmsg('', msg) + self.assertEquals( + self.db.data['urls']['swift'], + "https://meetpad.opendev.org/swift" + ) + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift url none']) + + self.bot.on_pubmsg('', msg) + self.assertFalse('swift' in self.db.data['urls']) + + def test_color(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift color #ffffff']) + + self.bot.on_pubmsg('', msg) + self.assertEquals( + self.db.data['colors']['swift'], + "#ffffff" + ) + + def test_location(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift location On the beach']) + + self.bot.on_pubmsg('', msg) + self.assertEquals( + self.db.data['location']['swift'], + "On the beach" + ) + + def test_book(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift book Aspen-FriP1']) + + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + self.bot.on_pubmsg('', msg) + mock_send.assert_called_with( + '#channel', + "johndoe: Room Aspen is now booked on FriP1 for swift" + ) + self.assertEquals( + self.db.data['schedule']['Aspen']['FriP1'], + "swift" + ) + + def test_unbook(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift unbook Vail-TueP2']) + + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + self.bot.on_pubmsg('', msg) + mock_send.assert_called_with( + '#channel', + "johndoe: Room Vail (previously booked for swift) is " + "now free on TueP2" + ) + self.assertEquals( + self.db.data['schedule']['Vail']['TueP2'], + "" + ) + + def test_invalid_book(self): + slots = ['Beach-TueP2', 'Vail-TueP2'] + + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + for slot in slots: + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift book ' + slot]) + self.bot.on_pubmsg('', msg) + mock_send.assert_called_with( + '#channel', + "johndoe: slot '%s' is invalid (or booked)" % slot + ) + mock_send.reset_mock() + + def test_invalid_unbook(self): + slots = ['Beach-TueP2', 'Aspen-FriP1'] + + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + for slot in slots: + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+#swift unbook ' + slot]) + self.bot.on_pubmsg('', msg) + mock_send.assert_called_with( + '#channel', + "johndoe: slot '%s' is invalid " + "(or not booked for swift)" % slot + ) + mock_send.reset_mock() + + def test_admin_cmds_only_admins(self): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+~list']) + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + self.bot.is_chanop = mock.MagicMock(return_value=False) + self.bot.on_pubmsg('', msg) + mock_send.assert_called_with( + '#channel', + "johndoe: Need op for admin commands", + ) + + def test_admin_cmds_parameters(self): + responses = { + '~m': "Unknown command 'm'", + '~motd': "Missing subcommand (~motd add|del|clean|reorder ...)", + '~motd foo': "Unknown motd subcommand foo", + '~motd add info': "Missing parameters (~motd add LEVEL MSG)", + '~motd add foo bar': "Incorrect message level 'foo' (should " + "be info, success, warning or danger)", + '~motd del': "Missing message number (~motd del NUM)", + '~motd del 999': "Incorrect message number 999", + '~motd clean 2': "'~motd clean' does not take parameters", + '~motd reorder': "Missing params (~motd reorder X Y...)", + '~motd reorder 999': "Incorrect message number 999", + '~add': "this command takes one or more arguments", + } + self.bot.is_chanop = mock.MagicMock(return_value=True) + original_db_data = copy.deepcopy(self.db.data) + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + for cmd, response in responses.items(): + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+' + cmd]) + self.bot.on_pubmsg('', msg) + mock_send.assert_called_with( + '#channel', + response + ) + self.assertEqual(self.db.data, original_db_data) + mock_send.reset_mock() + + def test_motd(self): + motdstates = [ + ('~motd add info foo bar', [ + {'level': 'info', 'message': 'foo bar'} + ]), + ('~motd add info open bar', [ + {'level': 'info', 'message': 'foo bar'}, + {'level': 'info', 'message': 'open bar'}, + ]), + ('~motd reorder 2 1', [ + {'level': 'info', 'message': 'open bar'}, + {'level': 'info', 'message': 'foo bar'}, + ]), + ('~motd del 1', [ + {'level': 'info', 'message': 'foo bar'}, + ]), + ('~motd add danger cocktails available', [ + {'level': 'info', 'message': 'foo bar'}, + {'level': 'danger', 'message': 'cocktails available'}, + ]), + ('~motd reorder 1', [ + {'level': 'info', 'message': 'foo bar'}, + ]), + ] + self.bot.is_chanop = mock.MagicMock(return_value=True) + for cmd, motd in motdstates: + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+' + cmd]) + self.bot.on_pubmsg('', msg) + self.assertEqual(self.db.data['motd'], motd) + + def test_require_voice(self): + self.bot.is_chanop = mock.MagicMock(return_value=True) + self.bot.is_voiced = mock.MagicMock(return_value=False) + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+~requirevoice']) + self.bot.on_pubmsg('', msg) + msg = Event('', + 'janedoe!~janedoe@openstack/member/janedoe', + '#channel', + ['+#swift now Looking at me']) + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + self.bot.on_pubmsg('', msg) + mock_send.assert_called_with( + '#channel', + "janedoe: Need voice to issue commands", + ) + msg = Event('', + 'johndoe!~johndoe@openstack/member/johndoe', + '#channel', + ['+~alloweveryone']) + self.bot.on_pubmsg('', msg) + msg = Event('', + 'janedoe!~janedoe@openstack/member/janedoe', + '#channel', + ['+#swift now Looking at me']) + self.bot.on_pubmsg('', msg) + self.assertEquals( + self.db.data['now']['swift'], + "Looking at me" + ) + + def test_airbag(self): + with mock.patch.object( + self.bot, 'send', + ) as mock_send: + self.bot.on_pubmsg() + mock_send.assert_called_with( + '#channel', + "Bot airbag activated: on_pubmsg() " + "missing 2 required positional arguments: 'c' and 'e'" + ) + mock_send.reset_mock() + self.bot.on_privmsg() + mock_send.assert_called_with( + '#channel', + "Bot airbag activated: on_privmsg() " + "missing 2 required positional arguments: 'c' and 'e'" + ) diff --git a/requirements.txt b/requirements.txt index 3883069..0525778 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ irc==15.1.1 python-daemon ib3 +requests diff --git a/test-requirements.txt b/test-requirements.txt index d038d93..7b06820 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,2 @@ hacking>=3.0,<3.1.0 # Apache-2.0 +stestr>=2.0.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index a07471c..14d04fd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,15 @@ [tox] -envlist = pep8,pyflakes +envlist = py3,pep8,pyflakes [testenv] -setenv = VIRTUAL_ENV={envdir} -sitepackages=True basepython = python3 +allowlist_externals = + find deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt -commands = nosetests {posargs} +commands = + find . -type f -name "*.pyc" -delete + stestr run --slowest {posargs} [testenv:pep8] commands = flake8