# -*- coding: utf8 -*- ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2010-2021, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from supybot.test import * import supybot.conf as conf import supybot.utils as utils import supybot.ircmsgs as ircmsgs import supybot.utils.minisix as minisix import supybot.callbacks as callbacks tokenize = callbacks.tokenize class TokenizerTestCase(SupyTestCase): def testEmpty(self): self.assertEqual(tokenize(''), []) def testNullCharacter(self): self.assertEqual(tokenize(utils.str.dqrepr('\0')), ['\0']) def testSingleDQInDQString(self): self.assertEqual(tokenize('"\\""'), ['"']) def testDQsWithBackslash(self): self.assertEqual(tokenize('"\\\\"'), ["\\"]) def testDoubleQuotes(self): self.assertEqual(tokenize('"\\"foo\\""'), ['"foo"']) def testSingleWord(self): self.assertEqual(tokenize('foo'), ['foo']) def testMultipleSimpleWords(self): words = 'one two three four five six seven eight'.split() for i in range(len(words)): self.assertEqual(tokenize(' '.join(words[:i])), words[:i]) def testSingleQuotesNotQuotes(self): self.assertEqual(tokenize("it's"), ["it's"]) def testQuotedWords(self): self.assertEqual(tokenize('"foo bar"'), ['foo bar']) self.assertEqual(tokenize('""'), ['']) self.assertEqual(tokenize('foo "" bar'), ['foo', '', 'bar']) self.assertEqual(tokenize('foo "bar baz" quux'), ['foo', 'bar baz', 'quux']) _testUnicode = """ def testUnicode(self): self.assertEqual(tokenize(u'好'), [u'好']) self.assertEqual(tokenize(u'"好"'), [u'好'])""" if minisix.PY3: _testUnicode = _testUnicode.replace("u'", "'") exec(_testUnicode) def testNesting(self): self.assertEqual(tokenize('[]'), [[]]) self.assertEqual(tokenize('[foo]'), [['foo']]) self.assertEqual(tokenize('[ foo ]'), [['foo']]) self.assertEqual(tokenize('foo [bar]'), ['foo', ['bar']]) self.assertEqual(tokenize('foo bar [baz quux]'), ['foo', 'bar', ['baz', 'quux']]) try: orig = conf.supybot.commands.nested() conf.supybot.commands.nested.setValue(False) self.assertEqual(tokenize('[]'), ['[]']) self.assertEqual(tokenize('[foo]'), ['[foo]']) self.assertEqual(tokenize('foo [bar]'), ['foo', '[bar]']) self.assertEqual(tokenize('foo bar [baz quux]'), ['foo', 'bar', '[baz', 'quux]']) finally: conf.supybot.commands.nested.setValue(orig) def testError(self): self.assertRaises(SyntaxError, tokenize, '[foo') #] self.assertRaises(SyntaxError, tokenize, '"foo') #" def testPipe(self): try: conf.supybot.commands.nested.pipeSyntax.setValue(True) self.assertRaises(SyntaxError, tokenize, '| foo') self.assertRaises(SyntaxError, tokenize, 'foo ||bar') self.assertRaises(SyntaxError, tokenize, 'bar |') self.assertEqual(tokenize('foo|bar'), ['bar', ['foo']]) self.assertEqual(tokenize('foo | bar'), ['bar', ['foo']]) self.assertEqual(tokenize('foo | bar | baz'), ['baz', ['bar',['foo']]]) self.assertEqual(tokenize('foo bar | baz'), ['baz', ['foo', 'bar']]) self.assertEqual(tokenize('foo | bar baz'), ['bar', 'baz', ['foo']]) self.assertEqual(tokenize('foo bar | baz quux'), ['baz', 'quux', ['foo', 'bar']]) finally: conf.supybot.commands.nested.pipeSyntax.setValue(False) self.assertEqual(tokenize('foo|bar'), ['foo|bar']) self.assertEqual(tokenize('foo | bar'), ['foo', '|', 'bar']) self.assertEqual(tokenize('foo | bar | baz'), ['foo', '|', 'bar', '|', 'baz']) self.assertEqual(tokenize('foo bar | baz'), ['foo', 'bar', '|', 'baz']) def testQuoteConfiguration(self): f = callbacks.tokenize self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('"[foo]"'), ['[foo]']) try: original = conf.supybot.commands.quotes() conf.supybot.commands.quotes.setValue('`') self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('`[foo]`'), ['[foo]']) conf.supybot.commands.quotes.setValue('\'') self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('\'[foo]\''), ['[foo]']) conf.supybot.commands.quotes.setValue('`\'') self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('`[foo]`'), ['[foo]']) self.assertEqual(f('[foo]'), [['foo']]) self.assertEqual(f('\'[foo]\''), ['[foo]']) finally: conf.supybot.commands.quotes.setValue(original) def testBold(self): s = '\x02foo\x02' self.assertEqual(tokenize(s), [s]) s = s[:-1] + '\x0f' self.assertEqual(tokenize(s), [s]) def testColor(self): s = '\x032,3foo\x03' self.assertEqual(tokenize(s), [s]) s = s[:-1] + '\x0f' self.assertEqual(tokenize(s), [s]) class FunctionsTestCase(SupyTestCase): def testCanonicalName(self): self.assertEqual('foo', callbacks.canonicalName('foo')) self.assertEqual('foobar', callbacks.canonicalName('foo-bar')) self.assertEqual('foobar', callbacks.canonicalName('foo_bar')) self.assertEqual('foobar', callbacks.canonicalName('FOO-bar')) self.assertEqual('foobar', callbacks.canonicalName('FOOBAR')) self.assertEqual('foobar', callbacks.canonicalName('foo___bar')) self.assertEqual('foobar', callbacks.canonicalName('_f_o_o-b_a_r')) # The following seems to be a hack for the Karma plugin; I'm not # entirely sure that it's completely necessary anymore. self.assertEqual('foobar--', callbacks.canonicalName('foobar--')) def testAddressed(self): irc = getTestIrc() oldprefixchars = str(conf.supybot.reply.whenAddressedBy.chars) nick = irc.nick conf.supybot.reply.whenAddressedBy.chars.set('~!@') inChannel = ['~foo', '@foo', '!foo', '%s: foo' % nick, '%s foo' % nick, '%s: foo' % nick.capitalize(), '%s: foo' % nick.upper()] inChannel = [ircmsgs.privmsg('#foo', s) for s in inChannel] badmsg = ircmsgs.privmsg('#foo', '%s:foo' % nick) self.assertFalse(callbacks.addressed(irc, badmsg)) badmsg = ircmsgs.privmsg('#foo', '%s^: foo' % nick) self.assertFalse(callbacks.addressed(irc, badmsg)) for msg in inChannel: self.assertEqual('foo', callbacks.addressed(irc, msg), msg) msg = ircmsgs.privmsg(nick, 'foo') irc._tagMsg(msg) self.assertEqual('foo', callbacks.addressed(irc, msg)) conf.supybot.reply.whenAddressedBy.chars.set(oldprefixchars) msg = ircmsgs.privmsg('#foo', '%s::::: bar' % nick) self.assertEqual('bar', callbacks.addressed(irc, msg)) msg = ircmsgs.privmsg('#foo', '%s: foo' % nick.upper()) self.assertEqual('foo', callbacks.addressed(irc, msg)) badmsg = ircmsgs.privmsg('#foo', '%s`: foo' % nick) self.assertFalse(callbacks.addressed(irc, badmsg)) def testAddressedLegacy(self): """Checks callbacks.addressed still accepts the 'nick' argument instead of 'irc'.""" irc = getTestIrc() oldprefixchars = str(conf.supybot.reply.whenAddressedBy.chars) nick = 'supybot' conf.supybot.reply.whenAddressedBy.chars.set('~!@') inChannel = ['~foo', '@foo', '!foo', '%s: foo' % nick, '%s foo' % nick, '%s: foo' % nick.capitalize(), '%s: foo' % nick.upper()] inChannel = [ircmsgs.privmsg('#foo', s) for s in inChannel] badmsg = ircmsgs.privmsg('#foo', '%s:foo' % nick) with self.assertWarnsRegex(DeprecationWarning, 'Irc object instead'): self.assertFalse(callbacks.addressed(nick, badmsg)) badmsg = ircmsgs.privmsg('#foo', '%s^: foo' % nick) with self.assertWarnsRegex(DeprecationWarning, 'Irc object instead'): self.assertFalse(callbacks.addressed(nick, badmsg)) for msg in inChannel: with self.assertWarns(DeprecationWarning): self.assertEqual('foo', callbacks.addressed(nick, msg), msg) msg = ircmsgs.privmsg(nick, 'foo') irc._tagMsg(msg) with self.assertWarns(DeprecationWarning): self.assertEqual('foo', callbacks.addressed(nick, msg)) conf.supybot.reply.whenAddressedBy.chars.set(oldprefixchars) msg = ircmsgs.privmsg('#foo', '%s::::: bar' % nick) with self.assertWarns(DeprecationWarning): self.assertEqual('bar', callbacks.addressed(nick, msg)) msg = ircmsgs.privmsg('#foo', '%s: foo' % nick.upper()) with self.assertWarns(DeprecationWarning): self.assertEqual('foo', callbacks.addressed(nick, msg)) badmsg = ircmsgs.privmsg('#foo', '%s`: foo' % nick) with self.assertWarns(DeprecationWarning): self.assertFalse(callbacks.addressed(nick, badmsg)) def testAddressedReplyWhenNotAddressed(self): msg1 = ircmsgs.privmsg('#foo', '@bar') msg2 = ircmsgs.privmsg('#foo', 'bar') self.assertEqual(callbacks.addressed('blah', msg1), 'bar') self.assertEqual(callbacks.addressed('blah', msg2), '') try: original = conf.supybot.reply.whenNotAddressed() conf.supybot.reply.whenNotAddressed.setValue(True) # need to recreate the msg objects since the old ones have already # been tagged msg1 = ircmsgs.privmsg('#foo', '@bar') msg2 = ircmsgs.privmsg('#foo', 'bar') self.assertEqual(callbacks.addressed('blah', msg1), 'bar') self.assertEqual(callbacks.addressed('blah', msg2), 'bar') finally: conf.supybot.reply.whenNotAddressed.setValue(original) def testAddressedWithMultipleNicks(self): irc = getTestIrc() msg = ircmsgs.privmsg('#foo', 'bar: baz') irc._tagMsg(msg) self.assertEqual(callbacks.addressed('bar', msg), 'baz') # need to recreate the msg objects since the old ones have already # been tagged msg = ircmsgs.privmsg('#foo', 'bar: baz') irc._tagMsg(msg) self.assertEqual(callbacks.addressed('biff', msg, nicks=['bar']), 'baz') def testAddressedWithNickAtEnd(self): irc = getTestIrc() msg = ircmsgs.privmsg('#foo', 'baz, bar') irc._tagMsg(msg) self.assertEqual(callbacks.addressed('bar', msg, whenAddressedByNickAtEnd=True), 'baz') # Test that it still works with trailing whitespace: msg = ircmsgs.privmsg('#foo', 'baz, bar \t') self.assertEqual(callbacks.addressed('bar', msg, whenAddressedByNickAtEnd=True), 'baz') def testAddressedPrefixCharsTakePrecedenceOverNickAtEnd(self): irc = getTestIrc() msg = ircmsgs.privmsg('#foo', '@echo foo') irc._tagMsg(msg) self.assertEqual(callbacks.addressed('foo', msg, whenAddressedByNickAtEnd=True, prefixChars='@'), 'echo foo') def testReply(self): irc = getTestIrc() prefix = 'foo!bar@baz' channelMsg = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix) nonChannelMsg = ircmsgs.privmsg('supybot', 'bar baz', prefix=prefix) irc._tagMsg(channelMsg) irc._tagMsg(nonChannelMsg) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks._makeReply(irc, channelMsg, 'foo', private=True)) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks._makeReply(irc, nonChannelMsg, 'foo')) self.assertEqual(ircmsgs.privmsg(channelMsg.args[0], '%s: foo' % channelMsg.nick), callbacks._makeReply(irc, channelMsg, 'foo')) self.assertEqual(ircmsgs.privmsg(channelMsg.args[0], 'foo'), callbacks._makeReply(irc, channelMsg, 'foo', prefixNick=False)) self.assertEqual(ircmsgs.notice(nonChannelMsg.nick, 'foo'), callbacks._makeReply(irc, channelMsg, 'foo', notice=True, private=True)) def testReplyStatusmsg(self): irc = getTestIrc() prefix = 'foo!bar@baz' msg = ircmsgs.privmsg('+#foo', 'bar baz', prefix=prefix) irc._tagMsg(msg) # No statusmsg set, so understood as being a private message, so # private reply self.assertEqual(ircmsgs.notice(msg.nick, 'foo'), callbacks._makeReply(irc, msg, 'foo')) irc.state.supported['statusmsg'] = '+' msg = ircmsgs.privmsg('+#foo', 'bar baz', prefix=prefix) irc._tagMsg(msg) self.assertEqual(ircmsgs.privmsg('+#foo', '%s: foo' % msg.nick), callbacks._makeReply(irc, msg, 'foo')) def testReplyTo(self): irc = getTestIrc() prefix = 'foo!bar@baz' msg = ircmsgs.privmsg('#foo', 'bar baz', prefix=prefix) irc._tagMsg(msg) self.assertEqual(callbacks._makeReply(irc, msg, 'blah', to='blah'), ircmsgs.privmsg('#foo', 'blah: blah')) self.assertEqual(callbacks._makeReply(irc, msg, 'blah', to='blah', private=True), ircmsgs.notice('blah', 'blah')) def testTokenize(self): self.assertEqual(callbacks.tokenize(''), []) self.assertEqual(callbacks.tokenize('foo'), ['foo']) self.assertEqual(callbacks.tokenize('foo'), ['foo']) self.assertEqual(callbacks.tokenize('bar [baz]'), ['bar', ['baz']]) class AmbiguityTestCase(PluginTestCase): plugins = ('Misc',) # Something so it doesn't complain. class Foo(callbacks.Plugin): def bar(self, irc, msg, args): irc.reply('foo.bar') class Bar(callbacks.Plugin): def bar(self, irc, msg, args): irc.reply('bar.bar') def testAmbiguityWithCommandSameNameAsPlugin(self): self.irc.addCallback(self.Foo(self.irc)) self.assertResponse('bar', 'foo.bar') self.irc.addCallback(self.Bar(self.irc)) self.assertResponse('bar', 'bar.bar') class ProperStringificationOfReplyArgs(PluginTestCase): plugins = ('Misc',) # Same as above. class NonString(callbacks.Plugin): def int(self, irc, msg, args): irc.reply(1) class ExpectsString(callbacks.Plugin): def lower(self, irc, msg, args): irc.reply(args[0].lower()) def test(self): self.irc.addCallback(self.NonString(self.irc)) self.irc.addCallback(self.ExpectsString(self.irc)) self.assertResponse('expectsstring lower [nonstring int]', '1') ## class PrivmsgTestCaseWithKarma(ChannelPluginTestCase): ## plugins = ('Utilities', 'Misc', 'Web', 'Karma', 'String') ## conf.allowEval = True ## timeout = 2 ## def testSecondInvalidCommandRespondsWithThreadedInvalidCommands(self): ## try: ## orig = conf.supybot.plugins.Karma.response() ## conf.supybot.plugins.Karma.response.setValue(True) ## self.assertNotRegexp('echo [foo++] [foo++]', 'not a valid') ## _ = self.irc.takeMsg() ## finally: ## conf.supybot.plugins.Karma.response.setValue(orig) class PrivmsgTestCase(ChannelPluginTestCase): plugins = ('Utilities', 'Misc', 'Web', 'String') conf.allowEval = True timeout = 2 def testEmptySquareBrackets(self): self.assertError('echo []') ## def testHelpNoNameError(self): ## # This will raise a NameError if some dynamic scoping isn't working ## self.assertNotError('load Http') ## self.assertHelp('extension') def testMaximumNestingDepth(self): original = conf.supybot.commands.nested.maximum() try: conf.supybot.commands.nested.maximum.setValue(3) self.assertResponse('echo foo', 'foo') self.assertResponse('echo [echo foo]', 'foo') self.assertResponse('echo [echo [echo foo]]', 'foo') self.assertResponse('echo [echo [echo [echo foo]]]', 'foo') self.assertError('echo [echo [echo [echo [echo foo]]]]') finally: conf.supybot.commands.nested.maximum.setValue(original) def testSimpleReply(self): self.assertResponse("eval irc.reply('foo')", 'foo') def testSimpleReplyAction(self): self.assertResponse("eval irc.reply('foo', action=True)", '\x01ACTION foo\x01') def testReplyWithNickPrefix(self): self.feedMsg('@len foo') m = self.irc.takeMsg() self.assertIsNotNone(m, 'm: %r' % m) self.assertTrue(m.args[1].startswith(self.nick)) try: original = conf.supybot.reply.withNickPrefix() conf.supybot.reply.withNickPrefix.setValue(False) self.feedMsg('@len foobar') m = self.irc.takeMsg() self.assertIsNotNone(m) self.assertFalse(m.args[1].startswith(self.nick)) finally: conf.supybot.reply.withNickPrefix.setValue(original) def testErrorPrivateKwarg(self): try: original = conf.supybot.reply.error.inPrivate() conf.supybot.reply.error.inPrivate.setValue(False) m = self.getMsg("eval irc.error('foo', private=True)") self.assertTrue(m, 'No message returned.') self.assertFalse(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.error.inPrivate.setValue(original) def testErrorNoArgumentIsSilent(self): self.assertResponse('eval irc.error()', 'None') def testErrorWithNotice(self): try: original = conf.supybot.reply.error.withNotice() conf.supybot.reply.error.withNotice.setValue(True) m = self.getMsg("eval irc.error('foo')") self.assertTrue(m, 'No message returned.') self.assertEqual(m.command, 'NOTICE') finally: conf.supybot.reply.error.withNotice.setValue(original) def testErrorReplyPrivate(self): try: original = str(conf.supybot.reply.error.inPrivate) conf.supybot.reply.error.inPrivate.set('False') # If this doesn't raise an error, we've got a problem, so the next # two assertions shouldn't run. So we first check that what we # expect to error actually does so we don't go on a wild goose # chase because our command never errored in the first place :) s = 're s/foo/bar baz' # will error; should be "re s/foo/bar/ baz" self.assertError(s) m = self.getMsg(s) self.assertTrue(ircutils.isChannel(m.args[0])) conf.supybot.reply.error.inPrivate.set('True') m = self.getMsg(s) self.assertFalse(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.error.inPrivate.set(original) # Now for stuff not based on the plugins. class First(callbacks.Plugin): def firstcmd(self, irc, msg, args): """First""" irc.reply('foo') class Second(callbacks.Plugin): def secondcmd(self, irc, msg, args): """Second""" irc.reply('bar') class FirstRepeat(callbacks.Plugin): def firstcmd(self, irc, msg, args): """FirstRepeat""" irc.reply('baz') class Third(callbacks.Plugin): def third(self, irc, msg, args): """Third""" irc.reply(' '.join(args)) def tearDown(self): if hasattr(self.First, 'first'): del self.First.first if hasattr(self.Second, 'second'): del self.Second.second if hasattr(self.FirstRepeat, 'firstrepeat'): del self.FirstRepeat.firstrepeat ChannelPluginTestCase.tearDown(self) def testDispatching(self): self.irc.addCallback(self.First(self.irc)) self.irc.addCallback(self.Second(self.irc)) self.assertResponse('firstcmd', 'foo') self.assertResponse('secondcmd', 'bar') self.assertResponse('first firstcmd', 'foo') self.assertResponse('second secondcmd', 'bar') self.assertRegexp('first first firstcmd', 'there is no command named "first" in it') def testAmbiguousError(self): self.irc.addCallback(self.First(self.irc)) self.assertNotError('firstcmd') self.irc.addCallback(self.FirstRepeat(self.irc)) self.assertError('firstcmd') self.assertError('firstcmd [firstcmd]') self.assertNotRegexp('firstcmd', '(foo.*baz|baz.*foo)') self.assertResponse('first firstcmd', 'foo') self.assertResponse('firstrepeat firstcmd', 'baz') def testAmbiguousHelpError(self): self.irc.addCallback(self.First(self.irc)) self.irc.addCallback(self.FirstRepeat(self.irc)) self.assertError('help first') def testHelpDispatching(self): self.irc.addCallback(self.First(self.irc)) self.assertHelp('help firstcmd') self.assertHelp('help first firstcmd') self.irc.addCallback(self.FirstRepeat(self.irc)) self.assertError('help firstcmd') self.assertRegexp('help first firstcmd', 'First', 0) # no re.I flag. self.assertRegexp('help firstrepeat firstcmd', 'FirstRepeat', 0) def testReplyInstant(self): self.assertNoResponse(' ') self.assertResponse( "eval 'foo '*300", "'" + "foo " * 110 + " \x02(2 more messages)\x02") self.assertNoResponse(" ", timeout=0.1) with conf.supybot.reply.mores.instant.context(2): self.assertResponse( "eval 'foo '*300", "'" + "foo " * 110) self.assertResponse( " ", "foo " * 111 + "\x02(1 more message)\x02") self.assertNoResponse(" ", timeout=0.1) with conf.supybot.reply.mores.instant.context(3): self.assertResponse( "eval 'foo '*300", "'" + "foo " * 110) self.assertResponse( " ", "foo " * 110 + "foo") self.assertResponse( " ", " " + "foo " * 79 + "'") self.assertNoResponse(" ", timeout=0.1) def testReplyPrivate(self): # Send from a very long nick, which should be taken into account when # computing the reply overhead. self.assertResponse( "eval irc.reply('foo '*300, private=True)", "foo " * 39 + "\x02(7 more messages)\x02", frm='foo'*100 + '!bar@baz') def testClientTagReply(self): self.irc.addCallback(self.First(self.irc)) # no CAP, no msgid, no experimentalExtensions -> no +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick))) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) # CAP and msgid, not no experimentalExtensions -> no +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) with conf.supybot.protocols.irc.experimentalExtensions.context(True): # no CAP, but msgid and experimentalExtensions -> no +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) # msgid and experimentalExtensions, but no CAP -> no +reply # (note that in theory it's impossible to receive msgid without # the CAP, but the +reply spec explicitly requires to check it) self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) try: self.irc.state.capabilities_ack.add('message-tags') # no msgid, but CAP and experimentalExtensions -> no +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick))) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick))) # all of CAP, msgid, experimentalExtensions -> yes +reply self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick), server_tags={'+draft/reply': 'foobar'})) finally: self.irc.state.capabilities_ack.remove('message-tags') def testClientTagReplyChannel(self): self.irc.addCallback(self.First(self.irc)) try: conf.supybot.protocols.irc.experimentalExtensions.setValue(True) self.irc.state.capabilities_ack.add('message-tags') # Reply in channel to channel message -> +draft/channel-context # is absent self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='PRIVMSG', args=('#foo', '%s: foo' % self.nick), server_tags={'+draft/reply': 'foobar'})) # Reply in private to channel message -> +draft/channel-context # is present with conf.supybot.reply.inPrivate.context(True): self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=('#foo', '%s: firstcmd' % self.nick), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='NOTICE', args=(self.nick, 'foo'), server_tags={'+draft/reply': 'foobar', '+draft/channel-context': '#foo'})) # Reply in private to private message -> +draft/channel-context # is absent self.irc.feedMsg(ircmsgs.IrcMsg( command='PRIVMSG', prefix=self.prefix, args=(self.nick, 'firstcmd'), server_tags={'msgid': 'foobar'})) msg = self.irc.takeMsg() self.assertEqual(msg, ircmsgs.IrcMsg( command='NOTICE', args=(self.nick, 'foo'), server_tags={'+draft/reply': 'foobar'})) finally: conf.supybot.protocols.irc.experimentalExtensions.setValue(False) self.irc.state.capabilities_ack.remove('message-tags') class TwoRepliesFirstAction(callbacks.Plugin): def testactionreply(self, irc, msg, args): irc.reply('foo', action=True) irc.reply('bar') # We're going to check that this isn't an action. def doNotice(self, irc, msg): irc.reply('foo', action=True) irc.reply('bar') # We're going to check that this isn't an action. def testNotActionSecondReply(self): self.irc.addCallback(self.TwoRepliesFirstAction(self.irc)) self.assertAction('testactionreply', 'foo') m = self.getMsg(' ') self.assertFalse(m.args[1].startswith('\x01ACTION')) def testNotActionSecondReplyNotCommand(self): """Same as testNotActionSecondReply, but tests ReplyIrcProxy instead of NestedCommandsIrcProxy.""" self.irc.addCallback(self.TwoRepliesFirstAction(self.irc)) self.irc.feedMsg(ircmsgs.notice(self.channel, 'test action reply', prefix=self.prefix)) self.assertAction(' ', 'foo') m = self.getMsg(' ') self.assertFalse(m.args[1].startswith('\x01ACTION')) def testEmptyNest(self): try: conf.supybot.reply.whenNotCommand.set('True') self.assertError('echo []') conf.supybot.reply.whenNotCommand.set('False') self.assertResponse('echo []', '[]') finally: conf.supybot.reply.whenNotCommand.set('False') def testDispatcherHelp(self): self.assertNotRegexp('help first', r'\(dispatcher') self.assertNotRegexp('help first', r'%s') def testDefaultCommand(self): self.irc.addCallback(self.First(self.irc)) self.irc.addCallback(self.Third(self.irc)) self.assertError('first blah') self.assertResponse('third foo bar baz', 'foo bar baz') def testSyntaxErrorNotEscaping(self): self.assertError('load [foo') self.assertError('load foo]') def testNoEscapingAttributeErrorFromTokenizeWithFirstElementList(self): self.assertError('[plugin list] list') class InvalidCommand(callbacks.Plugin): def invalidCommand(self, irc, msg, tokens): irc.reply('foo') def testInvalidCommandOneReplyOnly(self): try: original = str(conf.supybot.reply.whenNotCommand) conf.supybot.reply.whenNotCommand.set('True') self.assertRegexp('asdfjkl', 'not a valid command') self.irc.addCallback(self.InvalidCommand(self.irc)) self.assertResponse('asdfjkl', 'foo') self.assertNoResponse(' ', 2) finally: conf.supybot.reply.whenNotCommand.set(original) class BadInvalidCommand(callbacks.Plugin): def invalidCommand(self, irc, msg, tokens): s = 'This shouldn\'t keep Misc.invalidCommand from being called' raise Exception(s) def testBadInvalidCommandDoesNotKillAll(self): try: original = str(conf.supybot.reply.whenNotCommand) conf.supybot.reply.whenNotCommand.set('True') self.irc.addCallback(self.BadInvalidCommand(self.irc)) self.assertRegexp('asdfjkl', 'not a valid command', expectException=True) finally: conf.supybot.reply.whenNotCommand.set(original) class MultilinePrivmsgTestCase(ChannelPluginTestCase): plugins = ('Utilities', 'Misc', 'Web', 'String') conf.allowEval = True def setUp(self): super().setUp() # Enable multiline self.irc.state.capabilities_ack.add('batch') self.irc.state.capabilities_ack.add('draft/multiline') self.irc.state.capabilities_ls['draft/multiline'] = 'max-bytes=4096' # Enable msgid and +draft/reply self.irc.state.capabilities_ack.add('message-tags') conf.supybot.protocols.irc.experimentalExtensions.setValue(True) def tearDown(self): conf.supybot.protocols.irc.experimentalExtensions.setValue(False) conf.supybot.reply.mores.instant.setValue(1) super().tearDown() def testReplyInstantSingle(self): self.assertIsNone(self.irc.takeMsg()) # Single message as reply, no batch self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix)) m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110 + " \x02(2 more messages)\x02"))) self.assertIsNone(self.irc.takeMsg()) def testReplyInstantBatchPartial(self): """response is shared as a batch + (1 more message)""" self.assertIsNone(self.irc.takeMsg()) conf.supybot.reply.mores.instant.setValue(2) self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix)) # First message opens the batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110), server_tags={'batch': batch_name})) # Third message, last PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "foo " * 111 + "\x02(1 more message)\x02"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # Last message, closes the batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) def testReplyInstantBatchFull(self): """response is entirely instant""" self.assertIsNone(self.irc.takeMsg()) conf.supybot.reply.mores.instant.setValue(3) self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix)) # First message opens the batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110), server_tags={'batch': batch_name})) # Third message, a PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "foo " * 110 + "foo"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # Fourth message, last PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, " " + "foo " * 79 + "'"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # Last message, closes the batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) def testReplyInstantBatchFullMaxBytes(self): """response is entirely instant, but does not fit in a single batch""" self.irc.state.capabilities_ls['draft/multiline'] = 'max-bytes=900' self.assertIsNone(self.irc.takeMsg()) conf.supybot.reply.mores.instant.setValue(3) self.irc.feedMsg(ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix)) # First message opens the first batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110), server_tags={'batch': batch_name})) # Third message, a PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "foo " * 110 + "foo"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # closes the first batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) # opens the second batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel))) # last PRIVMSG (and also the first of its batch) m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, " " + "foo " * 79 + "'"), server_tags={'batch': batch_name})) # Last message, closes the second batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) def testReplyInstantBatchTags(self): """check a message's tags are 'lifted' to the initial BATCH command.""" self.assertIsNone(self.irc.takeMsg()) conf.supybot.reply.mores.instant.setValue(2) m = ircmsgs.privmsg( self.channel, "@eval 'foo '*300", prefix=self.prefix) m.server_tags['msgid'] = 'initialmsgid' self.irc.feedMsg(m) # First message opens the batch m = self.irc.takeMsg() self.assertEqual(m.command, 'BATCH', m) batch_name = m.args[0][1:] self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=('+' + batch_name, 'draft/multiline', self.channel), server_tags={'+draft/reply': 'initialmsgid'})) # Second message, first PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "test: '" + "foo " * 110), server_tags={'batch': batch_name})) # Third message, last PRIVMSG m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='PRIVMSG', args=(self.channel, "foo " * 111 + "\x02(1 more message)\x02"), server_tags={'batch': batch_name, 'draft/multiline-concat': None})) # Last message, closes the batch m = self.irc.takeMsg() self.assertEqual( m, ircmsgs.IrcMsg(command='BATCH', args=( '-' + batch_name,))) class PluginRegexpTestCase(ChannelPluginTestCase): plugins = () class PCAR(callbacks.PluginRegexp): regexps = ("test", "test2") def test(self, irc, msg, args): "" raise callbacks.ArgumentError def test2(self, irc, msg, args): "" irc.reply("hello") def setUp(self): super().setUp() self.irc.addCallback(self.PCAR(self.irc)) def testNoEscapingArgumentError(self): self.assertResponse('test', 'test ') def testReply(self): self.irc.feedMsg(ircmsgs.IrcMsg( prefix=self.prefix, command='PRIVMSG', args=(self.channel, 'foo baz'))) self.assertResponse(' ', 'hello') def testIgnoreChathistory(self): self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('+123', 'chathistory', self.channel))) self.irc.feedMsg(ircmsgs.IrcMsg( server_tags={'batch': '123'}, prefix=self.prefix, command='PRIVMSG', args=(self.channel, 'foo baz'))) self.irc.feedMsg(ircmsgs.IrcMsg( command='BATCH', args=('-123',))) self.assertNoResponse(' ') class RichReplyMethodsTestCase(PluginTestCase): plugins = ('Config',) class NoCapability(callbacks.Plugin): def error(self, irc, msg, args): irc.errorNoCapability('admin') def testErrorNoCapability(self): self.irc.addCallback(self.NoCapability(self.irc)) self.assertRegexp('error', 'You don\'t have the admin capability') self.assertNotError('config capabilities.private admin') self.assertRegexp('error', 'Error: You\'re missing some capability') self.assertNotError('config capabilities.private ""') class SourceNestedPluginTestCase(PluginTestCase): plugins = ('Utilities',) class E(callbacks.Plugin): def f(self, irc, msg, args): """takes no arguments F """ irc.reply('f') def empty(self, irc, msg, args): pass class g(callbacks.Commands): def h(self, irc, msg, args): """takes no arguments H """ irc.reply('h') class i(callbacks.Commands): def j(self, irc, msg, args): """takes no arguments J """ irc.reply('j') class same(callbacks.Commands): def same(self, irc, msg, args): """takes no arguments same """ irc.reply('same') def test(self): cb = self.E(self.irc) self.irc.addCallback(cb) self.assertEqual(cb.getCommand(['f']), ['f']) self.assertEqual(cb.getCommand(['same']), ['same']) self.assertEqual(cb.getCommand(['e', 'f']), ['e', 'f']) self.assertEqual(cb.getCommand(['e', 'g', 'h']), ['e', 'g', 'h']) self.assertEqual(cb.getCommand(['e', 'g', 'i', 'j']), ['e', 'g', 'i', 'j']) self.assertResponse('e f', 'f') self.assertResponse('e same', 'same') self.assertResponse('e g h', 'h') self.assertResponse('e g i j', 'j') self.assertHelp('help f') self.assertHelp('help empty') self.assertHelp('help same') self.assertHelp('help e g h') self.assertHelp('help e g i j') self.assertRegexp('list e', 'f, g h, g i j, and same') def testCommandSameNameAsNestedPlugin(self): cb = self.E(self.irc) self.irc.addCallback(cb) self.assertResponse('e f', 'f') # Just to make sure it was loaded. self.assertEqual(cb.getCommand(['e', 'same']), ['e', 'same']) self.assertResponse('e same', 'same') class WithPrivateNoticeTestCase(ChannelPluginTestCase): plugins = ('Utilities',) class WithPrivateNotice(callbacks.Plugin): def normal(self, irc, msg, args): irc.reply('should be with private notice') def explicit(self, irc, msg, args): irc.reply('should not be with private notice', private=False, notice=False) def implicit(self, irc, msg, args): irc.reply('should be with notice due to private', private=True) def test(self): self.irc.addCallback(self.WithPrivateNotice(self.irc)) # Check normal behavior. m = self.assertNotError('normal') self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) m = self.assertNotError('explicit') self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) # Check abnormal behavior. originalInPrivate = conf.supybot.reply.inPrivate() originalWithNotice = conf.supybot.reply.withNotice() try: conf.supybot.reply.inPrivate.setValue(True) conf.supybot.reply.withNotice.setValue(True) m = self.assertNotError('normal') self.assertEqual(m.command, 'NOTICE') self.assertFalse(ircutils.isChannel(m.args[0])) m = self.assertNotError('explicit') self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.inPrivate.setValue(originalInPrivate) conf.supybot.reply.withNotice.setValue(originalWithNotice) orig = conf.supybot.reply.withNoticeWhenPrivate() try: conf.supybot.reply.withNoticeWhenPrivate.setValue(True) m = self.assertNotError('implicit') self.assertEqual(m.command, 'NOTICE') self.assertFalse(ircutils.isChannel(m.args[0])) m = self.assertNotError('normal') self.assertNotEqual(m.command, 'NOTICE') self.assertTrue(ircutils.isChannel(m.args[0])) finally: conf.supybot.reply.withNoticeWhenPrivate.setValue(orig) def testWithNoticeWhenPrivateNotChannel(self): original = conf.supybot.reply.withNoticeWhenPrivate() try: conf.supybot.reply.withNoticeWhenPrivate.setValue(True) m = self.assertNotError("eval irc.reply('y',to='x',private=True)") self.assertEqual(m.command, 'NOTICE') m = self.getMsg(' ') m = self.assertNotError("eval irc.reply('y',to='#x',private=True)") self.assertNotEqual(m.command, 'NOTICE') finally: conf.supybot.reply.withNoticeWhenPrivate.setValue(original) class ProxyTestCase(SupyTestCase): def testHashing(self): irc = getTestIrc() msg = ircmsgs.ping('0') irc._tagMsg(msg) irc = irclib.Irc('test') proxy = callbacks.SimpleProxy(irc, msg) # First one way... self.assertEqual(proxy, irc) self.assertEqual(proxy, irc) self.assertEqual(hash(proxy), hash(irc)) # Then the other! self.assertEqual(irc, proxy) self.assertEqual(irc, proxy) self.assertEqual(hash(irc), hash(proxy)) # And now dictionaries... d = {} d[irc] = 'foo' self.assertEqual(len(d), 1) self.assertEqual(d[irc], 'foo') self.assertEqual(d[proxy], 'foo') d[proxy] = 'bar' self.assertEqual(len(d), 1) self.assertEqual(d[irc], 'bar') self.assertEqual(d[proxy], 'bar') d[irc] = 'foo' self.assertEqual(len(d), 1) self.assertEqual(d[irc], 'foo') self.assertEqual(d[proxy], 'foo') # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: