Change IRCv3 marks to reference capabilities instead of v3.1 / v3.2

This commit is contained in:
2021-02-27 10:36:30 +01:00
committed by Valentin Lorentz
parent 5ab2fa709e
commit 0352a83a73
20 changed files with 188 additions and 71 deletions

View File

@ -20,7 +20,7 @@ from .numerics import (
ERR_NOSUCHCHANNEL, ERR_NOSUCHCHANNEL,
ERR_TOOMANYCHANNELS, ERR_TOOMANYCHANNELS,
) )
from .specifications import Specifications from .specifications import Capabilities, IsupportTokens, Specifications
CHANNEL_JOIN_FAIL_NUMERICS = frozenset( CHANNEL_JOIN_FAIL_NUMERICS = frozenset(
[ [
@ -489,7 +489,7 @@ class BaseServerTestCase(_IrcTestCase):
) )
except AssertionError: except AssertionError:
if skip_if_cap_nak: if skip_if_cap_nak:
raise runner.NotImplementedByController(", ".join(capabilities)) raise runner.CapabilityNotSupported(" or ".join(capabilities))
else: else:
raise raise
self.sendLine(client, "CAP END") self.sendLine(client, "CAP END")
@ -614,7 +614,7 @@ class OptionalityHelper:
def mark_specifications(*specifications, deprecated=False, strict=False): def mark_specifications(*specifications, deprecated=False, strict=False):
specifications = frozenset( specifications = frozenset(
Specifications.of_name(s) if isinstance(s, str) else s for s in specifications Specifications.from_name(s) if isinstance(s, str) else s for s in specifications
) )
if None in specifications: if None in specifications:
raise ValueError("Invalid set of specifications: {}".format(specifications)) raise ValueError("Invalid set of specifications: {}".format(specifications))
@ -629,3 +629,35 @@ def mark_specifications(*specifications, deprecated=False, strict=False):
return f return f
return decorator return decorator
def mark_capabilities(*capabilities, deprecated=False, strict=False):
capabilities = frozenset(
Capabilities.from_name(c) if isinstance(c, str) else c for c in capabilities
)
if None in capabilities:
raise ValueError("Invalid set of capabilities: {}".format(capabilities))
def decorator(f):
for capability in capabilities:
f = getattr(pytest.mark, capability.value)(f)
# Support for any capability implies IRCv3
f = pytest.mark.IRCv3(f)
return f
return decorator
def mark_isupport(*tokens, deprecated=False, strict=False):
tokens = frozenset(
IsupportTokens.from_name(c) if isinstance(c, str) else c for c in tokens
)
if None in tokens:
raise ValueError("Invalid set of isupport tokens: {}".format(tokens))
def decorator(f):
for token in tokens:
f = getattr(pytest.mark, token.value)(f)
return f
return decorator

View File

@ -3,12 +3,12 @@ from irctest.irc_utils.message_parser import Message
class CapTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper): class CapTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper):
@cases.mark_specifications("IRCv3.1", "IRCv3.2") @cases.mark_specifications("IRCv3")
def testSendCap(self): def testSendCap(self):
"""Send CAP LS 302 and read the result.""" """Send CAP LS 302 and read the result."""
self.readCapLs() self.readCapLs()
@cases.mark_specifications("IRCv3.1", "IRCv3.2") @cases.mark_specifications("IRCv3")
def testEmptyCapLs(self): def testEmptyCapLs(self):
"""Empty result to CAP LS. Client should send CAP END.""" """Empty result to CAP LS. Client should send CAP END."""
m = self.negotiateCapabilities([]) m = self.negotiateCapabilities([])

View File

@ -30,6 +30,11 @@ class CapabilityNotSupported(unittest.SkipTest):
return "Unsupported capability: {}".format(self.args[0]) return "Unsupported capability: {}".format(self.args[0])
class IsupportTokenNotSupported(unittest.SkipTest):
def __str__(self):
return "Unsupported ISUPPORT token: {}".format(self.args[0])
class NotRequiredBySpecifications(unittest.SkipTest): class NotRequiredBySpecifications(unittest.SkipTest):
def __str__(self): def __str__(self):
return "Tests not required by the set of tested specification(s)." return "Tests not required by the set of tested specification(s)."

