Merge pull request #62 from ProgVal/unreal

Add Unreal controller
This commit is contained in:
Val Lorentz 2021-07-03 09:54:22 +02:00 committed by GitHub
commit d74b0e74c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 519 additions and 48 deletions

54
.github/workflows/unrealircd.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: irctest with UnrealIRCd
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Cache dependencies
uses: actions/cache@v2
with:
path: |
~/.cache
$GITHUB_WORKSPACE/unrealircd
key: ${{ runner.os }}-unrealircd
- name: Install dependencies
run: |
sudo apt-get install atheme-services
python -m pip install --upgrade pip
pip install pytest -r requirements.txt
- name: Checkout UnrealIRCd
uses: actions/checkout@v2
with:
repository: unrealircd/unrealircd
ref: unreal52
path: unrealircd
- name: Build UnrealIRCd
run: |
cd $GITHUB_WORKSPACE/unrealircd/
cp $GITHUB_WORKSPACE/unreal/* .
CFLAGS=-O0 ./Config -quick
make -j 4
make install
- name: Test with pytest
run: |
PATH=~/.local/unrealircd/bin:$PATH make unrealircd

View File

@ -60,9 +60,27 @@ SOPEL_SELECTORS := \
not testPlainNotAvailable \
$(EXTRA_SELECTORS)
.PHONY: all flakes ergo charybdis
# testNoticeNonexistentChannel fails: https://bugs.unrealircd.org/view.php?id=5949
# test_regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948
# test_messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947
# testCapRemovalByClient and testNakWhole fail pending https://github.com/unrealircd/unrealircd/pull/148
# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays
# Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149
UNREALIRCD_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not testNoticeNonexistentChannel \
and not (test_regressions and testTagCap) \
and not (test_messages and testLineTooLong) \
and not (test_cap and (testCapRemovalByClient or testNakWhole)) \
and not arbitrary_client_tags \
and not react_tag \
$(EXTRA_SELECTORS)
all: flakes ergo inspircd limnoria sopel solanum
.PHONY: all flakes charybdis ergo inspircd mammon limnoria sopel solanum unrealircd
all: flakes charybdis ergo inspircd mammon limnoria sopel solanum unrealircd
flakes:
pyflakes3 irctest
@ -87,3 +105,6 @@ solanum:
sopel:
$(PYTEST) $(PYTEST_ARGS) --controller=irctest.controllers.sopel -k '$(SOPEL_SELECTORS)'
unrealircd:
$(PYTEST) $(PYTEST_ARGS) --controller=irctest.controllers.unrealircd -k '$(UNREALIRCD_SELECTORS)'

View File

@ -183,6 +183,8 @@ class BaseServerController(_BaseController):
port: int
hostname: str
services_controller: BaseServicesController
extban_mute_char: Optional[str] = None
"""Character used for the 'mute' extban"""
def run(
self,

View File

@ -517,9 +517,15 @@ class BaseServerTestCase(
def getRegistrationMessage(self, client: TClientName) -> Message:
"""Filter notices, do not send pings."""
return self.getMessage(
client, synchronize=False, filter_pred=lambda m: m.command != "NOTICE"
)
while True:
msg = self.getMessage(
client, synchronize=False, filter_pred=lambda m: m.command != "NOTICE"
)
if msg.command == "PING":
# Hi Unreal
self.sendLine(client, "PONG :" + msg.params[0])
else:
return msg
def sendLine(self, client: TClientName, line: Union[str, bytes]) -> None:
return self.clients[client].sendLine(line)
@ -565,6 +571,9 @@ class BaseServerTestCase(
result.append(m)
if m.command == "001":
return result
elif m.command == "PING":
# Hi, Unreal
self.sendLine(client, "PONG :" + m.params[0])
def requestCapabilities(
self,

View File

@ -73,6 +73,7 @@ class CharybdisController(BaseServerController, DirectoryBasedController):
binary_name = "charybdis"
supported_sasl_mechanisms = {"PLAIN"}
supports_sts = False
extban_mute_char = None
def create_config(self) -> None:
super().create_config()

View File

@ -68,6 +68,7 @@ class InspircdController(BaseServerController, DirectoryBasedController):
software_name = "InspIRCd"
supported_sasl_mechanisms = {"PLAIN"}
supports_sts = False
extban_mute_char = "m"
def create_config(self) -> None:
super().create_config()

View File

@ -0,0 +1,182 @@
import os
import subprocess
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
include "modules.default.conf";
me {{
name "My.Little.Server";
info "ExampleNET Server";
sid "001";
}}
admin {{
"Bob Smith";
"bob";
"email@example.org";
}}
class clients {{
pingfreq 90;
maxclients 1000;
sendq 200k;
recvq 8000;
}}
class servers {{
pingfreq 60;
connfreq 15; /* try to connect every 15 seconds */
maxclients 10; /* max servers */
sendq 20M;
}}
allow {{
mask *;
class clients;
maxperip 50;
{password_field}
}}
listen {{
ip {hostname};
port {port};
}}
listen {{
ip {tls_hostname};
port {tls_port};
options {{ tls; }}
tls-options {{
certificate "{pem_path}";
key "{key_path}";
}};
}}
/* Special SSL/TLS servers-only port for linking */
listen {{
ip {services_hostname};
port {services_port};
options {{ serversonly; }}
}}
link services.example.org {{
incoming {{
mask localhost;
}}
password "password";
class servers;
}}
ulines {{
services.example.org;
}}
set {{
kline-address "example@example.org";
network-name "ExampleNET";
default-server "irc.example.org";
help-channel "#Help";
cloak-keys {{ "aaaA1"; "bbbB2"; "cccC3"; }}
options {{
identd-check; // Disable it, so it doesn't prefix idents with a tilde
}}
anti-flood {{
// Prevent throttling, especially test_buffering.py which
// triggers anti-flood with its very long lines
unknown-users {{
lag-penalty 1;
lag-penalty-bytes 10000;
}}
}}
}}
tld {{
mask *;
motd "{empty_file}";
botmotd "{empty_file}";
rules "{empty_file}";
}}
"""
class UnrealircdController(BaseServerController, DirectoryBasedController):
software_name = "InspIRCd"
supported_sasl_mechanisms = {"PLAIN"}
supports_sts = False
extban_mute_char = "q"
def create_config(self) -> None:
super().create_config()
with self.open_file("server.conf"):
pass
def run(
self,
hostname: str,
port: int,
*,
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
restricted_metadata_keys: Optional[Set[str]] = None,
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None
self.port = port
self.hostname = hostname
self.create_config()
(unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
password_field = 'password "{}";'.format(password) if password else ""
self.gen_ssl()
if ssl:
(tls_hostname, tls_port) = (hostname, port)
(hostname, port) = (unused_hostname, unused_port)
else:
# Unreal refuses to start without TLS enabled
(tls_hostname, tls_port) = (unused_hostname, unused_port)
with self.open_file("empty.txt") as fd:
fd.write("\n")
assert self.directory
with self.open_file("unrealircd.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
hostname=hostname,
port=port,
services_hostname=services_hostname,
services_port=services_port,
tls_hostname=tls_hostname,
tls_port=tls_port,
password_field=password_field,
key_path=self.key_path,
pem_path=self.pem_path,
empty_file=os.path.join(self.directory, "empty.txt"),
)
)
self.proc = subprocess.Popen(
[
"unrealircd",
"-F", # BOOT_NOFORK
"-f",
os.path.join(self.directory, "unrealircd.conf"),
],
stdout=subprocess.DEVNULL,
)
if run_services:
raise NotImplementedByController("Registration services")
def get_irctest_controller_class() -> Type[UnrealircdController]:
return UnrealircdController

View File

@ -35,6 +35,14 @@ class StrRe(Operator):
return f"StrRe(r'{self.regexp}')"
@dataclasses.dataclass(frozen=True)
class NotStrRe(Operator):
regexp: str
def __repr__(self) -> str:
return f"NotStrRe(r'{self.regexp}')"
@dataclasses.dataclass(frozen=True)
class RemainingKeys(Operator):
"""Used in a dict pattern to match all remaining keys.
@ -54,6 +62,15 @@ ANYDICT = {RemainingKeys(ANYSTR): AnyOptStr()}
`match_dict(got_tags, {"label": "foo", **ANYDICT})`"""
class _AnyListRemainder:
def __repr__(self) -> str:
return "*ANYLIST"
ANYLIST = [_AnyListRemainder()]
"""Matches any list remainder"""
def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bool:
if isinstance(expected, AnyOptStr):
return True
@ -62,6 +79,9 @@ def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bo
elif isinstance(expected, StrRe):
if got is None or not re.match(expected.regexp, got):
return False
elif isinstance(expected, NotStrRe):
if got is None or re.match(expected.regexp, got):
return False
elif isinstance(expected, Operator):
raise NotImplementedError(f"Unsupported operator: {expected}")
elif got != expected:
@ -78,6 +98,9 @@ def match_list(
The ANYSTR operator can be used on the 'expected' side as a wildcard,
matching any *single* value; and StrRe("<regexp>") can be used to match regular
expressions"""
if expected[-1] is ANYLIST[0]:
expected = expected[0:-1]
got = got[0 : len(expected)] # Ignore remaining
if len(got) != len(expected):
return False
return all(

View File

@ -4,7 +4,7 @@ import pytest
from irctest import cases
from irctest.irc_utils.message_parser import parse_message
from irctest.patma import ANYDICT, ANYSTR, StrRe
from irctest.patma import ANYDICT, ANYSTR, AnyOptStr, NotStrRe, RemainingKeys, StrRe
# fmt: off
MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [
@ -131,6 +131,27 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [
":foo!baz@qux PRIVMSG #chan hello",
]
),
(
# the specification:
dict(
tags={"tag1": "bar", RemainingKeys(NotStrRe("tag2")): AnyOptStr()},
command="PRIVMSG",
params=["#chan", "hello"],
),
# matches:
[
"@tag1=bar PRIVMSG #chan :hello",
"@tag1=bar :foo!baz@qux PRIVMSG #chan :hello",
"@tag1=bar;tag3= PRIVMSG #chan :hello",
],
# and does not match:
[
"PRIVMG #chan :hello",
"@tag1=value1 PRIVMSG #chan :hello",
"@tag1=bar;tag2= PRIVMSG #chan :hello",
"@tag1=bar;tag2=baz PRIVMSG #chan :hello",
]
),
]
# fmt: on

