1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
5 Tests for L{twisted.words.protocols.irc}.
10 from twisted.trial import unittest
11 from twisted.trial.unittest import TestCase
12 from twisted.words.protocols import irc
13 from twisted.words.protocols.irc import IRCClient
14 from twisted.internet import protocol, task
15 from twisted.test.proto_helpers import StringTransport, StringIOWithoutClosing
19 class ModeParsingTests(unittest.TestCase):
21 Tests for L{twisted.words.protocols.irc.parseModes}.
23 paramModes = ('klb', 'b')
26 def test_emptyModes(self):
28 Parsing an empty mode string raises L{irc.IRCBadModes}.
30 self.assertRaises(irc.IRCBadModes, irc.parseModes, '', [])
33 def test_emptyModeSequence(self):
35 Parsing a mode string that contains an empty sequence (either a C{+} or
36 C{-} followed directly by another C{+} or C{-}, or not followed by
37 anything at all) raises L{irc.IRCBadModes}.
39 self.assertRaises(irc.IRCBadModes, irc.parseModes, '++k', [])
40 self.assertRaises(irc.IRCBadModes, irc.parseModes, '-+k', [])
41 self.assertRaises(irc.IRCBadModes, irc.parseModes, '+', [])
42 self.assertRaises(irc.IRCBadModes, irc.parseModes, '-', [])
45 def test_malformedModes(self):
47 Parsing a mode string that does not start with C{+} or C{-} raises
50 self.assertRaises(irc.IRCBadModes, irc.parseModes, 'foo', [])
51 self.assertRaises(irc.IRCBadModes, irc.parseModes, '%', [])
54 def test_nullModes(self):
56 Parsing a mode string that contains no mode characters raises
59 self.assertRaises(irc.IRCBadModes, irc.parseModes, '+', [])
60 self.assertRaises(irc.IRCBadModes, irc.parseModes, '-', [])
63 def test_singleMode(self):
65 Parsing a single mode setting with no parameters results in that mode,
66 with no parameters, in the "added" direction and no modes in the
69 added, removed = irc.parseModes('+s', [])
70 self.assertEqual(added, [('s', None)])
71 self.assertEqual(removed, [])
73 added, removed = irc.parseModes('-s', [])
74 self.assertEqual(added, [])
75 self.assertEqual(removed, [('s', None)])
78 def test_singleDirection(self):
80 Parsing a single-direction mode setting with multiple modes and no
81 parameters, results in all modes falling into the same direction group.
83 added, removed = irc.parseModes('+stn', [])
84 self.assertEqual(added, [('s', None),
87 self.assertEqual(removed, [])
89 added, removed = irc.parseModes('-nt', [])
90 self.assertEqual(added, [])
91 self.assertEqual(removed, [('n', None),
95 def test_multiDirection(self):
97 Parsing a multi-direction mode setting with no parameters.
99 added, removed = irc.parseModes('+s-n+ti', [])
100 self.assertEqual(added, [('s', None),
103 self.assertEqual(removed, [('n', None)])
106 def test_consecutiveDirection(self):
108 Parsing a multi-direction mode setting containing two consecutive mode
109 sequences with the same direction results in the same result as if
110 there were only one mode sequence in the same direction.
112 added, removed = irc.parseModes('+sn+ti', [])
113 self.assertEqual(added, [('s', None),
117 self.assertEqual(removed, [])
120 def test_mismatchedParams(self):
122 If the number of mode parameters does not match the number of modes
123 expecting parameters, L{irc.IRCBadModes} is raised.
125 self.assertRaises(irc.IRCBadModes,
129 self.assertRaises(irc.IRCBadModes,
131 '+kl', ['foo', '10', 'lulz_extra_param'],
135 def test_parameters(self):
137 Modes which require parameters are parsed and paired with their relevant
138 parameter, modes which do not require parameters do not consume any of
141 added, removed = irc.parseModes(
143 ['somekey', '42', 'nick!user@host', 'other!*@*'],
145 self.assertEqual(added, [('k', 'somekey'),
147 ('b', 'nick!user@host'),
149 self.assertEqual(removed, [])
151 added, removed = irc.parseModes(
153 ['nick!user@host', 'other!*@*'],
155 self.assertEqual(added, [])
156 self.assertEqual(removed, [('k', None),
158 ('b', 'nick!user@host'),
161 # Mix a no-argument mode in with argument modes.
162 added, removed = irc.parseModes(
164 ['somekey', 'nick!user@host', 'other!*@*'],
166 self.assertEqual(added, [('k', 'somekey'),
168 ('b', 'nick!user@host'),
170 self.assertEqual(removed, [])
175 "Hello, this is a nice string with no complications.",
176 "xargs%(NUL)smight%(NUL)slike%(NUL)sthis" % {'NUL': irc.NUL },
177 "embedded%(CR)snewline%(CR)s%(NL)sFUN%(NL)s" % {'CR': irc.CR,
179 "escape!%(X)s escape!%(M)s %(X)s%(X)sa %(M)s0" % {'X': irc.X_QUOTE,
184 class QuotingTest(unittest.TestCase):
185 def test_lowquoteSanity(self):
187 Testing client-server level quote/dequote.
189 for s in stringSubjects:
190 self.assertEqual(s, irc.lowDequote(irc.lowQuote(s)))
193 def test_ctcpquoteSanity(self):
195 Testing CTCP message level quote/dequote.
197 for s in stringSubjects:
198 self.assertEqual(s, irc.ctcpDequote(irc.ctcpQuote(s)))
202 class Dispatcher(irc._CommandDispatcherMixin):
204 A dispatcher that exposes one known command and handles unknown commands.
208 def disp_working(self, a, b):
210 A known command that returns its input.
215 def disp_unknown(self, name, a, b):
217 Handle unknown commands by returning their name and inputs.
223 class DispatcherTests(unittest.TestCase):
225 Tests for L{irc._CommandDispatcherMixin}.
227 def test_dispatch(self):
229 Dispatching a command invokes the correct handler.
233 res = disp.dispatch('working', *args)
234 self.assertEqual(res, args)
237 def test_dispatchUnknown(self):
239 Dispatching an unknown command invokes the default handler.
244 res = disp.dispatch(name, *args)
245 self.assertEqual(res, (name,) + args)
248 def test_dispatchMissingUnknown(self):
250 Dispatching an unknown command, when no default handler is present,
251 results in an exception being raised.
254 disp.disp_unknown = None
255 self.assertRaises(irc.UnhandledCommand, disp.dispatch, 'bar')
259 class ServerSupportedFeatureTests(unittest.TestCase):
261 Tests for L{ServerSupportedFeatures} and related functions.
263 def test_intOrDefault(self):
265 L{_intOrDefault} converts values to C{int} if possible, otherwise
266 returns a default value.
268 self.assertEqual(irc._intOrDefault(None), None)
269 self.assertEqual(irc._intOrDefault([]), None)
270 self.assertEqual(irc._intOrDefault(''), None)
271 self.assertEqual(irc._intOrDefault('hello', 5), 5)
272 self.assertEqual(irc._intOrDefault('123'), 123)
273 self.assertEqual(irc._intOrDefault(123), 123)
276 def test_splitParam(self):
278 L{ServerSupportedFeatures._splitParam} splits ISUPPORT parameters
279 into key and values. Parameters without a separator are split into a
280 key and a list containing only the empty string. Escaped parameters
283 params = [('FOO', ('FOO', [''])),
284 ('FOO=', ('FOO', [''])),
285 ('FOO=1', ('FOO', ['1'])),
286 ('FOO=1,2,3', ('FOO', ['1', '2', '3'])),
287 ('FOO=A\\x20B', ('FOO', ['A B'])),
288 ('FOO=\\x5Cx', ('FOO', ['\\x'])),
289 ('FOO=\\', ('FOO', ['\\'])),
290 ('FOO=\\n', ('FOO', ['\\n']))]
292 _splitParam = irc.ServerSupportedFeatures._splitParam
294 for param, expected in params:
295 res = _splitParam(param)
296 self.assertEqual(res, expected)
298 self.assertRaises(ValueError, _splitParam, 'FOO=\\x')
299 self.assertRaises(ValueError, _splitParam, 'FOO=\\xNN')
300 self.assertRaises(ValueError, _splitParam, 'FOO=\\xN')
301 self.assertRaises(ValueError, _splitParam, 'FOO=\\x20\\x')
304 def test_splitParamArgs(self):
306 L{ServerSupportedFeatures._splitParamArgs} splits ISUPPORT parameter
307 arguments into key and value. Arguments without a separator are
308 split into a key and an empty string.
310 res = irc.ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2', 'C:', 'D'])
311 self.assertEqual(res, [('A', '1'),
317 def test_splitParamArgsProcessor(self):
319 L{ServerSupportedFeatures._splitParamArgs} uses the argument processor
320 passed to to convert ISUPPORT argument values to some more suitable
323 res = irc.ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2', 'C'],
325 self.assertEqual(res, [('A', 1),
330 def test_parsePrefixParam(self):
332 L{ServerSupportedFeatures._parsePrefixParam} parses the ISUPPORT PREFIX
333 parameter into a mapping from modes to prefix symbols, returns
334 C{None} if there is no parseable prefix parameter or raises
335 C{ValueError} if the prefix parameter is malformed.
337 _parsePrefixParam = irc.ServerSupportedFeatures._parsePrefixParam
338 self.assertEqual(_parsePrefixParam(''), None)
339 self.assertRaises(ValueError, _parsePrefixParam, 'hello')
340 self.assertEqual(_parsePrefixParam('(ov)@+'),
345 def test_parseChanModesParam(self):
347 L{ServerSupportedFeatures._parseChanModesParam} parses the ISUPPORT
348 CHANMODES parameter into a mapping from mode categories to mode
349 characters. Passing fewer than 4 parameters results in the empty string
350 for the relevant categories. Passing more than 4 parameters raises
353 _parseChanModesParam = irc.ServerSupportedFeatures._parseChanModesParam
355 _parseChanModesParam([]),
362 _parseChanModesParam(['b', 'k', 'l', 'imnpst']),
363 {'addressModes': 'b',
366 'noParam': 'imnpst'})
369 _parseChanModesParam(['b', 'k', 'l']),
370 {'addressModes': 'b',
377 _parseChanModesParam, ['a', 'b', 'c', 'd', 'e'])
380 def test_parse(self):
382 L{ServerSupportedFeatures.parse} changes the internal state of the
383 instance to reflect the features indicated by the parsed ISUPPORT
384 parameters, including unknown parameters and unsetting previously set
387 supported = irc.ServerSupportedFeatures()
388 supported.parse(['MODES=4',
389 'CHANLIMIT=#:20,&:10',
394 self.assertEqual(supported.getFeature('MODES'), 4)
395 self.assertEqual(supported.getFeature('CHANLIMIT'),
398 self.assertEqual(supported.getFeature('INVEX'), 'I')
399 self.assertEqual(supported.getFeature('EXCEPTS'), 'Z')
400 self.assertEqual(supported.getFeature('UNKNOWN'), ('A', 'B', 'C'))
402 self.assertTrue(supported.hasFeature('INVEX'))
403 supported.parse(['-INVEX'])
404 self.assertFalse(supported.hasFeature('INVEX'))
405 # Unsetting a previously unset parameter should not be a problem.
406 supported.parse(['-INVEX'])
409 def _parse(self, features):
411 Parse all specified features according to the ISUPPORT specifications.
413 @type features: C{list} of C{(featureName, value)}
414 @param features: Feature names and values to parse
416 @rtype: L{irc.ServerSupportedFeatures}
418 supported = irc.ServerSupportedFeatures()
419 features = ['%s=%s' % (name, value or '')
420 for name, value in features]
421 supported.parse(features)
425 def _parseFeature(self, name, value=None):
427 Parse a feature, with the given name and value, according to the
428 ISUPPORT specifications and return the parsed value.
430 supported = self._parse([(name, value)])
431 return supported.getFeature(name)
434 def _testIntOrDefaultFeature(self, name, default=None):
436 Perform some common tests on a feature known to use L{_intOrDefault}.
439 self._parseFeature(name, None),
442 self._parseFeature(name, 'notanint'),
445 self._parseFeature(name, '42'),
449 def _testFeatureDefault(self, name, features=None):
451 Features known to have default values are reported as being present by
452 L{irc.ServerSupportedFeatures.hasFeature}, and their value defaults
453 correctly, when they don't appear in an ISUPPORT message.
455 default = irc.ServerSupportedFeatures()._features[name]
458 features = [('DEFINITELY_NOT', 'a_feature')]
460 supported = self._parse(features)
461 self.assertTrue(supported.hasFeature(name))
462 self.assertEqual(supported.getFeature(name), default)
465 def test_support_CHANMODES(self):
467 The CHANMODES ISUPPORT parameter is parsed into a C{dict} giving the
468 four mode categories, C{'addressModes'}, C{'param'}, C{'setParam'}, and
471 self._testFeatureDefault('CHANMODES')
472 self._testFeatureDefault('CHANMODES', [('CHANMODES', 'b,,lk,')])
473 self._testFeatureDefault('CHANMODES', [('CHANMODES', 'b,,lk,ha,ha')])
476 self._parseFeature('CHANMODES', ''),
483 self._parseFeature('CHANMODES', ',A'),
490 self._parseFeature('CHANMODES', 'A,Bc,Def,Ghij'),
491 {'addressModes': 'A',
497 def test_support_IDCHAN(self):
499 The IDCHAN support parameter is parsed into a sequence of two-tuples
500 giving channel prefix and ID length pairs.
503 self._parseFeature('IDCHAN', '!:5'),
507 def test_support_MAXLIST(self):
509 The MAXLIST support parameter is parsed into a sequence of two-tuples
510 giving modes and their limits.
513 self._parseFeature('MAXLIST', 'b:25,eI:50'),
514 [('b', 25), ('eI', 50)])
515 # A non-integer parameter argument results in None.
517 self._parseFeature('MAXLIST', 'b:25,eI:50,a:3.1415'),
518 [('b', 25), ('eI', 50), ('a', None)])
520 self._parseFeature('MAXLIST', 'b:25,eI:50,a:notanint'),
521 [('b', 25), ('eI', 50), ('a', None)])
524 def test_support_NETWORK(self):
526 The NETWORK support parameter is parsed as the network name, as
527 specified by the server.
530 self._parseFeature('NETWORK', 'IRCNet'),
534 def test_support_SAFELIST(self):
536 The SAFELIST support parameter is parsed into a boolean indicating
537 whether the safe "list" command is supported or not.
540 self._parseFeature('SAFELIST'),
544 def test_support_STATUSMSG(self):
546 The STATUSMSG support parameter is parsed into a string of channel
547 status that support the exclusive channel notice method.
550 self._parseFeature('STATUSMSG', '@+'),
554 def test_support_TARGMAX(self):
556 The TARGMAX support parameter is parsed into a dictionary, mapping
557 strings to integers, of the maximum number of targets for a particular
561 self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3'),
564 # A non-integer parameter argument results in None.
566 self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3,KICK:3.1415'),
571 self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3,KICK:notanint'),
577 def test_support_NICKLEN(self):
579 The NICKLEN support parameter is parsed into an integer value
580 indicating the maximum length of a nickname the client may use,
581 otherwise, if the parameter is missing or invalid, the default value
582 (as specified by RFC 1459) is used.
584 default = irc.ServerSupportedFeatures()._features['NICKLEN']
585 self._testIntOrDefaultFeature('NICKLEN', default)
588 def test_support_CHANNELLEN(self):
590 The CHANNELLEN support parameter is parsed into an integer value
591 indicating the maximum channel name length, otherwise, if the
592 parameter is missing or invalid, the default value (as specified by
595 default = irc.ServerSupportedFeatures()._features['CHANNELLEN']
596 self._testIntOrDefaultFeature('CHANNELLEN', default)
599 def test_support_CHANTYPES(self):
601 The CHANTYPES support parameter is parsed into a tuple of
602 valid channel prefix characters.
604 self._testFeatureDefault('CHANTYPES')
607 self._parseFeature('CHANTYPES', '#&%'),
611 def test_support_KICKLEN(self):
613 The KICKLEN support parameter is parsed into an integer value
614 indicating the maximum length of a kick message a client may use.
616 self._testIntOrDefaultFeature('KICKLEN')
619 def test_support_PREFIX(self):
621 The PREFIX support parameter is parsed into a dictionary mapping
622 modes to two-tuples of status symbol and priority.
624 self._testFeatureDefault('PREFIX')
625 self._testFeatureDefault('PREFIX', [('PREFIX', 'hello')])
628 self._parseFeature('PREFIX', None),
631 self._parseFeature('PREFIX', '(ohv)@%+'),
636 self._parseFeature('PREFIX', '(hov)@%+'),
642 def test_support_TOPICLEN(self):
644 The TOPICLEN support parameter is parsed into an integer value
645 indicating the maximum length of a topic a client may set.
647 self._testIntOrDefaultFeature('TOPICLEN')
650 def test_support_MODES(self):
652 The MODES support parameter is parsed into an integer value
653 indicating the maximum number of "variable" modes (defined as being
654 modes from C{addressModes}, C{param} or C{setParam} categories for
655 the C{CHANMODES} ISUPPORT parameter) which may by set on a channel
656 by a single MODE command from a client.
658 self._testIntOrDefaultFeature('MODES')
661 def test_support_EXCEPTS(self):
663 The EXCEPTS support parameter is parsed into the mode character
664 to be used for "ban exception" modes. If no parameter is specified
665 then the character C{e} is assumed.
668 self._parseFeature('EXCEPTS', 'Z'),
671 self._parseFeature('EXCEPTS'),
675 def test_support_INVEX(self):
677 The INVEX support parameter is parsed into the mode character to be
678 used for "invite exception" modes. If no parameter is specified then
679 the character C{I} is assumed.
682 self._parseFeature('INVEX', 'Z'),
685 self._parseFeature('INVEX'),
690 class IRCClientWithoutLogin(irc.IRCClient):
695 class CTCPTest(unittest.TestCase):
697 Tests for L{twisted.words.protocols.irc.IRCClient} CTCP handling.
700 self.file = StringIOWithoutClosing()
701 self.transport = protocol.FileWrapper(self.file)
702 self.client = IRCClientWithoutLogin()
703 self.client.makeConnection(self.transport)
705 self.addCleanup(self.transport.loseConnection)
706 self.addCleanup(self.client.connectionLost, None)
709 def test_ERRMSG(self):
710 """Testing CTCP query ERRMSG.
712 Not because this is this is an especially important case in the
713 field, but it does go through the entire dispatch/decode/encode
717 errQuery = (":nick!guy@over.there PRIVMSG #theChan :"
718 "%(X)cERRMSG t%(X)c%(EOL)s"
720 'EOL': irc.CR + irc.LF})
722 errReply = ("NOTICE nick :%(X)cERRMSG t :"
723 "No error has occoured.%(X)c%(EOL)s"
725 'EOL': irc.CR + irc.LF})
727 self.client.dataReceived(errQuery)
728 reply = self.file.getvalue()
730 self.assertEqual(errReply, reply)
733 def test_noNumbersVERSION(self):
735 If attributes for version information on L{IRCClient} are set to
736 C{None}, the parts of the CTCP VERSION response they correspond to
739 self.client.versionName = "FrobozzIRC"
740 self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None)
741 versionReply = ("NOTICE nick :%(X)cVERSION %(vname)s::"
744 'EOL': irc.CR + irc.LF,
745 'vname': self.client.versionName})
746 reply = self.file.getvalue()
747 self.assertEqual(versionReply, reply)
750 def test_fullVERSION(self):
752 The response to a CTCP VERSION query includes the version number and
753 environment information, as specified by L{IRCClient.versionNum} and
754 L{IRCClient.versionEnv}.
756 self.client.versionName = "FrobozzIRC"
757 self.client.versionNum = "1.2g"
758 self.client.versionEnv = "ZorkOS"
759 self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None)
760 versionReply = ("NOTICE nick :%(X)cVERSION %(vname)s:%(vnum)s:%(venv)s"
763 'EOL': irc.CR + irc.LF,
764 'vname': self.client.versionName,
765 'vnum': self.client.versionNum,
766 'venv': self.client.versionEnv})
767 reply = self.file.getvalue()
768 self.assertEqual(versionReply, reply)
771 def test_noDuplicateCTCPDispatch(self):
773 Duplicated CTCP messages are ignored and no reply is made.
775 def testCTCP(user, channel, data):
779 self.client.ctcpQuery_TESTTHIS = testCTCP
781 self.client.irc_PRIVMSG(
782 'foo!bar@baz.quux', [
784 '%(X)sTESTTHIS%(X)sfoo%(X)sTESTTHIS%(X)s' % {'X': irc.X_DELIM}])
786 self.file.getvalue(),
788 self.assertEqual(self.called, 1)
791 def test_noDefaultDispatch(self):
793 The fallback handler is invoked for unrecognized CTCP messages.
795 def unknownQuery(user, channel, tag, data):
796 self.calledWith = (user, channel, tag, data)
800 self.patch(self.client, 'ctcpUnknownQuery', unknownQuery)
801 self.client.irc_PRIVMSG(
802 'foo!bar@baz.quux', [
804 '%(X)sNOTREAL%(X)s' % {'X': irc.X_DELIM}])
806 self.file.getvalue(),
810 ('foo!bar@baz.quux', '#chan', 'NOTREAL', None))
811 self.assertEqual(self.called, 1)
813 # The fallback handler is not invoked for duplicate unknown CTCP
815 self.client.irc_PRIVMSG(
816 'foo!bar@baz.quux', [
818 '%(X)sNOTREAL%(X)sfoo%(X)sNOTREAL%(X)s' % {'X': irc.X_DELIM}])
819 self.assertEqual(self.called, 2)
823 class NoticingClient(IRCClientWithoutLogin, object):
825 'created': ('when',),
826 'yourHost': ('info',),
827 'myInfo': ('servername', 'version', 'umodes', 'cmodes'),
828 'luserClient': ('info',),
830 'isupport': ('options',),
831 'luserChannels': ('channels',),
833 'luserMe': ('info',),
834 'receivedMOTD': ('motd',),
836 'privmsg': ('user', 'channel', 'message'),
837 'joined': ('channel',),
838 'left': ('channel',),
839 'noticed': ('user', 'channel', 'message'),
840 'modeChanged': ('user', 'channel', 'set', 'modes', 'args'),
841 'pong': ('user', 'secs'),
843 'kickedFrom': ('channel', 'kicker', 'message'),
844 'nickChanged': ('nick',),
846 'userJoined': ('user', 'channel'),
847 'userLeft': ('user', 'channel'),
848 'userKicked': ('user', 'channel', 'kicker', 'message'),
849 'action': ('user', 'channel', 'data'),
850 'topicUpdated': ('user', 'channel', 'newTopic'),
851 'userRenamed': ('oldname', 'newname')}
854 def __init__(self, *a, **kw):
855 # It is important that IRCClient.__init__ is not called since
856 # traditionally it did not exist, so it is important that nothing is
857 # initialised there that would prevent subclasses that did not (or
858 # could not) invoke the base implementation. Any protocol
859 # initialisation should happen in connectionMode.
863 def __getattribute__(self, name):
864 if name.startswith('__') and name.endswith('__'):
865 return super(NoticingClient, self).__getattribute__(name)
867 args = super(NoticingClient, self).__getattribute__('methods')[name]
869 return super(NoticingClient, self).__getattribute__(name)
871 return self.makeMethod(name, args)
874 def makeMethod(self, fname, args):
875 def method(*a, **kw):
876 if len(a) > len(args):
877 raise TypeError("TypeError: %s() takes %d arguments "
878 "(%d given)" % (fname, len(args), len(a)))
879 for (name, value) in zip(args, a):
881 raise TypeError("TypeError: %s() got multiple values "
882 "for keyword argument '%s'" % (fname, name))
885 if len(kw) != len(args):
886 raise TypeError("TypeError: %s() takes %d arguments "
887 "(%d given)" % (fname, len(args), len(a)))
888 self.calls.append((fname, kw))
892 def pop(dict, key, default):
903 class ClientImplementationTests(unittest.TestCase):
905 self.transport = StringTransport()
906 self.client = NoticingClient()
907 self.client.makeConnection(self.transport)
909 self.addCleanup(self.transport.loseConnection)
910 self.addCleanup(self.client.connectionLost, None)
913 def _serverTestImpl(self, code, msg, func, **kw):
914 host = pop(kw, 'host', 'server.host')
915 nick = pop(kw, 'nick', 'nickname')
916 args = pop(kw, 'args', '')
925 self.client.dataReceived(message)
931 def testYourHost(self):
932 msg = "Your host is some.host[blah.blah/6667], running version server-version-3"
933 self._serverTestImpl("002", msg, "yourHost", info=msg)
936 def testCreated(self):
937 msg = "This server was cobbled together Fri Aug 13 18:00:25 UTC 2004"
938 self._serverTestImpl("003", msg, "created", when=msg)
941 def testMyInfo(self):
942 msg = "server.host server-version abcDEF bcdEHI"
943 self._serverTestImpl("004", msg, "myInfo",
944 servername="server.host",
945 version="server-version",
950 def testLuserClient(self):
951 msg = "There are 9227 victims and 9542 hiding on 24 servers"
952 self._serverTestImpl("251", msg, "luserClient",
956 def _sendISUPPORT(self):
957 args = ("MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63 "
958 "TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=# "
959 "PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer")
960 msg = "are available on this server"
961 self._serverTestImpl("005", msg, "isupport", args=args,
978 def test_ISUPPORT(self):
980 The client parses ISUPPORT messages sent by the server and calls
981 L{IRCClient.isupport}.
986 def testBounce(self):
987 msg = "Try server some.host, port 321"
988 self._serverTestImpl("010", msg, "bounce",
992 def testLuserChannels(self):
994 msg = "channels formed"
995 self._serverTestImpl("254", msg, "luserChannels", args=args,
999 def testLuserOp(self):
1001 msg = "flagged staff members"
1002 self._serverTestImpl("252", msg, "luserOp", args=args,
1006 def testLuserMe(self):
1007 msg = "I have 1937 clients and 0 servers"
1008 self._serverTestImpl("255", msg, "luserMe",
1012 def test_receivedMOTD(self):
1014 Lines received in I{RPL_MOTDSTART} and I{RPL_MOTD} are delivered to
1015 L{IRCClient.receivedMOTD} when I{RPL_ENDOFMOTD} is received.
1018 ":host.name 375 nickname :- host.name Message of the Day -",
1019 ":host.name 372 nickname :- Welcome to host.name",
1020 ":host.name 376 nickname :End of /MOTD command."]
1022 self.assertEqual(self.client.calls, [])
1023 self.client.dataReceived(L + '\r\n')
1027 [("receivedMOTD", {"motd": ["host.name Message of the Day -", "Welcome to host.name"]})])
1029 # After the motd is delivered, the tracking variable should be
1031 self.assertIdentical(self.client.motd, None)
1034 def test_withoutMOTDSTART(self):
1036 If L{IRCClient} receives I{RPL_MOTD} and I{RPL_ENDOFMOTD} without
1037 receiving I{RPL_MOTDSTART}, L{IRCClient.receivedMOTD} is still
1038 called with a list of MOTD lines.
1041 ":host.name 372 nickname :- Welcome to host.name",
1042 ":host.name 376 nickname :End of /MOTD command."]
1045 self.client.dataReceived(L + '\r\n')
1049 [("receivedMOTD", {"motd": ["Welcome to host.name"]})])
1052 def _clientTestImpl(self, sender, group, type, msg, func, **kw):
1053 ident = pop(kw, 'ident', 'ident')
1054 host = pop(kw, 'host', 'host')
1056 wholeUser = sender + '!' + ident + '@' + host
1062 self.client.dataReceived(message)
1066 self.client.calls = []
1069 def testPrivmsg(self):
1070 msg = "Tooty toot toot."
1071 self._clientTestImpl("sender", "#group", "PRIVMSG", msg, "privmsg",
1072 ident="ident", host="host",
1073 # Expected results below
1074 user="sender!ident@host",
1078 self._clientTestImpl("sender", "recipient", "PRIVMSG", msg, "privmsg",
1079 ident="ident", host="host",
1080 # Expected results below
1081 user="sender!ident@host",
1082 channel="recipient",
1086 def test_getChannelModeParams(self):
1088 L{IRCClient.getChannelModeParams} uses ISUPPORT information, either
1089 given by the server or defaults, to determine which channel modes
1090 require arguments when being added or removed.
1092 add, remove = map(sorted, self.client.getChannelModeParams())
1093 self.assertEqual(add, ['b', 'h', 'k', 'l', 'o', 'v'])
1094 self.assertEqual(remove, ['b', 'h', 'o', 'v'])
1096 def removeFeature(name):
1098 msg = "are available on this server"
1099 self._serverTestImpl(
1100 '005', msg, 'isupport', args=name, options=[name])
1101 self.assertIdentical(
1102 self.client.supported.getFeature(name), None)
1103 self.client.calls = []
1105 # Remove CHANMODES feature, causing getFeature('CHANMODES') to return
1107 removeFeature('CHANMODES')
1108 add, remove = map(sorted, self.client.getChannelModeParams())
1109 self.assertEqual(add, ['h', 'o', 'v'])
1110 self.assertEqual(remove, ['h', 'o', 'v'])
1112 # Remove PREFIX feature, causing getFeature('PREFIX') to return None.
1113 removeFeature('PREFIX')
1114 add, remove = map(sorted, self.client.getChannelModeParams())
1115 self.assertEqual(add, [])
1116 self.assertEqual(remove, [])
1118 # Restore ISUPPORT features.
1119 self._sendISUPPORT()
1120 self.assertNotIdentical(
1121 self.client.supported.getFeature('PREFIX'), None)
1124 def test_getUserModeParams(self):
1126 L{IRCClient.getUserModeParams} returns a list of user modes (modes that
1127 the user sets on themself, outside of channel modes) that require
1128 parameters when added and removed, respectively.
1130 add, remove = map(sorted, self.client.getUserModeParams())
1131 self.assertEqual(add, [])
1132 self.assertEqual(remove, [])
1135 def _sendModeChange(self, msg, args='', target=None):
1137 Build a MODE string and send it to the client.
1141 message = ":Wolf!~wolf@yok.utu.fi MODE %s %s %s\r\n" % (
1143 self.client.dataReceived(message)
1146 def _parseModeChange(self, results, target=None):
1148 Parse the results, do some test and return the data to check.
1153 for n, result in enumerate(results):
1154 method, data = result
1155 self.assertEqual(method, 'modeChanged')
1156 self.assertEqual(data['user'], 'Wolf!~wolf@yok.utu.fi')
1157 self.assertEqual(data['channel'], target)
1158 results[n] = tuple([data[key] for key in ('set', 'modes', 'args')])
1162 def _checkModeChange(self, expected, target=None):
1164 Compare the expected result with the one returned by the client.
1166 result = self._parseModeChange(self.client.calls, target)
1167 self.assertEqual(result, expected)
1168 self.client.calls = []
1171 def test_modeMissingDirection(self):
1173 Mode strings that do not begin with a directional character, C{'+'} or
1174 C{'-'}, have C{'+'} automatically prepended.
1176 self._sendModeChange('s')
1177 self._checkModeChange([(True, 's', (None,))])
1180 def test_noModeParameters(self):
1182 No parameters are passed to L{IRCClient.modeChanged} for modes that
1183 don't take any parameters.
1185 self._sendModeChange('-s')
1186 self._checkModeChange([(False, 's', (None,))])
1187 self._sendModeChange('+n')
1188 self._checkModeChange([(True, 'n', (None,))])
1191 def test_oneModeParameter(self):
1193 Parameters are passed to L{IRCClient.modeChanged} for modes that take
1196 self._sendModeChange('+o', 'a_user')
1197 self._checkModeChange([(True, 'o', ('a_user',))])
1198 self._sendModeChange('-o', 'a_user')
1199 self._checkModeChange([(False, 'o', ('a_user',))])
1202 def test_mixedModes(self):
1204 Mixing adding and removing modes that do and don't take parameters
1205 invokes L{IRCClient.modeChanged} with mode characters and parameters
1208 self._sendModeChange('+osv', 'a_user another_user')
1209 self._checkModeChange([(True, 'osv', ('a_user', None, 'another_user'))])
1210 self._sendModeChange('+v-os', 'a_user another_user')
1211 self._checkModeChange([(True, 'v', ('a_user',)),
1212 (False, 'os', ('another_user', None))])
1215 def test_tooManyModeParameters(self):
1217 Passing an argument to modes that take no parameters results in
1218 L{IRCClient.modeChanged} not being called and an error being logged.
1220 self._sendModeChange('+s', 'wrong')
1221 self._checkModeChange([])
1222 errors = self.flushLoggedErrors(irc.IRCBadModes)
1223 self.assertEqual(len(errors), 1)
1224 self.assertSubstring(
1225 'Too many parameters', errors[0].getErrorMessage())
1228 def test_tooFewModeParameters(self):
1230 Passing no arguments to modes that do take parameters results in
1231 L{IRCClient.modeChange} not being called and an error being logged.
1233 self._sendModeChange('+o')
1234 self._checkModeChange([])
1235 errors = self.flushLoggedErrors(irc.IRCBadModes)
1236 self.assertEqual(len(errors), 1)
1237 self.assertSubstring(
1238 'Not enough parameters', errors[0].getErrorMessage())
1241 def test_userMode(self):
1243 A C{MODE} message whose target is our user (the nickname of our user,
1244 to be precise), as opposed to a channel, will be parsed according to
1245 the modes specified by L{IRCClient.getUserModeParams}.
1247 target = self.client.nickname
1248 # Mode "o" on channels is supposed to take a parameter, but since this
1249 # is not a channel this will not cause an exception.
1250 self._sendModeChange('+o', target=target)
1251 self._checkModeChange([(True, 'o', (None,))], target=target)
1253 def getUserModeParams():
1256 # Introduce our own user mode that takes an argument.
1257 self.patch(self.client, 'getUserModeParams', getUserModeParams)
1259 self._sendModeChange('+Z', 'an_arg', target=target)
1260 self._checkModeChange([(True, 'Z', ('an_arg',))], target=target)
1263 def test_heartbeat(self):
1265 When the I{RPL_WELCOME} message is received a heartbeat is started that
1266 will send a I{PING} message to the IRC server every
1267 L{irc.IRCClient.heartbeatInterval} seconds. When the transport is
1268 closed the heartbeat looping call is stopped too.
1270 def _createHeartbeat():
1271 heartbeat = self._originalCreateHeartbeat()
1272 heartbeat.clock = self.clock
1275 self.clock = task.Clock()
1276 self._originalCreateHeartbeat = self.client._createHeartbeat
1277 self.patch(self.client, '_createHeartbeat', _createHeartbeat)
1279 self.assertIdentical(self.client._heartbeat, None)
1280 self.client.irc_RPL_WELCOME('foo', [])
1281 self.assertNotIdentical(self.client._heartbeat, None)
1282 self.assertEqual(self.client.hostname, 'foo')
1284 # Pump the clock enough to trigger one LoopingCall.
1285 self.assertEqual(self.transport.value(), '')
1286 self.clock.advance(self.client.heartbeatInterval)
1287 self.assertEqual(self.transport.value(), 'PING foo\r\n')
1289 # When the connection is lost the heartbeat is stopped.
1290 self.transport.loseConnection()
1291 self.client.connectionLost(None)
1293 len(self.clock.getDelayedCalls()), 0)
1294 self.assertIdentical(self.client._heartbeat, None)
1297 def test_heartbeatDisabled(self):
1299 If L{irc.IRCClient.heartbeatInterval} is set to C{None} then no
1300 heartbeat is created.
1302 self.assertIdentical(self.client._heartbeat, None)
1303 self.client.heartbeatInterval = None
1304 self.client.irc_RPL_WELCOME('foo', [])
1305 self.assertIdentical(self.client._heartbeat, None)
1309 class BasicServerFunctionalityTestCase(unittest.TestCase):
1311 self.f = StringIOWithoutClosing()
1312 self.t = protocol.FileWrapper(self.f)
1314 self.p.makeConnection(self.t)
1318 self.assertEqual(self.f.getvalue(), s)
1321 def testPrivmsg(self):
1322 self.p.privmsg("this-is-sender", "this-is-recip", "this is message")
1323 self.check(":this-is-sender PRIVMSG this-is-recip :this is message\r\n")
1326 def testNotice(self):
1327 self.p.notice("this-is-sender", "this-is-recip", "this is notice")
1328 self.check(":this-is-sender NOTICE this-is-recip :this is notice\r\n")
1331 def testAction(self):
1332 self.p.action("this-is-sender", "this-is-recip", "this is action")
1333 self.check(":this-is-sender ACTION this-is-recip :this is action\r\n")
1337 self.p.join("this-person", "#this-channel")
1338 self.check(":this-person JOIN #this-channel\r\n")
1342 self.p.part("this-person", "#that-channel")
1343 self.check(":this-person PART #that-channel\r\n")
1346 def testWhois(self):
1348 Verify that a whois by the client receives the right protocol actions
1351 timestamp = int(time.time()-100)
1352 hostname = self.p.hostname
1353 req = 'requesting-nick'
1354 targ = 'target-nick'
1355 self.p.whois(req, targ, 'target', 'host.com',
1356 'Target User', 'irc.host.com', 'A fake server', False,
1357 12, timestamp, ['#fakeusers', '#fakemisc'])
1358 expected = '\r\n'.join([
1359 ':%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User',
1360 ':%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server',
1361 ':%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time',
1362 ':%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc',
1363 ':%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.',
1364 '']) % dict(hostname=hostname, timestamp=timestamp, req=req, targ=targ)
1365 self.check(expected)
1369 class DummyClient(irc.IRCClient):
1371 A L{twisted.words.protocols.irc.IRCClient} that stores sent lines in a
1372 C{list} rather than transmitting them.
1378 def connectionMade(self):
1379 irc.IRCClient.connectionMade(self)
1383 def _truncateLine(self, line):
1385 Truncate an IRC line to the maximum allowed length.
1387 return line[:irc.MAX_COMMAND_LENGTH - len(self.delimiter)]
1390 def lineReceived(self, line):
1391 # Emulate IRC servers throwing away our important data.
1392 line = self._truncateLine(line)
1393 return irc.IRCClient.lineReceived(self, line)
1396 def sendLine(self, m):
1397 self.lines.append(self._truncateLine(m))
1401 class ClientInviteTests(unittest.TestCase):
1403 Tests for L{IRCClient.invite}.
1407 Create a L{DummyClient} to call C{invite} on in test methods.
1409 self.client = DummyClient()
1412 def test_channelCorrection(self):
1414 If the channel name passed to L{IRCClient.invite} does not begin with a
1415 channel prefix character, one is prepended to it.
1417 self.client.invite('foo', 'bar')
1418 self.assertEqual(self.client.lines, ['INVITE foo #bar'])
1421 def test_invite(self):
1423 L{IRCClient.invite} sends an I{INVITE} message with the specified
1424 username and a channel.
1426 self.client.invite('foo', '#bar')
1427 self.assertEqual(self.client.lines, ['INVITE foo #bar'])
1431 class ClientMsgTests(unittest.TestCase):
1433 Tests for messages sent with L{twisted.words.protocols.irc.IRCClient}.
1436 self.client = DummyClient()
1437 self.client.connectionMade()
1440 def test_singleLine(self):
1442 A message containing no newlines is sent in a single command.
1444 self.client.msg('foo', 'bar')
1445 self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
1448 def test_invalidMaxLength(self):
1450 Specifying a C{length} value to L{IRCClient.msg} that is too short to
1451 contain the protocol command to send a message raises C{ValueError}.
1453 self.assertRaises(ValueError, self.client.msg, 'foo', 'bar', 0)
1454 self.assertRaises(ValueError, self.client.msg, 'foo', 'bar', 3)
1457 def test_multipleLine(self):
1459 Messages longer than the C{length} parameter to L{IRCClient.msg} will
1460 be split and sent in multiple commands.
1462 maxLen = len('PRIVMSG foo :') + 3 + 2 # 2 for line endings
1463 self.client.msg('foo', 'barbazbo', maxLen)
1466 ['PRIVMSG foo :bar',
1471 def test_sufficientWidth(self):
1473 Messages exactly equal in length to the C{length} paramtere to
1474 L{IRCClient.msg} are sent in a single command.
1477 maxLen = len('PRIVMSG foo :%s' % (msg,)) + 2
1478 self.client.msg('foo', msg, maxLen)
1479 self.assertEqual(self.client.lines, ['PRIVMSG foo :%s' % (msg,)])
1480 self.client.lines = []
1481 self.client.msg('foo', msg, maxLen-1)
1482 self.assertEqual(2, len(self.client.lines))
1483 self.client.lines = []
1484 self.client.msg('foo', msg, maxLen+1)
1485 self.assertEqual(1, len(self.client.lines))
1488 def test_newlinesAtStart(self):
1490 An LF at the beginning of the message is ignored.
1492 self.client.lines = []
1493 self.client.msg('foo', '\nbar')
1494 self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
1497 def test_newlinesAtEnd(self):
1499 An LF at the end of the message is ignored.
1501 self.client.lines = []
1502 self.client.msg('foo', 'bar\n')
1503 self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
1506 def test_newlinesWithinMessage(self):
1508 An LF within a message causes a new line.
1510 self.client.lines = []
1511 self.client.msg('foo', 'bar\nbaz')
1514 ['PRIVMSG foo :bar',
1515 'PRIVMSG foo :baz'])
1518 def test_consecutiveNewlines(self):
1520 Consecutive LFs do not cause a blank line.
1522 self.client.lines = []
1523 self.client.msg('foo', 'bar\n\nbaz')
1526 ['PRIVMSG foo :bar',
1527 'PRIVMSG foo :baz'])
1530 def assertLongMessageSplitting(self, message, expectedNumCommands,
1533 Assert that messages sent by L{IRCClient.msg} are split into an
1534 expected number of commands and the original message is transmitted in
1535 its entirety over those commands.
1537 responsePrefix = ':%s!%s@%s ' % (
1538 self.client.nickname,
1539 self.client.realname,
1540 self.client.hostname)
1542 self.client.msg('foo', message, length=length)
1545 self.patch(self.client, 'privmsg', lambda *a: privmsg.append(a))
1546 # Deliver these to IRCClient via the normal mechanisms.
1547 for line in self.client.lines:
1548 self.client.lineReceived(responsePrefix + line)
1550 self.assertEqual(len(privmsg), expectedNumCommands)
1551 receivedMessage = ''.join(
1552 message for user, target, message in privmsg)
1554 # Did the long message we sent arrive as intended?
1555 self.assertEqual(message, receivedMessage)
1558 def test_splitLongMessagesWithDefault(self):
1560 If a maximum message length is not provided to L{IRCClient.msg} a
1561 best-guess effort is made to determine a safe maximum, messages longer
1562 than this are split into multiple commands with the intent of
1563 delivering long messages without losing data due to message truncation
1564 when the server relays them.
1566 message = 'o' * (irc.MAX_COMMAND_LENGTH - 2)
1567 self.assertLongMessageSplitting(message, 2)
1570 def test_splitLongMessagesWithOverride(self):
1572 The maximum message length can be specified to L{IRCClient.msg},
1573 messages longer than this are split into multiple commands with the
1574 intent of delivering long messages without losing data due to message
1575 truncation when the server relays them.
1577 message = 'o' * (irc.MAX_COMMAND_LENGTH - 2)
1578 self.assertLongMessageSplitting(
1579 message, 3, length=irc.MAX_COMMAND_LENGTH / 2)
1582 def test_newlinesBeforeLineBreaking(self):
1584 IRCClient breaks on newlines before it breaks long lines.
1586 # Because MAX_COMMAND_LENGTH includes framing characters, this long
1587 # line is slightly longer than half the permissible message size.
1588 longline = 'o' * (irc.MAX_COMMAND_LENGTH // 2)
1590 self.client.msg('foo', longline + '\n' + longline)
1593 ['PRIVMSG foo :' + longline,
1594 'PRIVMSG foo :' + longline])
1597 def test_lineBreakOnWordBoundaries(self):
1599 IRCClient prefers to break long lines at word boundaries.
1601 # Because MAX_COMMAND_LENGTH includes framing characters, this long
1602 # line is slightly longer than half the permissible message size.
1603 longline = 'o' * (irc.MAX_COMMAND_LENGTH // 2)
1605 self.client.msg('foo', longline + ' ' + longline)
1608 ['PRIVMSG foo :' + longline,
1609 'PRIVMSG foo :' + longline])
1612 def test_splitSanity(self):
1614 L{twisted.words.protocols.irc.split} raises C{ValueError} if given a
1615 length less than or equal to C{0} and returns C{[]} when splitting
1619 self.assertRaises(ValueError, irc.split, 'foo', -1)
1620 self.assertRaises(ValueError, irc.split, 'foo', 0)
1621 self.assertEqual([], irc.split('', 1))
1622 self.assertEqual([], irc.split(''))
1625 def test_splitDelimiters(self):
1627 L{twisted.words.protocols.irc.split} skips any delimiter (space or
1628 newline) that it finds at the very beginning of the string segment it
1629 is operating on. Nothing should be added to the output list because of
1632 r = irc.split("xx yyz", 2)
1633 self.assertEqual(['xx', 'yy', 'z'], r)
1634 r = irc.split("xx\nyyz", 2)
1635 self.assertEqual(['xx', 'yy', 'z'], r)
1638 def test_splitValidatesLength(self):
1640 L{twisted.words.protocols.irc.split} raises C{ValueError} if given a
1641 length less than or equal to C{0}.
1643 self.assertRaises(ValueError, irc.split, "foo", 0)
1644 self.assertRaises(ValueError, irc.split, "foo", -1)
1649 L{IRCClient.say} prepends the channel prefix C{"#"} if necessary and
1650 then sends the message to the server for delivery to that channel.
1652 self.client.say("thechannel", "the message")
1654 self.client.lines, ["PRIVMSG #thechannel :the message"])
1658 class ClientTests(TestCase):
1660 Tests for the protocol-level behavior of IRCClient methods intended to
1661 be called by application code.
1665 Create and connect a new L{IRCClient} to a new L{StringTransport}.
1667 self.transport = StringTransport()
1668 self.protocol = IRCClient()
1669 self.protocol.performLogin = False
1670 self.protocol.makeConnection(self.transport)
1672 # Sanity check - we don't want anything to have happened at this
1673 # point, since we're not in a test yet.
1674 self.assertEqual(self.transport.value(), "")
1676 self.addCleanup(self.transport.loseConnection)
1677 self.addCleanup(self.protocol.connectionLost, None)
1680 def getLastLine(self, transport):
1682 Return the last IRC message in the transport buffer.
1684 return transport.value().split('\r\n')[-2]
1687 def test_away(self):
1689 L{IRCCLient.away} sends an AWAY command with the specified message.
1691 message = "Sorry, I'm not here."
1692 self.protocol.away(message)
1694 'AWAY :%s' % (message,),
1697 self.assertEqual(self.transport.value().split('\r\n'), expected)
1700 def test_back(self):
1702 L{IRCClient.back} sends an AWAY command with an empty message.
1704 self.protocol.back()
1709 self.assertEqual(self.transport.value().split('\r\n'), expected)
1712 def test_whois(self):
1714 L{IRCClient.whois} sends a WHOIS message.
1716 self.protocol.whois('alice')
1718 self.transport.value().split('\r\n'),
1719 ['WHOIS alice', ''])
1722 def test_whoisWithServer(self):
1724 L{IRCClient.whois} sends a WHOIS message with a server name if a
1725 value is passed for the C{server} parameter.
1727 self.protocol.whois('alice', 'example.org')
1729 self.transport.value().split('\r\n'),
1730 ['WHOIS example.org alice', ''])
1733 def test_register(self):
1735 L{IRCClient.register} sends NICK and USER commands with the
1736 username, name, hostname, server name, and real name specified.
1738 username = 'testuser'
1739 hostname = 'testhost'
1740 servername = 'testserver'
1741 self.protocol.realname = 'testname'
1742 self.protocol.password = None
1743 self.protocol.register(username, hostname, servername)
1745 'NICK %s' % (username,),
1746 'USER %s %s %s :%s' % (
1747 username, hostname, servername, self.protocol.realname),
1749 self.assertEqual(self.transport.value().split('\r\n'), expected)
1752 def test_registerWithPassword(self):
1754 If the C{password} attribute of L{IRCClient} is not C{None}, the
1755 C{register} method also sends a PASS command with it as the
1758 username = 'testuser'
1759 hostname = 'testhost'
1760 servername = 'testserver'
1761 self.protocol.realname = 'testname'
1762 self.protocol.password = 'testpass'
1763 self.protocol.register(username, hostname, servername)
1765 'PASS %s' % (self.protocol.password,),
1766 'NICK %s' % (username,),
1767 'USER %s %s %s :%s' % (
1768 username, hostname, servername, self.protocol.realname),
1770 self.assertEqual(self.transport.value().split('\r\n'), expected)
1773 def test_registerWithTakenNick(self):
1775 Verify that the client repeats the L{IRCClient.setNick} method with a
1776 new value when presented with an C{ERR_NICKNAMEINUSE} while trying to
1779 username = 'testuser'
1780 hostname = 'testhost'
1781 servername = 'testserver'
1782 self.protocol.realname = 'testname'
1783 self.protocol.password = 'testpass'
1784 self.protocol.register(username, hostname, servername)
1785 self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param'])
1786 lastLine = self.getLastLine(self.transport)
1787 self.assertNotEquals(lastLine, 'NICK %s' % (username,))
1789 # Keep chaining underscores for each collision
1790 self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param'])
1791 lastLine = self.getLastLine(self.transport)
1792 self.assertEqual(lastLine, 'NICK %s' % (username + '__',))
1795 def test_overrideAlterCollidedNick(self):
1797 L{IRCClient.alterCollidedNick} determines how a nickname is altered upon
1798 collision while a user is trying to change to that nickname.
1801 self.protocol.alterCollidedNick = lambda nick: nick + '***'
1802 self.protocol.register(nick)
1803 self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param'])
1804 lastLine = self.getLastLine(self.transport)
1806 lastLine, 'NICK %s' % (nick + '***',))
1809 def test_nickChange(self):
1811 When a NICK command is sent after signon, C{IRCClient.nickname} is set
1812 to the new nickname I{after} the server sends an acknowledgement.
1816 self.protocol.register(oldnick)
1817 self.protocol.irc_RPL_WELCOME('prefix', ['param'])
1818 self.protocol.setNick(newnick)
1819 self.assertEqual(self.protocol.nickname, oldnick)
1820 self.protocol.irc_NICK('%s!quux@qux' % (oldnick,), [newnick])
1821 self.assertEqual(self.protocol.nickname, newnick)
1824 def test_erroneousNick(self):
1826 Trying to register an illegal nickname results in the default legal
1827 nickname being set, and trying to change a nickname to an illegal
1828 nickname results in the old nickname being kept.
1830 # Registration case: change illegal nickname to erroneousNickFallback
1832 self.assertEqual(self.protocol._registered, False)
1833 self.protocol.register(badnick)
1834 self.protocol.irc_ERR_ERRONEUSNICKNAME('prefix', ['param'])
1835 lastLine = self.getLastLine(self.transport)
1837 lastLine, 'NICK %s' % (self.protocol.erroneousNickFallback,))
1838 self.protocol.irc_RPL_WELCOME('prefix', ['param'])
1839 self.assertEqual(self.protocol._registered, True)
1840 self.protocol.setNick(self.protocol.erroneousNickFallback)
1842 self.protocol.nickname, self.protocol.erroneousNickFallback)
1844 # Illegal nick change attempt after registration. Fall back to the old
1845 # nickname instead of erroneousNickFallback.
1846 oldnick = self.protocol.nickname
1847 self.protocol.setNick(badnick)
1848 self.protocol.irc_ERR_ERRONEUSNICKNAME('prefix', ['param'])
1849 lastLine = self.getLastLine(self.transport)
1851 lastLine, 'NICK %s' % (badnick,))
1852 self.assertEqual(self.protocol.nickname, oldnick)
1855 def test_describe(self):
1857 L{IRCClient.desrcibe} sends a CTCP ACTION message to the target
1863 self.protocol.describe(target, action)
1864 self.protocol.describe(channel, action)
1866 'PRIVMSG %s :\01ACTION %s\01' % (target, action),
1867 'PRIVMSG %s :\01ACTION %s\01' % (channel, action),
1869 self.assertEqual(self.transport.value().split('\r\n'), expected)
1872 def test_noticedDoesntPrivmsg(self):
1874 The default implementation of L{IRCClient.noticed} doesn't invoke
1877 def privmsg(user, channel, message):
1878 self.fail("privmsg() should not have been called")
1879 self.protocol.privmsg = privmsg
1880 self.protocol.irc_NOTICE(
1881 'spam', ['#greasyspooncafe', "I don't want any spam!"])
1885 class DccChatFactoryTests(unittest.TestCase):
1887 Tests for L{DccChatFactory}
1889 def test_buildProtocol(self):
1891 An instance of the DccChat protocol is returned, which has the factory
1892 property set to the factory which created it.
1894 queryData = ('fromUser', None, None)
1895 f = irc.DccChatFactory(None, queryData)
1896 p = f.buildProtocol('127.0.0.1')
1897 self.assertTrue(isinstance(p, irc.DccChat))
1898 self.assertEqual(p.factory, f)