Compare commits

...

29 Commits

Author SHA1 Message Date
Val Lorentz 886404bc3c
Merge 0816232c1c 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
Sadie Powell 61ae4bcf9e
Relink the modules directory as well as the lib directory. (#260) 2024-04-04 17:47:48 +02:00
Sadie Powell 0c5c91368a
Pass --nopid to Anope. (#259) 2024-03-21 21:04:13 +01:00
Shivaram Lingamneni c0e6ca4dde
add a test for WHOIS on nonexistent users (#258)
* add a test for WHOIS on nonexistent users

* skip test in Sable for now
2024-03-19 10:30:44 -04:00
Valentin Lorentz e6d54db9ce Fix chkNS on recent hybrid
Since e66f61f8a0
(which is itself a fix for 79c4eb8d75),
Hybrid sends this numeric right after the MOTD
2024-03-17 10:12:55 +01:00
Val Lorentz 03b6fbbfc2
Fix support for latest Anope 2.1 (#257)
enc_sha256 cannot be loaded since 6e0f0b8896
2024-03-10 11:20:57 +01:00
Shivaram Lingamneni ee6c56d84b
basic 005 parameter validation test (#255)
* basic 005 parameter validation test

The overall order of the registration burst is covered by
ConnectionRegistrationTestCase.testConnectionRegistration and doesn't need
to be checked here.

* Update irctest/server_tests/isupport.py

Co-authored-by: Val Lorentz <progval+github@progval.net>

---------

Co-authored-by: Val Lorentz <progval+github@progval.net>
2024-02-12 23:29:23 -05:00
Shivaram Lingamneni 85b519d93a
ci: upgrade actions/setup-go (#254) 2024-02-11 19:48:38 +01:00
Valentin Lorentz 56e0565512 Update Go 2024-02-11 19:29:58 +01:00
Shivaram Lingamneni df2880e379
add an incorrect password test for PLAIN (#253)
* add an incorrect password test for PLAIN

* derace test (hopefully)
2024-02-08 00:45:11 -05:00
Val Lorentz 61a6f047d2
Add support for '*' in place of server name in RPL_WHOREPLY/RPL_WHOSPCRPL (#252)
Sable users are no longer associated with a server, so it now returns
a blank where their server name used to be:
93ab9afa5c
2024-02-07 19:35:02 +01:00
Valentin Lorentz 0816232c1c Fix sync issue 2022-11-16 15:39:33 +01:00
Valentin Lorentz 3319920250 Check behavior of PRIVMSG when banned 2022-11-16 14:38:35 +01:00
27 changed files with 565 additions and 320 deletions

View File

@ -550,9 +550,9 @@ jobs:
path: ergo
ref: master
repository: ergochat/ergo
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: ^1.21.0
go-version: ^1.22.0
- run: go version
- name: Build Ergo
run: |

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: |
@ -626,9 +626,9 @@ jobs:
path: ergo
ref: irctest_stable
repository: ergochat/ergo
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: ^1.21.0
go-version: ^1.22.0
- run: go version
- name: Build Ergo
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

@ -372,6 +372,8 @@ class BaseServicesController(_BaseController):
pass
elif msg.command in ("MODE", "221"): # RPL_UMODEIS
pass
elif msg.command == "396": # RPL_VISIBLEHOST
pass
elif msg.command == "NOTICE":
assert msg.prefix is not None
if "!" not in msg.prefix and "." in msg.prefix:

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

@ -67,7 +67,7 @@ options {{
}}
module {{ name = "{module_prefix}sasl" }}
module {{ name = "enc_sha256" }}
module {{ name = "enc_bcrypt" }}
module {{ name = "ns_cert" }}
"""
@ -132,14 +132,16 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
# Config and code need to be in the same directory, *obviously*
(self.directory / "lib").symlink_to(Path(services_path).parent.parent / "lib")
(self.directory / "modules").symlink_to(
Path(services_path).parent.parent / "modules"
)
self.proc = subprocess.Popen(
[
"anope",
"-n", # don't fork
"--config=services.conf", # can't be an absolute path
# "--logdir",
# f"/tmp/services-{server_port}.log",
"--config=services.conf", # can't be an absolute path in 2.0
"--nofork", # don't fork
"--nopid", # don't write a pid
],
cwd=self.directory,
# stdout=subprocess.DEVNULL,

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

@ -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

@ -7,14 +7,18 @@ and ban exception (`Modern <https://modern.ircdocs.horse/#exception-channel-mode
"""
from irctest import cases, runner
from irctest.numerics import ERR_BANNEDFROMCHAN, RPL_BANLIST, RPL_ENDOFBANLIST
from irctest.numerics import (
ERR_BANNEDFROMCHAN,
ERR_CANNOTSENDTOCHAN,
RPL_BANLIST,
RPL_ENDOFBANLIST,
)
from irctest.patma import ANYSTR, StrRe
class BanModeTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
def testBan(self):
"""Basic ban operation"""
def testBanJoin(self):
self.connectClient("chanop", name="chanop")
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")
@ -32,6 +36,55 @@ class BanModeTestCase(cases.BaseServerTestCase):
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
@cases.mark_specifications("Modern")
def testBanPrivmsg(self):
"""
TODO: this checks the following quote is false:
"If `<target>` is a channel name and the client is [banned](#ban-channel-mode)
and not covered by a [ban exception](#ban-exception-channel-mode), the
message will not be delivered and the command will silently fail."
-- https://modern.ircdocs.horse/#privmsg-message
to check https://github.com/ircdocs/modern-irc/pull/201
"""
self.connectClient("chanop", name="chanop")
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")
self.connectClient("Bar", name="bar")
self.getMessages("bar")
self.sendLine("bar", "JOIN #chan")
self.getMessages("bar")
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan +b bar!*@*")
self.assertMessageMatch(self.getMessage("chanop"), command="MODE")
self.getMessages("chanop")
self.getMessages("bar")
self.sendLine("bar", "PRIVMSG #chan :hello world")
self.assertMessageMatch(
self.getMessage("bar"),
command=ERR_CANNOTSENDTOCHAN,
params=["Bar", "#chan", ANYSTR],
)
self.assertEqual(self.getMessages("bar"), [])
self.assertEqual(self.getMessages("chanop"), [])
self.sendLine("chanop", "MODE #chan -b bar!*@*")
self.assertMessageMatch(self.getMessage("chanop"), command="MODE")
self.getMessages("chanop")
self.getMessages("bar")
self.sendLine("bar", "PRIVMSG #chan :hello again")
self.assertEqual(self.getMessages("bar"), [])
self.assertMessageMatch(
self.getMessage("chanop"),
command="PRIVMSG",
params=["#chan", "hello again"],
)
@cases.mark_specifications("Modern")
def testBanList(self):
"""`RPL_BANLIST <https://modern.ircdocs.horse/#rplbanlist-367>`_"""

View File

@ -9,6 +9,38 @@ from irctest import cases, runner
class IsupportTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
@cases.mark_isupport("PREFIX")
def testParameters(self):
"""https://modern.ircdocs.horse/#rplisupport-005"""
# <https://modern.ircdocs.horse/#connection-registration>
# "Upon successful completion of the registration process,
# the server MUST send, in this order:
# [...]
# 5. at least one RPL_ISUPPORT (005) numeric to the client."
welcome_005s = [
msg for msg in self.connectClient("foo") if msg.command == "005"
]
self.assertGreaterEqual(len(welcome_005s), 1)
for msg in welcome_005s:
# first parameter is the client's nickname;
# last parameter is a human-readable trailing, typically
# "are supported by this server"
self.assertGreaterEqual(len(msg.params), 3)
self.assertEqual(msg.params[0], "foo")
# "As the maximum number of message parameters to any reply is 15,
# the maximum number of RPL_ISUPPORT tokens that can be advertised
# is 13."
self.assertLessEqual(len(msg.params), 15)
for param in msg.params[1:-1]:
self.validateIsupportParam(param)
def validateIsupportParam(self, param):
if not param.isascii():
raise ValueError("Invalid non-ASCII 005 parameter", param)
# TODO add more validation
@cases.mark_specifications("Modern")
@cases.mark_isupport("PREFIX")
def testPrefix(self):
@ -24,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,
@ -85,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

@ -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

@ -1,7 +1,7 @@
import base64
from irctest import cases, runner, scram
from irctest.numerics import ERR_SASLFAIL
from irctest.numerics import ERR_SASLFAIL, RPL_LOGGEDIN, RPL_SASLMECHS
from irctest.patma import ANYSTR
@ -48,11 +48,37 @@ class SaslTestCase(cases.BaseServerTestCase):
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="900",
command=RPL_LOGGEDIN,
params=[ANYSTR, ANYSTR, "jilles", ANYSTR],
fail_msg="Unexpected reply to correct SASL authentication: {msg}",
)
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN")
def testPlainFailure(self):
"""PLAIN authentication with incorrect username/password."""
self.controller.registerUser(self, "jilles", "sesame")
self.addClient()
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE PLAIN")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="AUTHENTICATE",
params=["+"],
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
)
# password 'millet'
self.sendLine(1, "AUTHENTICATE amlsbGVzAGppbGxlcwBtaWxsZXQ=")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command=ERR_SASLFAIL,
params=[ANYSTR, ANYSTR],
fail_msg="Unexpected reply to incorrect SASL authentication: {msg}",
)
@cases.mark_specifications("IRCv3")
@cases.skipUnlessHasMechanism("PLAIN")
def testPlainNonAscii(self):
@ -161,11 +187,11 @@ class SaslTestCase(cases.BaseServerTestCase):
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
self.sendLine(1, "AUTHENTICATE FOO")
m = self.getRegistrationMessage(1)
while m.command == "908": # RPL_SASLMECHS
while m.command == RPL_SASLMECHS:
m = self.getRegistrationMessage(1)
self.assertMessageMatch(
m,
command="904",
command=ERR_SASLFAIL,
fail_msg="Did not reply with 904 to “AUTHENTICATE FOO”: {msg}",
)

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

@ -60,7 +60,7 @@ class BaseWhoTestCase:
"*", # no chan
StrRe("~?" + self.username),
StrRe(host_re),
"My.Little.Server",
StrRe(r"(My.Little.Server|\*)"),
"coolNick",
flags,
StrRe(realname_regexp(self.realname)),
@ -76,7 +76,7 @@ class BaseWhoTestCase:
"#chan",
StrRe("~?" + self.username),
StrRe(host_re),
"My.Little.Server",
StrRe(r"(My.Little.Server|\*)"),
"coolNick",
flags + "@",
StrRe(realname_regexp(self.realname)),
@ -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()
@ -336,7 +336,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
"#chan",
StrRe("~?" + self.username),
StrRe(host_re),
"My.Little.Server",
StrRe(r"(My.Little.Server|\*)"),
"coolNick",
"G@",
StrRe(realname_regexp(self.realname)),
@ -351,7 +351,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
"#chan",
ANYSTR,
ANYSTR,
"My.Little.Server",
StrRe(r"(My.Little.Server|\*)"),
"otherNick",
"H",
StrRe("[0-9]+ .*"),
@ -398,7 +398,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
chan,
ANYSTR,
ANYSTR,
"My.Little.Server",
StrRe(r"(My.Little.Server|\*)"),
"coolNick",
ANYSTR,
ANYSTR,
@ -413,7 +413,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
chan,
ANYSTR,
ANYSTR,
"My.Little.Server",
StrRe(r"(My.Little.Server|\*)"),
"otherNick",
ANYSTR,
ANYSTR,
@ -479,7 +479,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
StrRe("~?myusernam"),
ANYSTR,
ANYSTR,
"My.Little.Server",
StrRe(r"(My.Little.Server|\*)"),
"coolNick",
StrRe("H@?"),
ANYSTR, # hopcount
@ -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

@ -8,6 +8,7 @@ import pytest
from irctest import cases
from irctest.numerics import (
ERR_NOSUCHNICK,
RPL_AWAY,
RPL_ENDOFWHOIS,
RPL_WHOISACCOUNT,
@ -96,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:
@ -217,6 +220,25 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
whois_user.params[3], [nick, username, "~" + username, realname]
)
@cases.mark_specifications("RFC2812")
@cases.xfailIfSoftware(["Sable"], "https://github.com/Libera-Chat/sable/issues/101")
def testWhoisMissingUser(self):
"""Test WHOIS on a nonexistent nickname."""
self.connectClient("qux", name="qux")
self.sendLine("qux", "WHOIS bar")
messages = self.getMessages("qux")
self.assertEqual(len(messages), 2)
self.assertMessageMatch(
messages[0],
command=ERR_NOSUCHNICK,
params=["qux", "bar", ANYSTR],
)
self.assertMessageMatch(
messages[1],
command=RPL_ENDOFWHOIS,
params=["qux", "bar", ANYSTR],
)
@pytest.mark.parametrize(
"away,oper",
[(False, False), (True, False), (False, True)],

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

@ -134,9 +134,9 @@ software:
path: ergo
prefix: ~/go
pre_deps:
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: '^1.21.0'
go-version: '^1.22.0'
- run: go version
separate_build_job: false
build_script: |
@ -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