View File

@ -32,7 +32,7 @@ class AccountTagTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
self.sendLine(2, "CAP END") self.sendLine(2, "CAP END")
self.skipToWelcome(2) self.skipToWelcome(2)
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPrivmsg(self): def testPrivmsg(self):
self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True) self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True)

View File

@ -6,7 +6,7 @@ from irctest import cases
class AwayNotifyTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): class AwayNotifyTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("IRCv3.1") @cases.mark_capabilities("away-notify")
def testAwayNotify(self): def testAwayNotify(self):
"""Basic away-notify test.""" """Basic away-notify test."""
self.connectClient("foo", capabilities=["away-notify"], skip_if_cap_nak=True) self.connectClient("foo", capabilities=["away-notify"], skip_if_cap_nak=True)
@ -29,7 +29,7 @@ class AwayNotifyTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
"Unexpected away-notify source: %s" % (awayNotify.prefix,), "Unexpected away-notify source: %s" % (awayNotify.prefix,),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("away-notify")
def testAwayNotifyOnJoin(self): def testAwayNotifyOnJoin(self):
"""The away-notify specification states: """The away-notify specification states:
"Clients will be sent an AWAY message [...] when a user joins "Clients will be sent an AWAY message [...] when a user joins

View File

@ -3,7 +3,7 @@ from irctest.runner import CapabilityNotSupported, ImplementationChoice
class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
def testNoReq(self): def testNoReq(self):
"""Test the server handles gracefully clients which do not send """Test the server handles gracefully clients which do not send
REQs. REQs.
@ -23,7 +23,7 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}." m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
def testReqUnavailable(self): def testReqUnavailable(self):
"""Test the server handles gracefully clients which request """Test the server handles gracefully clients which request
capabilities that are not available. capabilities that are not available.
@ -50,7 +50,7 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}." m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
def testNakExactString(self): def testNakExactString(self):
"""“The argument of the NAK subcommand MUST consist of at least the """“The argument of the NAK subcommand MUST consist of at least the
first 100 characters of the capability list in the REQ subcommand which first 100 characters of the capability list in the REQ subcommand which
@ -73,7 +73,7 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
"sending “CAP REQ :foo qux bar baz qux quux”, but got {msg}.", "sending “CAP REQ :foo qux bar baz qux quux”, but got {msg}.",
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
def testNakWhole(self): def testNakWhole(self):
"""“The capability identifier set must be accepted as a whole, or """“The capability identifier set must be accepted as a whole, or
rejected entirely.” rejected entirely.”
@ -124,7 +124,7 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
"sending “CAP REQ :multi-prefix”, but got {msg}.", "sending “CAP REQ :multi-prefix”, but got {msg}.",
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
def testCapRemovalByClient(self): def testCapRemovalByClient(self):
"""Test CAP LIST and removal of caps via CAP REQ :-tagname.""" """Test CAP LIST and removal of caps via CAP REQ :-tagname."""
cap1 = "echo-message" cap1 = "echo-message"

View File

@ -712,9 +712,13 @@ class testChannelCaseSensitivity(cases.BaseServerTestCase):
class InviteTestCase(cases.BaseServerTestCase): class InviteTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("Modern")
def testInvites(self): def testInvites(self):
"""Test some basic functionality related to INVITE and the +i mode.""" """Test some basic functionality related to INVITE and the +i mode.
https://modern.ircdocs.horse/#invite-only-channel-mode
https://modern.ircdocs.horse/#rplinviting-341
"""
self.connectClient("foo") self.connectClient("foo")
self.joinChannel(1, "#chan") self.joinChannel(1, "#chan")
self.sendLine(1, "MODE #chan +i") self.sendLine(1, "MODE #chan +i")

