Compare commits

...

28 Commits

Author SHA1 Message Date
Val Lorentz 05d60a1b6b
Merge 754df829d4 into df626de5ed 2024-05-06 07:33:10 -05:00
Val Lorentz df626de5ed
Enable WHOWAS and USERHOST tests on Sable (#273)
It now implements these commands.
2024-05-04 16:15:54 +02:00
Val Lorentz 79223d35f1
Enable WHO mask tests on Sable (#272)
* Sable: Hide NickServ/ChanServ when running without services

They interfere with 'WHO *' as they are returned as matches

* Enable WHO mask tests on Sable

* Bump Sable
2024-05-04 13:33:50 +02:00
Shivaram Lingamneni 723991c7ec
add test for RPL_NAMREPLY for secret channels (#265)
Ergo and ngIRCd were getting this wrong
2024-05-01 07:53:27 +02:00
Valentin Lorentz 1bc8741479 dashboard: Don't use <details> for tests with no docstring 2024-04-20 15:27:51 +02:00
Val Lorentz 9f8e712776 testNonutf8Realname/testNonutf8Username: Add support for ERROR instead of FAIL/ERR_INVALIDUSERNAME
This is what Sable does, at it fails to decode non-UTF8 data before
it even tries to parse commands.
2024-04-19 15:43:21 +02:00
Val Lorentz a1f8fcac49 testNonutf8Username: Actually test a non-UTF8 username 2024-04-19 15:43:21 +02:00
Valentin Lorentz d3c919e0f5 dashboard: Fix for parametrized tests 2024-04-19 15:16:36 +02:00
Val Lorentz ce51dddc15
Display method docstrings on the dashboard (#270)
Collapsed with <details> because they can be pretty long and make the table
harder to read.
2024-04-19 15:15:27 +02:00
Val Lorentz 7f9b4b315f
xfail testJoinNamreply on Bahamut and irc2 (#269) 2024-04-17 20:26:18 +02:00
Val Lorentz 9d43a002c2
Simplify multi-prefix-related tests and add testNoMultiPrefix (#262)
* Simplify RPL_NAMREPLY-on-join tests

* Simplify testMultiPrefix

* Add testNoMultiPrefix
2024-04-16 21:25:35 +02:00
Val Lorentz ea66a8f9a4
Make re.match actually check the whole string matches the pattern (#261)
And explicitly allow trailing space in RPL_WHOISCHANNELS
2024-04-16 21:05:25 +02:00
Valentin Lorentz 473db1cc5b ngircd: Disable PAM
It breaks irctest when ngircd was compiled with --with-pam
2024-04-16 21:00:24 +02:00
Val Lorentz f4a01cfe49
Enable CAP tests for Sable (#267)
It now implements userhost-in-names and multi-prefix, which these tests depend on
2024-04-14 21:07:29 +02:00
Val Lorentz e6dfb87759
testMonitorForbidsMasks: Allow ERR_ERRONEUSNICKNAME reply (#266)
This is returned by Sable
2024-04-14 20:01:28 +02:00
Val Lorentz 2ae612c68f
Makefile: Add selectors in preparation for Sable adding message-tags support (#264)
Some tests Sable would fail are currently disabled only because Sable does not
support message-tags; but it probably will in the near future.
2024-04-13 14:41:45 +02:00
Val Lorentz d908699674
chathistory: Skip assertions based on MSGREFTYPES (#263)
This will be useful to test Sable, which does not support CHATHISTORY
with msgid= yet
2024-04-13 14:41:13 +02:00
Valentin Lorentz 754df829d4 Add tests for SUB 2023-04-16 09:19:50 +02:00
Valentin Lorentz 10c1772121 Use real metadata keys 2023-04-16 09:19:50 +02:00
Valentin Lorentz 6c5595539c Add tests for KEY_NO_PERMISSION 2023-04-16 09:19:50 +02:00
Valentin Lorentz 34f078e04a Add a bunch of GetSet tests 2023-04-16 09:19:50 +02:00
Valentin Lorentz f18162c02d Add untested 'before-connect' test 2023-04-16 09:19:50 +02:00
Valentin Lorentz 8a6a12e7f7 Fix fail code 2023-04-16 09:19:50 +02:00
Valentin Lorentz 0318a37075 Test for nick instead of * as target 2023-04-16 09:19:50 +02:00
Valentin Lorentz 8d01063aee Improve testing for invalid UTF8, and add test for too-long values 2023-04-16 09:19:50 +02:00
Valentin Lorentz 88f8be0ff6 expect batches 2023-04-16 09:19:50 +02:00
Valentin Lorentz e17ab347f4 Adapt metadata_2 to stdreplies, and run it on Unreal
Passes on 3968354247
2023-04-16 09:19:50 +02:00
Valentin Lorentz 604041aa13 Copy metadata.py to metadata2.py 2023-04-16 09:19:50 +02:00
26 changed files with 1037 additions and 296 deletions

View File

@ -228,7 +228,7 @@ jobs:
uses: actions/checkout@v3
with:
path: ngircd
ref: 0714466af88d71d6c395629cd7fb624b099507d4
ref: 3e3f6cbeceefd9357b53b27c2386bb39306ab353
repository: ngircd/ngircd
- name: Build ngircd
run: |
@ -1106,7 +1106,7 @@ jobs:
uses: actions/checkout@v3
with:
path: sable
ref: dcf8b53cac54f460b86861908d36d67969cf1eb2
ref: b9deaa930c49f2939d9a584bedbfc3236da0d707
repository: Libera-Chat/sable
- name: Install rust toolchain
uses: actions-rs/toolchain@v1

View File

@ -83,11 +83,17 @@ LIMNORIA_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS)
# Tests marked with arbitrary_client_tags or react_tag can't pass because Sable does not support client tags yet
# Tests marked with private_chathistory can't pass because Sable does not implement CHATHISTORY for DMs
SABLE_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not whowas and not list and not lusers and not userhost and not time and not info \
and not arbitrary_client_tags \
and not react_tag \
and not private_chathistory \
and not list and not lusers and not time and not info \
$(EXTRA_SELECTORS)
SOLANUM_SELECTORS := \

View File

@ -160,6 +160,7 @@ class _IrcTestCase(Generic[TController]):
def messageDiffers(
self,
msg: Message,
command: Union[str, None, patma.Operator] = None,
params: Optional[List[Union[str, None, patma.Operator]]] = None,
target: Optional[str] = None,
tags: Optional[
@ -186,6 +187,14 @@ class _IrcTestCase(Generic[TController]):
msg=msg,
)
if command is not None and not patma.match_string(msg.command, command):
fail_msg = (
fail_msg or "expected command to match {expects}, got {got}: {msg}"
)
return fail_msg.format(
*extra_format, got=msg.command, expects=command, msg=msg
)
if prefix is not None and not patma.match_string(msg.prefix, prefix):
fail_msg = (
fail_msg or "expected prefix to match {expects}, got {got}: {msg}"
@ -214,7 +223,7 @@ class _IrcTestCase(Generic[TController]):
or "expected nick to be {expects}, got {got} instead: {msg}"
)
return fail_msg.format(
*extra_format, got=got_nick, expects=nick, param=key, msg=msg
*extra_format, got=got_nick, expects=nick, msg=msg
)
return None

View File

@ -74,7 +74,19 @@ class ClientMock:
continue
if not synchronize:
got_pong = True
for line in data.decode().split("\r\n"):
try:
decoded_data = data.decode()
except UnicodeDecodeError:
print(
"{time:.3f}{ssl} S -> {client} - failed to decode: {data!r}".format(
time=time.time(),
ssl=" (ssl)" if self.ssl else "",
client=self.name,
data=data,
)
)
raise
for line in decoded_data.split("\r\n"):
if line:
if self.show_io:
print(

View File

@ -23,6 +23,7 @@ TEMPLATE_CONFIG = """
[Options]
MorePrivacy = no # by default, always replies to WHOWAS with ERR_WASNOSUCHNICK
PAM = no
[Operator]
Name = operuser

View File

@ -116,20 +116,7 @@ NETWORK_CONFIG_CONFIG = """
],
"alias_users": [
{
"nick": "ChanServ",
"user": "ChanServ",
"host": "services.",
"realname": "Channel services compatibility layer",
"command_alias": "CS"
},
{
"nick": "NickServ",
"user": "NickServ",
"host": "services.",
"realname": "Account services compatibility layer",
"command_alias": "NS"
}
%(services_alias_users)s
],
"default_roles": {
@ -160,6 +147,23 @@ NETWORK_CONFIG_CONFIG = """
}
"""
SERVICES_ALIAS_USERS = """
{
"nick": "ChanServ",
"user": "ChanServ",
"host": "services.",
"realname": "Channel services compatibility layer",
"command_alias": "CS"
},
{
"nick": "NickServ",
"user": "NickServ",
"host": "services.",
"realname": "Account services compatibility layer",
"command_alias": "NS"
}
"""
SERVER_CONFIG = """
{
"server_id": 1,
@ -374,6 +378,7 @@ class SableController(BaseServerController, DirectoryBasedController):
.strip(),
services_management_hostname=services_management_hostname,
services_management_port=services_management_port,
services_alias_users=SERVICES_ALIAS_USERS if run_services else "",
)
with self.open_file("configs/network.conf") as fd:

View File

@ -11,6 +11,7 @@ from irctest.basecontrollers import BaseServerController, DirectoryBasedControll
TEMPLATE_CONFIG = """
include "modules.default.conf";
loadmodule "third/metadata2";
include "operclass.default.conf";
{extras}
include "help/help.conf";

View File

@ -245,8 +245,19 @@ def build_test_table(
# TODO: only hash test parameter
row_anchor = md5sum(row_anchor)
doc = docstring(
getattr(getattr(module, class_name), test_name.split("[")[0])
)
row = HTML.tr(
HTML.th(HTML.a(test_name, href=f"#{row_anchor}"), class_="test-name"),
HTML.th(
HTML.details(
HTML.summary(HTML.a(test_name, href=f"#{row_anchor}")),
doc,
)
if doc
else HTML.a(test_name, href=f"#{row_anchor}"),
class_="test-name",
),
id=row_anchor,
)
rows.append(row)

View File

@ -1,23 +0,0 @@
"""
Handles ambiguities of RFCs.
"""
from typing import List
def normalize_namreply_params(params: List[str]) -> List[str]:
# So… RFC 2812 says:
# "( "=" / "*" / "@" ) <channel>
# :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
# but spaces seem to be missing (eg. before the colon), so we
# don't know if there should be one before the <channel> and its
# prefix.
# So let's normalize this to “with space”, and strip spaces at the
# end of the nick list.
params = list(params) # copy the list
if len(params) == 3:
assert params[1][0] in "=*@", params
params.insert(1, params[1][0])
params[2] = params[2][1:]
params[3] = params[3].rstrip()
return params

View File

@ -15,7 +15,7 @@ TAG_ESCAPE = [
unescape_tag_value = MultipleReplacer(dict(map(lambda x: (x[1], x[0]), TAG_ESCAPE)))
# TODO: validate host
tag_key_validator = re.compile(r"\+?(\S+/)?[a-zA-Z0-9-]+")
tag_key_validator = re.compile(r"^\+?(\S+/)?[a-zA-Z0-9-]+$")
def parse_tags(s: str) -> Dict[str, Optional[str]]:

View File

@ -106,15 +106,15 @@ def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bo
elif isinstance(expected, _AnyStr) and got is not None:
return True
elif isinstance(expected, StrRe):
if got is None or not re.match(expected.regexp, got):
if got is None or not re.match(expected.regexp + "$", got):
return False
elif isinstance(expected, OptStrRe):
if got is None:
return True
if not re.match(expected.regexp, got):
if not re.match(expected.regexp + "$", got):
return False
elif isinstance(expected, NotStrRe):
if got is None or re.match(expected.regexp, got):
if got is None or re.match(expected.regexp + "$", got):
return False
elif isinstance(expected, InsensitiveStr):
if got is None or got.lower() != expected.string.lower():

View File

@ -173,7 +173,7 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
],
# and they each error with:
[
"expected command to be PRIVMSG, got PRIVMG",
"expected command to match PRIVMSG, got PRIVMG",
"expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
@ -206,7 +206,7 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
],
# and they each error with:
[
"expected command to be PRIVMSG, got PRIVMG",
"expected command to match PRIVMSG, got PRIVMG",
"expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
@ -235,7 +235,7 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
],
# and they each error with:
[
"expected command to be PRIVMSG, got PRIVMG",
"expected command to match PRIVMSG, got PRIVMG",
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'value1'}",
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': ''}",
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': 'baz'}",
@ -345,7 +345,7 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
],
# and they each error with:
[
"expected command to be PING, got PONG"
"expected command to match PING, got PONG"
]
),
]

View File

@ -56,10 +56,6 @@ class CapTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["Sable"],
"does not support multi-prefix",
)
def testReqOne(self):
"""Tests requesting a single capability"""
self.addClient(1)
@ -93,7 +89,7 @@ class CapTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd", "Sable"],
["ngIRCd"],
"does not support userhost-in-names",
)
def testReqTwo(self):
@ -135,7 +131,7 @@ class CapTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd", "Sable"],
["ngIRCd"],
"does not support userhost-in-names",
)
def testReqOneThenOne(self):
@ -187,7 +183,7 @@ class CapTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd", "Sable"],
["ngIRCd"],
"does not support userhost-in-names",
)
def testReqPostRegistration(self):

View File

@ -58,6 +58,16 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
def config() -> cases.TestCaseControllerConfig:
return cases.TestCaseControllerConfig(chathistory=True)
def _supports_msgid(self):
return "msgid" in self.server_support.get(
"MSGREFTYPES", "msgid,timestamp"
).split(",")
def _supports_timestamp(self):
return "timestamp" in self.server_support.get(
"MSGREFTYPES", "msgid,timestamp"
).split(",")
@skip_ngircd
def testInvalidTargets(self):
bar, pw = random_name("bar"), random_name("pw")
@ -460,172 +470,195 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[-1:], result)
self.sendLine(
user,
"CHATHISTORY LATEST %s msgid=%s %d"
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[5:], result)
if self._supports_msgid():
self.sendLine(
user,
"CHATHISTORY LATEST %s msgid=%s %d"
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[5:], result)
self.sendLine(
user,
"CHATHISTORY LATEST %s timestamp=%s %d"
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[5:], result)
if self._supports_timestamp():
self.sendLine(
user,
"CHATHISTORY LATEST %s timestamp=%s %d"
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[5:], result)
def _validate_chathistory_BEFORE(self, echo_messages, user, chname):
INCLUSIVE_LIMIT = len(echo_messages) * 2
self.sendLine(
user,
"CHATHISTORY BEFORE %s msgid=%s %d"
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[:6], result)
if self._supports_msgid():
self.sendLine(
user,
"CHATHISTORY BEFORE %s msgid=%s %d"
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[:6], result)
self.sendLine(
user,
"CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[:6], result)
if self._supports_timestamp():
self.sendLine(
user,
"CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[:6], result)
self.sendLine(
user,
"CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, 2),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:6], result)
self.sendLine(
user,
"CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, 2),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:6], result)
def _validate_chathistory_AFTER(self, echo_messages, user, chname):
INCLUSIVE_LIMIT = len(echo_messages) * 2
self.sendLine(
user,
"CHATHISTORY AFTER %s msgid=%s %d"
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:], result)
if self._supports_msgid():
self.sendLine(
user,
"CHATHISTORY AFTER %s msgid=%s %d"
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:], result)
self.sendLine(
user,
"CHATHISTORY AFTER %s timestamp=%s %d"
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:], result)
if self._supports_timestamp():
self.sendLine(
user,
"CHATHISTORY AFTER %s timestamp=%s %d"
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:], result)
self.sendLine(
user,
"CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:7], result)
self.sendLine(
user,
"CHATHISTORY AFTER %s timestamp=%s %d"
% (chname, echo_messages[3].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:7], result)
def _validate_chathistory_BETWEEN(self, echo_messages, user, chname):
INCLUSIVE_LIMIT = len(echo_messages) * 2
# BETWEEN forwards and backwards
self.sendLine(
user,
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (
chname,
echo_messages[0].msgid,
echo_messages[-1].msgid,
INCLUSIVE_LIMIT,
),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result)
if self._supports_msgid():
# BETWEEN forwards and backwards
self.sendLine(
user,
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (
chname,
echo_messages[0].msgid,
echo_messages[-1].msgid,
INCLUSIVE_LIMIT,
),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (
chname,
echo_messages[-1].msgid,
echo_messages[0].msgid,
INCLUSIVE_LIMIT,
),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (
chname,
echo_messages[-1].msgid,
echo_messages[0].msgid,
INCLUSIVE_LIMIT,
),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result)
# BETWEEN forwards and backwards with a limit, should get
# different results this time
self.sendLine(
user,
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:4], result)
# BETWEEN forwards and backwards with a limit, should get
# different results this time
self.sendLine(
user,
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:4], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[-4:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[-4:-1], result)
# same stuff again but with timestamps
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[0].time, echo_messages[-1].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[-1].time, echo_messages[0].time, INCLUSIVE_LIMIT),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[0].time, echo_messages[-1].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:4], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[-1].time, echo_messages[0].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[-4:-1], result)
if self._supports_timestamp():
# same stuff again but with timestamps
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (
chname,
echo_messages[0].time,
echo_messages[-1].time,
INCLUSIVE_LIMIT,
),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (
chname,
echo_messages[-1].time,
echo_messages[0].time,
INCLUSIVE_LIMIT,
),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[0].time, echo_messages[-1].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:4], result)
self.sendLine(
user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[-1].time, echo_messages[0].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[-4:-1], result)
def _validate_chathistory_AROUND(self, echo_messages, user, chname):
self.sendLine(
user,
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual([echo_messages[7]], result)
if self._supports_msgid():
self.sendLine(
user,
"CHATHISTORY AROUND %s msgid=%s %d"
% (chname, echo_messages[7].msgid, 1),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual([echo_messages[7]], result)
self.sendLine(
user,
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[6:9], result)
self.sendLine(
user,
"CHATHISTORY AROUND %s msgid=%s %d"
% (chname, echo_messages[7].msgid, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[6:9], result)
self.sendLine(
user,
"CHATHISTORY AROUND %s timestamp=%s %d"
% (chname, echo_messages[7].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertIn(echo_messages[7], result)
if self._supports_timestamp():
self.sendLine(
user,
"CHATHISTORY AROUND %s timestamp=%s %d"
% (chname, echo_messages[7].time, 3),
)
result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertIn(echo_messages[7], result)
@pytest.mark.arbitrary_client_tags
@skip_ngircd

View File

@ -56,7 +56,8 @@ class IsupportTestCase(cases.BaseServerTestCase):
return
m = re.match(
r"\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)", self.server_support["PREFIX"]
r"^\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)$",
self.server_support["PREFIX"],
)
self.assertTrue(
m,
@ -117,5 +118,5 @@ class IsupportTestCase(cases.BaseServerTestCase):
parts = self.server_support["TARGMAX"].split(",")
for part in parts:
self.assertTrue(
re.match("[A-Z]+:[0-9]*", part), "Invalid TARGMAX key:value: %r", part
re.match("^[A-Z]+:[0-9]*$", part), "Invalid TARGMAX key:value: %r", part
)

View File

@ -6,7 +6,6 @@ The JOIN command (`RFC 1459
"""
from irctest import cases, runner
from irctest.irc_utils import ambiguities
from irctest.numerics import (
ERR_BADCHANMASK,
ERR_FORBIDDENCHANNEL,
@ -61,6 +60,7 @@ class JoinTestCase(cases.BaseServerTestCase):
),
)
@cases.xfailIfSoftware(["Bahamut", "irc2"], "trailing space on RPL_NAMREPLY")
@cases.mark_specifications("RFC2812")
def testJoinNamreply(self):
"""“353 RPL_NAMREPLY
@ -75,33 +75,23 @@ class JoinTestCase(cases.BaseServerTestCase):
for m in self.getMessages(1):
if m.command == "353":
self.assertIn(
len(m.params),
(3, 4),
m,
fail_msg="RPL_NAM_REPLY with number of arguments "
"<3 or >4: {msg}",
self.assertMessageMatch(
m, params=["foo", StrRe(r"[=\*@]"), "#chan", StrRe("[@+]?foo")]
)
params = ambiguities.normalize_namreply_params(m.params)
self.assertIn(
params[1],
"=*@",
self.connectClient("bar")
self.sendLine(2, "JOIN #chan")
for m in self.getMessages(2):
if m.command == "353":
self.assertMessageMatch(
m,
fail_msg="Bad channel prefix: {item} not in {list}: {msg}",
)
self.assertEqual(
params[2],
"#chan",
m,
fail_msg="Bad channel name: {got} instead of " "{expects}: {msg}",
)
self.assertIn(
params[3],
{"foo", "@foo", "+foo"},
m,
fail_msg="Bad user list: should contain only user "
'"foo" with an optional "+" or "@" prefix, but got: '
"{msg}",
params=[
"bar",
StrRe(r"[=\*@]"),
"#chan",
StrRe("([@+]?foo bar|bar [@+]?foo)"),
],
)
def testJoinTwice(self):
@ -115,34 +105,8 @@ class JoinTestCase(cases.BaseServerTestCase):
# if the join is successful, or has an error among the given set.
for m in self.getMessages(1):
if m.command == "353":
self.assertIn(
len(m.params),
(3, 4),
m,
fail_msg="RPL_NAM_REPLY with number of arguments "
"<3 or >4: {msg}",
)
params = ambiguities.normalize_namreply_params(m.params)
self.assertIn(
params[1],
"=*@",
m,
fail_msg="Bad channel prefix: {item} not in {list}: {msg}",
)
self.assertEqual(
params[2],
"#chan",
m,
fail_msg="Bad channel name: {got} instead of " "{expects}: {msg}",
)
self.assertIn(
params[3],
{"foo", "@foo", "+foo"},
m,
fail_msg='Bad user list after user "foo" joined twice '
"the same channel: should contain only user "
'"foo" with an optional "+" or "@" prefix, but got: '
"{msg}",
self.assertMessageMatch(
m, params=["foo", StrRe(r"[=\*@]"), "#chan", StrRe("[@+]?foo")]
)
def testJoinPartiallyInvalid(self):

View File

@ -0,0 +1,617 @@
"""
`IRCv3 Metadata 2 <https://github.com/ircv3/ircv3-specifications/pull/501>`_
(not to be confused with the `deprecated IRCv3 Metadata
<https://ircv3.net/specs/core/metadata-3.2>`_)
"""
import itertools
import pytest
from irctest import cases, runner
from irctest.patma import ANYDICT, ANYLIST, ANYSTR, StrRe
CLIENT_NICKS = {
1: "foo",
2: "bar",
}
class MetadataTestCase(cases.BaseServerTestCase):
def getBatchMessages(self, client):
messages = self.getMessages(client)
first_msg = messages.pop(0)
last_msg = messages.pop(-1)
self.assertMessageMatch(
first_msg, command="BATCH", params=[StrRe(r"\+.*"), "metadata"]
)
batch_id = first_msg.params[0][1:]
self.assertMessageMatch(last_msg, command="BATCH", params=["-" + batch_id])
return (batch_id, messages)
def sub(self, client, keys):
self.sendLine(2, "METADATA * SUB " + " ".join(keys))
acknowledged_subs = []
for m in self.getMessages(2):
self.assertMessageMatch(
m,
command="770", # RPL_METADATASUBOK
params=["bar", *ANYLIST],
)
acknowledged_subs.extend(m.params[1:])
self.assertEqual(
sorted(acknowledged_subs),
sorted(keys),
fail_msg="Expected RPL_METADATASUBOK to ack {expects}, got {got}",
)
@cases.mark_specifications("IRCv3")
def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * GET display-name")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 1, fail_msg="Expected one ERR_NOMATCHINGKEY")
self.assertMessageMatch(
messages[0],
tags={"batch": batch_id, **ANYDICT},
command="766", # ERR_NOMATCHINGKEY
fail_msg="Did not reply with 766 (ERR_NOMATCHINGKEY) to a "
"request to an unset valid METADATA key: {msg}",
)
@cases.mark_specifications("IRCv3")
def testGetTwoUnsetValid(self):
"""“Multiple keys may be given. The response will be either RPL_KEYVALUE,
ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * GET display-name avatar")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 2, fail_msg="Expected two ERR_NOMATCHINGKEY")
self.assertMessageMatch(
messages[0],
command="766", # RPL_KEYNOTSET
fail_msg="Did not reply with 766 (RPL_KEYNOTSET) to a "
"request to two unset valid METADATA key: {msg}",
)
self.assertMessageMatch(
messages[0],
params=["foo", "foo", "display-name", ANYSTR],
fail_msg="Response to “METADATA * GET display-name avatar” "
"did not respond to display-name first: {msg}",
)
self.assertMessageMatch(
messages[1],
command="766", # RPL_KEYNOTSET
fail_msg="Did not reply with two 766 (RPL_KEYNOTSET) to a "
"request to two unset valid METADATA key: {msg}",
)
self.assertMessageMatch(
messages[1],
params=["foo", "foo", "avatar", ANYSTR],
fail_msg="Response to “METADATA * GET display-name avatar” "
"did not respond to avatar as second response: {msg}",
)
@cases.mark_specifications("IRCv3")
def testListNoSet(self):
"""“This subcommand MUST list all currently-set metadata keys along
with their values. The response will be zero or more RPL_KEYVALUE
events, following by RPL_METADATAEND event.
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * LIST")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 0, fail_msg="Expected empty batch")
@cases.mark_specifications("IRCv3")
def testListInvalidTarget(self):
"""“In case of invalid target RPL_METADATAEND MUST NOT be sent.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA foobar LIST")
m = self.getMessage(1)
self.assertMessageMatch(
m,
command="FAIL",
params=["METADATA", "INVALID_TARGET", "foobar", ANYSTR],
fail_msg="Response to “METADATA <invalid target> LIST” was "
"not 765 (ERR_TARGETINVALID) but: {msg}",
)
commands = {m.command for m in self.getMessages(1)}
self.assertNotIn(
"762",
commands,
fail_msg="Sent “METADATA <invalid target> LIST”, got FAIL INVALID_TARGET, "
"and then 762 (RPL_METADATAEND)",
)
def assertSetValue(self, client, target, key, value):
self.sendLine(client, "METADATA {} SET {} :{}".format(target, key, value))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
self.assertMessageMatch(
self.getMessage(client),
command="761", # RPL_KEYVALUE
params=[CLIENT_NICKS[client], target, key, ANYSTR, value],
)
def assertUnsetValue(self, client, target, key):
self.sendLine(client, "METADATA {} SET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
self.assertMessageMatch(
self.getMessage(client),
command="766", # RPL_KEYNOTSET
params=[CLIENT_NICKS[client], target, key, ANYSTR],
)
def assertGetValue(self, client, target, key, value):
self.sendLine(client, "METADATA {} GET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
(batch_id, messages) = self.getBatchMessages(client)
self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE")
self.assertMessageMatch(
messages[0],
command="761", # RPL_KEYVALUE
params=[CLIENT_NICKS[client], target, key, ANYSTR, value],
)
def assertValueNotSet(self, client, target, key):
self.sendLine(client, "METADATA {} GET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
(batch_id, messages) = self.getBatchMessages(client)
self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE")
self.assertMessageMatch(
messages[0],
command="766", # RPL_KEYNOTSET
params=[CLIENT_NICKS[client], target, key, ANYSTR],
)
def assertSetGetValue(self, client, target, key, value):
self.assertSetValue(client, target, key, value)
self.assertGetValue(client, target, key, value)
def assertUnsetGetValue(self, client, target, key):
self.assertUnsetValue(client, target, key)
self.assertValueNotSet(client, target, key)
@pytest.mark.parametrize(
"set_target,get_target", itertools.product(["*", "foo"], ["*", "foo"])
)
@cases.mark_specifications("IRCv3")
def testSetGetUser(self, set_target, get_target):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, set_target, "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetGetUserAgain(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertSetGetValue(1, "*", "display-name", "Foo The Second")
@cases.mark_specifications("IRCv3")
def testSetUnsetUser(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertUnsetGetValue(1, "*", "display-name")
@cases.mark_specifications("IRCv3")
def testGetOtherUser(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# As of 2023-04-15, the Unreal module requires users to share a channel for
# metadata to be visible to each other
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetValue(1, "*", "display-name", "Foo The First")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertGetValue(2, "foo", "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetOtherUser(self):
"""Not required by the spec, but it makes little sense to allow anyone to
write a channel's metadata"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# As of 2023-04-15, the Unreal module requires users to share a channel for
# metadata to be visible to each other.
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.sendLine(1, "METADATA bar SET display-name :Totally Not Foo")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["METADATA", "KEY_NO_PERMISSION", "bar", "display-name", ANYSTR],
)
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
@cases.mark_specifications("IRCv3")
def testSubUser(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["avatar", "display-name"])
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The First"],
)
self.assertSetGetValue(1, "*", "display-name", "Foo The Second")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The Second"],
)
self.assertUnsetGetValue(1, "*", "display-name")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR],
)
@cases.mark_specifications("IRCv3")
def testSubUserSetBeforeJoin(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["display-name", "avatar"])
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="'bar' got message when 'foo' set its display-name even though "
"they don't share a channel",
)
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.sendLine(2, "JOIN #chan")
messages = self.getMessages(2)
metadata_messages = [m for m in messages if m.command == "METADATA"]
self.assertEqual(
len(metadata_messages),
1,
fail_msg="Expected exactly one METADATA message when joining a channel, "
"got: {got}",
)
self.assertMessageMatch(
metadata_messages[0],
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The First"],
)
@cases.mark_specifications("IRCv3")
def testSetGetChannel(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertGetValue(2, "#chan", "display-name", "Hash Channel")
@cases.mark_specifications("IRCv3")
def testSetUnsetChannel(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertUnsetGetValue(1, "#chan", "display-name")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertValueNotSet(2, "#chan", "display-name")
@cases.mark_specifications("IRCv3")
def testSetGetChannelNotOp(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.sendLine(2, "METADATA #chan SET display-name :Sharp Channel")
self.assertMessageMatch(
self.getMessage(2),
command="FAIL",
params=["METADATA", "KEY_NO_PERMISSION", "#chan", "display-name", ANYSTR],
)
self.assertEqual(
self.getMessages(1),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
@cases.mark_specifications("IRCv3")
def testSubChannel(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["avatar", "display-name"])
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Hash Channel"],
)
self.assertSetGetValue(1, "#chan", "display-name", "Harsh Channel")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Harsh Channel"],
)
@cases.mark_specifications("IRCv3")
def testSubChannelSetBeforeJoin(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["display-name", "avatar"])
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="'bar' got message when 'foo' set #chan's display-name even "
"though they are not in it",
)
self.sendLine(2, "JOIN #chan")
messages = self.getMessages(2)
metadata_messages = [m for m in messages if m.command == "METADATA"]
self.assertEqual(
len(metadata_messages),
1,
fail_msg="Expected exactly one METADATA message when joining a channel, "
"got: {got}",
)
self.assertMessageMatch(
metadata_messages[0],
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Hash Channel"],
)
@cases.mark_specifications("IRCv3")
def testSetGetValidBeforeConnect(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.addClient(1)
self.sendLine(1, "CAP LS 302")
caps = self.getCapLs(1)
if "before-connect" not in (caps["draft/metadata-2"] or "").split(","):
raise runner.OptionalExtensionNotSupported(
"draft/metadata-2=before-connect"
)
self.requestCapabilities(1, ["draft/metadata-2", "batch"], skip_if_cap_nak=True)
self.assertSetValue(1, "*", "display-name", "Foo The First")
self.sendLine(1, "NICK foo")
self.sendLine(1, "USER foo 0 * :foo")
self.sendLine(1, "CAP END")
self.skipToWelcome(1)
self.assertGetValue(1, "*", "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetGetHeartInValue(self):
"""“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
heart = b"\xf0\x9f\x92\x9c".decode()
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(
1,
"*",
"display-name",
"->{}<-".format(heart),
)
def _testSetInvalidValue(self, value):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# Sending directly because it is not valid UTF-8 so Python would
# not like it
self.clients[1].conn.sendall(
b"METADATA * SET display-name :invalid UTF-8 ->\xc3<-\r\n"
)
try:
commands = {m.command for m in self.getMessages(1)}
except UnicodeDecodeError:
assert False, "Server sent invalid UTF-8"
self.assertNotIn(
"761",
commands, # RPL_KEYVALUE
fail_msg="Setting METADATA key to a value containing invalid "
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.clients[1].conn.sendall(b"METADATA * SET display-name :" + value + b"\r\n")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["METADATA", "VALUE_INVALID", ANYSTR],
)
messages = self.getMessages(1)
self.assertNotIn(
"761", # RPL_KEYVALUE
{m.command for m in messages},
fail_msg="Setting METADATA key to a value containing invalid "
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.assertEqual(
messages,
[],
fail_msg="Unexpected response to METADATA SET with invalid value: {got}",
)
@cases.mark_specifications("IRCv3")
def testSetInvalidUtf8(self):
"""“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
self._testSetInvalidValue(b"invalid UTF-8: \xc3")
@cases.mark_specifications("IRCv3")
def testSetTooManyChars(self):
"""Assumes all servers reject values over 480 bytes. This isn't required by the
spec, but makes them risk overflowing when printing the value, so they probably
won't allow that.
"""
self._testSetInvalidValue(b"abcd" * 120)
@cases.mark_specifications("IRCv3")
def testSetTooManyBytes(self):
"""Assumes all servers reject values over 480 bytes. This isn't required by the
spec, but makes them risk overflowing when printing the value, so they probably
won't allow that.
"""
heart = b"\xf0\x9f\x92\x9c"
self._testSetInvalidValue(heart * 120)

View File

@ -8,6 +8,7 @@ import pytest
from irctest import cases, runner
from irctest.client_mock import NoMessageException
from irctest.numerics import (
ERR_ERRONEUSNICKNAME,
RPL_ENDOFMONLIST,
RPL_MONLIST,
RPL_MONOFFLINE,
@ -190,14 +191,15 @@ class MonitorTestCase(_BaseMonitorTestCase):
self.check_server_support()
self.sendLine(1, "MONITOR + *!username@localhost")
self.sendLine(1, "MONITOR + *!username@127.0.0.1")
expected_command = StrRe(f"({RPL_MONOFFLINE}|{ERR_ERRONEUSNICKNAME})")
try:
m = self.getMessage(1)
self.assertMessageMatch(m, command="731")
self.assertMessageMatch(m, command=expected_command)
except NoMessageException:
pass
else:
m = self.getMessage(1)
self.assertMessageMatch(m, command="731")
self.assertMessageMatch(m, command=expected_command)
self.connectClient("bar")
try:
m = self.getMessage(1)

View File

@ -24,11 +24,6 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
self.sendLine(1, "NAMES #chan")
reply = self.getMessage(1)
self.assertMessageMatch(
reply,
command="353",
fail_msg="Expected NAMES response (353) with @+foo, got: {msg}",
)
self.assertMessageMatch(
reply,
command="353",
@ -47,9 +42,57 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
8,
"Expected WHO response (352) with 8 params, got: {msg}".format(msg=msg),
)
self.assertTrue(
"@+" in msg.params[6],
self.assertIn(
"@+",
msg.params[6],
'Expected WHO response (352) with "@+" in param 7, got: {msg}'.format(
msg=msg
),
)
@cases.xfailIfSoftware(
["irc2", "Bahamut"], "irc2 and Bahamut send a trailing space"
)
def testNoMultiPrefix(self):
"""When not requested, only the highest prefix should be sent"""
self.connectClient("foo")
self.joinChannel(1, "#chan")
self.sendLine(1, "MODE #chan +v foo")
self.getMessages(1)
# TODO(dan): Make sure +v is voice
self.sendLine(1, "NAMES #chan")
reply = self.getMessage(1)
self.assertMessageMatch(
reply,
command="353",
params=["foo", ANYSTR, "#chan", "@foo"],
fail_msg="Expected NAMES response (353) with @foo, got: {msg}",
)
self.getMessages(1)
self.sendLine(1, "WHO #chan")
msg = self.getMessage(1)
self.assertEqual(
msg.command, "352", msg, fail_msg="Expected WHO response (352), got: {msg}"
)
self.assertGreaterEqual(
len(msg.params),
8,
"Expected WHO response (352) with 8 params, got: {msg}".format(msg=msg),
)
self.assertIn(
"@",
msg.params[6],
'Expected WHO response (352) with "@" in param 7, got: {msg}'.format(
msg=msg
),
)
self.assertNotIn(
"+",
msg.params[6],
'Expected WHO response (352) with no "+" in param 7, got: {msg}'.format(
msg=msg
),
)

View File

@ -11,7 +11,7 @@ from irctest.patma import ANYSTR, StrRe
class NamesTestCase(cases.BaseServerTestCase):
def _testNames(self, symbol):
def _testNames(self, symbol: bool, allow_trailing_space: bool):
self.connectClient("nick1")
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
@ -31,7 +31,10 @@ class NamesTestCase(cases.BaseServerTestCase):
"nick1",
*(["="] if symbol else []),
"#chan",
StrRe("(nick2 @nick1|@nick1 nick2)"),
StrRe(
"(nick2 @nick1|@nick1 nick2)"
+ (" ?" if allow_trailing_space else "")
),
],
)
@ -44,20 +47,59 @@ class NamesTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", deprecated=True)
def testNames1459(self):
"""
https://modern.ircdocs.horse/#names-message
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNames(symbol=False)
self._testNames(symbol=False, allow_trailing_space=True)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.mark_specifications("RFC2812", "Modern")
def testNames2812(self):
"""
https://modern.ircdocs.horse/#names-message
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
"""
self._testNames(symbol=True)
self._testNames(symbol=True, allow_trailing_space=True)
@cases.mark_specifications("Modern")
@cases.xfailIfSoftware(
["Bahamut", "irc2"], "Bahamut and irc2 send a trailing space in RPL_NAMREPLY"
)
def testNamesModern(self):
"""
https://modern.ircdocs.horse/#names-message
"""
self._testNames(symbol=True, allow_trailing_space=False)
@cases.mark_specifications("RFC2812", "Modern")
def testNames2812Secret(self):
"""The symbol sent for a secret channel is `@` instead of `=`:
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
https://modern.ircdocs.horse/#rplnamreply-353
"""
self.connectClient("nick1")
self.sendLine(1, "JOIN #chan")
# enable secret channel mode
self.sendLine(1, "MODE #chan +s")
self.getMessages(1)
self.sendLine(1, "NAMES #chan")
messages = self.getMessages(1)
self.assertMessageMatch(
messages[0],
command=RPL_NAMREPLY,
params=["nick1", "@", "#chan", StrRe("@nick1 ?")],
)
self.assertMessageMatch(
messages[1],
command=RPL_ENDOFNAMES,
params=["nick1", "#chan", ANYSTR],
)
self.connectClient("nick2")
self.sendLine(2, "JOIN #chan")
namreplies = [msg for msg in self.getMessages(2) if msg.command == RPL_NAMREPLY]
self.assertNotEqual(len(namreplies), 0)
for msg in namreplies:
self.assertMessageMatch(
msg, command=RPL_NAMREPLY, params=["nick2", "@", "#chan", ANYSTR]
)
def _testNamesMultipleChannels(self, symbol):
self.connectClient("nick1")

View File

@ -57,8 +57,16 @@ class Utf8TestCase(cases.BaseServerTestCase):
self.sendLine(2, "NICK bar")
self.clients[2].conn.sendall(b"USER username * * :i\xe8rc\xe9\r\n")
d = self.clients[2].conn.recv(1024)
if b"FAIL " in d or b"468 " in d: # ERR_INVALIDUSERNAME
d = b""
while True:
try:
buf = self.clients[2].conn.recv(1024)
except TimeoutError:
break
if d and not buf:
break
d += buf
if b"FAIL " in d or b"ERROR " in d or b"468 " in d: # ERR_INVALIDUSERNAME
return # nothing more to test
self.assertIn(b"001 ", d)
@ -72,14 +80,21 @@ class Utf8TestCase(cases.BaseServerTestCase):
self.addClient()
self.sendLine(2, "NICK bar")
self.sendLine(2, "USER 😊😊😊😊😊😊😊😊😊😊 * * :realname")
m = self.getRegistrationMessage(2)
if m.command in ("FAIL", "468"): # ERR_INVALIDUSERNAME
self.clients[2].conn.sendall(b"USER \xe8rc\xe9 * * :readlname\r\n")
d = b""
while True:
try:
buf = self.clients[2].conn.recv(1024)
except TimeoutError:
break
if d and not buf:
break
d += buf
if b"FAIL " in d or b"ERROR " in d or b"468 " in d: # ERR_INVALIDUSERNAME
return # nothing more to test
self.assertMessageMatch(
m,
command="001",
)
self.assertIn(b"001 ", d)
self.sendLine(2, "WHOIS bar")
self.getMessages(2)

View File

@ -87,7 +87,7 @@ class BaseWhoTestCase:
class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testWhoStar(self):
if self.controller.software_name in ("Bahamut", "Sable"):
if self.controller.software_name in ("Bahamut",):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -118,7 +118,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNick(self, mask):
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
if "*" in mask and self.controller.software_name in ("Bahamut",):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -148,7 +148,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
ids=["username", "realname-mask", "hostname"],
)
def testWhoUsernameRealName(self, mask):
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
if "*" in mask and self.controller.software_name in ("Bahamut",):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -201,7 +201,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNickAway(self, mask):
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
if "*" in mask and self.controller.software_name in ("Bahamut",):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -235,7 +235,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNickOper(self, mask):
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
if "*" in mask and self.controller.software_name in ("Bahamut",):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -274,7 +274,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNickAwayAndOper(self, mask):
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
if "*" in mask and self.controller.software_name in ("Bahamut",):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -308,7 +308,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
@pytest.mark.parametrize("mask", ["#chan", "#CHAN"], ids=["exact", "casefolded"])
@cases.mark_specifications("Modern")
def testWhoChan(self, mask):
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
if "*" in mask and self.controller.software_name in ("Bahamut",):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -632,7 +632,7 @@ class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
class WhoInvisibleTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testWhoInvisible(self):
if self.controller.software_name in ("Bahamut", "Sable"):
if self.controller.software_name in ("Bahamut",):
raise runner.OptionalExtensionNotSupported("WHO mask")
self.connectClient("evan", name="evan")

View File

@ -97,7 +97,9 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
params=[
"nick1",
"nick2",
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1)"),
# trailing space was required by the RFCs, and Modern explicitly
# allows it
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1) ?"),
],
)
elif m.command == RPL_WHOISSPECIAL:

View File

@ -154,6 +154,9 @@ class WhowasTestCase(cases.BaseServerTestCase):
except ConnectionClosed:
pass
if self.controller.software_name == "Sable":
time.sleep(1) # may take a little while to record the historical user
self.sendLine(1, whowas_command)
messages = self.getMessages(1)

View File

@ -27,6 +27,7 @@ markers =
extended-monitor
labeled-response
message-tags
metadata-2
draft/multiline
multi-prefix
server-time

View File

@ -230,7 +230,7 @@ software:
name: ngircd
repository: ngircd/ngircd
refs:
stable: 0714466af88d71d6c395629cd7fb624b099507d4 # two years ahead of rel-26.1
stable: 3e3f6cbeceefd9357b53b27c2386bb39306ab353 # three years ahead of rel-26.1
release: null
devel: master
devel_release: null
@ -249,7 +249,7 @@ software:
name: Sable
repository: Libera-Chat/sable
refs:
stable: dcf8b53cac54f460b86861908d36d67969cf1eb2
stable: b9deaa930c49f2939d9a584bedbfc3236da0d707
release: null
devel: master
devel_release: null