View File

@ -33,11 +33,16 @@ class BotModeTestCase(cases.BaseServerTestCase):
self.sendLine("bot", f"MODE botnick +{self._mode_char}")
# Check echoed mode
self.assertMessageMatch(
self.getMessage("bot"),
command="MODE",
params=["botnick", StrRe(r"\+?" + self._mode_char)],
)
while True:
msg = self.getMessage("bot")
if msg.command != "NOTICE":
# Unreal sends the BOTMOTD here
self.assertMessageMatch(
msg,
command="MODE",
params=["botnick", StrRe(r"\+?" + self._mode_char)],
)
break
def testBotMode(self):
self._initBot()

View File

@ -125,6 +125,7 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
cap1 = "echo-message"
cap2 = "server-time"
self.addClient(1)
self.connectClient("sender")
self.sendLine(1, "CAP LS 302")
m = self.getRegistrationMessage(1)
if not ({cap1, cap2} <= set(m.params[2].split())):
@ -146,7 +147,10 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
enabled_caps = set(cap_list.params[2].split())
enabled_caps.discard("cap-notify") # implicitly added by some impls
self.assertEqual(enabled_caps, {cap1, cap2})
self.assertIn("time", cap_list.tags, cap_list)
self.sendLine(2, "PRIVMSG bar :hi")
m = self.getMessage(1)
self.assertIn("time", m.tags, m)
# remove the server-time cap
self.sendLine(1, f"CAP REQ :-{cap2}")