View File

@ -128,9 +128,22 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
"both got 001.", "both got 001.",
) )
@cases.mark_specifications("IRCv3.1", "IRCv3.2") @cases.mark_specifications("IRCv3")
def testIrc301CapLs(self): def testIrc301CapLs(self):
"""IRCv3.1: “The LS subcommand is used to list the capabilities """
Current version:
"The LS subcommand is used to list the capabilities supported by the server.
The client should send an LS subcommand with no other arguments to solicit
a list of all capabilities."
"If a client has not indicated support for CAP LS 302 features,
the server MUST NOT send these new features to the client."
-- <https://ircv3.net/specs/core/capability-negotiation.html>
Before the v3.1 / v3.2 merge:
IRCv3.1: “The LS subcommand is used to list the capabilities
supported by the server. The client should send an LS subcommand with supported by the server. The client should send an LS subcommand with
no other arguments to solicit a list of all capabilities.” no other arguments to solicit a list of all capabilities.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-ls-subcommand> -- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-ls-subcommand>
@ -156,7 +169,7 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
"request: {}".format(m), "request: {}".format(m),
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
def testEmptyCapList(self): def testEmptyCapList(self):
"""“If no capabilities are active, an empty parameter must be sent.” """“If no capabilities are active, an empty parameter must be sent.”
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand> -- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand>

View File

@ -58,7 +58,7 @@ class DMEchoMessageTestCase(cases.BaseServerTestCase):
class EchoMessageTestCase(cases.BaseServerTestCase): class EchoMessageTestCase(cases.BaseServerTestCase):
def _testEchoMessage(command, solo, server_time): def _testEchoMessage(command, solo, server_time):
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message")
def f(self): def f(self):
"""<http://ircv3.net/specs/extensions/echo-message-3.2.html>""" """<http://ircv3.net/specs/extensions/echo-message-3.2.html>"""
self.addClient() self.addClient()

View File

@ -32,7 +32,7 @@ class MetadataTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
self.sendLine(2, "CAP END") self.sendLine(2, "CAP END")
self.skipToWelcome(2) self.skipToWelcome(2)
@cases.mark_specifications("IRCv3.1") @cases.mark_capabilities("extended-join")
def testNotLoggedIn(self): def testNotLoggedIn(self):
self.connectClient("foo", capabilities=["extended-join"], skip_if_cap_nak=True) self.connectClient("foo", capabilities=["extended-join"], skip_if_cap_nak=True)
self.joinChannel(1, "#chan") self.joinChannel(1, "#chan")
@ -47,7 +47,7 @@ class MetadataTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
"unregistered user joined, got: {msg}", "unregistered user joined, got: {msg}",
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_capabilities("extended-join")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testLoggedIn(self): def testLoggedIn(self):
self.connectClient("foo", capabilities=["extended-join"], skip_if_cap_nak=True) self.connectClient("foo", capabilities=["extended-join"], skip_if_cap_nak=True)

View File

