From 53f916991f2ecabc300dc3370780e1eb6a827cf7 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 22 Dec 2015 22:33:23 +0100 Subject: [PATCH] Add METADATA tests. --- irctest/basecontrollers.py | 3 +- irctest/cases.py | 23 +++- irctest/controllers/charybdis.py | 7 +- irctest/controllers/inspircd.py | 6 +- irctest/controllers/mammon.py | 15 ++- irctest/server_tests/test_metadata.py | 179 ++++++++++++++++++++++++++ 6 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 irctest/server_tests/test_metadata.py diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 813a762..82764d7 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -58,7 +58,8 @@ class BaseClientController(_BaseController): class BaseServerController(_BaseController): """Base controller for IRC server.""" - def run(self, hostname, port, start_wait): + def run(self, hostname, port, password, + valid_metadata_keys, invalid_metadata_keys): raise NotImplementedError() def registerUser(self, case, username, password=None): raise NotImplementedByController('registration') diff --git a/irctest/cases.py b/irctest/cases.py index 6ca48c8..0cab66e 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -33,7 +33,8 @@ class _IrcTestCase(unittest.TestCase): if self.show_io: print('---- new test ----') def assertMessageEqual(self, msg, subcommand=None, subparams=None, - target=None, nick=None, fail_msg=None, **kwargs): + target=None, nick=None, fail_msg=None, extra_format=(), + **kwargs): """Helper for partially comparing a message. Takes the message as first arguments, and comparisons to be made @@ -43,7 +44,8 @@ class _IrcTestCase(unittest.TestCase): `subparams`, and `target` are given.""" fail_msg = fail_msg or '{msg}' for (key, value) in kwargs.items(): - self.assertEqual(getattr(msg, key), value, msg, fail_msg) + self.assertEqual(getattr(msg, key), value, msg, fail_msg, + extra_format=extra_format) if nick: self.assertNotEqual(msg.prefix, None, msg, fail_msg) self.assertEqual(msg.prefix.split('!')[0], nick, msg, fail_msg) @@ -54,16 +56,23 @@ class _IrcTestCase(unittest.TestCase): msg_subparams = msg.params[2:] if subcommand: with self.subTest(key='subcommand'): - self.assertEqual(msg_subcommand, subcommand, msg, fail_msg) + self.assertEqual(msg_subcommand, subcommand, msg, fail_msg, + extra_format=extra_format) if subparams is not None: with self.subTest(key='subparams'): - self.assertEqual(msg_subparams, subparams, msg, fail_msg) + self.assertEqual(msg_subparams, subparams, msg, fail_msg, + extra_format=extra_format) def assertIn(self, item, list_, msg=None, fail_msg=None, extra_format=()): if fail_msg: fail_msg = fail_msg.format(*extra_format, item=item, list=list_, msg=msg) super().assertIn(item, list_, fail_msg) + def assertNotIn(self, item, list_, msg=None, fail_msg=None, extra_format=()): + if fail_msg: + fail_msg = fail_msg.format(*extra_format, + item=item, list=list_, msg=msg) + super().assertNotIn(item, list_, fail_msg) def assertEqual(self, got, expects, msg=None, fail_msg=None, extra_format=()): if fail_msg: fail_msg = fail_msg.format(*extra_format, @@ -203,10 +212,14 @@ class BaseServerTestCase(_IrcTestCase): """Basic class for server tests. Handles spawning a server and exchanging messages with it.""" password = None + valid_metadata_keys = frozenset() + invalid_metadata_keys = frozenset() def setUp(self): super().setUp() self.find_hostname_and_port() - self.controller.run(self.hostname, self.port, password=self.password) + self.controller.run(self.hostname, self.port, password=self.password, + valid_metadata_keys=self.valid_metadata_keys, + invalid_metadata_keys=self.invalid_metadata_keys) self.clients = {} def tearDown(self): self.controller.kill() diff --git a/irctest/controllers/charybdis.py b/irctest/controllers/charybdis.py index 91ff7c2..3338caf 100644 --- a/irctest/controllers/charybdis.py +++ b/irctest/controllers/charybdis.py @@ -6,6 +6,7 @@ import subprocess from irctest import client_mock from irctest import authentication +from irctest.basecontrollers import NotImplementedByController from irctest.basecontrollers import BaseServerController, DirectoryBasedController TEMPLATE_CONFIG = """ @@ -39,7 +40,11 @@ class CharybdisController(BaseServerController, DirectoryBasedController): with self.open_file('server.conf'): pass - def run(self, hostname, port, password=None): + def run(self, hostname, port, password=None, + valid_metadata_keys=None, invalid_metadata_keys=None): + if valid_metadata_keys or invalid_metadata_keys: + raise NotImplementedByController( + 'Defining valid and invalid METADATA keys.') assert self.proc is None self.create_config() password_field = 'password = "{}";'.format(password) if password else '' diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index ba6fffb..d2d5ee4 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -27,7 +27,11 @@ class InspircdController(BaseServerController, DirectoryBasedController): with self.open_file('server.conf'): pass - def run(self, hostname, port, password=None): + def run(self, hostname, port, password=None, restricted_metadata_keys=None, + valid_metadata_keys=None, invalid_metadata_keys=None): + if valid_metadata_keys or invalid_metadata_keys: + raise NotImplementedByController( + 'Defining valid and invalid METADATA keys.') assert self.proc is None self.create_config() password_field = 'password="{}"'.format(password) if password else '' diff --git a/irctest/controllers/mammon.py b/irctest/controllers/mammon.py index 4e388dc..b5d61c4 100644 --- a/irctest/controllers/mammon.py +++ b/irctest/controllers/mammon.py @@ -2,8 +2,8 @@ import os import time import subprocess -from irctest.basecontrollers import BaseServerController, DirectoryBasedController from irctest.basecontrollers import NotImplementedByController +from irctest.basecontrollers import BaseServerController, DirectoryBasedController TEMPLATE_CONFIG = """ clients: @@ -29,7 +29,10 @@ extensions: - mammon.ext.ircv3.sasl - mammon.ext.misc.nopost metadata: - restricted_keys: + restricted_keys: +{restricted_keys} + whitelist: +{authorized_keys} monitor: limit: 20 motd: @@ -55,6 +58,9 @@ server: recvq_len: 20 """ +def make_list(l): + return '\n'.join(map(' - {}'.format, l)) + class MammonController(BaseServerController, DirectoryBasedController): software_name = 'Mammon' supported_sasl_mechanisms = { @@ -69,7 +75,8 @@ class MammonController(BaseServerController, DirectoryBasedController): # Mammon does not seem to handle SIGTERM very well self.proc.kill() - def run(self, hostname, port, password=None): + def run(self, hostname, port, password=None, restricted_metadata_keys=(), + valid_metadata_keys=(), invalid_metadata_keys=()): if password is not None: raise NotImplementedByController('PASS command') assert self.proc is None @@ -79,6 +86,8 @@ class MammonController(BaseServerController, DirectoryBasedController): directory=self.directory, hostname=hostname, port=port, + authorized_keys=make_list(valid_metadata_keys), + restricted_keys=make_list(restricted_metadata_keys), )) #with self.open_file('server.yml', 'r') as fd: # print(fd.read()) diff --git a/irctest/server_tests/test_metadata.py b/irctest/server_tests/test_metadata.py new file mode 100644 index 0000000..5c43691 --- /dev/null +++ b/irctest/server_tests/test_metadata.py @@ -0,0 +1,179 @@ +""" +Tests METADATA features. + +""" + +from irctest import cases +from irctest.irc_utils.message_parser import Message + +class MetadataTestCase(cases.BaseServerTestCase): + valid_metadata_keys = {'valid_key1', 'valid_key2'} + invalid_metadata_keys = {'invalid_key1', 'invalid_key2'} + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testInIsupport(self): + """“If METADATA is supported, it MUST be specified in RPL_ISUPPORT + using the METADATA key.” + -- + """ + self.addClient() + self.sendLine(1, 'CAP LS 302') + self.getCapLs(1) + self.sendLine(1, 'USER foo foo foo :foo') + self.sendLine(1, 'NICK foo') + self.sendLine(1, 'CAP END') + self.skipToWelcome(1) + m = self.getMessage(1) + while m.command != '005': # RPL_ISUPPORT + m = self.getMessage(1) + self.assertIn('METADATA', {x.split('=')[0] for x in m.params[1:-1]}, + fail_msg='{item} missing from RPL_ISUPPORT') + self.getMessages(1) + + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testGetOneUnsetValid(self): + """ + """ + self.connectClient('foo') + self.sendLine(1, 'METADATA * GET valid_key1') + m = self.getMessage(1) + self.assertMessageEqual(m, command='766', # ERR_NOMATCHINGKEY + fail_msg='Did not reply with 766 (ERR_NOMATCHINGKEY) to a ' + 'request to an unset valid METADATA key.') + + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testGetTwoUnsetValid(self): + """“Multiple keys may be given. The response will be either RPL_KEYVALUE, + ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.” + -- + """ + self.connectClient('foo') + self.sendLine(1, 'METADATA * GET valid_key1 valid_key2') + m = self.getMessage(1) + self.assertMessageEqual(m, command='766', # ERR_NOMATCHINGKEY + fail_msg='Did not reply with 766 (ERR_NOMATCHINGKEY) to a ' + 'request to two unset valid METADATA key: {msg}') + self.assertEqual(m.params[1], 'valid_key1', m, + fail_msg='Response to “METADATA * GET valid_key1 valid_key2” ' + 'did not respond to valid_key1 first: {msg}') + m = self.getMessage(1) + self.assertMessageEqual(m, command='766', # ERR_NOMATCHINGKEY + fail_msg='Did not reply with two 766 (ERR_NOMATCHINGKEY) to a ' + 'request to two unset valid METADATA key: {msg}') + self.assertEqual(m.params[1], 'valid_key2', m, + fail_msg='Response to “METADATA * GET valid_key1 valid_key2” ' + 'did not respond to valid_key2 as second response: {msg}') + + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testListNoSet(self): + """“This subcommand MUST list all currently-set metadata keys along + with their values. The response will be zero or more RPL_KEYVALUE + events, following by RPL_METADATAEND event.” + -- + """ + self.connectClient('foo') + self.sendLine(1, 'METADATA * LIST') + m = self.getMessage(1) + self.assertMessageEqual(m, command='762', # RPL_METADATAEND + fail_msg='Response to “METADATA * LIST” was not ' + '762 (RPL_METADATAEND) but: {msg}') + + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testListInvalidTarget(self): + """“In case of invalid target RPL_METADATAEND MUST NOT be sent.” + -- + """ + self.connectClient('foo') + self.sendLine(1, 'METADATA foobar LIST') + m = self.getMessage(1) + self.assertMessageEqual(m, command='765', # ERR_TARGETINVALID + fail_msg='Response to “METADATA LIST” was ' + 'not 765 (ERR_TARGETINVALID) but: {msg}') + commands = {m.command for m in self.getMessages(1)} + self.assertNotIn('762', commands, + fail_msg='Sent “METADATA LIST”, got 765 ' + '(ERR_TARGETINVALID), and then 762 (RPL_METADATAEND)') + + def assertSetValue(self, target, key, value, displayable_value=None): + if displayable_value is None: + displayable_value = value + self.sendLine(1, 'METADATA {} SET {} :{}'.format(target, key, value)) + m = self.getMessage(1) + self.assertMessageEqual(m, command='761', # RPL_KEYVALUE + fail_msg='Did not reply with 761 (RPL_KEYVALUE) to a valid ' + '“METADATA * SET {} :{}”: {msg}', + extra_format=(key, displayable_value,)) + self.assertEqual(m.params[1], 'valid_key1', m, + fail_msg='Second param of 761 after setting “{expects}” to ' + '“{}” is not “{expects}”: {msg}.', + extra_format=(displayable_value,)) + self.assertEqual(m.params[3], value, m, + fail_msg='Fourth param of 761 after setting “{0}” to ' + '“{1}” is not “{1}”: {msg}.', + extra_format=(key, displayable_value)) + m = self.getMessage(1) + self.assertMessageEqual(m, command='762', # RPL_METADATAEND + fail_msg='Did not send RPL_METADATAEND after setting ' + 'a valid METADATA key.') + def assertGetValue(self, target, key, value, displayable_value=None): + self.sendLine(1, 'METADATA * GET {}'.format(key)) + m = self.getMessage(1) + self.assertMessageEqual(m, command='761', # RPL_KEYVALUE + fail_msg='Did not reply with 761 (RPL_KEYVALUE) to a valid ' + '“METADATA * GET” when the key is set is set: {msg}') + self.assertEqual(m.params[1], key, m, + fail_msg='Second param of 761 after getting “{expects}” ' + '(which is set) is not “{expects}”: {msg}.') + self.assertEqual(m.params[3], value, m, + fail_msg='Fourth param of 761 after getting “{0}” ' + '(which is set to “{1}”) is not ”{1}”: {msg}.', + extra_format=(key, displayable_value)) + def assertSetGetValue(self, target, key, value, displayable_value=None): + self.assertSetValue(target, key, value, displayable_value) + self.assertGetValue(target, key, value, displayable_value) + + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testSetGetValid(self): + """ + """ + self.connectClient('foo') + self.assertSetGetValue('*', 'valid_key1', 'myvalue') + + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testSetGetZeroCharInValue(self): + """“Values are unrestricted, except that they MUST be UTF-8.” + -- + """ + self.connectClient('foo') + self.assertSetGetValue('*', 'valid_key1', 'zero->\0<-zero', + 'zero->\\0<-zero') + + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testSetGetHeartInValue(self): + """“Values are unrestricted, except that they MUST be UTF-8.” + -- + """ + heart = b'\xf0\x9f\x92\x9c'.decode() + self.connectClient('foo') + self.assertSetGetValue('*', 'valid_key1', '->{}<-'.format(heart), + 'zero->{}<-zero'.format(heart.encode())) + + @cases.SpecificationSelector.requiredBySpecification('IRCv3.2') + def testSetInvalidUtf8(self): + """“Values are unrestricted, except that they MUST be UTF-8.” + -- + """ + self.connectClient('foo') + # Sending directly because it is not valid UTF-8 so Python would + # not like it + self.clients[1].conn.sendall(b'METADATA * SET valid_key1 ' + b':invalid UTF-8 ->\xc3<-\r\n') + commands = {m.command for m in self.getMessages(1)} + self.assertNotIn('761', commands, # RPL_KEYVALUE + fail_msg='Setting METADATA key to a value containing invalid ' + 'UTF-8 was answered with 761 (RPL_KEYVALUE)') + self.clients[1].conn.sendall(b'METADATA * SET valid_key1 ' + b':invalid UTF-8: \xc3\r\n') + commands = {m.command for m in self.getMessages(1)} + self.assertNotIn('761', commands, # RPL_KEYVALUE + fail_msg='Setting METADATA key to a value containing invalid ' + 'UTF-8 was answered with 761 (RPL_KEYVALUE)')