View File

@ -26,7 +26,7 @@ from irctest.numerics import (
RPL_TOPIC,
RPL_TOPICTIME,
)
from irctest.patma import ANYSTR, StrRe
from irctest.patma import ANYLIST, ANYSTR, StrRe
MODERN_CAPS = [
"server-time",
@ -1296,16 +1296,13 @@ class OpModerated(cases.BaseServerTestCase):
class MuteExtban(cases.BaseServerTestCase):
"""https://defs.ircdocs.horse/defs/isupport.html#extban
These tests assume that if the server advertizes the 'm' extban,
then it supports mute.
It magically guesses what char the IRCd uses for mutes."""
This is not true of Charybdis, which introduced a conflicting 'm'
exban for matching hostmasks in 2015
(e2a9fa9cab3720215d8081e940109416e8214a29).
But Unreal was already using 'm' for muting since 2008
(f474e7e6dc2d36f96150ebe33b23b4ea76814415) and it is the most popular
definition so we're going with that one."""
def char(self):
if self.controller.extban_mute_char is None:
raise runner.ExtbanNotSupported("", "mute")
else:
return self.controller.extban_mute_char
@cases.mark_specifications("Ergo")
def testISupport(self):
@ -1313,7 +1310,7 @@ class MuteExtban(cases.BaseServerTestCase):
isupport = self.server_support
token = isupport["EXTBAN"]
prefix, comma, types = token.partition(",")
self.assertIn("m", types, "Missing 'm' in ISUPPORT EXTBAN")
self.assertIn(self.char, types, f"Missing '{self.char()}' in ISUPPORT EXTBAN")
self.assertEqual(prefix, "")
self.assertEqual(comma, ",")
@ -1325,15 +1322,15 @@ class MuteExtban(cases.BaseServerTestCase):
isupport = self.server_support
token = isupport.get("EXTBAN", "")
prefix, comma, types = token.partition(",")
if "m" not in types:
raise runner.ExtbanNotSupported("m", "mute")
if self.char() not in types:
raise runner.ExtbanNotSupported(self.char(), "mute")
clients = ("chanop", "bar")
# Mute "bar"
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan +b m:bar!*@*")
self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:bar!*@*")
replies = {msg.command for msg in self.getMessages("chanop")}
self.assertIn("MODE", replies)
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
@ -1344,6 +1341,21 @@ class MuteExtban(cases.BaseServerTestCase):
for client in clients:
self.getMessages(client)
# "bar" sees the MODE too
self.sendLine("bar", "MODE #chan +b")
self.assertMessageMatch(
self.getMessage("bar"),
command="367",
params=[
"bar",
"#chan",
f"{prefix}{self.char()}:bar!*@*",
"chanop",
*ANYLIST,
],
)
self.getMessages("bar")
# "bar" talks: rejected
self.sendLine("bar", "PRIVMSG #chan :hi from bar")
replies = self.getMessages("bar")
@ -1354,7 +1366,7 @@ class MuteExtban(cases.BaseServerTestCase):
# remove mute on "bar" with -b
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan -b m:bar!*@*")
self.sendLine("chanop", f"MODE #chan -b {prefix}{self.char()}:bar!*@*")
replies = {msg.command for msg in self.getMessages("chanop")}
self.assertIn("MODE", replies)
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
@ -1378,15 +1390,15 @@ class MuteExtban(cases.BaseServerTestCase):
isupport = self.server_support
token = isupport.get("EXTBAN", "")
prefix, comma, types = token.partition(",")
if "m" not in types:
raise runner.ExtbanNotSupported("m", "mute")
if self.char() not in types:
raise runner.ExtbanNotSupported(self.char(), "mute")
clients = ("chanop", "qux")
# Mute "qux"
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan +b m:qux!*@*")
self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:qux!*@*")
replies = {msg.command for msg in self.getMessages("chanop")}
self.assertIn("MODE", replies)
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
@ -1437,17 +1449,17 @@ class MuteExtban(cases.BaseServerTestCase):
isupport = self.server_support
token = isupport.get("EXTBAN", "")
prefix, comma, types = token.partition(",")
if "m" not in types:
raise runner.ExtbanNotSupported("m", "mute")
if self.char() not in types:
raise runner.ExtbanNotSupported(self.char(), "mute")
if "e" not in self.server_support["CHANMODES"]:
raise runner.ChannelModeNotSupported("m", "mute")
raise runner.ChannelModeNotSupported(self.char(), "mute")
clients = ("chanop", "qux")
# Mute "qux"
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan +b m:qux!*@*")
self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:qux!*@*")
replies = {msg.command for msg in self.getMessages("chanop")}
self.assertIn("MODE", replies)
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
@ -1472,11 +1484,13 @@ class MuteExtban(cases.BaseServerTestCase):
self.getMessages(client)
# +e grants an exemption to +b
self.sendLine("chanop", "MODE #chan +e m:*!~evan@*")
self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!~evan@*")
replies = {msg.command for msg in self.getMessages("chanop")}
self.assertIn("MODE", replies)
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
self.getMessages("qux")
# so "qux" can now talk
self.sendLine("qux", "PRIVMSG #chan :thanks for mute-excepting me")
replies = self.getMessages("qux")
@ -1500,9 +1514,14 @@ class MuteExtban(cases.BaseServerTestCase):
clients = ("chanop", "bar")
self.connectClient("chanop", name="chanop")
isupport = self.server_support
token = isupport.get("EXTBAN", "")
prefix, comma, types = token.partition(",")
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan +b m:BAR!*@*")
self.sendLine("chanop", f"MODE #chan +b {prefix}{self.char()}:BAR!*@*")
replies = {msg.command for msg in self.getMessages("chanop")}
self.assertIn("MODE", replies)
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
@ -1521,7 +1540,7 @@ class MuteExtban(cases.BaseServerTestCase):
self.assertEqual(self.getMessages("chanop"), [])
# remove mute with -b
self.sendLine("chanop", "MODE #chan -b m:bar!*@*")
self.sendLine("chanop", f"MODE #chan -b {prefix}{self.char()}:bar!*@*")
replies = {msg.command for msg in self.getMessages("chanop")}
self.assertIn("MODE", replies)
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)

