6 Commits

Author SHA1 Message Date
a60c5c376b Merge 0816232c1c5159d46ea5013f78f72f6f72024147 into a1324407893b603fe6b55ce7c4ee385938291ae1 2024-07-07 13:16:04 +01:00
a132440789 add various channel mode tests (#276) 2024-07-07 08:33:48 +02:00
aaa2e26b6e Fix file name 2024-06-23 20:37:10 +02:00
052198c61b Add support for Hybrid > 8.2.44 (#283)
The module system changed, modules now need to be loaded explicitly
2024-06-21 21:38:16 +02:00
9f33633cc7 Fix the help filename as of the latest commit. (#282) 2024-06-17 19:15:02 +02:00
465f6637ed enable backtraces in sable (#280) 2024-06-15 08:25:14 +02:00
9 changed files with 229 additions and 3 deletions

View File

@ -189,7 +189,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
path: inspircd path: inspircd
ref: v3.17.0 ref: v3.17.1
repository: inspircd/inspircd repository: inspircd/inspircd
- name: Build InspIRCd - name: Build InspIRCd
run: | run: |

View File

@ -1,3 +1,4 @@
from pathlib import Path
import shutil import shutil
import subprocess import subprocess
from typing import Optional from typing import Optional
@ -51,6 +52,8 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
) )
else: else:
ssl_config = "" ssl_config = ""
binary_path = shutil.which(self.binary_name)
assert binary_path, f"Could not find '{binary_path}' executable"
with self.open_file("server.conf") as fd: with self.open_file("server.conf") as fd:
fd.write( fd.write(
(self.template_config).format( (self.template_config).format(
@ -60,6 +63,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
services_port=services_port, services_port=services_port,
password_field=password_field, password_field=password_field,
ssl_config=ssl_config, ssl_config=ssl_config,
install_prefix=Path(binary_path).parent.parent,
) )
) )
assert self.directory assert self.directory

View File

@ -3,6 +3,9 @@ from typing import Set, Type
from .base_hybrid import BaseHybridController from .base_hybrid import BaseHybridController
TEMPLATE_CONFIG = """ TEMPLATE_CONFIG = """
module_base_path = "{install_prefix}/lib/ircd-hybrid/modules";
.include "./reference.modules.conf"
serverinfo {{ serverinfo {{
name = "My.Little.Server"; name = "My.Little.Server";
sid = "42X"; sid = "42X";

View File

@ -101,7 +101,7 @@ TEMPLATE_V4_CONFIG = """
# HELP/HELPOP # HELP/HELPOP
<module name="help"> <module name="help">
<include file="examples/help.conf.example"> <include file="examples/help.example.conf">
""" """

View File

@ -28,6 +28,9 @@ TEMPLATE_CONFIG = """
[Operator] [Operator]
Name = operuser Name = operuser
Password = operpassword Password = operpassword
[Limits]
MaxNickLength = 32 # defaults to 9
""" """

View File

@ -408,6 +408,7 @@ class SableController(BaseServerController, DirectoryBasedController):
], ],
cwd=self.directory, cwd=self.directory,
preexec_fn=os.setsid, preexec_fn=os.setsid,
env={"RUST_BACKTRACE": "1", **os.environ},
) )
self.pgroup_id = os.getpgid(self.proc.pid) self.pgroup_id = os.getpgid(self.proc.pid)
@ -485,6 +486,7 @@ class SableServicesController(BaseServicesController):
], ],
cwd=self.server_controller.directory, cwd=self.server_controller.directory,
preexec_fn=os.setsid, preexec_fn=os.setsid,
env={"RUST_BACKTRACE": "1", **os.environ},
) )
self.pgroup_id = os.getpgid(self.proc.pid) self.pgroup_id = os.getpgid(self.proc.pid)

View File