@ -8,7 +8,7 @@ from irctest import cases
class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("batch", "echo-message", "labeled-response")
def testLabeledPrivmsgResponsesToMultipleClients(self): def testLabeledPrivmsgResponsesToMultipleClients(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -92,7 +92,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
m, command="BATCH", fail_msg="No BATCH echo received after sending one out" m, command="BATCH", fail_msg="No BATCH echo received after sending one out"
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response")
def testLabeledPrivmsgResponsesToClient(self): def testLabeledPrivmsgResponsesToClient(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -152,7 +152,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
), ),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response")
def testLabeledPrivmsgResponsesToChannel(self): def testLabeledPrivmsgResponsesToChannel(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -220,7 +220,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
), ),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response")
def testLabeledPrivmsgResponsesToSelf(self): def testLabeledPrivmsgResponsesToSelf(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -263,7 +263,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
).format(number_of_labels), ).format(number_of_labels),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response")
def testLabeledNoticeResponsesToClient(self): def testLabeledNoticeResponsesToClient(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -323,7 +323,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
), ),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response")
def testLabeledNoticeResponsesToChannel(self): def testLabeledNoticeResponsesToChannel(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -391,7 +391,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
), ),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response")
def testLabeledNoticeResponsesToSelf(self): def testLabeledNoticeResponsesToSelf(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -432,7 +432,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
).format(number_of_labels), ).format(number_of_labels),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response", "message-tags")
def testLabeledTagMsgResponsesToClient(self): def testLabeledTagMsgResponsesToClient(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -540,7 +540,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
fail_msg="React tag wasn't the same on the source user's TAGMSG: {msg}", fail_msg="React tag wasn't the same on the source user's TAGMSG: {msg}",
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response", "message-tags")
def testLabeledTagMsgResponsesToChannel(self): def testLabeledTagMsgResponsesToChannel(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -606,7 +606,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
), ),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("echo-message", "labeled-response", "message-tags")
def testLabeledTagMsgResponsesToSelf(self): def testLabeledTagMsgResponsesToSelf(self):
self.connectClient( self.connectClient(
"foo", "foo",
@ -647,7 +647,9 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
).format(number_of_labels), ).format(number_of_labels),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities(
"echo-message", "labeled-response", "message-tags", "server-time"
)
def testBatchedJoinMessages(self): def testBatchedJoinMessages(self):
self.connectClient( self.connectClient(
"bar", "bar",

View File

@ -8,7 +8,7 @@ from irctest.numerics import ERR_INPUTTOOLONG
class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("message-tags") @cases.mark_capabilities("message-tags")
def testBasic(self): def testBasic(self):
def getAllMessages(): def getAllMessages():
for name in ["alice", "bob", "carol", "dave"]: for name in ["alice", "bob", "carol", "dave"]:
@ -87,7 +87,7 @@ class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
self.assertTrue(alice_msg.tags["msgid"]) self.assertTrue(alice_msg.tags["msgid"])
self.assertEqual(alice_msg.tags["msgid"], bob_msg.tags["msgid"]) self.assertEqual(alice_msg.tags["msgid"], bob_msg.tags["msgid"])
@cases.mark_specifications("message-tags") @cases.mark_capabilities("message-tags")
def testLengthLimits(self): def testLengthLimits(self):
self.connectClient( self.connectClient(
"alice", "alice",

View File

@ -10,7 +10,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
valid_metadata_keys = {"valid_key1", "valid_key2"} valid_metadata_keys = {"valid_key1", "valid_key2"}
invalid_metadata_keys = {"invalid_key1", "invalid_key2"} invalid_metadata_keys = {"invalid_key1", "invalid_key2"}
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testInIsupport(self): def testInIsupport(self):
"""“If METADATA is supported, it MUST be specified in RPL_ISUPPORT """“If METADATA is supported, it MUST be specified in RPL_ISUPPORT
using the METADATA key.” using the METADATA key.”
@ -33,7 +33,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
) )
self.getMessages(1) self.getMessages(1)
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testGetOneUnsetValid(self): def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>""" """<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient("foo") self.connectClient("foo")
@ -46,7 +46,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
"request to an unset valid METADATA key.", "request to an unset valid METADATA key.",
) )
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testGetTwoUnsetValid(self): def testGetTwoUnsetValid(self):
"""“Multiple keys may be given. The response will be either RPL_KEYVALUE, """“Multiple keys may be given. The response will be either RPL_KEYVALUE,
ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.” ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.”
@ -83,7 +83,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
"did not respond to valid_key2 as second response: {msg}", "did not respond to valid_key2 as second response: {msg}",
) )
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testListNoSet(self): def testListNoSet(self):
"""“This subcommand MUST list all currently-set metadata keys along """“This subcommand MUST list all currently-set metadata keys along
with their values. The response will be zero or more RPL_KEYVALUE with their values. The response will be zero or more RPL_KEYVALUE
@ -100,7 +100,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
"762 (RPL_METADATAEND) but: {msg}", "762 (RPL_METADATAEND) but: {msg}",
) )
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testListInvalidTarget(self): def testListInvalidTarget(self):
"""“In case of invalid target RPL_METADATAEND MUST NOT be sent.” """“In case of invalid target RPL_METADATAEND MUST NOT be sent.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list> -- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
@ -187,13 +187,13 @@ class MetadataTestCase(cases.BaseServerTestCase):
self.assertSetValue(target, key, value, displayable_value) self.assertSetValue(target, key, value, displayable_value)
self.assertGetValue(target, key, value, displayable_value) self.assertGetValue(target, key, value, displayable_value)
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetValid(self): def testSetGetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>""" """<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient("foo") self.connectClient("foo")
self.assertSetGetValue("*", "valid_key1", "myvalue") self.assertSetGetValue("*", "valid_key1", "myvalue")
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetZeroCharInValue(self): def testSetGetZeroCharInValue(self):
"""“Values are unrestricted, except that they MUST be UTF-8.” """“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions> -- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
@ -201,7 +201,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
self.connectClient("foo") self.connectClient("foo")
self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero") self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero")
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetHeartInValue(self): def testSetGetHeartInValue(self):
"""“Values are unrestricted, except that they MUST be UTF-8.” """“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions> -- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
@ -215,7 +215,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
"zero->{}<-zero".format(heart.encode()), "zero->{}<-zero".format(heart.encode()),
) )
@cases.mark_specifications("IRCv3.2", deprecated=True) @cases.mark_specifications("IRCv3", deprecated=True)
def testSetInvalidUtf8(self): def testSetInvalidUtf8(self):
"""“Values are unrestricted, except that they MUST be UTF-8.” """“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions> -- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>