View File

@ -119,15 +119,38 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
self.sendLine(2, "NICK foo")
self.sendLine(1, "USER username * * :Realname")
self.sendLine(2, "USER username * * :Realname")
m1 = self.getRegistrationMessage(1)
m2 = self.getRegistrationMessage(2)
try:
m1 = self.getRegistrationMessage(1)
except (ConnectionClosed, ConnectionResetError):
# Unreal closes the connection, see
# https://bugs.unrealircd.org/view.php?id=5950
command1 = None
else:
command1 = m1.command
try:
m2 = self.getRegistrationMessage(2)
except (ConnectionClosed, ConnectionResetError):
# ditto
command2 = None
else:
command2 = m2.command
self.assertNotEqual(
(m1.command, m2.command),
(command1, command2),
("001", "001"),
"Two concurrently registering requesting the same nickname "
"both got 001.",
)
self.assertIn(
"001",
(command1, command2),
"Two concurrently registering requesting the same nickname "
"neither got 001.",
)
@cases.mark_specifications("IRCv3")
def testIrc301CapLs(self):
"""

View File

@ -2,6 +2,8 @@
<http://ircv3.net/specs/extensions/echo-message-3.2.html>
"""
import pytest
from irctest import cases
from irctest.basecontrollers import NotImplementedByController
from irctest.irc_utils.junkdrawer import random_name
@ -97,6 +99,7 @@ def _testEchoMessage(command, solo, server_time):
class EchoMessageTestCase(cases.BaseServerTestCase):
@pytest.mark.arbitrary_client_tags
@cases.mark_capabilities(
"batch", "labeled-response", "echo-message", "message-tags"
)

