mirror of https://github.com/progval/irctest.git
Compare commits
38 Commits
ab7ae58653
...
281b887e7d
Author | SHA1 | Date | |
---|---|---|---|
Val Lorentz | 281b887e7d | ||
Val Lorentz | df626de5ed | ||
Val Lorentz | 79223d35f1 | ||
Shivaram Lingamneni | 723991c7ec | ||
Valentin Lorentz | 1bc8741479 | ||
Val Lorentz | 9f8e712776 | ||
Val Lorentz | a1f8fcac49 | ||
Valentin Lorentz | d3c919e0f5 | ||
Val Lorentz | ce51dddc15 | ||
Val Lorentz | 7f9b4b315f | ||
Val Lorentz | 9d43a002c2 | ||
Val Lorentz | ea66a8f9a4 | ||
Valentin Lorentz | 473db1cc5b | ||
Val Lorentz | f4a01cfe49 | ||
Val Lorentz | e6dfb87759 | ||
Val Lorentz | 2ae612c68f | ||
Val Lorentz | d908699674 | ||
Sadie Powell | 61ae4bcf9e | ||
Sadie Powell | 0c5c91368a | ||
Shivaram Lingamneni | c0e6ca4dde | ||
Valentin Lorentz | e6d54db9ce | ||
Val Lorentz | 03b6fbbfc2 | ||
Shivaram Lingamneni | ee6c56d84b | ||
Shivaram Lingamneni | 85b519d93a | ||
Valentin Lorentz | 56e0565512 | ||
Shivaram Lingamneni | df2880e379 | ||
Val Lorentz | 61a6f047d2 | ||
Val Lorentz | d75e3fae34 | ||
Sadie Powell | 0ebfbdf6ab | ||
Sadie Powell | 0f6a485d7d | ||
Sadie Powell | dfd429014a | ||
Val Lorentz | d9ad638791 | ||
Sadie Powell | 246a259111 | ||
Mitchell Riley | 18d04e8f80 | ||
Shivaram Lingamneni | 6425e707ac | ||
Shivaram Lingamneni | 032d0e32de | ||
Shivaram Lingamneni | 62a039498b | ||
Valentin Lorentz | 6243908ecc |
|
@ -25,15 +25,15 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: anope
|
||||
ref: 2.0.9
|
||||
ref: '2.1'
|
||||
repository: anope/anope
|
||||
- name: Build Anope
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/anope/
|
||||
cp $GITHUB_WORKSPACE/data/anope/* .
|
||||
CFLAGS=-O0 ./Config -quick
|
||||
make -C build -j 4
|
||||
make -C build install
|
||||
sudo apt-get install ninja-build --no-install-recommends
|
||||
mkdir build && cd build
|
||||
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local/ -DPROGRAM_NAME=anope -DUSE_PCH=ON -GNinja ..
|
||||
ninja install
|
||||
- name: Make artefact tarball
|
||||
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
|
||||
- name: Upload build artefacts
|
||||
|
@ -153,12 +153,7 @@ jobs:
|
|||
- name: Build InspIRCd
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/inspircd/
|
||||
|
||||
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
|
||||
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
|
||||
|
||||
./configure --prefix=$HOME/.local/inspircd --development
|
||||
|
||||
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
||||
make install
|
||||
- name: Make artefact tarball
|
||||
|
@ -555,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: |
|
||||
|
|
|
@ -25,15 +25,15 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: anope
|
||||
ref: 2.0.9
|
||||
ref: '2.0'
|
||||
repository: anope/anope
|
||||
- name: Build Anope
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/anope/
|
||||
cp $GITHUB_WORKSPACE/data/anope/* .
|
||||
CFLAGS=-O0 ./Config -quick
|
||||
make -C build -j 4
|
||||
make -C build install
|
||||
sudo apt-get install ninja-build --no-install-recommends
|
||||
mkdir build && cd build
|
||||
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local/ -DPROGRAM_NAME=anope -DUSE_PCH=ON -GNinja ..
|
||||
ninja install
|
||||
- name: Make artefact tarball
|
||||
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
|
||||
- name: Upload build artefacts
|
||||
|
@ -61,12 +61,7 @@ jobs:
|
|||
- name: Build InspIRCd
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/inspircd/
|
||||
|
||||
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
|
||||
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
|
||||
|
||||
./configure --prefix=$HOME/.local/inspircd --development
|
||||
|
||||
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
||||
make install
|
||||
- name: Make artefact tarball
|
||||
|
|
|
@ -25,15 +25,15 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: anope
|
||||
ref: 2.0.9
|
||||
ref: 2.0.14
|
||||
repository: anope/anope
|
||||
- name: Build Anope
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/anope/
|
||||
cp $GITHUB_WORKSPACE/data/anope/* .
|
||||
CFLAGS=-O0 ./Config -quick
|
||||
make -C build -j 4
|
||||
make -C build install
|
||||
sudo apt-get install ninja-build --no-install-recommends
|
||||
mkdir build && cd build
|
||||
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local/ -DPROGRAM_NAME=anope -DUSE_PCH=ON -GNinja ..
|
||||
ninja install
|
||||
- name: Make artefact tarball
|
||||
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
|
||||
- name: Upload build artefacts
|
||||
|
@ -189,17 +189,12 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: inspircd
|
||||
ref: v3.15.0
|
||||
ref: v3.17.0
|
||||
repository: inspircd/inspircd
|
||||
- name: Build InspIRCd
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/inspircd/
|
||||
|
||||
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
|
||||
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
|
||||
|
||||
./configure --prefix=$HOME/.local/inspircd --development
|
||||
|
||||
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
||||
make install
|
||||
- name: Make artefact tarball
|
||||
|
@ -233,7 +228,7 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ngircd
|
||||
ref: 0714466af88d71d6c395629cd7fb624b099507d4
|
||||
ref: 3e3f6cbeceefd9357b53b27c2386bb39306ab353
|
||||
repository: ngircd/ngircd
|
||||
- name: Build ngircd
|
||||
run: |
|
||||
|
@ -631,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: |
|
||||
|
@ -1111,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
|
||||
|
|
8
Makefile
8
Makefile
|
@ -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 := \
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
INSTDIR="$HOME/.local/"
|
||||
RUNGROUP=""
|
||||
UMASK=077
|
||||
DEBUG="yes"
|
||||
USE_PCH="yes"
|
||||
EXTRA_INCLUDE_DIRS=""
|
||||
EXTRA_LIB_DIRS=""
|
||||
EXTRA_CONFIG_ARGS=""
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import functools
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Type
|
||||
from typing import Tuple, Type
|
||||
|
||||
from irctest.basecontrollers import BaseServicesController, DirectoryBasedController
|
||||
|
||||
|
@ -48,6 +49,8 @@ module {{
|
|||
client = "NickServ"
|
||||
forceemail = no
|
||||
passlen = 1000 # Some tests need long passwords
|
||||
maxpasslen = 1000
|
||||
minpasslen = 1
|
||||
}}
|
||||
command {{ service = "NickServ"; name = "HELP"; command = "generic/help"; }}
|
||||
|
||||
|
@ -63,17 +66,28 @@ options {{
|
|||
warningtimeout = 4h
|
||||
}}
|
||||
|
||||
module {{ name = "m_sasl" }}
|
||||
module {{ name = "enc_sha256" }}
|
||||
module {{ name = "{module_prefix}sasl" }}
|
||||
module {{ name = "enc_bcrypt" }}
|
||||
module {{ name = "ns_cert" }}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def installed_version() -> Tuple[int, ...]:
|
||||
output = subprocess.run(
|
||||
["anope", "--version"], stdout=subprocess.PIPE, universal_newlines=True
|
||||
).stdout
|
||||
(anope, version, *trailing) = output.split()[0].split("-")
|
||||
assert anope == "Anope"
|
||||
return tuple(map(int, version.split(".")))
|
||||
|
||||
|
||||
class AnopeController(BaseServicesController, DirectoryBasedController):
|
||||
"""Collaborator for server controllers that rely on Anope"""
|
||||
|
||||
software_name = "Anope"
|
||||
software_version = None
|
||||
|
||||
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
||||
self.create_config()
|
||||
|
@ -88,32 +102,46 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
|
|||
"ngircd",
|
||||
)
|
||||
|
||||
assert self.directory
|
||||
services_path = shutil.which("anope")
|
||||
assert services_path
|
||||
|
||||
# Rewrite Anope 2.0 module names for 2.1
|
||||
if not self.software_version:
|
||||
self.software_version = installed_version()
|
||||
if self.software_version >= (2, 1, 0):
|
||||
if protocol == "charybdis":
|
||||
protocol = "solanum"
|
||||
elif protocol == "inspircd3":
|
||||
protocol = "inspircd"
|
||||
elif protocol == "unreal4":
|
||||
protocol = "unrealircd"
|
||||
|
||||
with self.open_file("conf/services.conf") as fd:
|
||||
fd.write(
|
||||
TEMPLATE_CONFIG.format(
|
||||
protocol=protocol,
|
||||
server_hostname=server_hostname,
|
||||
server_port=server_port,
|
||||
module_prefix="" if self.software_version >= (2, 1, 2) else "m_",
|
||||
)
|
||||
)
|
||||
|
||||
with self.open_file("conf/empty_file") as fd:
|
||||
pass
|
||||
|
||||
assert self.directory
|
||||
services_path = shutil.which("services")
|
||||
assert services_path
|
||||
|
||||
# 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(
|
||||
[
|
||||
"services",
|
||||
"-n", # don't fork
|
||||
"--config=services.conf", # can't be an absolute path
|
||||
# "--logdir",
|
||||
# f"/tmp/services-{server_port}.log",
|
||||
"anope",
|
||||
"--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,
|
||||
|
|
|
@ -14,6 +14,7 @@ BASE_CONFIG = {
|
|||
"name": "My.Little.Server",
|
||||
"listeners": {},
|
||||
"max-sendq": "16k",
|
||||
"casemapping": "ascii",
|
||||
"connection-limits": {
|
||||
"enabled": True,
|
||||
"cidr-len-ipv4": 32,
|
||||
|
|
|
@ -104,6 +104,7 @@ def installed_version() -> int:
|
|||
|
||||
class InspircdController(BaseServerController, DirectoryBasedController):
|
||||
software_name = "InspIRCd"
|
||||
software_version = installed_version()
|
||||
supported_sasl_mechanisms = {"PLAIN"}
|
||||
supports_sts = False
|
||||
extban_mute_char = "m"
|
||||
|
|
|
@ -23,6 +23,7 @@ TEMPLATE_CONFIG = """
|
|||
|
||||
[Options]
|
||||
MorePrivacy = no # by default, always replies to WHOWAS with ERR_WASNOSUCHNICK
|
||||
PAM = no
|
||||
|
||||
[Operator]
|
||||
Name = operuser
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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]]:
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,8 +12,8 @@ class ConfusablesTestCase(cases.BaseServerTestCase):
|
|||
@staticmethod
|
||||
def config() -> cases.TestCaseControllerConfig:
|
||||
return cases.TestCaseControllerConfig(
|
||||
ergo_config=lambda config: config["accounts"].update(
|
||||
{"nick-reservation": {"enabled": True, "method": "strict"}}
|
||||
ergo_config=lambda config: config["server"].update(
|
||||
{"casemapping": "precis"},
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -42,14 +42,21 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
|||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
|
||||
# case change: both alice and bob should get a successful nick line
|
||||
# 'alice' is claimed, so 'Alice' is reserved and Bob cannot take it:
|
||||
self.sendLine(2, "NICK Alice")
|
||||
ms = self.getMessages(2)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageMatch(ms[0], command=ERR_NICKNAMEINUSE)
|
||||
|
||||
# but alice can change case to 'Alice'; both alice and bob should get
|
||||
# a successful NICK line
|
||||
self.sendLine(1, "NICK Alice")
|
||||
ms = self.getMessages(1)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageMatch(ms[0], command="NICK", params=["Alice"])
|
||||
self.assertMessageMatch(ms[0], nick="alice", command="NICK", params=["Alice"])
|
||||
ms = self.getMessages(2)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageMatch(ms[0], command="NICK", params=["Alice"])
|
||||
self.assertMessageMatch(ms[0], nick="alice", command="NICK", params=["Alice"])
|
||||
|
||||
# no responses, either to the user or to friends, from a no-op nick change
|
||||
self.sendLine(1, "NICK Alice")
|
||||
|
@ -190,3 +197,27 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
|||
self.sendLine(2, "USER u s e r")
|
||||
reply = self.getRegistrationMessage(2)
|
||||
self.assertMessageMatch(reply, command=RPL_WELCOME)
|
||||
|
||||
@cases.mark_specifications("IRCv3")
|
||||
def testLabeledNick(self):
|
||||
"""
|
||||
InspIRCd up to 3.16.1 used the new nick as source of NICK changes
|
||||
|
||||
https://github.com/inspircd/inspircd/issues/2067
|
||||
|
||||
https://github.com/inspircd/inspircd/commit/83f01b36a11734fd91a4e7aad99c15463858fe4a
|
||||
"""
|
||||
self.connectClient(
|
||||
"alice",
|
||||
capabilities=["batch", "labeled-response"],
|
||||
skip_if_cap_nak=True,
|
||||
)
|
||||
|
||||
self.sendLine(1, "@label=abc NICK alice2")
|
||||
self.assertMessageMatch(
|
||||
self.getMessage(1),
|
||||
nick="alice",
|
||||
command="NICK",
|
||||
params=["alice2"],
|
||||
tags={"label": "abc", **ANYDICT},
|
||||
)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import base64
|
||||
from typing import List
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -11,8 +12,34 @@ class RegistrationTestCase(cases.BaseServerTestCase):
|
|||
self.controller.registerUser(self, "testuser", "mypassword")
|
||||
|
||||
|
||||
@cases.mark_services
|
||||
class SaslTestCase(cases.BaseServerTestCase):
|
||||
class _BaseSasl(cases.BaseServerTestCase):
|
||||
sasl_ir: bool
|
||||
capabilities: List[str]
|
||||
|
||||
def _doInitialExchange(self, client, mechanism: str, chunk: str):
|
||||
"""Does the initial C->S, S->C, C->S exchange.
|
||||
|
||||
With ``sasl_ir=False``, this is done with the usual three messages exchange
|
||||
(``AUTHENTICATE <mechanism>``, ``AUTHENTICATE +``, ``AUTHENTICATE <chunk>``)
|
||||
with ``sasl_ir=True``, this is done in a single C->S message
|
||||
(``AUTHENTICATE <mechanism> <chunk>``)
|
||||
|
||||
See the [sasl-ir spec](https://github.com/ircv3/ircv3-specifications/pull/520)
|
||||
"""
|
||||
if self.sasl_ir:
|
||||
self.sendLine(client, f"AUTHENTICATE {mechanism} {chunk}")
|
||||
else:
|
||||
self.sendLine(client, f"AUTHENTICATE {mechanism}")
|
||||
m = self.getRegistrationMessage(1)
|
||||
self.assertMessageMatch(
|
||||
m,
|
||||
command="AUTHENTICATE",
|
||||
params=["+"],
|
||||
fail_msg=f"Sent “AUTHENTICATE {mechanism}”, server should have "
|
||||
f"replied with “AUTHENTICATE +”, but instead sent: {{msg}}",
|
||||
)
|
||||
self.sendLine(client, f"AUTHENTICATE {chunk}")
|
||||
|
||||
@cases.mark_specifications("IRCv3")
|
||||
@cases.skipUnlessHasMechanism("PLAIN")
|
||||
def testPlain(self):
|
||||
|
@ -34,33 +61,21 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
capabilities["sasl"],
|
||||
fail_msg="Does not have PLAIN mechanism as the controller " "claims",
|
||||
)
|
||||
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
|
||||
self.sendLine(1, "AUTHENTICATE PLAIN")
|
||||
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
|
||||
self._doInitialExchange(1, "PLAIN", "amlsbGVzAGppbGxlcwBzZXNhbWU=")
|
||||
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}",
|
||||
)
|
||||
self.sendLine(1, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")
|
||||
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 testPlainNonAscii(self):
|
||||
password = "é" * 100
|
||||
authstring = base64.b64encode(
|
||||
b"\x00".join([b"foo", b"foo", password.encode()])
|
||||
).decode()
|
||||
self.controller.registerUser(self, "foo", password)
|
||||
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")
|
||||
|
@ -72,7 +87,27 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
|
||||
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
|
||||
)
|
||||
self.sendLine(1, "AUTHENTICATE " + authstring)
|
||||
# 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):
|
||||
password = "é" * 100
|
||||
authstring = base64.b64encode(
|
||||
b"\x00".join([b"foo", b"foo", password.encode()])
|
||||
).decode()
|
||||
self.controller.registerUser(self, "foo", password)
|
||||
self.addClient()
|
||||
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
|
||||
self._doInitialExchange(1, "PLAIN", authstring)
|
||||
m = self.getRegistrationMessage(1)
|
||||
self.assertMessageMatch(
|
||||
m,
|
||||
|
@ -122,17 +157,8 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
capabilities["sasl"],
|
||||
fail_msg="Does not have PLAIN mechanism as the controller " "claims",
|
||||
)
|
||||
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}",
|
||||
)
|
||||
self.sendLine(1, "AUTHENTICATE AGppbGxlcwBzZXNhbWU=")
|
||||
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
|
||||
self._doInitialExchange(1, "PLAIN", "AGppbGxlcwBzZXNhbWU=")
|
||||
m = self.getRegistrationMessage(1)
|
||||
self.assertMessageMatch(
|
||||
m,
|
||||
|
@ -158,14 +184,17 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
capabilities,
|
||||
fail_msg="Does not have SASL as the controller claims.",
|
||||
)
|
||||
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
|
||||
self.sendLine(1, "AUTHENTICATE FOO")
|
||||
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
|
||||
if self.sasl_ir:
|
||||
self.sendLine(1, "AUTHENTICATE FOO AGppbGxlcwBzZXNhbWU=")
|
||||
else:
|
||||
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}",
|
||||
)
|
||||
|
||||
|
@ -209,17 +238,8 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
capabilities["sasl"],
|
||||
fail_msg="Does not have PLAIN mechanism as the controller " "claims",
|
||||
)
|
||||
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”, expected "
|
||||
"“AUTHENTICATE +” as a response, but got: {msg}",
|
||||
)
|
||||
self.sendLine(1, "AUTHENTICATE {}".format(authstring[0:400]))
|
||||
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
|
||||
self._doInitialExchange(1, "PLAIN", authstring[0:400])
|
||||
self.sendLine(1, "AUTHENTICATE {}".format(authstring[400:]))
|
||||
|
||||
self.confirmSuccessfulAuth()
|
||||
|
@ -279,17 +299,8 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
capabilities["sasl"],
|
||||
fail_msg="Does not have PLAIN mechanism as the controller " "claims",
|
||||
)
|
||||
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”, expected "
|
||||
"“AUTHENTICATE +” as a response, but got: {msg}",
|
||||
)
|
||||
self.sendLine(1, "AUTHENTICATE {}".format(authstring))
|
||||
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
|
||||
self._doInitialExchange(1, "PLAIN", authstring)
|
||||
self.sendLine(1, "AUTHENTICATE +")
|
||||
|
||||
self.confirmSuccessfulAuth()
|
||||
|
@ -298,6 +309,12 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
# I don't know how to do it, because it would make the registration
|
||||
# message's length too big for it to be valid.
|
||||
|
||||
|
||||
@cases.mark_services
|
||||
class SaslTestCase(_BaseSasl):
|
||||
sasl_ir = False
|
||||
capabilities = ["sasl"]
|
||||
|
||||
@cases.mark_specifications("IRCv3")
|
||||
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
||||
def testScramSha256Success(self):
|
||||
|
@ -318,7 +335,7 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
fail_msg="Does not have SCRAM-SHA-256 mechanism as the "
|
||||
"controller claims",
|
||||
)
|
||||
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
|
||||
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
|
||||
|
||||
self.sendLine(1, "AUTHENTICATE SCRAM-SHA-256")
|
||||
m = self.getRegistrationMessage(1)
|
||||
|
@ -374,7 +391,7 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
fail_msg="Does not have SCRAM-SHA-256 mechanism as the "
|
||||
"controller claims",
|
||||
)
|
||||
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
|
||||
self.requestCapabilities(1, self.capabilities, skip_if_cap_nak=False)
|
||||
|
||||
self.sendLine(1, "AUTHENTICATE SCRAM-SHA-256")
|
||||
m = self.getRegistrationMessage(1)
|
||||
|
@ -404,3 +421,36 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||
)
|
||||
m = self.getRegistrationMessage(1)
|
||||
self.assertMessageMatch(m, command=ERR_SASLFAIL)
|
||||
|
||||
|
||||
@cases.mark_services
|
||||
class SaslIrTestCase(_BaseSasl):
|
||||
"""Tests SASL with clients requesting the
|
||||
[sasl-ir](https://github.com/ircv3/ircv3-specifications/pull/520) cap and using it.
|
||||
"""
|
||||
|
||||
sasl_ir = True
|
||||
capabilities = ["sasl", "draft/sasl-ir"]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.connectClient(
|
||||
"capgetter", capabilities=["draft/sasl-ir"], skip_if_cap_nak=True
|
||||
)
|
||||
|
||||
|
||||
@cases.mark_services
|
||||
class ImplicitSaslIrTestCase(_BaseSasl):
|
||||
"""Tests SASL with clients using the
|
||||
[sasl-ir](https://github.com/ircv3/ircv3-specifications/pull/520) CAP without
|
||||
requesting it.
|
||||
"""
|
||||
|
||||
sasl_ir = True
|
||||
capabilities = ["sasl"]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.connectClient(
|
||||
"capgetter", capabilities=["draft/sasl-ir"], skip_if_cap_nak=True
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"""
|
||||
|
||||
from irctest import cases, runner
|
||||
from irctest.numerics import ERR_ERRONEUSNICKNAME
|
||||
from irctest.patma import ANYSTR
|
||||
|
||||
|
||||
|
@ -53,15 +54,23 @@ class Utf8TestCase(cases.BaseServerTestCase):
|
|||
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
||||
|
||||
self.addClient()
|
||||
self.sendLine(2, "NICK foo")
|
||||
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)
|
||||
self.assertIn(b"001 ", d)
|
||||
|
||||
self.sendLine(2, "WHOIS foo")
|
||||
self.sendLine(2, "WHOIS bar")
|
||||
self.getMessages(2)
|
||||
|
||||
def testNonutf8Username(self):
|
||||
|
@ -70,14 +79,56 @@ class Utf8TestCase(cases.BaseServerTestCase):
|
|||
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
||||
|
||||
self.addClient()
|
||||
self.sendLine(2, "NICK foo")
|
||||
self.sendLine(2, "USER 😊😊😊😊😊😊😊😊😊😊 * * :realname")
|
||||
m = self.getRegistrationMessage(2)
|
||||
if m.command in ("FAIL", "468"): # ERR_INVALIDUSERNAME
|
||||
self.sendLine(2, "NICK bar")
|
||||
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.sendLine(2, "WHOIS foo")
|
||||
self.assertIn(b"001 ", d)
|
||||
|
||||
self.sendLine(2, "WHOIS bar")
|
||||
self.getMessages(2)
|
||||
|
||||
|
||||
class ErgoUtf8NickEnabledTestCase(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config() -> cases.TestCaseControllerConfig:
|
||||
return cases.TestCaseControllerConfig(
|
||||
ergo_config=lambda config: config["server"].update(
|
||||
{"casemapping": "precis"},
|
||||
)
|
||||
)
|
||||
|
||||
@cases.mark_specifications("Ergo")
|
||||
def testUtf8NonAsciiNick(self):
|
||||
"""Ergo accepts certain non-ASCII UTF8 nicknames if PRECIS is enabled."""
|
||||
self.connectClient("Işıl")
|
||||
self.joinChannel(1, "#test")
|
||||
|
||||
self.connectClient("Claire")
|
||||
self.joinChannel(2, "#test")
|
||||
|
||||
self.sendLine(1, "PRIVMSG #test :hi there")
|
||||
self.getMessages(1)
|
||||
self.assertMessageMatch(
|
||||
self.getMessage(2), nick="Işıl", params=["#test", "hi there"]
|
||||
)
|
||||
|
||||
|
||||
class ErgoUtf8NickDisabledTestCase(cases.BaseServerTestCase):
|
||||
@cases.mark_specifications("Ergo")
|
||||
def testUtf8NonAsciiNick(self):
|
||||
"""Ergo rejects non-ASCII nicknames in its default configuration."""
|
||||
self.addClient(1)
|
||||
self.sendLine(1, "USER u s e r")
|
||||
self.sendLine(1, "NICK Işıl")
|
||||
self.assertMessageMatch(self.getMessage(1), command=ERR_ERRONEUSNICKNAME)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)],
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
When a client registers (ie. sends USER+NICK), InspIRCd does not
|
||||
immediately answers with 001. Instead it waits for the next iteration
|
||||
of the main loop to call `DoBackgroundUserStuff`.
|
||||
|
||||
However, this main loop executes only once a second. This is usually
|
||||
fine, but makes irctest considerably slower, as irctest uses hundreds
|
||||
of very short-lived connections.
|
||||
|
||||
This patch removes the frequency limitation of the main loop to make
|
||||
InspIRCd more responsive.
|
||||
|
||||
diff --git a/src/inspircd.cpp b/src/inspircd.cpp
|
||||
index 5760e631b..1da0285fb 100644
|
||||
--- a/src/inspircd.cpp
|
||||
+++ b/src/inspircd.cpp
|
||||
@@ -680,7 +680,7 @@ void InspIRCd::Run()
|
||||
* timing using this event, so we dont have to
|
||||
* time this exactly).
|
||||
*/
|
||||
- if (TIME.tv_sec != OLDTIME)
|
||||
+ if (true)
|
||||
{
|
||||
CollectStats();
|
||||
CheckTimeSkip(OLDTIME, TIME.tv_sec);
|
||||
|
|
@ -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: |
|
||||
|
@ -148,7 +148,7 @@ software:
|
|||
name: InspIRCd
|
||||
repository: inspircd/inspircd
|
||||
refs: &inspircd_refs
|
||||
stable: v3.15.0
|
||||
stable: v3.17.0
|
||||
release: null
|
||||
devel: master
|
||||
devel_release: insp3
|
||||
|
@ -158,12 +158,7 @@ software:
|
|||
separate_build_job: true
|
||||
build_script: &inspircd_build_script |
|
||||
cd $GITHUB_WORKSPACE/inspircd/
|
||||
|
||||
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
|
||||
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
|
||||
|
||||
./configure --prefix=$HOME/.local/inspircd --development
|
||||
|
||||
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
||||
make install
|
||||
irc2:
|
||||
|
@ -235,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
|
||||
|
@ -254,7 +249,7 @@ software:
|
|||
name: Sable
|
||||
repository: Libera-Chat/sable
|
||||
refs:
|
||||
stable: dcf8b53cac54f460b86861908d36d67969cf1eb2
|
||||
stable: b9deaa930c49f2939d9a584bedbfc3236da0d707
|
||||
release: null
|
||||
devel: master
|
||||
devel_release: null
|
||||
|
@ -348,16 +343,16 @@ software:
|
|||
separate_build_job: true
|
||||
path: anope
|
||||
refs:
|
||||
stable: "2.0.9"
|
||||
release: "2.0.9"
|
||||
devel: "2.0.9"
|
||||
devel_release: "2.0.9"
|
||||
stable: "2.0.14"
|
||||
release: "2.1.1"
|
||||
devel: "2.1"
|
||||
devel_release: "2.0"
|
||||
build_script: |
|
||||
cd $GITHUB_WORKSPACE/anope/
|
||||
cp $GITHUB_WORKSPACE/data/anope/* .
|
||||
CFLAGS=-O0 ./Config -quick
|
||||
make -C build -j 4
|
||||
make -C build install
|
||||
sudo apt-get install ninja-build --no-install-recommends
|
||||
mkdir build && cd build
|
||||
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local/ -DPROGRAM_NAME=anope -DUSE_PCH=ON -GNinja ..
|
||||
ninja install
|
||||
|
||||
dlk:
|
||||
name: Dlk
|
||||
|
|
Loading…
Reference in New Issue