View File

@ -66,7 +66,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
extra_format=(nick,), extra_format=(nick,),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorOneDisconnected(self): def testMonitorOneDisconnected(self):
"""“If any of the targets being added are online, the server will """“If any of the targets being added are online, the server will
generate RPL_MONONLINE numerics listing those targets that are generate RPL_MONONLINE numerics listing those targets that are
@ -86,7 +87,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
pass pass
self.assertMonoffline(1, "bar") self.assertMonoffline(1, "bar")
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorOneConnection(self): def testMonitorOneConnection(self):
self.connectClient("foo") self.connectClient("foo")
self.check_server_support() self.check_server_support()
@ -95,7 +97,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
self.connectClient("bar") self.connectClient("bar")
self.assertMononline(1, "bar") self.assertMononline(1, "bar")
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorOneConnected(self): def testMonitorOneConnected(self):
"""“If any of the targets being added are offline, the server will """“If any of the targets being added are offline, the server will
generate RPL_MONOFFLINE numerics listing those targets that are generate RPL_MONOFFLINE numerics listing those targets that are
@ -114,7 +117,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
pass pass
self.assertMonoffline(1, "bar") self.assertMonoffline(1, "bar")
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorOneConnectionWithQuit(self): def testMonitorOneConnectionWithQuit(self):
self.connectClient("foo") self.connectClient("foo")
self.check_server_support() self.check_server_support()
@ -130,7 +134,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
self.connectClient("bar") self.connectClient("bar")
self.assertMononline(1, "bar") self.assertMononline(1, "bar")
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorConnectedAndDisconnected(self): def testMonitorConnectedAndDisconnected(self):
"""“If any of the targets being added are online, the server will """“If any of the targets being added are online, the server will
generate RPL_MONONLINE numerics listing those targets that are generate RPL_MONONLINE numerics listing those targets that are
@ -185,7 +190,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
"“MONITOR + bar,baz” and “baz” is disconnected: {msg}", "“MONITOR + bar,baz” and “baz” is disconnected: {msg}",
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testUnmonitor(self): def testUnmonitor(self):
self.connectClient("foo") self.connectClient("foo")
self.check_server_support() self.check_server_support()
@ -210,7 +216,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
fail_msg="Got messages after disconnection of unmonitored " "nick: {got}", fail_msg="Got messages after disconnection of unmonitored " "nick: {got}",
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorForbidsMasks(self): def testMonitorForbidsMasks(self):
"""“The MONITOR implementation also enhances user privacy by """“The MONITOR implementation also enhances user privacy by
disallowing subscription to hostmasks, allowing users to avoid disallowing subscription to hostmasks, allowing users to avoid
@ -247,7 +254,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
"was requested via hostmask connected: {}".format(m) "was requested via hostmask connected: {}".format(m)
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testTwoMonitoringOneRemove(self): def testTwoMonitoringOneRemove(self):
"""Tests the following scenario: """Tests the following scenario:
* foo MONITORs qux * foo MONITORs qux
@ -286,7 +294,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
extra_format=(messages,), extra_format=(messages,),
) )
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorList(self): def testMonitorList(self):
def checkMonitorSubjects(messages, client_nick, expected_targets): def checkMonitorSubjects(messages, client_nick, expected_targets):
# collect all the RPL_MONLIST nicks into a set: # collect all the RPL_MONLIST nicks into a set:
@ -320,7 +329,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
self.sendLine(1, "MONITOR L") self.sendLine(1, "MONITOR L")
checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"}) checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"})
@cases.mark_specifications("IRCv3.2") @cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testNickChange(self): def testNickChange(self):
# see oragono issue #1076: nickname changes must trigger RPL_MONOFFLINE # see oragono issue #1076: nickname changes must trigger RPL_MONOFFLINE
self.connectClient("bar") self.connectClient("bar")