@ -0,0 +1,67 @@
from irctest import cases
from irctest.numerics import RPL_CHANNELCREATED, RPL_CHANNELMODEIS
from irctest.patma import ANYSTR, ListRemainder, StrRe
class RplChannelModeIsTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testChannelModeIs(self):
"""Test RPL_CHANNELMODEIS and RPL_CHANNELCREATED as responses to
`MODE #channel`:
<https://modern.ircdocs.horse/#rplcreationtime-329>
<https://modern.ircdocs.horse/#rplchannelmodeis-324>
"""
expected_numerics = {RPL_CHANNELMODEIS, RPL_CHANNELCREATED}
if self.controller.software_name in ("irc2", "Sable"):
# irc2 and Sable don't use timestamps for conflict resolution,
# consequently they don't store the channel creation timestamp
# and don't send RPL_CHANNELCREATED
expected_numerics = {RPL_CHANNELMODEIS}
self.connectClient("chanop", name="chanop")
self.joinChannel("chanop", "#chan")
# i, n, and t are specified by RFC1459; some of them may be on by default,
# but after this, at least those three should be enabled:
self.sendLine("chanop", "MODE #chan +int")
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan")
messages = self.getMessages("chanop")
self.assertEqual(expected_numerics, {msg.command for msg in messages})
for message in messages:
if message.command == RPL_CHANNELMODEIS:
# the final parameters are the mode string (e.g. `+int`),
# and then optionally any mode parameters (in case the ircd
# lists a mode that takes a parameter)
self.assertMessageMatch(
message,
command=RPL_CHANNELMODEIS,
params=["chanop", "#chan", ListRemainder(ANYSTR, min_length=1)],
)
final_param = message.params[2]
self.assertEqual(final_param[0], "+")
enabled_modes = list(final_param[1:])
break
self.assertLessEqual({"i", "n", "t"}, set(enabled_modes))
# remove all the modes listed by RPL_CHANNELMODEIS
self.sendLine("chanop", f"MODE #chan -{''.join(enabled_modes)}")
response = self.getMessage("chanop")
# we should get something like: MODE #chan -int
self.assertMessageMatch(
response, command="MODE", params=["#chan", StrRe("^-.*")]
)
self.assertEqual(set(response.params[1][1:]), set(enabled_modes))
self.sendLine("chanop", "MODE #chan")
messages = self.getMessages("chanop")
self.assertEqual(expected_numerics, {msg.command for msg in messages})
# all modes have been disabled; the correct representation of this is `+`
for message in messages:
if message.command == RPL_CHANNELMODEIS:
self.assertMessageMatch(
message,
command=RPL_CHANNELMODEIS,
params=["chanop", "#chan", "+"],
)

View File

