Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / words / test / test_irc.py
1 # Copyright (c) Twisted Matrix Laboratories.
2 # See LICENSE for details.
3
4 """
5 Tests for L{twisted.words.protocols.irc}.
6 """
7
8 import time
9
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
16
17
18
19 class ModeParsingTests(unittest.TestCase):
20     """
21     Tests for L{twisted.words.protocols.irc.parseModes}.
22     """
23     paramModes = ('klb', 'b')
24
25
26     def test_emptyModes(self):
27         """
28         Parsing an empty mode string raises L{irc.IRCBadModes}.
29         """
30         self.assertRaises(irc.IRCBadModes, irc.parseModes, '', [])
31
32
33     def test_emptyModeSequence(self):
34         """
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}.
38         """
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, '-', [])
43
44
45     def test_malformedModes(self):
46         """
47         Parsing a mode string that does not start with C{+} or C{-} raises
48         L{irc.IRCBadModes}.
49         """
50         self.assertRaises(irc.IRCBadModes, irc.parseModes, 'foo', [])
51         self.assertRaises(irc.IRCBadModes, irc.parseModes, '%', [])
52
53
54     def test_nullModes(self):
55         """
56         Parsing a mode string that contains no mode characters raises
57         L{irc.IRCBadModes}.
58         """
59         self.assertRaises(irc.IRCBadModes, irc.parseModes, '+', [])
60         self.assertRaises(irc.IRCBadModes, irc.parseModes, '-', [])
61
62
63     def test_singleMode(self):
64         """
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
67         "removed" direction.
68         """
69         added, removed = irc.parseModes('+s', [])
70         self.assertEqual(added, [('s', None)])
71         self.assertEqual(removed, [])
72
73         added, removed = irc.parseModes('-s', [])
74         self.assertEqual(added, [])
75         self.assertEqual(removed, [('s', None)])
76
77
78     def test_singleDirection(self):
79         """
80         Parsing a single-direction mode setting with multiple modes and no
81         parameters, results in all modes falling into the same direction group.
82         """
83         added, removed = irc.parseModes('+stn', [])
84         self.assertEqual(added, [('s', None),
85                                   ('t', None),
86                                   ('n', None)])
87         self.assertEqual(removed, [])
88
89         added, removed = irc.parseModes('-nt', [])
90         self.assertEqual(added, [])
91         self.assertEqual(removed, [('n', None),
92                                     ('t', None)])
93
94
95     def test_multiDirection(self):
96         """
97         Parsing a multi-direction mode setting with no parameters.
98         """
99         added, removed = irc.parseModes('+s-n+ti', [])
100         self.assertEqual(added, [('s', None),
101                                   ('t', None),
102                                   ('i', None)])
103         self.assertEqual(removed, [('n', None)])
104
105
106     def test_consecutiveDirection(self):
107         """
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.
111         """
112         added, removed = irc.parseModes('+sn+ti', [])
113         self.assertEqual(added, [('s', None),
114                                   ('n', None),
115                                   ('t', None),
116                                   ('i', None)])
117         self.assertEqual(removed, [])
118
119
120     def test_mismatchedParams(self):
121         """
122         If the number of mode parameters does not match the number of modes
123         expecting parameters, L{irc.IRCBadModes} is raised.
124         """
125         self.assertRaises(irc.IRCBadModes,
126                           irc.parseModes,
127                           '+k', [],
128                           self.paramModes)
129         self.assertRaises(irc.IRCBadModes,
130                           irc.parseModes,
131                           '+kl', ['foo', '10', 'lulz_extra_param'],
132                           self.paramModes)
133
134
135     def test_parameters(self):
136         """
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
139         the parameters.
140         """
141         added, removed = irc.parseModes(
142             '+klbb',
143             ['somekey', '42', 'nick!user@host', 'other!*@*'],
144             self.paramModes)
145         self.assertEqual(added, [('k', 'somekey'),
146                                   ('l', '42'),
147                                   ('b', 'nick!user@host'),
148                                   ('b', 'other!*@*')])
149         self.assertEqual(removed, [])
150
151         added, removed = irc.parseModes(
152             '-klbb',
153             ['nick!user@host', 'other!*@*'],
154             self.paramModes)
155         self.assertEqual(added, [])
156         self.assertEqual(removed, [('k', None),
157                                     ('l', None),
158                                     ('b', 'nick!user@host'),
159                                     ('b', 'other!*@*')])
160
161         # Mix a no-argument mode in with argument modes.
162         added, removed = irc.parseModes(
163             '+knbb',
164             ['somekey', 'nick!user@host', 'other!*@*'],
165             self.paramModes)
166         self.assertEqual(added, [('k', 'somekey'),
167                                   ('n', None),
168                                   ('b', 'nick!user@host'),
169                                   ('b', 'other!*@*')])
170         self.assertEqual(removed, [])
171
172
173
174 stringSubjects = [
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,
178                                                     'NL': irc.NL},
179     "escape!%(X)s escape!%(M)s %(X)s%(X)sa %(M)s0" % {'X': irc.X_QUOTE,
180                                                       'M': irc.M_QUOTE}
181     ]
182
183
184 class QuotingTest(unittest.TestCase):
185     def test_lowquoteSanity(self):
186         """
187         Testing client-server level quote/dequote.
188         """
189         for s in stringSubjects:
190             self.assertEqual(s, irc.lowDequote(irc.lowQuote(s)))
191
192
193     def test_ctcpquoteSanity(self):
194         """
195         Testing CTCP message level quote/dequote.
196         """
197         for s in stringSubjects:
198             self.assertEqual(s, irc.ctcpDequote(irc.ctcpQuote(s)))
199
200
201
202 class Dispatcher(irc._CommandDispatcherMixin):
203     """
204     A dispatcher that exposes one known command and handles unknown commands.
205     """
206     prefix = 'disp'
207
208     def disp_working(self, a, b):
209         """
210         A known command that returns its input.
211         """
212         return a, b
213
214
215     def disp_unknown(self, name, a, b):
216         """
217         Handle unknown commands by returning their name and inputs.
218         """
219         return name, a, b
220
221
222
223 class DispatcherTests(unittest.TestCase):
224     """
225     Tests for L{irc._CommandDispatcherMixin}.
226     """
227     def test_dispatch(self):
228         """
229         Dispatching a command invokes the correct handler.
230         """
231         disp = Dispatcher()
232         args = (1, 2)
233         res = disp.dispatch('working', *args)
234         self.assertEqual(res, args)
235
236
237     def test_dispatchUnknown(self):
238         """
239         Dispatching an unknown command invokes the default handler.
240         """
241         disp = Dispatcher()
242         name = 'missing'
243         args = (1, 2)
244         res = disp.dispatch(name, *args)
245         self.assertEqual(res, (name,) + args)
246
247
248     def test_dispatchMissingUnknown(self):
249         """
250         Dispatching an unknown command, when no default handler is present,
251         results in an exception being raised.
252         """
253         disp = Dispatcher()
254         disp.disp_unknown = None
255         self.assertRaises(irc.UnhandledCommand, disp.dispatch, 'bar')
256
257
258
259 class ServerSupportedFeatureTests(unittest.TestCase):
260     """
261     Tests for L{ServerSupportedFeatures} and related functions.
262     """
263     def test_intOrDefault(self):
264         """
265         L{_intOrDefault} converts values to C{int} if possible, otherwise
266         returns a default value.
267         """
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)
274
275
276     def test_splitParam(self):
277         """
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
281         are unescaped.
282         """
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']))]
291
292         _splitParam = irc.ServerSupportedFeatures._splitParam
293
294         for param, expected in params:
295             res = _splitParam(param)
296             self.assertEqual(res, expected)
297
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')
302
303
304     def test_splitParamArgs(self):
305         """
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.
309         """
310         res = irc.ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2', 'C:', 'D'])
311         self.assertEqual(res, [('A', '1'),
312                                 ('B', '2'),
313                                 ('C', ''),
314                                 ('D', '')])
315
316
317     def test_splitParamArgsProcessor(self):
318         """
319         L{ServerSupportedFeatures._splitParamArgs} uses the argument processor
320         passed to to convert ISUPPORT argument values to some more suitable
321         form.
322         """
323         res = irc.ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2', 'C'],
324                                            irc._intOrDefault)
325         self.assertEqual(res, [('A', 1),
326                                 ('B', 2),
327                                 ('C', None)])
328
329
330     def test_parsePrefixParam(self):
331         """
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.
336         """
337         _parsePrefixParam = irc.ServerSupportedFeatures._parsePrefixParam
338         self.assertEqual(_parsePrefixParam(''), None)
339         self.assertRaises(ValueError, _parsePrefixParam, 'hello')
340         self.assertEqual(_parsePrefixParam('(ov)@+'),
341                           {'o': ('@', 0),
342                            'v': ('+', 1)})
343
344
345     def test_parseChanModesParam(self):
346         """
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
351         C{ValueError}.
352         """
353         _parseChanModesParam = irc.ServerSupportedFeatures._parseChanModesParam
354         self.assertEqual(
355             _parseChanModesParam([]),
356             {'addressModes': '',
357              'param': '',
358              'setParam': '',
359              'noParam': ''})
360
361         self.assertEqual(
362             _parseChanModesParam(['b', 'k', 'l', 'imnpst']),
363             {'addressModes': 'b',
364              'param': 'k',
365              'setParam': 'l',
366              'noParam': 'imnpst'})
367
368         self.assertEqual(
369             _parseChanModesParam(['b', 'k', 'l']),
370             {'addressModes': 'b',
371              'param': 'k',
372              'setParam': 'l',
373              'noParam': ''})
374
375         self.assertRaises(
376             ValueError,
377             _parseChanModesParam, ['a', 'b', 'c', 'd', 'e'])
378
379
380     def test_parse(self):
381         """
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
385         parameters.
386         """
387         supported = irc.ServerSupportedFeatures()
388         supported.parse(['MODES=4',
389                         'CHANLIMIT=#:20,&:10',
390                         'INVEX',
391                         'EXCEPTS=Z',
392                         'UNKNOWN=A,B,C'])
393
394         self.assertEqual(supported.getFeature('MODES'), 4)
395         self.assertEqual(supported.getFeature('CHANLIMIT'),
396                           [('#', 20),
397                            ('&', 10)])
398         self.assertEqual(supported.getFeature('INVEX'), 'I')
399         self.assertEqual(supported.getFeature('EXCEPTS'), 'Z')
400         self.assertEqual(supported.getFeature('UNKNOWN'), ('A', 'B', 'C'))
401
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'])
407
408
409     def _parse(self, features):
410         """
411         Parse all specified features according to the ISUPPORT specifications.
412
413         @type features: C{list} of C{(featureName, value)}
414         @param features: Feature names and values to parse
415
416         @rtype: L{irc.ServerSupportedFeatures}
417         """
418         supported = irc.ServerSupportedFeatures()
419         features = ['%s=%s' % (name, value or '')
420                     for name, value in features]
421         supported.parse(features)
422         return supported
423
424
425     def _parseFeature(self, name, value=None):
426         """
427         Parse a feature, with the given name and value, according to the
428         ISUPPORT specifications and return the parsed value.
429         """
430         supported = self._parse([(name, value)])
431         return supported.getFeature(name)
432
433
434     def _testIntOrDefaultFeature(self, name, default=None):
435         """
436         Perform some common tests on a feature known to use L{_intOrDefault}.
437         """
438         self.assertEqual(
439             self._parseFeature(name, None),
440             default)
441         self.assertEqual(
442             self._parseFeature(name, 'notanint'),
443             default)
444         self.assertEqual(
445             self._parseFeature(name, '42'),
446             42)
447
448
449     def _testFeatureDefault(self, name, features=None):
450         """
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.
454         """
455         default = irc.ServerSupportedFeatures()._features[name]
456
457         if features is None:
458             features = [('DEFINITELY_NOT', 'a_feature')]
459
460         supported = self._parse(features)
461         self.assertTrue(supported.hasFeature(name))
462         self.assertEqual(supported.getFeature(name), default)
463
464
465     def test_support_CHANMODES(self):
466         """
467         The CHANMODES ISUPPORT parameter is parsed into a C{dict} giving the
468         four mode categories, C{'addressModes'}, C{'param'}, C{'setParam'}, and
469         C{'noParam'}.
470         """
471         self._testFeatureDefault('CHANMODES')
472         self._testFeatureDefault('CHANMODES', [('CHANMODES', 'b,,lk,')])
473         self._testFeatureDefault('CHANMODES', [('CHANMODES', 'b,,lk,ha,ha')])
474
475         self.assertEqual(
476             self._parseFeature('CHANMODES', ''),
477             {'addressModes': '',
478              'param': '',
479              'setParam': '',
480              'noParam': ''})
481
482         self.assertEqual(
483             self._parseFeature('CHANMODES', ',A'),
484             {'addressModes': '',
485              'param': 'A',
486              'setParam': '',
487              'noParam': ''})
488
489         self.assertEqual(
490             self._parseFeature('CHANMODES', 'A,Bc,Def,Ghij'),
491             {'addressModes': 'A',
492              'param': 'Bc',
493              'setParam': 'Def',
494              'noParam': 'Ghij'})
495
496
497     def test_support_IDCHAN(self):
498         """
499         The IDCHAN support parameter is parsed into a sequence of two-tuples
500         giving channel prefix and ID length pairs.
501         """
502         self.assertEqual(
503             self._parseFeature('IDCHAN', '!:5'),
504             [('!', '5')])
505
506
507     def test_support_MAXLIST(self):
508         """
509         The MAXLIST support parameter is parsed into a sequence of two-tuples
510         giving modes and their limits.
511         """
512         self.assertEqual(
513             self._parseFeature('MAXLIST', 'b:25,eI:50'),
514             [('b', 25), ('eI', 50)])
515         # A non-integer parameter argument results in None.
516         self.assertEqual(
517             self._parseFeature('MAXLIST', 'b:25,eI:50,a:3.1415'),
518             [('b', 25), ('eI', 50), ('a', None)])
519         self.assertEqual(
520             self._parseFeature('MAXLIST', 'b:25,eI:50,a:notanint'),
521             [('b', 25), ('eI', 50), ('a', None)])
522
523
524     def test_support_NETWORK(self):
525         """
526         The NETWORK support parameter is parsed as the network name, as
527         specified by the server.
528         """
529         self.assertEqual(
530             self._parseFeature('NETWORK', 'IRCNet'),
531             'IRCNet')
532
533
534     def test_support_SAFELIST(self):
535         """
536         The SAFELIST support parameter is parsed into a boolean indicating
537         whether the safe "list" command is supported or not.
538         """
539         self.assertEqual(
540             self._parseFeature('SAFELIST'),
541             True)
542
543
544     def test_support_STATUSMSG(self):
545         """
546         The STATUSMSG support parameter is parsed into a string of channel
547         status that support the exclusive channel notice method.
548         """
549         self.assertEqual(
550             self._parseFeature('STATUSMSG', '@+'),
551             '@+')
552
553
554     def test_support_TARGMAX(self):
555         """
556         The TARGMAX support parameter is parsed into a dictionary, mapping
557         strings to integers, of the maximum number of targets for a particular
558         command.
559         """
560         self.assertEqual(
561             self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3'),
562             {'PRIVMSG': 4,
563              'NOTICE': 3})
564         # A non-integer parameter argument results in None.
565         self.assertEqual(
566             self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3,KICK:3.1415'),
567             {'PRIVMSG': 4,
568              'NOTICE': 3,
569              'KICK': None})
570         self.assertEqual(
571             self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3,KICK:notanint'),
572             {'PRIVMSG': 4,
573              'NOTICE': 3,
574              'KICK': None})
575
576
577     def test_support_NICKLEN(self):
578         """
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.
583         """
584         default = irc.ServerSupportedFeatures()._features['NICKLEN']
585         self._testIntOrDefaultFeature('NICKLEN', default)
586
587
588     def test_support_CHANNELLEN(self):
589         """
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
593         RFC 1459) is used.
594         """
595         default = irc.ServerSupportedFeatures()._features['CHANNELLEN']
596         self._testIntOrDefaultFeature('CHANNELLEN', default)
597
598
599     def test_support_CHANTYPES(self):
600         """
601         The CHANTYPES support parameter is parsed into a tuple of
602         valid channel prefix characters.
603         """
604         self._testFeatureDefault('CHANTYPES')
605
606         self.assertEqual(
607             self._parseFeature('CHANTYPES', '#&%'),
608             ('#', '&', '%'))
609
610
611     def test_support_KICKLEN(self):
612         """
613         The KICKLEN support parameter is parsed into an integer value
614         indicating the maximum length of a kick message a client may use.
615         """
616         self._testIntOrDefaultFeature('KICKLEN')
617
618
619     def test_support_PREFIX(self):
620         """
621         The PREFIX support parameter is parsed into a dictionary mapping
622         modes to two-tuples of status symbol and priority.
623         """
624         self._testFeatureDefault('PREFIX')
625         self._testFeatureDefault('PREFIX', [('PREFIX', 'hello')])
626
627         self.assertEqual(
628             self._parseFeature('PREFIX', None),
629             None)
630         self.assertEqual(
631             self._parseFeature('PREFIX', '(ohv)@%+'),
632             {'o': ('@', 0),
633              'h': ('%', 1),
634              'v': ('+', 2)})
635         self.assertEqual(
636             self._parseFeature('PREFIX', '(hov)@%+'),
637             {'o': ('%', 1),
638              'h': ('@', 0),
639              'v': ('+', 2)})
640
641
642     def test_support_TOPICLEN(self):
643         """
644         The TOPICLEN support parameter is parsed into an integer value
645         indicating the maximum length of a topic a client may set.
646         """
647         self._testIntOrDefaultFeature('TOPICLEN')
648
649
650     def test_support_MODES(self):
651         """
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.
657         """
658         self._testIntOrDefaultFeature('MODES')
659
660
661     def test_support_EXCEPTS(self):
662         """
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.
666         """
667         self.assertEqual(
668             self._parseFeature('EXCEPTS', 'Z'),
669             'Z')
670         self.assertEqual(
671             self._parseFeature('EXCEPTS'),
672             'e')
673
674
675     def test_support_INVEX(self):
676         """
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.
680         """
681         self.assertEqual(
682             self._parseFeature('INVEX', 'Z'),
683             'Z')
684         self.assertEqual(
685             self._parseFeature('INVEX'),
686             'I')
687
688
689
690 class IRCClientWithoutLogin(irc.IRCClient):
691     performLogin = 0
692
693
694
695 class CTCPTest(unittest.TestCase):
696     """
697     Tests for L{twisted.words.protocols.irc.IRCClient} CTCP handling.
698     """
699     def setUp(self):
700         self.file = StringIOWithoutClosing()
701         self.transport = protocol.FileWrapper(self.file)
702         self.client = IRCClientWithoutLogin()
703         self.client.makeConnection(self.transport)
704
705         self.addCleanup(self.transport.loseConnection)
706         self.addCleanup(self.client.connectionLost, None)
707
708
709     def test_ERRMSG(self):
710         """Testing CTCP query ERRMSG.
711
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
714         process.
715         """
716
717         errQuery = (":nick!guy@over.there PRIVMSG #theChan :"
718                     "%(X)cERRMSG t%(X)c%(EOL)s"
719                     % {'X': irc.X_DELIM,
720                        'EOL': irc.CR + irc.LF})
721
722         errReply = ("NOTICE nick :%(X)cERRMSG t :"
723                     "No error has occoured.%(X)c%(EOL)s"
724                     % {'X': irc.X_DELIM,
725                        'EOL': irc.CR + irc.LF})
726
727         self.client.dataReceived(errQuery)
728         reply = self.file.getvalue()
729
730         self.assertEqual(errReply, reply)
731
732
733     def test_noNumbersVERSION(self):
734         """
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
737         are omitted.
738         """
739         self.client.versionName = "FrobozzIRC"
740         self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None)
741         versionReply = ("NOTICE nick :%(X)cVERSION %(vname)s::"
742                         "%(X)c%(EOL)s"
743                         % {'X': irc.X_DELIM,
744                            'EOL': irc.CR + irc.LF,
745                            'vname': self.client.versionName})
746         reply = self.file.getvalue()
747         self.assertEqual(versionReply, reply)
748
749
750     def test_fullVERSION(self):
751         """
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}.
755         """
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"
761                         "%(X)c%(EOL)s"
762                         % {'X': irc.X_DELIM,
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)
769
770
771     def test_noDuplicateCTCPDispatch(self):
772         """
773         Duplicated CTCP messages are ignored and no reply is made.
774         """
775         def testCTCP(user, channel, data):
776             self.called += 1
777
778         self.called = 0
779         self.client.ctcpQuery_TESTTHIS = testCTCP
780
781         self.client.irc_PRIVMSG(
782             'foo!bar@baz.quux', [
783                 '#chan',
784                 '%(X)sTESTTHIS%(X)sfoo%(X)sTESTTHIS%(X)s' % {'X': irc.X_DELIM}])
785         self.assertEqual(
786             self.file.getvalue(),
787             '')
788         self.assertEqual(self.called, 1)
789
790
791     def test_noDefaultDispatch(self):
792         """
793         The fallback handler is invoked for unrecognized CTCP messages.
794         """
795         def unknownQuery(user, channel, tag, data):
796             self.calledWith = (user, channel, tag, data)
797             self.called += 1
798
799         self.called = 0
800         self.patch(self.client, 'ctcpUnknownQuery', unknownQuery)
801         self.client.irc_PRIVMSG(
802             'foo!bar@baz.quux', [
803                 '#chan',
804                 '%(X)sNOTREAL%(X)s' % {'X': irc.X_DELIM}])
805         self.assertEqual(
806             self.file.getvalue(),
807             '')
808         self.assertEqual(
809             self.calledWith,
810             ('foo!bar@baz.quux', '#chan', 'NOTREAL', None))
811         self.assertEqual(self.called, 1)
812
813         # The fallback handler is not invoked for duplicate unknown CTCP
814         # messages.
815         self.client.irc_PRIVMSG(
816             'foo!bar@baz.quux', [
817                 '#chan',
818                 '%(X)sNOTREAL%(X)sfoo%(X)sNOTREAL%(X)s' % {'X': irc.X_DELIM}])
819         self.assertEqual(self.called, 2)
820
821
822
823 class NoticingClient(IRCClientWithoutLogin, object):
824     methods = {
825         'created': ('when',),
826         'yourHost': ('info',),
827         'myInfo': ('servername', 'version', 'umodes', 'cmodes'),
828         'luserClient': ('info',),
829         'bounce': ('info',),
830         'isupport': ('options',),
831         'luserChannels': ('channels',),
832         'luserOp': ('ops',),
833         'luserMe': ('info',),
834         'receivedMOTD': ('motd',),
835
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'),
842         'signedOn': (),
843         'kickedFrom': ('channel', 'kicker', 'message'),
844         'nickChanged': ('nick',),
845
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')}
852
853
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.
860         self.calls = []
861
862
863     def __getattribute__(self, name):
864         if name.startswith('__') and name.endswith('__'):
865             return super(NoticingClient, self).__getattribute__(name)
866         try:
867             args = super(NoticingClient, self).__getattribute__('methods')[name]
868         except KeyError:
869             return super(NoticingClient, self).__getattribute__(name)
870         else:
871             return self.makeMethod(name, args)
872
873
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):
880                 if name in kw:
881                     raise TypeError("TypeError: %s() got multiple values "
882                                     "for keyword argument '%s'" % (fname, name))
883                 else:
884                     kw[name] = value
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))
889         return method
890
891
892 def pop(dict, key, default):
893     try:
894         value = dict[key]
895     except KeyError:
896         return default
897     else:
898         del dict[key]
899         return value
900
901
902
903 class ClientImplementationTests(unittest.TestCase):
904     def setUp(self):
905         self.transport = StringTransport()
906         self.client = NoticingClient()
907         self.client.makeConnection(self.transport)
908
909         self.addCleanup(self.transport.loseConnection)
910         self.addCleanup(self.client.connectionLost, None)
911
912
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', '')
917
918         message = (":" +
919                    host + " " +
920                    code + " " +
921                    nick + " " +
922                    args + " :" +
923                    msg + "\r\n")
924
925         self.client.dataReceived(message)
926         self.assertEqual(
927             self.client.calls,
928             [(func, kw)])
929
930
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)
934
935
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)
939
940
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",
946                              umodes="abcDEF",
947                              cmodes="bcdEHI")
948
949
950     def testLuserClient(self):
951         msg = "There are 9227 victims and 9542 hiding on 24 servers"
952         self._serverTestImpl("251", msg, "luserClient",
953                              info=msg)
954
955
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,
962                              options=['MODES=4',
963                                       'CHANLIMIT=#:20',
964                                       'NICKLEN=16',
965                                       'USERLEN=10',
966                                       'HOSTLEN=63',
967                                       'TOPICLEN=450',
968                                       'KICKLEN=450',
969                                       'CHANNELLEN=30',
970                                       'KEYLEN=23',
971                                       'CHANTYPES=#',
972                                       'PREFIX=(ov)@+',
973                                       'CASEMAPPING=ascii',
974                                       'CAPAB',
975                                       'IRCD=dancer'])
976
977
978     def test_ISUPPORT(self):
979         """
980         The client parses ISUPPORT messages sent by the server and calls
981         L{IRCClient.isupport}.
982         """
983         self._sendISUPPORT()
984
985
986     def testBounce(self):
987         msg = "Try server some.host, port 321"
988         self._serverTestImpl("010", msg, "bounce",
989                              info=msg)
990
991
992     def testLuserChannels(self):
993         args = "7116"
994         msg = "channels formed"
995         self._serverTestImpl("254", msg, "luserChannels", args=args,
996                              channels=int(args))
997
998
999     def testLuserOp(self):
1000         args = "34"
1001         msg = "flagged staff members"
1002         self._serverTestImpl("252", msg, "luserOp", args=args,
1003                              ops=int(args))
1004
1005
1006     def testLuserMe(self):
1007         msg = "I have 1937 clients and 0 servers"
1008         self._serverTestImpl("255", msg, "luserMe",
1009                              info=msg)
1010
1011
1012     def test_receivedMOTD(self):
1013         """
1014         Lines received in I{RPL_MOTDSTART} and I{RPL_MOTD} are delivered to
1015         L{IRCClient.receivedMOTD} when I{RPL_ENDOFMOTD} is received.
1016         """
1017         lines = [
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."]
1021         for L in lines:
1022             self.assertEqual(self.client.calls, [])
1023             self.client.dataReceived(L + '\r\n')
1024
1025         self.assertEqual(
1026             self.client.calls,
1027             [("receivedMOTD", {"motd": ["host.name Message of the Day -", "Welcome to host.name"]})])
1028
1029         # After the motd is delivered, the tracking variable should be
1030         # reset.
1031         self.assertIdentical(self.client.motd, None)
1032
1033
1034     def test_withoutMOTDSTART(self):
1035         """
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.
1039         """
1040         lines = [
1041             ":host.name 372 nickname :- Welcome to host.name",
1042             ":host.name 376 nickname :End of /MOTD command."]
1043
1044         for L in lines:
1045             self.client.dataReceived(L + '\r\n')
1046
1047         self.assertEqual(
1048             self.client.calls,
1049             [("receivedMOTD", {"motd": ["Welcome to host.name"]})])
1050
1051
1052     def _clientTestImpl(self, sender, group, type, msg, func, **kw):
1053         ident = pop(kw, 'ident', 'ident')
1054         host = pop(kw, 'host', 'host')
1055
1056         wholeUser = sender + '!' + ident + '@' + host
1057         message = (":" +
1058                    wholeUser + " " +
1059                    type + " " +
1060                    group + " :" +
1061                    msg + "\r\n")
1062         self.client.dataReceived(message)
1063         self.assertEqual(
1064             self.client.calls,
1065             [(func, kw)])
1066         self.client.calls = []
1067
1068
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",
1075                              channel="#group",
1076                              message=msg)
1077
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",
1083                              message=msg)
1084
1085
1086     def test_getChannelModeParams(self):
1087         """
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.
1091         """
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'])
1095
1096         def removeFeature(name):
1097             name = '-' + 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 = []
1104
1105         # Remove CHANMODES feature, causing getFeature('CHANMODES') to return
1106         # None.
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'])
1111
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, [])
1117
1118         # Restore ISUPPORT features.
1119         self._sendISUPPORT()
1120         self.assertNotIdentical(
1121             self.client.supported.getFeature('PREFIX'), None)
1122
1123
1124     def test_getUserModeParams(self):
1125         """
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.
1129         """
1130         add, remove = map(sorted, self.client.getUserModeParams())
1131         self.assertEqual(add, [])
1132         self.assertEqual(remove, [])
1133
1134
1135     def _sendModeChange(self, msg, args='', target=None):
1136         """
1137         Build a MODE string and send it to the client.
1138         """
1139         if target is None:
1140             target = '#chan'
1141         message = ":Wolf!~wolf@yok.utu.fi MODE %s %s %s\r\n" % (
1142             target, msg, args)
1143         self.client.dataReceived(message)
1144
1145
1146     def _parseModeChange(self, results, target=None):
1147         """
1148         Parse the results, do some test and return the data to check.
1149         """
1150         if target is None:
1151             target = '#chan'
1152
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')])
1159         return results
1160
1161
1162     def _checkModeChange(self, expected, target=None):
1163         """
1164         Compare the expected result with the one returned by the client.
1165         """
1166         result = self._parseModeChange(self.client.calls, target)
1167         self.assertEqual(result, expected)
1168         self.client.calls = []
1169
1170
1171     def test_modeMissingDirection(self):
1172         """
1173         Mode strings that do not begin with a directional character, C{'+'} or
1174         C{'-'}, have C{'+'} automatically prepended.
1175         """
1176         self._sendModeChange('s')
1177         self._checkModeChange([(True, 's', (None,))])
1178
1179
1180     def test_noModeParameters(self):
1181         """
1182         No parameters are passed to L{IRCClient.modeChanged} for modes that
1183         don't take any parameters.
1184         """
1185         self._sendModeChange('-s')
1186         self._checkModeChange([(False, 's', (None,))])
1187         self._sendModeChange('+n')
1188         self._checkModeChange([(True, 'n', (None,))])
1189
1190
1191     def test_oneModeParameter(self):
1192         """
1193         Parameters are passed to L{IRCClient.modeChanged} for modes that take
1194         parameters.
1195         """
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',))])
1200
1201
1202     def test_mixedModes(self):
1203         """
1204         Mixing adding and removing modes that do and don't take parameters
1205         invokes L{IRCClient.modeChanged} with mode characters and parameters
1206         that match up.
1207         """
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))])
1213
1214
1215     def test_tooManyModeParameters(self):
1216         """
1217         Passing an argument to modes that take no parameters results in
1218         L{IRCClient.modeChanged} not being called and an error being logged.
1219         """
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())
1226
1227
1228     def test_tooFewModeParameters(self):
1229         """
1230         Passing no arguments to modes that do take parameters results in
1231         L{IRCClient.modeChange} not being called and an error being logged.
1232         """
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())
1239
1240
1241     def test_userMode(self):
1242         """
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}.
1246         """
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)
1252
1253         def getUserModeParams():
1254             return ['Z', '']
1255
1256         # Introduce our own user mode that takes an argument.
1257         self.patch(self.client, 'getUserModeParams', getUserModeParams)
1258
1259         self._sendModeChange('+Z', 'an_arg', target=target)
1260         self._checkModeChange([(True, 'Z', ('an_arg',))], target=target)
1261
1262
1263     def test_heartbeat(self):
1264         """
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.
1269         """
1270         def _createHeartbeat():
1271             heartbeat = self._originalCreateHeartbeat()
1272             heartbeat.clock = self.clock
1273             return heartbeat
1274
1275         self.clock = task.Clock()
1276         self._originalCreateHeartbeat = self.client._createHeartbeat
1277         self.patch(self.client, '_createHeartbeat', _createHeartbeat)
1278
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')
1283
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')
1288
1289         # When the connection is lost the heartbeat is stopped.
1290         self.transport.loseConnection()
1291         self.client.connectionLost(None)
1292         self.assertEqual(
1293             len(self.clock.getDelayedCalls()), 0)
1294         self.assertIdentical(self.client._heartbeat, None)
1295
1296
1297     def test_heartbeatDisabled(self):
1298         """
1299         If L{irc.IRCClient.heartbeatInterval} is set to C{None} then no
1300         heartbeat is created.
1301         """
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)
1306
1307
1308
1309 class BasicServerFunctionalityTestCase(unittest.TestCase):
1310     def setUp(self):
1311         self.f = StringIOWithoutClosing()
1312         self.t = protocol.FileWrapper(self.f)
1313         self.p = irc.IRC()
1314         self.p.makeConnection(self.t)
1315
1316
1317     def check(self, s):
1318         self.assertEqual(self.f.getvalue(), s)
1319
1320
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")
1324
1325
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")
1329
1330
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")
1334
1335
1336     def testJoin(self):
1337         self.p.join("this-person", "#this-channel")
1338         self.check(":this-person JOIN #this-channel\r\n")
1339
1340
1341     def testPart(self):
1342         self.p.part("this-person", "#that-channel")
1343         self.check(":this-person PART #that-channel\r\n")
1344
1345
1346     def testWhois(self):
1347         """
1348         Verify that a whois by the client receives the right protocol actions
1349         from the server.
1350         """
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)
1366
1367
1368
1369 class DummyClient(irc.IRCClient):
1370     """
1371     A L{twisted.words.protocols.irc.IRCClient} that stores sent lines in a
1372     C{list} rather than transmitting them.
1373     """
1374     def __init__(self):
1375         self.lines = []
1376
1377
1378     def connectionMade(self):
1379         irc.IRCClient.connectionMade(self)
1380         self.lines = []
1381
1382
1383     def _truncateLine(self, line):
1384         """
1385         Truncate an IRC line to the maximum allowed length.
1386         """
1387         return line[:irc.MAX_COMMAND_LENGTH - len(self.delimiter)]
1388
1389
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)
1394
1395
1396     def sendLine(self, m):
1397         self.lines.append(self._truncateLine(m))
1398
1399
1400
1401 class ClientInviteTests(unittest.TestCase):
1402     """
1403     Tests for L{IRCClient.invite}.
1404     """
1405     def setUp(self):
1406         """
1407         Create a L{DummyClient} to call C{invite} on in test methods.
1408         """
1409         self.client = DummyClient()
1410
1411
1412     def test_channelCorrection(self):
1413         """
1414         If the channel name passed to L{IRCClient.invite} does not begin with a
1415         channel prefix character, one is prepended to it.
1416         """
1417         self.client.invite('foo', 'bar')
1418         self.assertEqual(self.client.lines, ['INVITE foo #bar'])
1419
1420
1421     def test_invite(self):
1422         """
1423         L{IRCClient.invite} sends an I{INVITE} message with the specified
1424         username and a channel.
1425         """
1426         self.client.invite('foo', '#bar')
1427         self.assertEqual(self.client.lines, ['INVITE foo #bar'])
1428
1429
1430
1431 class ClientMsgTests(unittest.TestCase):
1432     """
1433     Tests for messages sent with L{twisted.words.protocols.irc.IRCClient}.
1434     """
1435     def setUp(self):
1436         self.client = DummyClient()
1437         self.client.connectionMade()
1438
1439
1440     def test_singleLine(self):
1441         """
1442         A message containing no newlines is sent in a single command.
1443         """
1444         self.client.msg('foo', 'bar')
1445         self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
1446
1447
1448     def test_invalidMaxLength(self):
1449         """
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}.
1452         """
1453         self.assertRaises(ValueError, self.client.msg, 'foo', 'bar', 0)
1454         self.assertRaises(ValueError, self.client.msg, 'foo', 'bar', 3)
1455
1456
1457     def test_multipleLine(self):
1458         """
1459         Messages longer than the C{length} parameter to L{IRCClient.msg} will
1460         be split and sent in multiple commands.
1461         """
1462         maxLen = len('PRIVMSG foo :') + 3 + 2 # 2 for line endings
1463         self.client.msg('foo', 'barbazbo', maxLen)
1464         self.assertEqual(
1465             self.client.lines,
1466             ['PRIVMSG foo :bar',
1467              'PRIVMSG foo :baz',
1468              'PRIVMSG foo :bo'])
1469
1470
1471     def test_sufficientWidth(self):
1472         """
1473         Messages exactly equal in length to the C{length} paramtere to
1474         L{IRCClient.msg} are sent in a single command.
1475         """
1476         msg = 'barbazbo'
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))
1486
1487
1488     def test_newlinesAtStart(self):
1489         """
1490         An LF at the beginning of the message is ignored.
1491         """
1492         self.client.lines = []
1493         self.client.msg('foo', '\nbar')
1494         self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
1495
1496
1497     def test_newlinesAtEnd(self):
1498         """
1499         An LF at the end of the message is ignored.
1500         """
1501         self.client.lines = []
1502         self.client.msg('foo', 'bar\n')
1503         self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
1504
1505
1506     def test_newlinesWithinMessage(self):
1507         """
1508         An LF within a message causes a new line.
1509         """
1510         self.client.lines = []
1511         self.client.msg('foo', 'bar\nbaz')
1512         self.assertEqual(
1513             self.client.lines,
1514             ['PRIVMSG foo :bar',
1515              'PRIVMSG foo :baz'])
1516
1517
1518     def test_consecutiveNewlines(self):
1519         """
1520         Consecutive LFs do not cause a blank line.
1521         """
1522         self.client.lines = []
1523         self.client.msg('foo', 'bar\n\nbaz')
1524         self.assertEqual(
1525             self.client.lines,
1526             ['PRIVMSG foo :bar',
1527              'PRIVMSG foo :baz'])
1528
1529
1530     def assertLongMessageSplitting(self, message, expectedNumCommands,
1531                                    length=None):
1532         """
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.
1536         """
1537         responsePrefix = ':%s!%s@%s ' % (
1538             self.client.nickname,
1539             self.client.realname,
1540             self.client.hostname)
1541
1542         self.client.msg('foo', message, length=length)
1543
1544         privmsg = []
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)
1549
1550         self.assertEqual(len(privmsg), expectedNumCommands)
1551         receivedMessage = ''.join(
1552             message for user, target, message in privmsg)
1553
1554         # Did the long message we sent arrive as intended?
1555         self.assertEqual(message, receivedMessage)
1556
1557
1558     def test_splitLongMessagesWithDefault(self):
1559         """
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.
1565         """
1566         message = 'o' * (irc.MAX_COMMAND_LENGTH - 2)
1567         self.assertLongMessageSplitting(message, 2)
1568
1569
1570     def test_splitLongMessagesWithOverride(self):
1571         """
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.
1576         """
1577         message = 'o' * (irc.MAX_COMMAND_LENGTH - 2)
1578         self.assertLongMessageSplitting(
1579             message, 3, length=irc.MAX_COMMAND_LENGTH / 2)
1580
1581
1582     def test_newlinesBeforeLineBreaking(self):
1583         """
1584         IRCClient breaks on newlines before it breaks long lines.
1585         """
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)
1589
1590         self.client.msg('foo', longline + '\n' + longline)
1591         self.assertEqual(
1592             self.client.lines,
1593             ['PRIVMSG foo :' + longline,
1594              'PRIVMSG foo :' + longline])
1595
1596
1597     def test_lineBreakOnWordBoundaries(self):
1598         """
1599         IRCClient prefers to break long lines at word boundaries.
1600         """
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)
1604
1605         self.client.msg('foo', longline + ' ' + longline)
1606         self.assertEqual(
1607             self.client.lines,
1608             ['PRIVMSG foo :' + longline,
1609              'PRIVMSG foo :' + longline])
1610
1611
1612     def test_splitSanity(self):
1613         """
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
1616         C{''}.
1617         """
1618         # Whiteboxing
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(''))
1623
1624
1625     def test_splitDelimiters(self):
1626         """
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
1630         it.
1631         """
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)
1636
1637
1638     def test_splitValidatesLength(self):
1639         """
1640         L{twisted.words.protocols.irc.split} raises C{ValueError} if given a
1641         length less than or equal to C{0}.
1642         """
1643         self.assertRaises(ValueError, irc.split, "foo", 0)
1644         self.assertRaises(ValueError, irc.split, "foo", -1)
1645
1646
1647     def test_say(self):
1648         """
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.
1651         """
1652         self.client.say("thechannel", "the message")
1653         self.assertEquals(
1654             self.client.lines, ["PRIVMSG #thechannel :the message"])
1655
1656
1657
1658 class ClientTests(TestCase):
1659     """
1660     Tests for the protocol-level behavior of IRCClient methods intended to
1661     be called by application code.
1662     """
1663     def setUp(self):
1664         """
1665         Create and connect a new L{IRCClient} to a new L{StringTransport}.
1666         """
1667         self.transport = StringTransport()
1668         self.protocol = IRCClient()
1669         self.protocol.performLogin = False
1670         self.protocol.makeConnection(self.transport)
1671
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(), "")
1675
1676         self.addCleanup(self.transport.loseConnection)
1677         self.addCleanup(self.protocol.connectionLost, None)
1678
1679
1680     def getLastLine(self, transport):
1681         """
1682         Return the last IRC message in the transport buffer.
1683         """
1684         return transport.value().split('\r\n')[-2]
1685
1686
1687     def test_away(self):
1688         """
1689         L{IRCCLient.away} sends an AWAY command with the specified message.
1690         """
1691         message = "Sorry, I'm not here."
1692         self.protocol.away(message)
1693         expected = [
1694             'AWAY :%s' % (message,),
1695             '',
1696         ]
1697         self.assertEqual(self.transport.value().split('\r\n'), expected)
1698
1699
1700     def test_back(self):
1701         """
1702         L{IRCClient.back} sends an AWAY command with an empty message.
1703         """
1704         self.protocol.back()
1705         expected = [
1706             'AWAY :',
1707             '',
1708         ]
1709         self.assertEqual(self.transport.value().split('\r\n'), expected)
1710
1711
1712     def test_whois(self):
1713         """
1714         L{IRCClient.whois} sends a WHOIS message.
1715         """
1716         self.protocol.whois('alice')
1717         self.assertEqual(
1718             self.transport.value().split('\r\n'),
1719             ['WHOIS alice', ''])
1720
1721
1722     def test_whoisWithServer(self):
1723         """
1724         L{IRCClient.whois} sends a WHOIS message with a server name if a
1725         value is passed for the C{server} parameter.
1726         """
1727         self.protocol.whois('alice', 'example.org')
1728         self.assertEqual(
1729             self.transport.value().split('\r\n'),
1730             ['WHOIS example.org alice', ''])
1731
1732
1733     def test_register(self):
1734         """
1735         L{IRCClient.register} sends NICK and USER commands with the
1736         username, name, hostname, server name, and real name specified.
1737         """
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)
1744         expected = [
1745             'NICK %s' % (username,),
1746             'USER %s %s %s :%s' % (
1747                 username, hostname, servername, self.protocol.realname),
1748             '']
1749         self.assertEqual(self.transport.value().split('\r\n'), expected)
1750
1751
1752     def test_registerWithPassword(self):
1753         """
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
1756         argument.
1757         """
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)
1764         expected = [
1765             'PASS %s' % (self.protocol.password,),
1766             'NICK %s' % (username,),
1767             'USER %s %s %s :%s' % (
1768                 username, hostname, servername, self.protocol.realname),
1769             '']
1770         self.assertEqual(self.transport.value().split('\r\n'), expected)
1771
1772
1773     def test_registerWithTakenNick(self):
1774         """
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
1777         register.
1778         """
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,))
1788
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 + '__',))
1793
1794
1795     def test_overrideAlterCollidedNick(self):
1796         """
1797         L{IRCClient.alterCollidedNick} determines how a nickname is altered upon
1798         collision while a user is trying to change to that nickname.
1799         """
1800         nick = 'foo'
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)
1805         self.assertEqual(
1806             lastLine, 'NICK %s' % (nick + '***',))
1807
1808
1809     def test_nickChange(self):
1810         """
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.
1813         """
1814         oldnick = 'foo'
1815         newnick = 'bar'
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)
1822
1823
1824     def test_erroneousNick(self):
1825         """
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.
1829         """
1830         # Registration case: change illegal nickname to erroneousNickFallback
1831         badnick = 'foo'
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)
1836         self.assertEqual(
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)
1841         self.assertEqual(
1842             self.protocol.nickname, self.protocol.erroneousNickFallback)
1843
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)
1850         self.assertEqual(
1851             lastLine, 'NICK %s' % (badnick,))
1852         self.assertEqual(self.protocol.nickname, oldnick)
1853
1854
1855     def test_describe(self):
1856         """
1857         L{IRCClient.desrcibe} sends a CTCP ACTION message to the target
1858         specified.
1859         """
1860         target = 'foo'
1861         channel = '#bar'
1862         action = 'waves'
1863         self.protocol.describe(target, action)
1864         self.protocol.describe(channel, action)
1865         expected = [
1866             'PRIVMSG %s :\01ACTION %s\01' % (target, action),
1867             'PRIVMSG %s :\01ACTION %s\01' % (channel, action),
1868             '']
1869         self.assertEqual(self.transport.value().split('\r\n'), expected)
1870
1871
1872     def test_noticedDoesntPrivmsg(self):
1873         """
1874         The default implementation of L{IRCClient.noticed} doesn't invoke
1875         C{privmsg()}
1876         """
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!"])
1882
1883
1884
1885 class DccChatFactoryTests(unittest.TestCase):
1886     """
1887     Tests for L{DccChatFactory}
1888     """
1889     def test_buildProtocol(self):
1890         """
1891         An instance of the DccChat protocol is returned, which has the factory
1892         property set to the factory which created it.
1893         """
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)