View File

@ -7,7 +7,7 @@ from irctest import cases
class MultiPrefixTestCase(cases.BaseServerTestCase): class MultiPrefixTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3.1") @cases.mark_capabilities("multi-prefix")
def testMultiPrefix(self): def testMultiPrefix(self):
"""“When requested, the multi-prefix client capability will cause the """“When requested, the multi-prefix client capability will cause the
IRC server to send all possible prefixes which apply to a user in NAMES IRC server to send all possible prefixes which apply to a user in NAMES

View File

@ -12,7 +12,7 @@ base_caps = ["message-tags", "batch", "echo-message", "server-time", "labeled-re
class MultilineTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): class MultilineTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("multiline") @cases.mark_capabilities("draft/multiline")
def testBasic(self): def testBasic(self):
self.connectClient( self.connectClient(
"alice", capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=True "alice", capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=True
@ -79,7 +79,7 @@ class MultilineTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
self.assertNotIn(CONCAT_TAG, msg.tags) self.assertNotIn(CONCAT_TAG, msg.tags)
self.assertEqual(relayed_fmsgids, [msgid] + [None] * (len(fallback_relay) - 1)) self.assertEqual(relayed_fmsgids, [msgid] + [None] * (len(fallback_relay) - 1))
@cases.mark_specifications("multiline") @cases.mark_capabilities("draft/multiline")
def testBlankLines(self): def testBlankLines(self):
self.connectClient( self.connectClient(
"alice", capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=True "alice", capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=True

View File

@ -52,7 +52,7 @@ class RegressionsTestCase(cases.BaseServerTestCase):
ms = self.getMessages(2) ms = self.getMessages(2)
self.assertEqual(ms, []) self.assertEqual(ms, [])
@cases.mark_specifications("IRCv3.2") @cases.mark_capabilities("message-tags", "batch", "echo-message", "server-time")
def testTagCap(self): def testTagCap(self):
# regression test for oragono #754 # regression test for oragono #754
self.connectClient( self.connectClient(

View File

@ -9,7 +9,7 @@ class RegistrationTestCase(cases.BaseServerTestCase):
class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlain(self): def testPlain(self):
"""PLAIN authentication with correct username/password.""" """PLAIN authentication with correct username/password."""
@ -54,7 +54,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
"({expects}), not {got}: {msg}", "({expects}), not {got}: {msg}",
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainNoAuthzid(self): def testPlainNoAuthzid(self):
"""“message = [authzid] UTF8NUL authcid UTF8NUL passwd """“message = [authzid] UTF8NUL authcid UTF8NUL passwd
@ -119,7 +119,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
"({expects}), not {got}: {msg}", "({expects}), not {got}: {msg}",
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
def testMechanismNotAvailable(self): def testMechanismNotAvailable(self):
"""“If authentication fails, a 904 or 905 numeric will be sent” """“If authentication fails, a 904 or 905 numeric will be sent”
-- <http://ircv3.net/specs/extensions/sasl-3.1.html#the-authenticate-command> -- <http://ircv3.net/specs/extensions/sasl-3.1.html#the-authenticate-command>
@ -141,7 +141,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
fail_msg="Did not reply with 904 to “AUTHENTICATE FOO”: {msg}", fail_msg="Did not reply with 904 to “AUTHENTICATE FOO”: {msg}",
) )
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainLarge(self): def testPlainLarge(self):
"""Test the client splits large AUTHENTICATE messages whose payload """Test the client splits large AUTHENTICATE messages whose payload
@ -202,7 +202,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
# I don't know how to do it, because it would make the registration # I don't know how to do it, because it would make the registration
# message's length too big for it to be valid. # message's length too big for it to be valid.
@cases.mark_specifications("IRCv3.1") @cases.mark_specifications("IRCv3")
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
def testPlainLargeEquals400(self): def testPlainLargeEquals400(self):
"""Test the client splits large AUTHENTICATE messages whose payload """Test the client splits large AUTHENTICATE messages whose payload

View File

@ -5,16 +5,51 @@ import enum
class Specifications(enum.Enum): class Specifications(enum.Enum):
RFC1459 = "RFC1459" RFC1459 = "RFC1459"
RFC2812 = "RFC2812" RFC2812 = "RFC2812"
IRC301 = "IRCv3.1" IRCv3 = "IRCv3" # Mark with capabilities whenever possible
IRC302 = "IRCv3.2"
Oragono = "Oragono" Oragono = "Oragono"
Multiline = "multiline"
MessageTags = "message-tags" Ircdocs = "ircdocs"
"""Any document on ircdocs.horse (especially defs.ircdocs.horse),
excluding modern.ircdocs.horse"""
Modern = "modern"
@classmethod @classmethod
def of_name(cls, name): def from_name(cls, name):
name = name.upper() name = name.upper()
for spec in cls: for spec in cls:
if spec.value.upper() == name: if spec.value.upper() == name:
return spec return spec
raise ValueError(name) raise ValueError(name)
@enum.unique
class Capabilities(enum.Enum):
AWAY_NOTIFY = "away-notify"
BATCH = "batch"
ECHO_MESSAGE = "echo-message"
EXTENDED_JOIN = "extended-join"
LABELED_RESPONSE = "labeled-response"
MESSAGE_TAGS = "message-tags"
MULTILINE = "draft/multiline"
MULTI_PREFIX = "multi-prefix"
SERVER_TIME = "server-time"
@classmethod
def from_name(cls, name):
try:
return cls(name.lower())
except ValueError:
raise ValueError(name) from None
@enum.unique
class IsupportTokens(enum.Enum):
MONITOR = "MONITOR"
@classmethod
def from_name(cls, name):
try:
return cls(name.upper())
except ValueError:
raise ValueError(name) from None

View File

@ -1,11 +1,27 @@
[pytest] [pytest]
markers = markers =
# specifications
RFC1459 RFC1459
RFC2812 RFC2812
IRCv3.1 IRCv3
IRCv3.2 modern
message-tags ircdocs
multiline
Oragono Oragono
# misc marks
strict strict
deprecated deprecated
# capabilities
away-notify
batch
echo-message
extended-join
labeled-response
message-tags
draft/multiline
multi-prefix
server-time
# isupport tokens
MONITOR