@ -0,0 +1,147 @@
from irctest import cases
from irctest.numerics import (
ERR_CHANOPRIVSNEEDED,
ERR_NOSUCHCHANNEL,
ERR_NOSUCHNICK,
ERR_NOTONCHANNEL,
ERR_USERNOTINCHANNEL,
)
class ChannelOperatorModeTestCase(cases.BaseServerTestCase):
"""Test various error and success cases around the channel operator mode:
<https://modern.ircdocs.horse/#channel-operators>
<https://modern.ircdocs.horse/#mode-message>
"""
def setupNicks(self):
"""Set up a standard set of three nicknames and two channels
for testing channel-user MODE interactions."""
# first nick to join the channel is privileged:
self.connectClient("chanop", name="chanop")
self.joinChannel("chanop", "#chan")
self.connectClient("unprivileged", name="unprivileged")
self.joinChannel("unprivileged", "#chan")
self.getMessages("chanop")
self.connectClient("unrelated", name="unrelated")
self.joinChannel("unrelated", "#unrelated")
self.joinChannel("unprivileged", "#unrelated")
self.getMessages("unrelated")
@cases.mark_specifications("Modern")
@cases.xfailIfSoftware(["irc2"], "broken in irc2")
def testChannelOperatorModeSenderPrivsNeeded(self):
"""Test that +o from a channel member without the necessary privileges
fails as expected."""
self.setupNicks()
# sender is a channel member but without the necessary privileges:
self.sendLine("unprivileged", "MODE #chan +o unprivileged")
messages = self.getMessages("unprivileged")
self.assertEqual(len(messages), 1)
self.assertMessageMatch(messages[0], command=ERR_CHANOPRIVSNEEDED)
@cases.mark_specifications("Modern")
def testChannelOperatorModeTargetNotInChannel(self):
"""Test that +o targeting a user not present in the channel fails
as expected."""
self.setupNicks()
# sender is a chanop, but target nick is not in the channel:
self.sendLine("chanop", "MODE #chan +o unrelated")
messages = self.getMessages("chanop")
self.assertEqual(len(messages), 1)
self.assertMessageMatch(messages[0], command=ERR_USERNOTINCHANNEL)
@cases.mark_specifications("Modern")
def testChannelOperatorModeTargetDoesNotExist(self):
"""Test that +o targeting a nonexistent nick fails as expected."""
self.setupNicks()
# sender is a chanop, but target nick does not exist:
self.sendLine("chanop", "MODE #chan +o nobody")
messages = self.getMessages("chanop")
# ERR_NOSUCHNICK is typical, Bahamut additionally sends ERR_USERNOTINCHANNEL
if self.controller.software_name != "Bahamut":
self.assertEqual(len(messages), 1)
self.assertMessageMatch(messages[0], command=ERR_NOSUCHNICK)
else:
self.assertLessEqual(len(messages), 2)
commands = {message.command for message in messages}
self.assertLessEqual({ERR_NOSUCHNICK}, commands)
self.assertLessEqual(commands, {ERR_NOSUCHNICK, ERR_USERNOTINCHANNEL})
@cases.mark_specifications("Modern")
def testChannelOperatorModeChannelDoesNotExist(self):
"""Test that +o targeting a nonexistent channel fails as expected."""
self.setupNicks()
# target channel does not exist, but target nick does:
self.sendLine("chanop", "MODE #nonexistentchan +o chanop")
messages = self.getMessages("chanop")
self.assertEqual(len(messages), 1)
# Modern: "If <target> is a channel that does not exist on the network,
# the ERR_NOSUCHCHANNEL (403) numeric is returned."
# However, Unreal and ngircd send 401 ERR_NOSUCHNICK here instead:
if self.controller.software_name not in ("UnrealIRCd", "ngIRCd"):
self.assertEqual(messages[0].command, ERR_NOSUCHCHANNEL)
else:
self.assertIn(messages[0].command, [ERR_NOSUCHCHANNEL, ERR_NOSUCHNICK])
@cases.mark_specifications("Modern")
def testChannelOperatorModeChannelAndTargetDoNotExist(self):
"""Test that +o targeting a nonexistent channel and nickname
fails as expected."""
self.setupNicks()
# neither target channel nor target nick exist:
self.sendLine("chanop", "MODE #nonexistentchan +o nobody")
messages = self.getMessages("chanop")
self.assertEqual(len(messages), 1)
self.assertIn(
messages[0].command,
[ERR_NOSUCHCHANNEL, ERR_NOTONCHANNEL, ERR_NOSUCHNICK, ERR_USERNOTINCHANNEL],
)
@cases.mark_specifications("Modern")
def testChannelOperatorModeSenderNonMember(self):
"""Test that +o where the sender is not a channel member
fails as expected."""
self.setupNicks()
# sender is not a channel member, target nick exists and is a channel member:
self.sendLine("chanop", "MODE #unrelated +o unprivileged")
messages = self.getMessages("chanop")
self.assertEqual(len(messages), 1)
self.assertIn(messages[0].command, [ERR_NOTONCHANNEL, ERR_CHANOPRIVSNEEDED])
@cases.mark_specifications("Modern")
def testChannelOperatorModeSenderAndTargetNonMembers(self):
"""Test that +o where neither the sender nor the target is a channel
member fails as expected."""
self.setupNicks()
# sender is not a channel member, target nick exists but is not a channel member:
self.sendLine("chanop", "MODE #unrelated +o chanop")
messages = self.getMessages("chanop")
self.assertEqual(len(messages), 1)
self.assertIn(
messages[0].command,
[ERR_NOTONCHANNEL, ERR_CHANOPRIVSNEEDED, ERR_USERNOTINCHANNEL],
)
@cases.mark_specifications("Modern")
def testChannelOperatorModeSuccess(self):
"""Tests a successful grant of +o in a channel."""
self.setupNicks()
self.sendLine("chanop", "MODE #chan +o unprivileged")
messages = self.getMessages("chanop")
self.assertEqual(len(messages), 1)
self.assertMessageMatch(
messages[0],
command="MODE",
params=["#chan", "+o", "unprivileged"],
)
messages = self.getMessages("unprivileged")
self.assertEqual(len(messages), 1)
self.assertMessageMatch(
messages[0],
command="MODE",
params=["#chan", "+o", "unprivileged"],
)

View File

@ -148,7 +148,7 @@ software:
name: InspIRCd name: InspIRCd
repository: inspircd/inspircd repository: inspircd/inspircd
refs: &inspircd_refs refs: &inspircd_refs
stable: v3.17.0 stable: v3.17.1
release: null release: null
devel: master devel: master
devel_release: insp3 devel_release: insp3