View File

@ -7,8 +7,10 @@ so there may be many false positives.
import re
import pytest
from irctest import cases
from irctest.patma import ANYDICT, StrRe
from irctest.patma import ANYDICT, AnyOptStr, NotStrRe, RemainingKeys, StrRe
class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@ -89,6 +91,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
self.assertMessageMatch(m, command="PRIVMSG", tags={"label": "12345"})
@pytest.mark.react_tag
@cases.mark_capabilities("echo-message", "batch", "labeled-response")
def testLabeledPrivmsgResponsesToChannel(self):
self.connectClient(
@ -190,6 +193,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
self.assertMessageMatch(m, command="NOTICE", tags={"label": "12345"})
@pytest.mark.react_tag
@cases.mark_capabilities("echo-message", "batch", "labeled-response")
def testLabeledNoticeResponsesToChannel(self):
self.connectClient(
@ -265,6 +269,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
).format(number_of_labels),
)
@pytest.mark.react_tag
@cases.mark_capabilities(
"echo-message", "batch", "labeled-response", "message-tags"
)
@ -282,7 +287,14 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
)
self.getMessages(2)
self.sendLine(1, "@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG bar")
# Need to get a valid msgid because Unreal validates them
self.sendLine(1, "PRIVMSG bar :hi")
msgid = self.getMessage(1).tags["msgid"]
assert msgid == self.getMessage(2).tags["msgid"]
self.sendLine(
1, f"@label=12345;+draft/reply={msgid};+draft/react=l😃l TAGMSG bar"
)
m = self.getMessage(1)
m2 = self.getMessage(2)
@ -290,7 +302,11 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
self.assertMessageMatch(
m2,
command="TAGMSG",
tags={"+draft/reply": "123", "+draft/react": "l😃l", **ANYDICT},
tags={
"+draft/reply": msgid,
"+draft/react": "l😃l",
RemainingKeys(NotStrRe("label")): AnyOptStr(),
},
)
self.assertNotIn(
"label",
@ -308,12 +324,13 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
command="TAGMSG",
tags={
"label": "12345",
"+draft/reply": "123",
"+draft/reply": msgid,
"+draft/react": "l😃l",
**ANYDICT,
},
)
@pytest.mark.react_tag
@cases.mark_capabilities(
"echo-message", "batch", "labeled-response", "message-tags"
)
@ -338,7 +355,14 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
self.getMessages(2)
self.getMessages(1)
self.sendLine(1, "@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG #test")
# Need to get a valid msgid because Unreal validates them
self.sendLine(1, "PRIVMSG #test :hi")
msgid = self.getMessage(1).tags["msgid"]
assert msgid == self.getMessage(2).tags["msgid"]
self.sendLine(
1, f"@label=12345;+draft/reply={msgid};+draft/react=l😃l TAGMSG #test"
)
ms = self.getMessage(1)
mt = self.getMessage(2)
@ -346,6 +370,11 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
self.assertMessageMatch(
mt,
command="TAGMSG",
tags={
"+draft/reply": msgid,
"+draft/react": "l😃l",
RemainingKeys(NotStrRe("label")): AnyOptStr(),
},
fail_msg="No TAGMSG received by the target after sending one out",
)
self.assertNotIn(
@ -361,9 +390,12 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
# ensure sender correctly receives msg
self.assertMessageMatch(
ms, command="TAGMSG", tags={"label": "12345", **ANYDICT}
ms,
command="TAGMSG",
tags={"label": "12345", "+draft/reply": msgid, **ANYDICT},
)
@pytest.mark.react_tag
@cases.mark_capabilities(
"echo-message", "batch", "labeled-response", "message-tags"
)

View File

@ -2,6 +2,8 @@
https://ircv3.net/specs/extensions/message-tags.html
"""
import pytest
from irctest import cases
from irctest.irc_utils.message_parser import parse_message
from irctest.numerics import ERR_INPUTTOOLONG
@ -9,6 +11,7 @@ from irctest.patma import ANYDICT, ANYSTR, StrRe
class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
@pytest.mark.arbitrary_client_tags
@cases.mark_capabilities("message-tags")
def testBasic(self):
def getAllMessages():
@ -107,6 +110,7 @@ class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
self.assertNotIn("cat", msg.tags)
self.assertEqual(alice_msg.tags["msgid"], bob_msg.tags["msgid"])
@pytest.mark.arbitrary_client_tags
@cases.mark_capabilities("message-tags")
@cases.mark_specifications("ircdocs")
def testLengthLimits(self):

View File

@ -74,9 +74,13 @@ class TagsTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("message-tags")
def testLineTooLong(self):
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
self.connectClient(
"recver", capabilities=["message-tags"], skip_if_cap_nak=True
)
self.joinChannel(1, "#xyz")
monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!"
self.sendLine(1, monsterMessage)
self.assertEqual(self.getMessages(2), [], "overflowing message was relayed")
replies = self.getMessages(1)
self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies))

View File

@ -109,8 +109,13 @@ class RegressionsTestCase(cases.BaseServerTestCase):
self.sendLine(1, "NICK valid")
replies = {"NOTICE"}
while replies <= {"NOTICE"}:
replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
while replies <= {"NOTICE", "PING"}:
msgs = self.getMessages(1, synchronize=False)
for msg in msgs:
if msg.command == "PING":
# Hi Unreal
self.sendLine(1, "PONG :" + msg.params[0])
replies = set(msg.command for msg in msgs)
self.assertNotIn(ERR_ERRONEUSNICKNAME, replies)
self.assertIn(RPL_WELCOME, replies)

View File

@ -12,6 +12,8 @@ markers =
strict
deprecated
services
arbitrary_client_tags
react_tag
# capabilities
account-tag

24
unreal/config.settings Normal file
View File

@ -0,0 +1,24 @@
BASEPATH="$HOME/.local/unrealircd"
BINDIR="$HOME/.local/unrealircd/bin"
DATADIR="$HOME/.local/unrealircd/data"
CONFDIR="$HOME/.local/unrealircd/conf"
MODULESDIR="$HOME/.local/unrealircd/modules"
LOGDIR="$HOME/.local/unrealircd/logs"
CACHEDIR="$HOME/.local/unrealircd/cache"
DOCDIR="$HOME/.local/unrealircd/doc"
TMPDIR="$HOME/.local/unrealircd/tmp"
PRIVATELIBDIR="$HOME/.local/unrealircd/lib"
PREFIXAQ="1"
MAXCONNECTIONS_REQUEST="auto"
NICKNAMEHISTORYLENGTH="2000"
DEFPERM="0600"
SSLDIR=""
REMOTEINC=""
CURLDIR=""
SHOWLISTMODES="1"
NOOPEROVERRIDE=""
OPEROVERRIDEVERIFY=""
GENCERTIFICATE="1"
EXTRAPARA=""
ADVANCED=""

14
unreal/server.cert.pem Normal file
View File

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICGDCCAZ6gAwIBAgIUeHAOQnvT7N9kCmUuIklelkzz8SUwCgYIKoZIzj0EAwIw
QzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRIwEAYDVQQKDAlJUkMg
Z2Vla3MxDTALBgNVBAsMBElSQ2QwHhcNMjEwNzAyMTk1MTM5WhcNMzEwNjMwMTk1
MTM5WjBDMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxEjAQBgNVBAoM
CUlSQyBnZWVrczENMAsGA1UECwwESVJDZDB2MBAGByqGSM49AgEGBSuBBAAiA2IA
BHA6iqLQgkS42xHg/dEPq9dKjlLi0HWvCM7nOCXAyFy1DjrmbFoSCQBCUbISsk/C
Txru3YIfXe6jSCS8UTb15m70mrmmiUr/umxiqjAOiso051hCrzxVmjTpEAqMSnrc
zKNTMFEwHQYDVR0OBBYEFFNHqsBNxDNhVxfAgdv6/y4Xd6/ZMB8GA1UdIwQYMBaA
FFNHqsBNxDNhVxfAgdv6/y4Xd6/ZMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E
AwIDaAAwZQIwAo29xUEAzqOMgPAWtMifHFLuPQPuWcNGbaI5S4W81NO8uIcNv/kM
mFocuITr76p0AjEApzGjc5wM+KydwoVTP+fg1aGQA13Ba2nCzN3R5XwR/USCigjv
na1QtWAKjpvR/rsp
-----END CERTIFICATE-----

9
unreal/server.key.pem Normal file
View File

@ -0,0 +1,9 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDCWkDHktJiTqC7im+Ni37fbXxtMBqIKPwkAItpKMeuh28QrXWwNE1a5
wSa38C1nd8igBwYFK4EEACKhZANiAARwOoqi0IJEuNsR4P3RD6vXSo5S4tB1rwjO
5zglwMhctQ465mxaEgkAQlGyErJPwk8a7t2CH13uo0gkvFE29eZu9Jq5polK/7ps
YqowDorKNOdYQq88VZo06RAKjEp63Mw=
-----END EC PRIVATE KEY-----

9
unreal/server.req.pem Normal file
View File

@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBOjCBwgIBADBDMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxEjAQ
BgNVBAoMCUlSQyBnZWVrczENMAsGA1UECwwESVJDZDB2MBAGByqGSM49AgEGBSuB
BAAiA2IABHA6iqLQgkS42xHg/dEPq9dKjlLi0HWvCM7nOCXAyFy1DjrmbFoSCQBC
UbISsk/CTxru3YIfXe6jSCS8UTb15m70mrmmiUr/umxiqjAOiso051hCrzxVmjTp
EAqMSnrczKAAMAoGCCqGSM49BAMCA2cAMGQCMEL5ezlauGUaxh+pXt897ffmzqci
fqYm3FgVW5x6EdtCxtcwwAwnR84LKcd/YRKOygIwNmZiRVKeSeC7Ess1PxuzT1Mu
Cw3bBqkE5LmO1hu/+0lK+QoFPEeLDrygIh+SDdGH
-----END CERTIFICATE REQUEST-----