%PDF- %PDF-
Direktori : /lib/python3/dist-packages/twisted/words/test/ |
Current File : //lib/python3/dist-packages/twisted/words/test/test_irc.py |
# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ Tests for L{twisted.words.protocols.irc}. """ import errno import operator import time from twisted.internet import protocol, task from twisted.python.filepath import FilePath from twisted.test.proto_helpers import StringIOWithoutClosing, StringTransport from twisted.trial.unittest import TestCase from twisted.words.protocols import irc from twisted.words.protocols.irc import IRCClient, attributes as A class IRCTestCase(TestCase): def assertEqualBufferValue(self, buf, val): """ A buffer is always bytes, but sometimes we need to compare it to a utf-8 unicode string @param buf: the buffer @type buf: L{bytes} or L{unicode} or L{list} @param val: the value to compare @type val: L{bytes} or L{unicode} or L{list} """ bufferValue = buf if isinstance(val, str): bufferValue = bufferValue.decode("utf-8") if isinstance(bufferValue, list): if isinstance(val[0], str): bufferValue = [b.decode("utf8") for b in bufferValue] self.assertEqual(bufferValue, val) class ModeParsingTests(IRCTestCase): """ Tests for L{twisted.words.protocols.irc.parseModes}. """ paramModes = ("klb", "b") def test_emptyModes(self): """ Parsing an empty mode string raises L{irc.IRCBadModes}. """ self.assertRaises(irc.IRCBadModes, irc.parseModes, "", []) def test_emptyModeSequence(self): """ Parsing a mode string that contains an empty sequence (either a C{+} or C{-} followed directly by another C{+} or C{-}, or not followed by anything at all) raises L{irc.IRCBadModes}. """ self.assertRaises(irc.IRCBadModes, irc.parseModes, "++k", []) self.assertRaises(irc.IRCBadModes, irc.parseModes, "-+k", []) self.assertRaises(irc.IRCBadModes, irc.parseModes, "+", []) self.assertRaises(irc.IRCBadModes, irc.parseModes, "-", []) def test_malformedModes(self): """ Parsing a mode string that does not start with C{+} or C{-} raises L{irc.IRCBadModes}. """ self.assertRaises(irc.IRCBadModes, irc.parseModes, "foo", []) self.assertRaises(irc.IRCBadModes, irc.parseModes, "%", []) def test_nullModes(self): """ Parsing a mode string that contains no mode characters raises L{irc.IRCBadModes}. """ self.assertRaises(irc.IRCBadModes, irc.parseModes, "+", []) self.assertRaises(irc.IRCBadModes, irc.parseModes, "-", []) def test_singleMode(self): """ Parsing a single mode setting with no parameters results in that mode, with no parameters, in the "added" direction and no modes in the "removed" direction. """ added, removed = irc.parseModes("+s", []) self.assertEqual(added, [("s", None)]) self.assertEqual(removed, []) added, removed = irc.parseModes("-s", []) self.assertEqual(added, []) self.assertEqual(removed, [("s", None)]) def test_singleDirection(self): """ Parsing a single-direction mode setting with multiple modes and no parameters, results in all modes falling into the same direction group. """ added, removed = irc.parseModes("+stn", []) self.assertEqual(added, [("s", None), ("t", None), ("n", None)]) self.assertEqual(removed, []) added, removed = irc.parseModes("-nt", []) self.assertEqual(added, []) self.assertEqual(removed, [("n", None), ("t", None)]) def test_multiDirection(self): """ Parsing a multi-direction mode setting with no parameters. """ added, removed = irc.parseModes("+s-n+ti", []) self.assertEqual(added, [("s", None), ("t", None), ("i", None)]) self.assertEqual(removed, [("n", None)]) def test_consecutiveDirection(self): """ Parsing a multi-direction mode setting containing two consecutive mode sequences with the same direction results in the same result as if there were only one mode sequence in the same direction. """ added, removed = irc.parseModes("+sn+ti", []) self.assertEqual(added, [("s", None), ("n", None), ("t", None), ("i", None)]) self.assertEqual(removed, []) def test_mismatchedParams(self): """ If the number of mode parameters does not match the number of modes expecting parameters, L{irc.IRCBadModes} is raised. """ self.assertRaises(irc.IRCBadModes, irc.parseModes, "+k", [], self.paramModes) self.assertRaises( irc.IRCBadModes, irc.parseModes, "+kl", ["foo", "10", "lulz_extra_param"], self.paramModes, ) def test_parameters(self): """ Modes which require parameters are parsed and paired with their relevant parameter, modes which do not require parameters do not consume any of the parameters. """ added, removed = irc.parseModes( "+klbb", ["somekey", "42", "nick!user@host", "other!*@*"], self.paramModes ) self.assertEqual( added, [ ("k", "somekey"), ("l", "42"), ("b", "nick!user@host"), ("b", "other!*@*"), ], ) self.assertEqual(removed, []) added, removed = irc.parseModes( "-klbb", ["nick!user@host", "other!*@*"], self.paramModes ) self.assertEqual(added, []) self.assertEqual( removed, [("k", None), ("l", None), ("b", "nick!user@host"), ("b", "other!*@*")], ) # Mix a no-argument mode in with argument modes. added, removed = irc.parseModes( "+knbb", ["somekey", "nick!user@host", "other!*@*"], self.paramModes ) self.assertEqual( added, [ ("k", "somekey"), ("n", None), ("b", "nick!user@host"), ("b", "other!*@*"), ], ) self.assertEqual(removed, []) class MiscTests(IRCTestCase): """ Tests for miscellaneous functions. """ def test_foldr(self): """ Apply a function of two arguments cumulatively to the items of a sequence, from right to left, so as to reduce the sequence to a single value. """ self.assertEqual(irc._foldr(operator.sub, 0, [1, 2, 3, 4]), -2) def insertTop(l, x): l.insert(0, x) return l self.assertEqual( irc._foldr(insertTop, [], [[1], [2], [3], [4]]), [[[[[], 4], 3], 2], 1] ) class FormattedTextTests(IRCTestCase): """ Tests for parsing and assembling formatted IRC text. """ def assertAssembledEqually(self, text, expectedFormatted): """ Assert that C{text} is parsed and assembled to the same value as what C{expectedFormatted} is assembled to. This provides a way to ignore meaningless differences in the formatting structure that would be difficult to detect without rendering the structures. """ formatted = irc.parseFormattedText(text) self.assertAssemblesTo(formatted, expectedFormatted) def assertAssemblesTo(self, formatted, expectedFormatted): """ Assert that C{formatted} and C{expectedFormatted} assemble to the same value. """ text = irc.assembleFormattedText(formatted) expectedText = irc.assembleFormattedText(expectedFormatted) self.assertEqual( irc.assembleFormattedText(formatted), expectedText, "%r (%r) is not equivalent to %r (%r)" % (text, formatted, expectedText, expectedFormatted), ) def test_parseEmpty(self): """ An empty string parses to a I{normal} attribute with no text. """ self.assertAssembledEqually("", A.normal) def test_assembleEmpty(self): """ An attribute with no text assembles to the empty string. An attribute whose text is the empty string assembles to two control codes: C{off} and that of the attribute. """ self.assertEqual(irc.assembleFormattedText(A.normal), "") # Attempting to apply an attribute to the empty string should still # produce two control codes. self.assertEqual(irc.assembleFormattedText(A.bold[""]), "\x0f\x02") def test_assembleNormal(self): """ A I{normal} string assembles to a string prefixed with the I{off} control code. """ self.assertEqual(irc.assembleFormattedText(A.normal["hello"]), "\x0fhello") def test_assembleBold(self): """ A I{bold} string assembles to a string prefixed with the I{off} and I{bold} control codes. """ self.assertEqual(irc.assembleFormattedText(A.bold["hello"]), "\x0f\x02hello") def test_assembleUnderline(self): """ An I{underline} string assembles to a string prefixed with the I{off} and I{underline} control codes. """ self.assertEqual( irc.assembleFormattedText(A.underline["hello"]), "\x0f\x1fhello" ) def test_assembleReverseVideo(self): """ A I{reverse video} string assembles to a string prefixed with the I{off} and I{reverse video} control codes. """ self.assertEqual( irc.assembleFormattedText(A.reverseVideo["hello"]), "\x0f\x16hello" ) def test_assembleForegroundColor(self): """ A I{foreground color} string assembles to a string prefixed with the I{off} and I{color} (followed by the relevant foreground color code) control codes. """ self.assertEqual( irc.assembleFormattedText(A.fg.blue["hello"]), "\x0f\x0302hello" ) def test_assembleBackgroundColor(self): """ A I{background color} string assembles to a string prefixed with the I{off} and I{color} (followed by a I{,} to indicate the absence of a foreground color, followed by the relevant background color code) control codes. """ self.assertEqual( irc.assembleFormattedText(A.bg.blue["hello"]), "\x0f\x03,02hello" ) def test_assembleColor(self): """ A I{foreground} and I{background} color string assembles to a string prefixed with the I{off} and I{color} (followed by the relevant foreground color, I{,} and the relevant background color code) control codes. """ self.assertEqual( irc.assembleFormattedText(A.fg.red[A.bg.blue["hello"]]), "\x0f\x0305,02hello", ) def test_assembleNested(self): """ Nested attributes retain the attributes of their parents. """ self.assertEqual( irc.assembleFormattedText(A.bold["hello", A.underline[" world"]]), "\x0f\x02hello\x0f\x02\x1f world", ) self.assertEqual( irc.assembleFormattedText( A.normal[ A.fg.red[A.bg.green["hello"], " world"], A.reverseVideo[" yay"] ] ), "\x0f\x0305,03hello\x0f\x0305 world\x0f\x16 yay", ) def test_parseUnformattedText(self): """ Parsing unformatted text results in text with attributes that constitute a no-op. """ self.assertEqual(irc.parseFormattedText("hello"), A.normal["hello"]) def test_colorFormatting(self): """ Correctly formatted text with colors uses 2 digits to specify foreground and (optionally) background. """ self.assertEqual(irc.parseFormattedText("\x0301yay\x03"), A.fg.black["yay"]) self.assertEqual( irc.parseFormattedText("\x0301,02yay\x03"), A.fg.black[A.bg.blue["yay"]] ) self.assertEqual( irc.parseFormattedText("\x0301yay\x0302yipee\x03"), A.fg.black["yay", A.fg.blue["yipee"]], ) def test_weirdColorFormatting(self): """ Formatted text with colors can use 1 digit for both foreground and background, as long as the text part does not begin with a digit. Foreground and background colors are only processed to a maximum of 2 digits per component, anything else is treated as text. Color sequences must begin with a digit, otherwise processing falls back to unformatted text. """ self.assertAssembledEqually("\x031kinda valid", A.fg.black["kinda valid"]) self.assertAssembledEqually( "\x03999,999kinda valid", A.fg.green["9,999kinda valid"] ) self.assertAssembledEqually( "\x031,2kinda valid", A.fg.black[A.bg.blue["kinda valid"]] ) self.assertAssembledEqually( "\x031,999kinda valid", A.fg.black[A.bg.green["9kinda valid"]] ) self.assertAssembledEqually( "\x031,242 is a special number", A.fg.black[A.bg.yellow["2 is a special number"]], ) self.assertAssembledEqually("\x03,02oops\x03", A.normal[",02oops"]) self.assertAssembledEqually("\x03wrong", A.normal["wrong"]) self.assertAssembledEqually("\x031,hello", A.fg.black["hello"]) self.assertAssembledEqually("\x03\x03", A.normal) def test_clearColorFormatting(self): """ An empty color format specifier clears foreground and background colors. """ self.assertAssembledEqually( "\x0301yay\x03reset", A.normal[A.fg.black["yay"], "reset"] ) self.assertAssembledEqually( "\x0301,02yay\x03reset", A.normal[A.fg.black[A.bg.blue["yay"]], "reset"] ) def test_resetFormatting(self): """ A reset format specifier clears all formatting attributes. """ self.assertAssembledEqually( "\x02\x1fyay\x0freset", A.normal[A.bold[A.underline["yay"]], "reset"] ) self.assertAssembledEqually( "\x0301yay\x0freset", A.normal[A.fg.black["yay"], "reset"] ) self.assertAssembledEqually( "\x0301,02yay\x0freset", A.normal[A.fg.black[A.bg.blue["yay"]], "reset"] ) def test_stripFormatting(self): """ Strip formatting codes from formatted text, leaving only the text parts. """ self.assertEqual( irc.stripFormatting( irc.assembleFormattedText( A.bold[ A.underline[ A.reverseVideo[A.fg.red[A.bg.green["hello"]]], " world" ] ] ) ), "hello world", ) class FormattingStateAttributeTests(IRCTestCase): """ Tests for L{twisted.words.protocols.irc._FormattingState}. """ def test_equality(self): """ L{irc._FormattingState}s must have matching character attribute values (bold, underline, etc) with the same values to be considered equal. """ self.assertEqual(irc._FormattingState(), irc._FormattingState()) self.assertEqual(irc._FormattingState(), irc._FormattingState(off=False)) self.assertEqual( irc._FormattingState( bold=True, underline=True, off=False, reverseVideo=True, foreground=irc._IRC_COLORS["blue"], ), irc._FormattingState( bold=True, underline=True, off=False, reverseVideo=True, foreground=irc._IRC_COLORS["blue"], ), ) self.assertNotEqual( irc._FormattingState(bold=True), irc._FormattingState(bold=False) ) stringSubjects = [ "Hello, this is a nice string with no complications.", "xargs{NUL}might{NUL}like{NUL}this".format(NUL=irc.NUL), "embedded{CR}newline{CR}{NL}FUN{NL}".format(CR=irc.CR, NL=irc.NL), "escape!%(X)s escape!%(M)s %(X)s%(X)sa %(M)s0" % {"X": irc.X_QUOTE, "M": irc.M_QUOTE}, ] class QuotingTests(IRCTestCase): def test_lowquoteSanity(self): """ Testing client-server level quote/dequote. """ for s in stringSubjects: self.assertEqual(s, irc.lowDequote(irc.lowQuote(s))) def test_ctcpquoteSanity(self): """ Testing CTCP message level quote/dequote. """ for s in stringSubjects: self.assertEqual(s, irc.ctcpDequote(irc.ctcpQuote(s))) class Dispatcher(irc._CommandDispatcherMixin): """ A dispatcher that exposes one known command and handles unknown commands. """ prefix = "disp" def disp_working(self, a, b): """ A known command that returns its input. """ return a, b def disp_unknown(self, name, a, b): """ Handle unknown commands by returning their name and inputs. """ return name, a, b class DispatcherTests(IRCTestCase): """ Tests for L{irc._CommandDispatcherMixin}. """ def test_dispatch(self): """ Dispatching a command invokes the correct handler. """ disp = Dispatcher() args = (1, 2) res = disp.dispatch("working", *args) self.assertEqual(res, args) def test_dispatchUnknown(self): """ Dispatching an unknown command invokes the default handler. """ disp = Dispatcher() name = "missing" args = (1, 2) res = disp.dispatch(name, *args) self.assertEqual(res, (name,) + args) def test_dispatchMissingUnknown(self): """ Dispatching an unknown command, when no default handler is present, results in an exception being raised. """ disp = Dispatcher() disp.disp_unknown = None self.assertRaises(irc.UnhandledCommand, disp.dispatch, "bar") class ServerSupportedFeatureTests(IRCTestCase): """ Tests for L{ServerSupportedFeatures} and related functions. """ def test_intOrDefault(self): """ L{_intOrDefault} converts values to C{int} if possible, otherwise returns a default value. """ self.assertEqual(irc._intOrDefault(None), None) self.assertEqual(irc._intOrDefault([]), None) self.assertEqual(irc._intOrDefault(""), None) self.assertEqual(irc._intOrDefault("hello", 5), 5) self.assertEqual(irc._intOrDefault("123"), 123) self.assertEqual(irc._intOrDefault(123), 123) def test_splitParam(self): """ L{ServerSupportedFeatures._splitParam} splits ISUPPORT parameters into key and values. Parameters without a separator are split into a key and a list containing only the empty string. Escaped parameters are unescaped. """ params = [ ("FOO", ("FOO", [""])), ("FOO=", ("FOO", [""])), ("FOO=1", ("FOO", ["1"])), ("FOO=1,2,3", ("FOO", ["1", "2", "3"])), ("FOO=A\\x20B", ("FOO", ["A B"])), ("FOO=\\x5Cx", ("FOO", ["\\x"])), ("FOO=\\", ("FOO", ["\\"])), ("FOO=\\n", ("FOO", ["\\n"])), ] _splitParam = irc.ServerSupportedFeatures._splitParam for param, expected in params: res = _splitParam(param) self.assertEqual(res, expected) self.assertRaises(ValueError, _splitParam, "FOO=\\x") self.assertRaises(ValueError, _splitParam, "FOO=\\xNN") self.assertRaises(ValueError, _splitParam, "FOO=\\xN") self.assertRaises(ValueError, _splitParam, "FOO=\\x20\\x") def test_splitParamArgs(self): """ L{ServerSupportedFeatures._splitParamArgs} splits ISUPPORT parameter arguments into key and value. Arguments without a separator are split into a key and an empty string. """ res = irc.ServerSupportedFeatures._splitParamArgs(["A:1", "B:2", "C:", "D"]) self.assertEqual(res, [("A", "1"), ("B", "2"), ("C", ""), ("D", "")]) def test_splitParamArgsProcessor(self): """ L{ServerSupportedFeatures._splitParamArgs} uses the argument processor passed to convert ISUPPORT argument values to some more suitable form. """ res = irc.ServerSupportedFeatures._splitParamArgs( ["A:1", "B:2", "C"], irc._intOrDefault ) self.assertEqual(res, [("A", 1), ("B", 2), ("C", None)]) def test_parsePrefixParam(self): """ L{ServerSupportedFeatures._parsePrefixParam} parses the ISUPPORT PREFIX parameter into a mapping from modes to prefix symbols, returns L{None} if there is no parseable prefix parameter or raises C{ValueError} if the prefix parameter is malformed. """ _parsePrefixParam = irc.ServerSupportedFeatures._parsePrefixParam self.assertEqual(_parsePrefixParam(""), None) self.assertRaises(ValueError, _parsePrefixParam, "hello") self.assertEqual(_parsePrefixParam("(ov)@+"), {"o": ("@", 0), "v": ("+", 1)}) def test_parseChanModesParam(self): """ L{ServerSupportedFeatures._parseChanModesParam} parses the ISUPPORT CHANMODES parameter into a mapping from mode categories to mode characters. Passing fewer than 4 parameters results in the empty string for the relevant categories. Passing more than 4 parameters raises C{ValueError}. """ _parseChanModesParam = irc.ServerSupportedFeatures._parseChanModesParam self.assertEqual( _parseChanModesParam(["", "", "", ""]), {"addressModes": "", "param": "", "setParam": "", "noParam": ""}, ) self.assertEqual( _parseChanModesParam(["b", "k", "l", "imnpst"]), {"addressModes": "b", "param": "k", "setParam": "l", "noParam": "imnpst"}, ) self.assertEqual( _parseChanModesParam(["b", "k", "l", ""]), {"addressModes": "b", "param": "k", "setParam": "l", "noParam": ""}, ) self.assertRaises(ValueError, _parseChanModesParam, ["a", "b", "c", "d", "e"]) def test_parse(self): """ L{ServerSupportedFeatures.parse} changes the internal state of the instance to reflect the features indicated by the parsed ISUPPORT parameters, including unknown parameters and unsetting previously set parameters. """ supported = irc.ServerSupportedFeatures() supported.parse( ["MODES=4", "CHANLIMIT=#:20,&:10", "INVEX", "EXCEPTS=Z", "UNKNOWN=A,B,C"] ) self.assertEqual(supported.getFeature("MODES"), 4) self.assertEqual(supported.getFeature("CHANLIMIT"), [("#", 20), ("&", 10)]) self.assertEqual(supported.getFeature("INVEX"), "I") self.assertEqual(supported.getFeature("EXCEPTS"), "Z") self.assertEqual(supported.getFeature("UNKNOWN"), ("A", "B", "C")) self.assertTrue(supported.hasFeature("INVEX")) supported.parse(["-INVEX"]) self.assertFalse(supported.hasFeature("INVEX")) # Unsetting a previously unset parameter should not be a problem. supported.parse(["-INVEX"]) def _parse(self, features): """ Parse all specified features according to the ISUPPORT specifications. @type features: C{list} of C{(featureName, value)} @param features: Feature names and values to parse @rtype: L{irc.ServerSupportedFeatures} """ supported = irc.ServerSupportedFeatures() features = ["{}={}".format(name, value or "") for name, value in features] supported.parse(features) return supported def _parseFeature(self, name, value=None): """ Parse a feature, with the given name and value, according to the ISUPPORT specifications and return the parsed value. """ supported = self._parse([(name, value)]) return supported.getFeature(name) def _testIntOrDefaultFeature(self, name, default=None): """ Perform some common tests on a feature known to use L{_intOrDefault}. """ self.assertEqual(self._parseFeature(name, None), default) self.assertEqual(self._parseFeature(name, "notanint"), default) self.assertEqual(self._parseFeature(name, "42"), 42) def _testFeatureDefault(self, name, features=None): """ Features known to have default values are reported as being present by L{irc.ServerSupportedFeatures.hasFeature}, and their value defaults correctly, when they don't appear in an ISUPPORT message. """ default = irc.ServerSupportedFeatures()._features[name] if features is None: features = [("DEFINITELY_NOT", "a_feature")] supported = self._parse(features) self.assertTrue(supported.hasFeature(name)) self.assertEqual(supported.getFeature(name), default) def test_support_CHANMODES(self): """ The CHANMODES ISUPPORT parameter is parsed into a C{dict} giving the four mode categories, C{'addressModes'}, C{'param'}, C{'setParam'}, and C{'noParam'}. """ self._testFeatureDefault("CHANMODES") self._testFeatureDefault("CHANMODES", [("CHANMODES", "b,,lk,")]) self._testFeatureDefault("CHANMODES", [("CHANMODES", "b,,lk,ha,ha")]) self.assertEqual( self._parseFeature("CHANMODES", ",,,"), {"addressModes": "", "param": "", "setParam": "", "noParam": ""}, ) self.assertEqual( self._parseFeature("CHANMODES", ",A,,"), {"addressModes": "", "param": "A", "setParam": "", "noParam": ""}, ) self.assertEqual( self._parseFeature("CHANMODES", "A,Bc,Def,Ghij"), {"addressModes": "A", "param": "Bc", "setParam": "Def", "noParam": "Ghij"}, ) def test_support_IDCHAN(self): """ The IDCHAN support parameter is parsed into a sequence of two-tuples giving channel prefix and ID length pairs. """ self.assertEqual(self._parseFeature("IDCHAN", "!:5"), [("!", "5")]) def test_support_MAXLIST(self): """ The MAXLIST support parameter is parsed into a sequence of two-tuples giving modes and their limits. """ self.assertEqual( self._parseFeature("MAXLIST", "b:25,eI:50"), [("b", 25), ("eI", 50)] ) # A non-integer parameter argument results in None. self.assertEqual( self._parseFeature("MAXLIST", "b:25,eI:50,a:3.1415"), [("b", 25), ("eI", 50), ("a", None)], ) self.assertEqual( self._parseFeature("MAXLIST", "b:25,eI:50,a:notanint"), [("b", 25), ("eI", 50), ("a", None)], ) def test_support_NETWORK(self): """ The NETWORK support parameter is parsed as the network name, as specified by the server. """ self.assertEqual(self._parseFeature("NETWORK", "IRCNet"), "IRCNet") def test_support_SAFELIST(self): """ The SAFELIST support parameter is parsed into a boolean indicating whether the safe "list" command is supported or not. """ self.assertEqual(self._parseFeature("SAFELIST"), True) def test_support_STATUSMSG(self): """ The STATUSMSG support parameter is parsed into a string of channel status that support the exclusive channel notice method. """ self.assertEqual(self._parseFeature("STATUSMSG", "@+"), "@+") def test_support_TARGMAX(self): """ The TARGMAX support parameter is parsed into a dictionary, mapping strings to integers, of the maximum number of targets for a particular command. """ self.assertEqual( self._parseFeature("TARGMAX", "PRIVMSG:4,NOTICE:3"), {"PRIVMSG": 4, "NOTICE": 3}, ) # A non-integer parameter argument results in None. self.assertEqual( self._parseFeature("TARGMAX", "PRIVMSG:4,NOTICE:3,KICK:3.1415"), {"PRIVMSG": 4, "NOTICE": 3, "KICK": None}, ) self.assertEqual( self._parseFeature("TARGMAX", "PRIVMSG:4,NOTICE:3,KICK:notanint"), {"PRIVMSG": 4, "NOTICE": 3, "KICK": None}, ) def test_support_NICKLEN(self): """ The NICKLEN support parameter is parsed into an integer value indicating the maximum length of a nickname the client may use, otherwise, if the parameter is missing or invalid, the default value (as specified by RFC 1459) is used. """ default = irc.ServerSupportedFeatures()._features["NICKLEN"] self._testIntOrDefaultFeature("NICKLEN", default) def test_support_CHANNELLEN(self): """ The CHANNELLEN support parameter is parsed into an integer value indicating the maximum channel name length, otherwise, if the parameter is missing or invalid, the default value (as specified by RFC 1459) is used. """ default = irc.ServerSupportedFeatures()._features["CHANNELLEN"] self._testIntOrDefaultFeature("CHANNELLEN", default) def test_support_CHANTYPES(self): """ The CHANTYPES support parameter is parsed into a tuple of valid channel prefix characters. """ self._testFeatureDefault("CHANTYPES") self.assertEqual(self._parseFeature("CHANTYPES", "#&%"), ("#", "&", "%")) def test_support_KICKLEN(self): """ The KICKLEN support parameter is parsed into an integer value indicating the maximum length of a kick message a client may use. """ self._testIntOrDefaultFeature("KICKLEN") def test_support_PREFIX(self): """ The PREFIX support parameter is parsed into a dictionary mapping modes to two-tuples of status symbol and priority. """ self._testFeatureDefault("PREFIX") self._testFeatureDefault("PREFIX", [("PREFIX", "hello")]) self.assertEqual(self._parseFeature("PREFIX", None), None) self.assertEqual( self._parseFeature("PREFIX", "(ohv)@%+"), {"o": ("@", 0), "h": ("%", 1), "v": ("+", 2)}, ) self.assertEqual( self._parseFeature("PREFIX", "(hov)@%+"), {"o": ("%", 1), "h": ("@", 0), "v": ("+", 2)}, ) def test_support_TOPICLEN(self): """ The TOPICLEN support parameter is parsed into an integer value indicating the maximum length of a topic a client may set. """ self._testIntOrDefaultFeature("TOPICLEN") def test_support_MODES(self): """ The MODES support parameter is parsed into an integer value indicating the maximum number of "variable" modes (defined as being modes from C{addressModes}, C{param} or C{setParam} categories for the C{CHANMODES} ISUPPORT parameter) which may by set on a channel by a single MODE command from a client. """ self._testIntOrDefaultFeature("MODES") def test_support_EXCEPTS(self): """ The EXCEPTS support parameter is parsed into the mode character to be used for "ban exception" modes. If no parameter is specified then the character C{e} is assumed. """ self.assertEqual(self._parseFeature("EXCEPTS", "Z"), "Z") self.assertEqual(self._parseFeature("EXCEPTS"), "e") def test_support_INVEX(self): """ The INVEX support parameter is parsed into the mode character to be used for "invite exception" modes. If no parameter is specified then the character C{I} is assumed. """ self.assertEqual(self._parseFeature("INVEX", "Z"), "Z") self.assertEqual(self._parseFeature("INVEX"), "I") class IRCClientWithoutLogin(irc.IRCClient): performLogin = 0 class CTCPTests(IRCTestCase): """ Tests for L{twisted.words.protocols.irc.IRCClient} CTCP handling. """ def setUp(self): self.file = StringIOWithoutClosing() self.transport = protocol.FileWrapper(self.file) self.client = IRCClientWithoutLogin() self.client.makeConnection(self.transport) self.addCleanup(self.transport.loseConnection) self.addCleanup(self.client.connectionLost, None) def test_ERRMSG(self): """Testing CTCP query ERRMSG. Not because this is this is an especially important case in the field, but it does go through the entire dispatch/decode/encode process. """ errQuery = ( ":nick!guy@over.there PRIVMSG #theChan :" "%(X)cERRMSG t%(X)c%(EOL)s" % {"X": irc.X_DELIM, "EOL": irc.CR + irc.LF} ) errReply = ( "NOTICE nick :%(X)cERRMSG t :" "No error has occurred.%(X)c%(EOL)s" % {"X": irc.X_DELIM, "EOL": irc.CR + irc.LF} ) self.client.dataReceived(errQuery) reply = self.file.getvalue() self.assertEqualBufferValue(reply, errReply) def test_noNumbersVERSION(self): """ If attributes for version information on L{IRCClient} are set to L{None}, the parts of the CTCP VERSION response they correspond to are omitted. """ self.client.versionName = "FrobozzIRC" self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None) versionReply = "NOTICE nick :%(X)cVERSION %(vname)s::" "%(X)c%(EOL)s" % { "X": irc.X_DELIM, "EOL": irc.CR + irc.LF, "vname": self.client.versionName, } reply = self.file.getvalue() self.assertEqualBufferValue(reply, versionReply) def test_fullVERSION(self): """ The response to a CTCP VERSION query includes the version number and environment information, as specified by L{IRCClient.versionNum} and L{IRCClient.versionEnv}. """ self.client.versionName = "FrobozzIRC" self.client.versionNum = "1.2g" self.client.versionEnv = "ZorkOS" self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None) versionReply = ( "NOTICE nick :%(X)cVERSION %(vname)s:%(vnum)s:%(venv)s" "%(X)c%(EOL)s" % { "X": irc.X_DELIM, "EOL": irc.CR + irc.LF, "vname": self.client.versionName, "vnum": self.client.versionNum, "venv": self.client.versionEnv, } ) reply = self.file.getvalue() self.assertEqualBufferValue(reply, versionReply) def test_noDuplicateCTCPDispatch(self): """ Duplicated CTCP messages are ignored and no reply is made. """ def testCTCP(user, channel, data): self.called += 1 self.called = 0 self.client.ctcpQuery_TESTTHIS = testCTCP self.client.irc_PRIVMSG( "foo!bar@baz.quux", ["#chan", "{X}TESTTHIS{X}foo{X}TESTTHIS{X}".format(X=irc.X_DELIM)], ) self.assertEqualBufferValue(self.file.getvalue(), "") self.assertEqual(self.called, 1) def test_noDefaultDispatch(self): """ The fallback handler is invoked for unrecognized CTCP messages. """ def unknownQuery(user, channel, tag, data): self.calledWith = (user, channel, tag, data) self.called += 1 self.called = 0 self.patch(self.client, "ctcpUnknownQuery", unknownQuery) self.client.irc_PRIVMSG( "foo!bar@baz.quux", ["#chan", "{X}NOTREAL{X}".format(X=irc.X_DELIM)] ) self.assertEqualBufferValue(self.file.getvalue(), "") self.assertEqual( self.calledWith, ("foo!bar@baz.quux", "#chan", "NOTREAL", None) ) self.assertEqual(self.called, 1) # The fallback handler is not invoked for duplicate unknown CTCP # messages. self.client.irc_PRIVMSG( "foo!bar@baz.quux", ["#chan", "{X}NOTREAL{X}foo{X}NOTREAL{X}".format(X=irc.X_DELIM)], ) self.assertEqual(self.called, 2) class NoticingClient(IRCClientWithoutLogin): methods = { "created": ("when",), "yourHost": ("info",), "myInfo": ("servername", "version", "umodes", "cmodes"), "luserClient": ("info",), "bounce": ("info",), "isupport": ("options",), "luserChannels": ("channels",), "luserOp": ("ops",), "luserMe": ("info",), "receivedMOTD": ("motd",), "privmsg": ("user", "channel", "message"), "joined": ("channel",), "left": ("channel",), "noticed": ("user", "channel", "message"), "modeChanged": ("user", "channel", "set", "modes", "args"), "pong": ("user", "secs"), "signedOn": (), "kickedFrom": ("channel", "kicker", "message"), "nickChanged": ("nick",), "userJoined": ("user", "channel"), "userLeft": ("user", "channel"), "userKicked": ("user", "channel", "kicker", "message"), "action": ("user", "channel", "data"), "topicUpdated": ("user", "channel", "newTopic"), "userRenamed": ("oldname", "newname"), } def __init__(self, *a, **kw): # It is important that IRCClient.__init__ is not called since # traditionally it did not exist, so it is important that nothing is # initialised there that would prevent subclasses that did not (or # could not) invoke the base implementation. Any protocol # initialisation should happen in connectionMode. self.calls = [] def __getattribute__(self, name): if name.startswith("__") and name.endswith("__"): return super().__getattribute__(name) try: args = super().__getattribute__("methods")[name] except KeyError: return super().__getattribute__(name) else: return self.makeMethod(name, args) def makeMethod(self, fname, args): def method(*a, **kw): if len(a) > len(args): raise TypeError( "TypeError: %s() takes %d arguments " "(%d given)" % (fname, len(args), len(a)) ) for (name, value) in zip(args, a): if name in kw: raise TypeError( "TypeError: %s() got multiple values " "for keyword argument '%s'" % (fname, name) ) else: kw[name] = value if len(kw) != len(args): raise TypeError( "TypeError: %s() takes %d arguments " "(%d given)" % (fname, len(args), len(a)) ) self.calls.append((fname, kw)) return method def pop(dict, key, default): try: value = dict[key] except KeyError: return default else: del dict[key] return value class ClientImplementationTests(IRCTestCase): def setUp(self): self.transport = StringTransport() self.client = NoticingClient() self.client.makeConnection(self.transport) self.addCleanup(self.transport.loseConnection) self.addCleanup(self.client.connectionLost, None) def _serverTestImpl(self, code, msg, func, **kw): host = pop(kw, "host", "server.host") nick = pop(kw, "nick", "nickname") args = pop(kw, "args", "") message = ( ":" + host + " " + code + " " + nick + " " + args + " :" + msg + "\r\n" ) self.client.dataReceived(message) self.assertEqual(self.client.calls, [(func, kw)]) def testYourHost(self): msg = "Your host is some.host[blah.blah/6667], running version server-version-3" self._serverTestImpl("002", msg, "yourHost", info=msg) def testCreated(self): msg = "This server was cobbled together Fri Aug 13 18:00:25 UTC 2004" self._serverTestImpl("003", msg, "created", when=msg) def testMyInfo(self): msg = "server.host server-version abcDEF bcdEHI" self._serverTestImpl( "004", msg, "myInfo", servername="server.host", version="server-version", umodes="abcDEF", cmodes="bcdEHI", ) def testLuserClient(self): msg = "There are 9227 victims and 9542 hiding on 24 servers" self._serverTestImpl("251", msg, "luserClient", info=msg) def _sendISUPPORT(self): args = ( "MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63 " "TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=# " "PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer" ) msg = "are available on this server" self._serverTestImpl( "005", msg, "isupport", args=args, options=[ "MODES=4", "CHANLIMIT=#:20", "NICKLEN=16", "USERLEN=10", "HOSTLEN=63", "TOPICLEN=450", "KICKLEN=450", "CHANNELLEN=30", "KEYLEN=23", "CHANTYPES=#", "PREFIX=(ov)@+", "CASEMAPPING=ascii", "CAPAB", "IRCD=dancer", ], ) def test_ISUPPORT(self): """ The client parses ISUPPORT messages sent by the server and calls L{IRCClient.isupport}. """ self._sendISUPPORT() def testBounce(self): msg = "Try server some.host, port 321" self._serverTestImpl("010", msg, "bounce", info=msg) def testLuserChannels(self): args = "7116" msg = "channels formed" self._serverTestImpl("254", msg, "luserChannels", args=args, channels=int(args)) def testLuserOp(self): args = "34" msg = "flagged staff members" self._serverTestImpl("252", msg, "luserOp", args=args, ops=int(args)) def testLuserMe(self): msg = "I have 1937 clients and 0 servers" self._serverTestImpl("255", msg, "luserMe", info=msg) def test_receivedMOTD(self): """ Lines received in I{RPL_MOTDSTART} and I{RPL_MOTD} are delivered to L{IRCClient.receivedMOTD} when I{RPL_ENDOFMOTD} is received. """ lines = [ ":host.name 375 nickname :- host.name Message of the Day -", ":host.name 372 nickname :- Welcome to host.name", ":host.name 376 nickname :End of /MOTD command.", ] for L in lines: self.assertEqual(self.client.calls, []) self.client.dataReceived(L + "\r\n") self.assertEqual( self.client.calls, [ ( "receivedMOTD", { "motd": [ "host.name Message of the Day -", "Welcome to host.name", ] }, ) ], ) # After the motd is delivered, the tracking variable should be # reset. self.assertIdentical(self.client.motd, None) def test_withoutMOTDSTART(self): """ If L{IRCClient} receives I{RPL_MOTD} and I{RPL_ENDOFMOTD} without receiving I{RPL_MOTDSTART}, L{IRCClient.receivedMOTD} is still called with a list of MOTD lines. """ lines = [ ":host.name 372 nickname :- Welcome to host.name", ":host.name 376 nickname :End of /MOTD command.", ] for L in lines: self.client.dataReceived(L + "\r\n") self.assertEqual( self.client.calls, [("receivedMOTD", {"motd": ["Welcome to host.name"]})] ) def _clientTestImpl(self, sender, group, type, msg, func, **kw): ident = pop(kw, "ident", "ident") host = pop(kw, "host", "host") wholeUser = sender + "!" + ident + "@" + host message = ":" + wholeUser + " " + type + " " + group + " :" + msg + "\r\n" self.client.dataReceived(message) self.assertEqual(self.client.calls, [(func, kw)]) self.client.calls = [] def testPrivmsg(self): msg = "Tooty toot toot." self._clientTestImpl( "sender", "#group", "PRIVMSG", msg, "privmsg", ident="ident", host="host", # Expected results below user="sender!ident@host", channel="#group", message=msg, ) self._clientTestImpl( "sender", "recipient", "PRIVMSG", msg, "privmsg", ident="ident", host="host", # Expected results below user="sender!ident@host", channel="recipient", message=msg, ) def test_getChannelModeParams(self): """ L{IRCClient.getChannelModeParams} uses ISUPPORT information, either given by the server or defaults, to determine which channel modes require arguments when being added or removed. """ add, remove = map(sorted, self.client.getChannelModeParams()) self.assertEqual(add, ["b", "h", "k", "l", "o", "v"]) self.assertEqual(remove, ["b", "h", "o", "v"]) def removeFeature(name): name = "-" + name msg = "are available on this server" self._serverTestImpl("005", msg, "isupport", args=name, options=[name]) self.assertIdentical(self.client.supported.getFeature(name), None) self.client.calls = [] # Remove CHANMODES feature, causing getFeature('CHANMODES') to return # None. removeFeature("CHANMODES") add, remove = map(sorted, self.client.getChannelModeParams()) self.assertEqual(add, ["h", "o", "v"]) self.assertEqual(remove, ["h", "o", "v"]) # Remove PREFIX feature, causing getFeature('PREFIX') to return None. removeFeature("PREFIX") add, remove = map(sorted, self.client.getChannelModeParams()) self.assertEqual(add, []) self.assertEqual(remove, []) # Restore ISUPPORT features. self._sendISUPPORT() self.assertNotIdentical(self.client.supported.getFeature("PREFIX"), None) def test_getUserModeParams(self): """ L{IRCClient.getUserModeParams} returns a list of user modes (modes that the user sets on themself, outside of channel modes) that require parameters when added and removed, respectively. """ add, remove = map(sorted, self.client.getUserModeParams()) self.assertEqual(add, []) self.assertEqual(remove, []) def _sendModeChange(self, msg, args="", target=None): """ Build a MODE string and send it to the client. """ if target is None: target = "#chan" message = f":Wolf!~wolf@yok.utu.fi MODE {target} {msg} {args}\r\n" self.client.dataReceived(message) def _parseModeChange(self, results, target=None): """ Parse the results, do some test and return the data to check. """ if target is None: target = "#chan" for n, result in enumerate(results): method, data = result self.assertEqual(method, "modeChanged") self.assertEqual(data["user"], "Wolf!~wolf@yok.utu.fi") self.assertEqual(data["channel"], target) results[n] = tuple(data[key] for key in ("set", "modes", "args")) return results def _checkModeChange(self, expected, target=None): """ Compare the expected result with the one returned by the client. """ result = self._parseModeChange(self.client.calls, target) self.assertEqual(result, expected) self.client.calls = [] def test_modeMissingDirection(self): """ Mode strings that do not begin with a directional character, C{'+'} or C{'-'}, have C{'+'} automatically prepended. """ self._sendModeChange("s") self._checkModeChange([(True, "s", (None,))]) def test_noModeParameters(self): """ No parameters are passed to L{IRCClient.modeChanged} for modes that don't take any parameters. """ self._sendModeChange("-s") self._checkModeChange([(False, "s", (None,))]) self._sendModeChange("+n") self._checkModeChange([(True, "n", (None,))]) def test_oneModeParameter(self): """ Parameters are passed to L{IRCClient.modeChanged} for modes that take parameters. """ self._sendModeChange("+o", "a_user") self._checkModeChange([(True, "o", ("a_user",))]) self._sendModeChange("-o", "a_user") self._checkModeChange([(False, "o", ("a_user",))]) def test_mixedModes(self): """ Mixing adding and removing modes that do and don't take parameters invokes L{IRCClient.modeChanged} with mode characters and parameters that match up. """ self._sendModeChange("+osv", "a_user another_user") self._checkModeChange([(True, "osv", ("a_user", None, "another_user"))]) self._sendModeChange("+v-os", "a_user another_user") self._checkModeChange( [(True, "v", ("a_user",)), (False, "os", ("another_user", None))] ) def test_tooManyModeParameters(self): """ Passing an argument to modes that take no parameters results in L{IRCClient.modeChanged} not being called and an error being logged. """ self._sendModeChange("+s", "wrong") self._checkModeChange([]) errors = self.flushLoggedErrors(irc.IRCBadModes) self.assertEqual(len(errors), 1) self.assertSubstring("Too many parameters", errors[0].getErrorMessage()) def test_tooFewModeParameters(self): """ Passing no arguments to modes that do take parameters results in L{IRCClient.modeChange} not being called and an error being logged. """ self._sendModeChange("+o") self._checkModeChange([]) errors = self.flushLoggedErrors(irc.IRCBadModes) self.assertEqual(len(errors), 1) self.assertSubstring("Not enough parameters", errors[0].getErrorMessage()) def test_userMode(self): """ A C{MODE} message whose target is our user (the nickname of our user, to be precise), as opposed to a channel, will be parsed according to the modes specified by L{IRCClient.getUserModeParams}. """ target = self.client.nickname # Mode "o" on channels is supposed to take a parameter, but since this # is not a channel this will not cause an exception. self._sendModeChange("+o", target=target) self._checkModeChange([(True, "o", (None,))], target=target) def getUserModeParams(): return ["Z", ""] # Introduce our own user mode that takes an argument. self.patch(self.client, "getUserModeParams", getUserModeParams) self._sendModeChange("+Z", "an_arg", target=target) self._checkModeChange([(True, "Z", ("an_arg",))], target=target) def test_heartbeat(self): """ When the I{RPL_WELCOME} message is received a heartbeat is started that will send a I{PING} message to the IRC server every L{irc.IRCClient.heartbeatInterval} seconds. When the transport is closed the heartbeat looping call is stopped too. """ def _createHeartbeat(): heartbeat = self._originalCreateHeartbeat() heartbeat.clock = self.clock return heartbeat self.clock = task.Clock() self._originalCreateHeartbeat = self.client._createHeartbeat self.patch(self.client, "_createHeartbeat", _createHeartbeat) self.assertIdentical(self.client._heartbeat, None) self.client.irc_RPL_WELCOME("foo", []) self.assertNotIdentical(self.client._heartbeat, None) self.assertEqual(self.client.hostname, "foo") # Pump the clock enough to trigger one LoopingCall. self.assertEqualBufferValue(self.transport.value(), "") self.clock.advance(self.client.heartbeatInterval) self.assertEqualBufferValue(self.transport.value(), "PING foo\r\n") # When the connection is lost the heartbeat is stopped. self.transport.loseConnection() self.client.connectionLost(None) self.assertEqual(len(self.clock.getDelayedCalls()), 0) self.assertIdentical(self.client._heartbeat, None) def test_heartbeatDisabled(self): """ If L{irc.IRCClient.heartbeatInterval} is set to L{None} then no heartbeat is created. """ self.assertIdentical(self.client._heartbeat, None) self.client.heartbeatInterval = None self.client.irc_RPL_WELCOME("foo", []) self.assertIdentical(self.client._heartbeat, None) class BasicServerFunctionalityTests(IRCTestCase): def setUp(self): self.f = StringIOWithoutClosing() self.t = protocol.FileWrapper(self.f) self.p = irc.IRC() self.p.makeConnection(self.t) def check(self, s): """ Make sure that the internal buffer equals a specified value. @param s: the value to compare against buffer @type s: L{bytes} or L{unicode} """ bufferValue = self.f.getvalue() if isinstance(s, str): bufferValue = bufferValue.decode("utf-8") self.assertEqual(bufferValue, s) def test_sendMessage(self): """ Passing a command and parameters to L{IRC.sendMessage} results in a query string that consists of the command and parameters, separated by a space, ending with '\r\n'. """ self.p.sendMessage("CMD", "param1", "param2") self.check("CMD param1 param2\r\n") def test_sendCommand(self): """ Passing a command and parameters to L{IRC.sendCommand} results in a query string that consists of the command and parameters, separated by a space, ending with '\r\n'. The format is described in more detail in U{RFC 1459 <https://tools.ietf.org/html/rfc1459.html#section-2.3>}. """ self.p.sendCommand("CMD", ("param1", "param2")) self.check("CMD param1 param2\r\n") def test_sendUnicodeCommand(self): """ Passing unicode parameters to L{IRC.sendCommand} encodes the parameters in UTF-8. """ self.p.sendCommand("CMD", ("param\u00b9", "param\u00b2")) self.check(b"CMD param\xc2\xb9 param\xc2\xb2\r\n") def test_sendMessageNoCommand(self): """ Passing L{None} as the command to L{IRC.sendMessage} raises a C{ValueError}. """ error = self.assertRaises( ValueError, self.p.sendMessage, None, "param1", "param2" ) self.assertEqual(str(error), "IRC message requires a command.") def test_sendCommandNoCommand(self): """ Passing L{None} as the command to L{IRC.sendCommand} raises a C{ValueError}. """ error = self.assertRaises( ValueError, self.p.sendCommand, None, ("param1", "param2") ) self.assertEqual(error.args[0], "IRC message requires a command.") def test_sendMessageInvalidCommand(self): """ Passing an invalid string command to L{IRC.sendMessage} raises a C{ValueError}. """ error = self.assertRaises( ValueError, self.p.sendMessage, " ", "param1", "param2" ) self.assertEqual( str(error), "Somebody screwed up, 'cuz this doesn't look like a command to " "me: ", ) def test_sendCommandInvalidCommand(self): """ Passing an invalid string command to L{IRC.sendCommand} raises a C{ValueError}. """ error = self.assertRaises( ValueError, self.p.sendCommand, " ", ("param1", "param2") ) self.assertEqual(error.args[0], 'Invalid command: " "') def test_sendCommandWithPrefix(self): """ Passing a command and parameters with a specified prefix to L{IRC.sendCommand} results in a proper query string including the specified line prefix. """ self.p.sendCommand("CMD", ("param1", "param2"), "irc.example.com") self.check(b":irc.example.com CMD param1 param2\r\n") def test_sendCommandWithTags(self): """ Passing a command and parameters with a specified prefix and tags to L{IRC.sendCommand} results in a proper query string including the specified line prefix and appropriate tags syntax. The query string should be output as follows: @tags :prefix COMMAND param1 param2\r\n The tags are a string of IRCv3 tags, preceded by '@'. The rest of the string is as described in test_sendMessage. For more on the message tag format, see U{the IRCv3 specification <https://ircv3.net/specs/core/message-tags-3.2.html>}. """ sendTags = {"aaa": "bbb", "ccc": None, "example.com/ddd": "eee"} expectedTags = (b"aaa=bbb", b"ccc", b"example.com/ddd=eee") self.p.sendCommand("CMD", ("param1", "param2"), "irc.example.com", sendTags) outMsg = self.f.getvalue() outTagStr, outLine = outMsg.split(b" ", 1) # We pull off the leading '@' sign so that the split tags can be # compared with what we expect. outTags = outTagStr[1:].split(b";") self.assertEqual(outLine, b":irc.example.com CMD param1 param2\r\n") self.assertEqual(sorted(expectedTags), sorted(outTags)) def test_sendCommandValidateEmptyTags(self): """ Passing empty tag names to L{IRC.sendCommand} raises a C{ValueError}. """ sendTags = {"aaa": "bbb", "ccc": None, "": ""} error = self.assertRaises( ValueError, self.p.sendCommand, "CMD", ("param1", "param2"), "irc.example.com", sendTags, ) self.assertEqual(error.args[0], "A tag name is required.") def test_sendCommandValidateNoneTags(self): """ Passing None as a tag name to L{IRC.sendCommand} raises a C{ValueError}. """ sendTags = {"aaa": "bbb", "ccc": None, None: "beep"} error = self.assertRaises( ValueError, self.p.sendCommand, "CMD", ("param1", "param2"), "irc.example.com", sendTags, ) self.assertEqual(error.args[0], "A tag name is required.") def test_sendCommandValidateTagsWithSpaces(self): """ Passing a tag name containing spaces to L{IRC.sendCommand} raises a C{ValueError}. """ sendTags = {"aaa bbb": "ccc"} error = self.assertRaises( ValueError, self.p.sendCommand, "CMD", ("param1", "param2"), "irc.example.com", sendTags, ) self.assertEqual(error.args[0], "Tag contains invalid characters.") def test_sendCommandValidateTagsWithInvalidChars(self): """ Passing a tag name containing invalid characters to L{IRC.sendCommand} raises a C{ValueError}. """ sendTags = {"aaa_b^@": "ccc"} error = self.assertRaises( ValueError, self.p.sendCommand, "CMD", ("param1", "param2"), "irc.example.com", sendTags, ) self.assertEqual(error.args[0], "Tag contains invalid characters.") def test_sendCommandValidateTagValueEscaping(self): """ Tags with values containing invalid characters passed to L{IRC.sendCommand} are escaped. """ sendTags = {"aaa": "bbb", "ccc": "test\r\n \\;;"} expectedTags = (b"aaa=bbb", b"ccc=test\\r\\n\\s\\\\\\:\\:") self.p.sendCommand("CMD", ("param1", "param2"), "irc.example.com", sendTags) outMsg = self.f.getvalue() outTagStr, outLine = outMsg.split(b" ", 1) # We pull off the leading '@' sign so that the split tags can be # compared with what we expect. outTags = outTagStr[1:].split(b";") self.assertEqual(sorted(outTags), sorted(expectedTags)) def testPrivmsg(self): self.p.privmsg("this-is-sender", "this-is-recip", "this is message") self.check(":this-is-sender PRIVMSG this-is-recip :this is message\r\n") def testNotice(self): self.p.notice("this-is-sender", "this-is-recip", "this is notice") self.check(":this-is-sender NOTICE this-is-recip :this is notice\r\n") def testAction(self): self.p.action("this-is-sender", "this-is-recip", "this is action") self.check(":this-is-sender ACTION this-is-recip :this is action\r\n") def testJoin(self): self.p.join("this-person", "#this-channel") self.check(":this-person JOIN #this-channel\r\n") def testPart(self): self.p.part("this-person", "#that-channel") self.check(":this-person PART #that-channel\r\n") def testWhois(self): """ Verify that a whois by the client receives the right protocol actions from the server. """ timestamp = int(time.time() - 100) hostname = self.p.hostname req = "requesting-nick" targ = "target-nick" self.p.whois( req, targ, "target", "host.com", "Target User", "irc.host.com", "A fake server", False, 12, timestamp, ["#fakeusers", "#fakemisc"], ) lines = [ ":%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User", ":%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server", ":%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time", ":%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc", ":%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.", "", ] expected = "\r\n".join(lines) % dict( hostname=hostname, timestamp=timestamp, req=req, targ=targ ) self.check(expected) class DummyClient(irc.IRCClient): """ A L{twisted.words.protocols.irc.IRCClient} that stores sent lines in a C{list} rather than transmitting them. """ def __init__(self): self.lines = [] def connectionMade(self): irc.IRCClient.connectionMade(self) self.lines = [] def _truncateLine(self, line): """ Truncate an IRC line to the maximum allowed length. """ return line[: irc.MAX_COMMAND_LENGTH - len(self.delimiter)] def lineReceived(self, line): # Emulate IRC servers throwing away our important data. line = self._truncateLine(line) return irc.IRCClient.lineReceived(self, line) def sendLine(self, m): self.lines.append(self._truncateLine(m)) class ClientInviteTests(IRCTestCase): """ Tests for L{IRCClient.invite}. """ def setUp(self): """ Create a L{DummyClient} to call C{invite} on in test methods. """ self.client = DummyClient() def test_channelCorrection(self): """ If the channel name passed to L{IRCClient.invite} does not begin with a channel prefix character, one is prepended to it. """ self.client.invite("foo", "bar") self.assertEqual(self.client.lines, ["INVITE foo #bar"]) def test_invite(self): """ L{IRCClient.invite} sends an I{INVITE} message with the specified username and a channel. """ self.client.invite("foo", "#bar") self.assertEqual(self.client.lines, ["INVITE foo #bar"]) class ClientMsgTests(IRCTestCase): """ Tests for messages sent with L{twisted.words.protocols.irc.IRCClient}. """ def setUp(self): self.client = DummyClient() self.client.connectionMade() def test_singleLine(self): """ A message containing no newlines is sent in a single command. """ self.client.msg("foo", "bar") self.assertEqual(self.client.lines, ["PRIVMSG foo :bar"]) def test_invalidMaxLength(self): """ Specifying a C{length} value to L{IRCClient.msg} that is too short to contain the protocol command to send a message raises C{ValueError}. """ self.assertRaises(ValueError, self.client.msg, "foo", "bar", 0) self.assertRaises(ValueError, self.client.msg, "foo", "bar", 3) def test_multipleLine(self): """ Messages longer than the C{length} parameter to L{IRCClient.msg} will be split and sent in multiple commands. """ maxLen = len("PRIVMSG foo :") + 3 + 2 # 2 for line endings self.client.msg("foo", "barbazbo", maxLen) self.assertEqual( self.client.lines, ["PRIVMSG foo :bar", "PRIVMSG foo :baz", "PRIVMSG foo :bo"], ) def test_sufficientWidth(self): """ Messages exactly equal in length to the C{length} parameter to L{IRCClient.msg} are sent in a single command. """ msg = "barbazbo" maxLen = len(f"PRIVMSG foo :{msg}") + 2 self.client.msg("foo", msg, maxLen) self.assertEqual(self.client.lines, [f"PRIVMSG foo :{msg}"]) self.client.lines = [] self.client.msg("foo", msg, maxLen - 1) self.assertEqual(2, len(self.client.lines)) self.client.lines = [] self.client.msg("foo", msg, maxLen + 1) self.assertEqual(1, len(self.client.lines)) def test_newlinesAtStart(self): """ An LF at the beginning of the message is ignored. """ self.client.lines = [] self.client.msg("foo", "\nbar") self.assertEqual(self.client.lines, ["PRIVMSG foo :bar"]) def test_newlinesAtEnd(self): """ An LF at the end of the message is ignored. """ self.client.lines = [] self.client.msg("foo", "bar\n") self.assertEqual(self.client.lines, ["PRIVMSG foo :bar"]) def test_newlinesWithinMessage(self): """ An LF within a message causes a new line. """ self.client.lines = [] self.client.msg("foo", "bar\nbaz") self.assertEqual(self.client.lines, ["PRIVMSG foo :bar", "PRIVMSG foo :baz"]) def test_consecutiveNewlines(self): """ Consecutive LFs do not cause a blank line. """ self.client.lines = [] self.client.msg("foo", "bar\n\nbaz") self.assertEqual(self.client.lines, ["PRIVMSG foo :bar", "PRIVMSG foo :baz"]) def assertLongMessageSplitting(self, message, expectedNumCommands, length=None): """ Assert that messages sent by L{IRCClient.msg} are split into an expected number of commands and the original message is transmitted in its entirety over those commands. """ responsePrefix = ":{}!{}@{} ".format( self.client.nickname, self.client.realname, self.client.hostname, ) self.client.msg("foo", message, length=length) privmsg = [] self.patch(self.client, "privmsg", lambda *a: privmsg.append(a)) # Deliver these to IRCClient via the normal mechanisms. for line in self.client.lines: self.client.lineReceived(responsePrefix + line) self.assertEqual(len(privmsg), expectedNumCommands) receivedMessage = "".join(message for user, target, message in privmsg) # Did the long message we sent arrive as intended? self.assertEqual(message, receivedMessage) def test_splitLongMessagesWithDefault(self): """ If a maximum message length is not provided to L{IRCClient.msg} a best-guess effort is made to determine a safe maximum, messages longer than this are split into multiple commands with the intent of delivering long messages without losing data due to message truncation when the server relays them. """ message = "o" * (irc.MAX_COMMAND_LENGTH - 2) self.assertLongMessageSplitting(message, 2) def test_splitLongMessagesWithOverride(self): """ The maximum message length can be specified to L{IRCClient.msg}, messages longer than this are split into multiple commands with the intent of delivering long messages without losing data due to message truncation when the server relays them. """ message = "o" * (irc.MAX_COMMAND_LENGTH - 2) self.assertLongMessageSplitting(message, 3, length=irc.MAX_COMMAND_LENGTH // 2) def test_newlinesBeforeLineBreaking(self): """ IRCClient breaks on newlines before it breaks long lines. """ # Because MAX_COMMAND_LENGTH includes framing characters, this long # line is slightly longer than half the permissible message size. longline = "o" * (irc.MAX_COMMAND_LENGTH // 2) self.client.msg("foo", longline + "\n" + longline) self.assertEqual( self.client.lines, ["PRIVMSG foo :" + longline, "PRIVMSG foo :" + longline] ) def test_lineBreakOnWordBoundaries(self): """ IRCClient prefers to break long lines at word boundaries. """ # Because MAX_COMMAND_LENGTH includes framing characters, this long # line is slightly longer than half the permissible message size. longline = "o" * (irc.MAX_COMMAND_LENGTH // 2) self.client.msg("foo", longline + " " + longline) self.assertEqual( self.client.lines, ["PRIVMSG foo :" + longline, "PRIVMSG foo :" + longline] ) def test_splitSanity(self): """ L{twisted.words.protocols.irc.split} raises C{ValueError} if given a length less than or equal to C{0} and returns C{[]} when splitting C{''}. """ # Whiteboxing self.assertRaises(ValueError, irc.split, "foo", -1) self.assertRaises(ValueError, irc.split, "foo", 0) self.assertEqual([], irc.split("", 1)) self.assertEqual([], irc.split("")) def test_splitDelimiters(self): """ L{twisted.words.protocols.irc.split} skips any delimiter (space or newline) that it finds at the very beginning of the string segment it is operating on. Nothing should be added to the output list because of it. """ r = irc.split("xx yyz", 2) self.assertEqual(["xx", "yy", "z"], r) r = irc.split("xx\nyyz", 2) self.assertEqual(["xx", "yy", "z"], r) def test_splitValidatesLength(self): """ L{twisted.words.protocols.irc.split} raises C{ValueError} if given a length less than or equal to C{0}. """ self.assertRaises(ValueError, irc.split, "foo", 0) self.assertRaises(ValueError, irc.split, "foo", -1) def test_say(self): """ L{IRCClient.say} prepends the channel prefix C{"#"} if necessary and then sends the message to the server for delivery to that channel. """ self.client.say("thechannel", "the message") self.assertEqual(self.client.lines, ["PRIVMSG #thechannel :the message"]) class ClientTests(IRCTestCase): """ Tests for the protocol-level behavior of IRCClient methods intended to be called by application code. """ def setUp(self): """ Create and connect a new L{IRCClient} to a new L{StringTransport}. """ self.transport = StringTransport() self.protocol = IRCClient() self.protocol.performLogin = False self.protocol.makeConnection(self.transport) # Sanity check - we don't want anything to have happened at this # point, since we're not in a test yet. self.assertEqualBufferValue(self.transport.value(), "") self.addCleanup(self.transport.loseConnection) self.addCleanup(self.protocol.connectionLost, None) def getLastLine(self, transport): """ Return the last IRC message in the transport buffer. """ line = transport.value() if bytes != str and isinstance(line, bytes): line = line.decode("utf-8") return line.split("\r\n")[-2] def test_away(self): """ L{IRCClient.away} sends an AWAY command with the specified message. """ message = "Sorry, I'm not here." self.protocol.away(message) expected = [ f"AWAY :{message}", "", ] self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_back(self): """ L{IRCClient.back} sends an AWAY command with an empty message. """ self.protocol.back() expected = [ "AWAY :", "", ] self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_whois(self): """ L{IRCClient.whois} sends a WHOIS message. """ self.protocol.whois("alice") self.assertEqualBufferValue( self.transport.value().split(b"\r\n"), ["WHOIS alice", ""] ) def test_whoisWithServer(self): """ L{IRCClient.whois} sends a WHOIS message with a server name if a value is passed for the C{server} parameter. """ self.protocol.whois("alice", "example.org") self.assertEqualBufferValue( self.transport.value().split(b"\r\n"), ["WHOIS example.org alice", ""] ) def test_register(self): """ L{IRCClient.register} sends NICK and USER commands with the username, name, hostname, server name, and real name specified. """ username = "testuser" hostname = "testhost" servername = "testserver" self.protocol.realname = "testname" self.protocol.password = None self.protocol.register(username, hostname, servername) expected = [ f"NICK {username}", "USER %s %s %s :%s" % (username, hostname, servername, self.protocol.realname), "", ] self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_registerWithPassword(self): """ If the C{password} attribute of L{IRCClient} is not L{None}, the C{register} method also sends a PASS command with it as the argument. """ username = "testuser" hostname = "testhost" servername = "testserver" self.protocol.realname = "testname" self.protocol.password = "testpass" self.protocol.register(username, hostname, servername) expected = [ f"PASS {self.protocol.password}", f"NICK {username}", "USER %s %s %s :%s" % (username, hostname, servername, self.protocol.realname), "", ] self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_registerWithTakenNick(self): """ Verify that the client repeats the L{IRCClient.setNick} method with a new value when presented with an C{ERR_NICKNAMEINUSE} while trying to register. """ username = "testuser" hostname = "testhost" servername = "testserver" self.protocol.realname = "testname" self.protocol.password = "testpass" self.protocol.register(username, hostname, servername) self.protocol.irc_ERR_NICKNAMEINUSE("prefix", ["param"]) lastLine = self.getLastLine(self.transport) self.assertNotEqual(lastLine, f"NICK {username}") # Keep chaining underscores for each collision self.protocol.irc_ERR_NICKNAMEINUSE("prefix", ["param"]) lastLine = self.getLastLine(self.transport) self.assertEqual(lastLine, "NICK {}".format(username + "__")) def test_overrideAlterCollidedNick(self): """ L{IRCClient.alterCollidedNick} determines how a nickname is altered upon collision while a user is trying to change to that nickname. """ nick = "foo" self.protocol.alterCollidedNick = lambda nick: nick + "***" self.protocol.register(nick) self.protocol.irc_ERR_NICKNAMEINUSE("prefix", ["param"]) lastLine = self.getLastLine(self.transport) self.assertEqual(lastLine, "NICK {}".format(nick + "***")) def test_nickChange(self): """ When a NICK command is sent after signon, C{IRCClient.nickname} is set to the new nickname I{after} the server sends an acknowledgement. """ oldnick = "foo" newnick = "bar" self.protocol.register(oldnick) self.protocol.irc_RPL_WELCOME("prefix", ["param"]) self.protocol.setNick(newnick) self.assertEqual(self.protocol.nickname, oldnick) self.protocol.irc_NICK(f"{oldnick}!quux@qux", [newnick]) self.assertEqual(self.protocol.nickname, newnick) def test_erroneousNick(self): """ Trying to register an illegal nickname results in the default legal nickname being set, and trying to change a nickname to an illegal nickname results in the old nickname being kept. """ # Registration case: change illegal nickname to erroneousNickFallback badnick = "foo" self.assertEqual(self.protocol._registered, False) self.protocol.register(badnick) self.protocol.irc_ERR_ERRONEUSNICKNAME("prefix", ["param"]) lastLine = self.getLastLine(self.transport) self.assertEqual(lastLine, f"NICK {self.protocol.erroneousNickFallback}") self.protocol.irc_RPL_WELCOME("prefix", ["param"]) self.assertEqual(self.protocol._registered, True) self.protocol.setNick(self.protocol.erroneousNickFallback) self.assertEqual(self.protocol.nickname, self.protocol.erroneousNickFallback) # Illegal nick change attempt after registration. Fall back to the old # nickname instead of erroneousNickFallback. oldnick = self.protocol.nickname self.protocol.setNick(badnick) self.protocol.irc_ERR_ERRONEUSNICKNAME("prefix", ["param"]) lastLine = self.getLastLine(self.transport) self.assertEqual(lastLine, f"NICK {badnick}") self.assertEqual(self.protocol.nickname, oldnick) def test_describe(self): """ L{IRCClient.desrcibe} sends a CTCP ACTION message to the target specified. """ target = "foo" channel = "#bar" action = "waves" self.protocol.describe(target, action) self.protocol.describe(channel, action) expected = [ f"PRIVMSG {target} :\01ACTION {action}\01", f"PRIVMSG {channel} :\01ACTION {action}\01", "", ] self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_noticedDoesntPrivmsg(self): """ The default implementation of L{IRCClient.noticed} doesn't invoke C{privmsg()} """ def privmsg(user, channel, message): self.fail("privmsg() should not have been called") self.protocol.privmsg = privmsg self.protocol.irc_NOTICE("spam", ["#greasyspooncafe", "I don't want any spam!"]) def test_ping(self): """ L{IRCClient.ping} """ # Ping a user with no message self.protocol.ping("otheruser") self.assertTrue( self.transport.value().startswith(b"PRIVMSG otheruser :\x01PING") ) self.transport.clear() # Ping a user with a message self.protocol.ping("otheruser", "are you there") self.assertEqual( self.transport.value(), b"PRIVMSG otheruser :\x01PING are you there\x01\r\n" ) self.transport.clear() # Create a lot of pings, more than MAX_PINGRING self.protocol._pings = {} for pingNum in range(self.protocol._MAX_PINGRING + 3): self.protocol._pings[("otheruser"), (str(pingNum))] = time.time() + pingNum self.assertEqual(len(self.protocol._pings), self.protocol._MAX_PINGRING + 3) # Ping a user self.protocol.ping("otheruser", "I sent a lot of pings") # The excess pings should have been purged self.assertEqual(len(self.protocol._pings), self.protocol._MAX_PINGRING) self.assertEqual( self.transport.value(), b"PRIVMSG otheruser :\x01PING I sent a lot of pings\x01\r\n", ) class CollectorClient(irc.IRCClient): """ A client that saves in a list the names of the methods that got called. """ def __init__(self, methodsList): """ @param methodsList: list of methods' names that should be replaced. @type methodsList: C{list} """ self.methods = [] self.nickname = "Wolf" for method in methodsList: def fake_method(method=method): """ Collects C{method}s. """ def inner(*args): self.methods.append((method, args)) return inner setattr(self, method, fake_method()) class DccTests(IRCTestCase): """ Tests for C{dcc_*} methods. """ def setUp(self): methods = ["dccDoSend", "dccDoAcceptResume", "dccDoResume", "dccDoChat"] self.user = "Wolf!~wolf@yok.utu.fi" self.channel = "#twisted" self.client = CollectorClient(methods) def test_dccSend(self): """ L{irc.IRCClient.dcc_SEND} invokes L{irc.IRCClient.dccDoSend}. """ self.client.dcc_SEND(self.user, self.channel, "foo.txt 127.0.0.1 1025") self.assertEqual( self.client.methods, [ ( "dccDoSend", ( self.user, "127.0.0.1", 1025, "foo.txt", -1, ["foo.txt", "127.0.0.1", "1025"], ), ) ], ) def test_dccSendNotImplemented(self): """ L{irc.IRCClient.dccDoSend} is raises C{NotImplementedError} """ client = irc.IRCClient() self.assertRaises(NotImplementedError, client.dccSend, "username", None) def test_dccSendMalformedRequest(self): """ L{irc.IRCClient.dcc_SEND} raises L{irc.IRCBadMessage} when it is passed a malformed query string. """ result = self.assertRaises( irc.IRCBadMessage, self.client.dcc_SEND, self.user, self.channel, "foo" ) self.assertEqual(str(result), "malformed DCC SEND request: ['foo']") def test_dccSendIndecipherableAddress(self): """ L{irc.IRCClient.dcc_SEND} raises L{irc.IRCBadMessage} when it is passed a query string that doesn't contain a valid address. """ result = self.assertRaises( irc.IRCBadMessage, self.client.dcc_SEND, self.user, self.channel, "foo.txt #23 sd@d", ) self.assertEqual(str(result), "Indecipherable address '#23'") def test_dccSendIndecipherablePort(self): """ L{irc.IRCClient.dcc_SEND} raises L{irc.IRCBadMessage} when it is passed a query string that doesn't contain a valid port number. """ result = self.assertRaises( irc.IRCBadMessage, self.client.dcc_SEND, self.user, self.channel, "foo.txt 127.0.0.1 sd@d", ) self.assertEqual(str(result), "Indecipherable port 'sd@d'") def test_dccAccept(self): """ L{irc.IRCClient.dcc_ACCEPT} invokes L{irc.IRCClient.dccDoAcceptResume}. """ self.client.dcc_ACCEPT(self.user, self.channel, "foo.txt 1025 2") self.assertEqual( self.client.methods, [("dccDoAcceptResume", (self.user, "foo.txt", 1025, 2))], ) def test_dccAcceptMalformedRequest(self): """ L{irc.IRCClient.dcc_ACCEPT} raises L{irc.IRCBadMessage} when it is passed a malformed query string. """ result = self.assertRaises( irc.IRCBadMessage, self.client.dcc_ACCEPT, self.user, self.channel, "foo" ) self.assertEqual(str(result), "malformed DCC SEND ACCEPT request: ['foo']") def test_dccResume(self): """ L{irc.IRCClient.dcc_RESUME} invokes L{irc.IRCClient.dccDoResume}. """ self.client.dcc_RESUME(self.user, self.channel, "foo.txt 1025 2") self.assertEqual( self.client.methods, [("dccDoResume", (self.user, "foo.txt", 1025, 2))] ) def test_dccResumeMalformedRequest(self): """ L{irc.IRCClient.dcc_RESUME} raises L{irc.IRCBadMessage} when it is passed a malformed query string. """ result = self.assertRaises( irc.IRCBadMessage, self.client.dcc_RESUME, self.user, self.channel, "foo" ) self.assertEqual(str(result), "malformed DCC SEND RESUME request: ['foo']") def test_dccChat(self): """ L{irc.IRCClient.dcc_CHAT} invokes L{irc.IRCClient.dccDoChat}. """ self.client.dcc_CHAT(self.user, self.channel, "foo.txt 127.0.0.1 1025") self.assertEqual( self.client.methods, [ ( "dccDoChat", ( self.user, self.channel, "127.0.0.1", 1025, ["foo.txt", "127.0.0.1", "1025"], ), ) ], ) def test_dccChatMalformedRequest(self): """ L{irc.IRCClient.dcc_CHAT} raises L{irc.IRCBadMessage} when it is passed a malformed query string. """ result = self.assertRaises( irc.IRCBadMessage, self.client.dcc_CHAT, self.user, self.channel, "foo" ) self.assertEqual(str(result), "malformed DCC CHAT request: ['foo']") def test_dccChatIndecipherablePort(self): """ L{irc.IRCClient.dcc_CHAT} raises L{irc.IRCBadMessage} when it is passed a query string that doesn't contain a valid port number. """ result = self.assertRaises( irc.IRCBadMessage, self.client.dcc_CHAT, self.user, self.channel, "foo.txt 127.0.0.1 sd@d", ) self.assertEqual(str(result), "Indecipherable port 'sd@d'") class ServerToClientTests(IRCTestCase): """ Tests for the C{irc_*} methods sent from the server to the client. """ def setUp(self): self.user = "Wolf!~wolf@yok.utu.fi" self.channel = "#twisted" methods = [ "joined", "userJoined", "left", "userLeft", "userQuit", "noticed", "kickedFrom", "userKicked", "topicUpdated", ] self.client = CollectorClient(methods) def test_irc_JOIN(self): """ L{IRCClient.joined} is called when I join a channel; L{IRCClient.userJoined} is called when someone else joins. """ self.client.irc_JOIN(self.user, [self.channel]) self.client.irc_JOIN("Svadilfari!~svadi@yok.utu.fi", ["#python"]) self.assertEqual( self.client.methods, [("joined", (self.channel,)), ("userJoined", ("Svadilfari", "#python"))], ) def test_irc_PART(self): """ L{IRCClient.left} is called when I part the channel; L{IRCClient.userLeft} is called when someone else parts. """ self.client.irc_PART(self.user, [self.channel]) self.client.irc_PART("Svadilfari!~svadi@yok.utu.fi", ["#python"]) self.assertEqual( self.client.methods, [("left", (self.channel,)), ("userLeft", ("Svadilfari", "#python"))], ) def test_irc_QUIT(self): """ L{IRCClient.userQuit} is called whenever someone quits the channel (myself included). """ self.client.irc_QUIT("Svadilfari!~svadi@yok.utu.fi", ["Adios."]) self.client.irc_QUIT(self.user, ["Farewell."]) self.assertEqual( self.client.methods, [ ("userQuit", ("Svadilfari", "Adios.")), ("userQuit", ("Wolf", "Farewell.")), ], ) def test_irc_NOTICE(self): """ L{IRCClient.noticed} is called when a notice is received. """ msg = "%(X)cextended%(X)cdata1%(X)cextended%(X)cdata2%(X)c%(EOL)s" % { "X": irc.X_DELIM, "EOL": irc.CR + irc.LF, } self.client.irc_NOTICE(self.user, [self.channel, msg]) self.assertEqual( self.client.methods, [("noticed", (self.user, "#twisted", "data1 data2"))] ) def test_irc_KICK(self): """ L{IRCClient.kickedFrom} is called when I get kicked from the channel; L{IRCClient.userKicked} is called when someone else gets kicked. """ # Fight! self.client.irc_KICK( "Svadilfari!~svadi@yok.utu.fi", ["#python", "WOLF", "shoryuken!"] ) self.client.irc_KICK(self.user, [self.channel, "Svadilfari", "hadouken!"]) self.assertEqual( self.client.methods, [ ("kickedFrom", ("#python", "Svadilfari", "shoryuken!")), ("userKicked", ("Svadilfari", self.channel, "Wolf", "hadouken!")), ], ) def test_irc_TOPIC(self): """ L{IRCClient.topicUpdated} is called when someone sets the topic. """ self.client.irc_TOPIC(self.user, [self.channel, "new topic is new"]) self.assertEqual( self.client.methods, [("topicUpdated", ("Wolf", self.channel, "new topic is new"))], ) def test_irc_RPL_TOPIC(self): """ L{IRCClient.topicUpdated} is called when the topic is initially reported. """ self.client.irc_RPL_TOPIC(self.user, ["?", self.channel, "new topic is new"]) self.assertEqual( self.client.methods, [("topicUpdated", ("Wolf", self.channel, "new topic is new"))], ) def test_irc_RPL_NOTOPIC(self): """ L{IRCClient.topicUpdated} is called when the topic is removed. """ self.client.irc_RPL_NOTOPIC(self.user, ["?", self.channel]) self.assertEqual( self.client.methods, [("topicUpdated", ("Wolf", self.channel, ""))] ) class CTCPQueryTests(IRCTestCase): """ Tests for the C{ctcpQuery_*} methods. """ def setUp(self): self.user = "Wolf!~wolf@yok.utu.fi" self.channel = "#twisted" self.client = CollectorClient(["ctcpMakeReply"]) def test_ctcpQuery_PING(self): """ L{IRCClient.ctcpQuery_PING} calls L{IRCClient.ctcpMakeReply} with the correct args. """ self.client.ctcpQuery_PING(self.user, self.channel, "data") self.assertEqual( self.client.methods, [("ctcpMakeReply", ("Wolf", [("PING", "data")]))] ) def test_ctcpQuery_FINGER(self): """ L{IRCClient.ctcpQuery_FINGER} calls L{IRCClient.ctcpMakeReply} with the correct args. """ self.client.fingerReply = "reply" self.client.ctcpQuery_FINGER(self.user, self.channel, "data") self.assertEqual( self.client.methods, [("ctcpMakeReply", ("Wolf", [("FINGER", "reply")]))] ) def test_ctcpQuery_SOURCE(self): """ L{IRCClient.ctcpQuery_SOURCE} calls L{IRCClient.ctcpMakeReply} with the correct args. """ self.client.sourceURL = "url" self.client.ctcpQuery_SOURCE(self.user, self.channel, "data") self.assertEqual( self.client.methods, [("ctcpMakeReply", ("Wolf", [("SOURCE", "url"), ("SOURCE", None)]))], ) def test_ctcpQuery_USERINFO(self): """ L{IRCClient.ctcpQuery_USERINFO} calls L{IRCClient.ctcpMakeReply} with the correct args. """ self.client.userinfo = "info" self.client.ctcpQuery_USERINFO(self.user, self.channel, "data") self.assertEqual( self.client.methods, [("ctcpMakeReply", ("Wolf", [("USERINFO", "info")]))] ) def test_ctcpQuery_CLIENTINFO(self): """ L{IRCClient.ctcpQuery_CLIENTINFO} calls L{IRCClient.ctcpMakeReply} with the correct args. """ self.client.ctcpQuery_CLIENTINFO(self.user, self.channel, "") self.client.ctcpQuery_CLIENTINFO(self.user, self.channel, "PING PONG") info = ( "ACTION CLIENTINFO DCC ERRMSG FINGER PING SOURCE TIME " "USERINFO VERSION" ) self.assertEqual( self.client.methods, [ ("ctcpMakeReply", ("Wolf", [("CLIENTINFO", info)])), ("ctcpMakeReply", ("Wolf", [("CLIENTINFO", None)])), ], ) def test_ctcpQuery_TIME(self): """ L{IRCClient.ctcpQuery_TIME} calls L{IRCClient.ctcpMakeReply} with the correct args. """ self.client.ctcpQuery_TIME(self.user, self.channel, "data") self.assertEqual(self.client.methods[0][1][0], "Wolf") def test_ctcpQuery_DCC(self): """ L{IRCClient.ctcpQuery_DCC} calls L{IRCClient.ctcpMakeReply} with the correct args. """ self.client.ctcpQuery_DCC(self.user, self.channel, "data") self.assertEqual( self.client.methods, [ ( "ctcpMakeReply", ("Wolf", [("ERRMSG", "DCC data :Unknown DCC type 'DATA'")]), ) ], ) class DccChatFactoryTests(IRCTestCase): """ Tests for L{DccChatFactory}. """ def test_buildProtocol(self): """ An instance of the L{irc.DccChat} protocol is returned, which has the factory property set to the factory which created it. """ queryData = ("fromUser", None, None) factory = irc.DccChatFactory(None, queryData) protocol = factory.buildProtocol("127.0.0.1") self.assertIsInstance(protocol, irc.DccChat) self.assertEqual(protocol.factory, factory) class DccDescribeTests(IRCTestCase): """ Tests for L{dccDescribe}. """ def test_address(self): """ L{irc.dccDescribe} supports long IP addresses. """ result = irc.dccDescribe("CHAT arg 3232235522 6666") self.assertEqual(result, "CHAT for host 192.168.0.2, port 6666") class DccFileReceiveTests(IRCTestCase): """ Tests for L{DccFileReceive}. """ def makeConnectedDccFileReceive(self, filename, resumeOffset=0, overwrite=None): """ Factory helper that returns a L{DccFileReceive} instance for a specific test case. @param filename: Path to the local file where received data is stored. @type filename: L{str} @param resumeOffset: An integer representing the amount of bytes from where the transfer of data should be resumed. @type resumeOffset: L{int} @param overwrite: A boolean specifying whether the file to write to should be overwritten by calling L{DccFileReceive.set_overwrite} or not. @type overwrite: L{bool} @return: An instance of L{DccFileReceive}. @rtype: L{DccFileReceive} """ protocol = irc.DccFileReceive(filename, resumeOffset=resumeOffset) if overwrite: protocol.set_overwrite(True) transport = StringTransport() protocol.makeConnection(transport) return protocol def allDataReceivedForProtocol(self, protocol, data): """ Arrange the protocol so that it received all data. @param protocol: The protocol which will receive the data. @type: L{DccFileReceive} @param data: The received data. @type data: L{bytest} """ protocol.dataReceived(data) protocol.connectionLost(None) def test_resumeFromResumeOffset(self): """ If given a resumeOffset argument, L{DccFileReceive} will attempt to resume from that number of bytes if the file exists. """ fp = FilePath(self.mktemp()) fp.setContent(b"Twisted is awesome!") protocol = self.makeConnectedDccFileReceive(fp.path, resumeOffset=11) self.allDataReceivedForProtocol(protocol, b"amazing!") self.assertEqual(fp.getContent(), b"Twisted is amazing!") def test_resumeFromResumeOffsetInTheMiddleOfAlreadyWrittenData(self): """ When resuming from an offset somewhere in the middle of the file, for example, if there are 50 bytes in a file, and L{DccFileReceive} is given a resumeOffset of 25, and after that 15 more bytes are written to the file, then the resultant file should have just 40 bytes of data. """ fp = FilePath(self.mktemp()) fp.setContent(b"Twisted is amazing!") protocol = self.makeConnectedDccFileReceive(fp.path, resumeOffset=11) self.allDataReceivedForProtocol(protocol, b"cool!") self.assertEqual(fp.getContent(), b"Twisted is cool!") def test_setOverwrite(self): """ When local file already exists it can be overwritten using the L{DccFileReceive.set_overwrite} method. """ fp = FilePath(self.mktemp()) fp.setContent(b"I love contributing to Twisted!") protocol = self.makeConnectedDccFileReceive(fp.path, overwrite=True) self.allDataReceivedForProtocol(protocol, b"Twisted rocks!") self.assertEqual(fp.getContent(), b"Twisted rocks!") def test_fileDoesNotExist(self): """ If the file does not already exist, then L{DccFileReceive} will create one and write the data to it. """ fp = FilePath(self.mktemp()) protocol = self.makeConnectedDccFileReceive(fp.path) self.allDataReceivedForProtocol(protocol, b"I <3 Twisted") self.assertEqual(fp.getContent(), b"I <3 Twisted") def test_resumeWhenFileDoesNotExist(self): """ If given a resumeOffset to resume writing to a file that does not exist, L{DccFileReceive} will raise L{OSError}. """ fp = FilePath(self.mktemp()) error = self.assertRaises( OSError, self.makeConnectedDccFileReceive, fp.path, resumeOffset=1 ) self.assertEqual(errno.ENOENT, error.errno) def test_fileAlreadyExistsNoOverwrite(self): """ If the file already exists and overwrite action was not asked, L{OSError} is raised. """ fp = FilePath(self.mktemp()) fp.touch() self.assertRaises(OSError, self.makeConnectedDccFileReceive, fp.path) def test_failToOpenLocalFile(self): """ L{IOError} is raised when failing to open the requested path. """ fp = FilePath(self.mktemp()).child("child-with-no-existing-parent") self.assertRaises(IOError, self.makeConnectedDccFileReceive, fp.path)