Files
bouncer/tests/test_namespace.py
user 8cc57a7af4 feat: multi-network namespace multiplexing
Multiplex all networks onto a single client connection using /network
suffixes on channels and nicks. PASS is now just the password (no
network prefix). Channels appear as #channel/network, foreign nicks as
nick/network, own nicks stay bare.

New namespace.py module with pure encode/decode functions. Router
tracks clients globally (not per-network), namespaces messages before
delivery. Client attaches to all networks on connect, sends synthetic
JOIN/TOPIC/NAMES for every channel across all networks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:03:58 +01:00

144 lines
4.4 KiB
Python

"""Tests for network namespace encoding/decoding."""
from bouncer.irc import IRCMessage
from bouncer.namespace import (
decode_channel,
decode_target,
encode_channel,
encode_message,
encode_nick,
encode_prefix,
)
OWN = {"libera": "mybot", "oftc": "mybot2"}
class TestEncodeChannel:
def test_basic(self):
assert encode_channel("#libera", "libera") == "#libera/libera"
def test_preserves_prefix(self):
assert encode_channel("&local", "net") == "&local/net"
class TestDecodeChannel:
def test_basic(self):
assert decode_channel("#libera/libera") == ("#libera", "libera")
def test_no_separator(self):
assert decode_channel("#libera") == ("#libera", None)
def test_channel_with_slash_in_name(self):
# Edge: channel name itself contains slash -- rfind picks the last one
assert decode_channel("#a/b/net") == ("#a/b", "net")
class TestEncodeNick:
def test_foreign_nick(self):
assert encode_nick("user123", "libera", OWN) == "user123/libera"
def test_own_nick_bare(self):
assert encode_nick("mybot", "libera", OWN) == "mybot"
def test_own_nick_other_network(self):
# Own nick on another network still shown bare
assert encode_nick("mybot2", "libera", OWN) == "mybot2"
class TestEncodePrefix:
def test_full_prefix(self):
result = encode_prefix("user!ident@host", "libera", OWN)
assert result == "user/libera!ident@host"
def test_own_prefix(self):
result = encode_prefix("mybot!ident@host", "libera", OWN)
assert result == "mybot!ident@host"
def test_nick_only(self):
result = encode_prefix("server.example.com", "libera", OWN)
assert result == "server.example.com/libera"
class TestDecodeTarget:
def test_channel(self):
assert decode_target("#libera/libera") == ("#libera", "libera")
def test_nick(self):
assert decode_target("user123/libera") == ("user123", "libera")
def test_bare(self):
assert decode_target("#libera") == ("#libera", None)
class TestEncodeMessage:
def test_privmsg_channel(self):
msg = IRCMessage(
command="PRIVMSG",
params=["#test", "hello"],
prefix="user!ident@host",
)
out = encode_message(msg, "libera", OWN)
assert out.params[0] == "#test/libera"
assert out.prefix == "user/libera!ident@host"
def test_privmsg_own_prefix(self):
msg = IRCMessage(
command="PRIVMSG",
params=["#test", "hello"],
prefix="mybot!ident@host",
)
out = encode_message(msg, "libera", OWN)
assert out.prefix == "mybot!ident@host"
def test_namreply(self):
msg = IRCMessage(
command="353",
params=["mybot", "=", "#test", "@op +voice regular mybot"],
)
out = encode_message(msg, "libera", OWN)
assert out.params[2] == "#test/libera"
names = out.params[3].split()
assert names[0] == "@op/libera"
assert names[1] == "+voice/libera"
assert names[2] == "regular/libera"
assert names[3] == "mybot" # own nick stays bare
def test_nick_change(self):
msg = IRCMessage(
command="NICK",
params=["newnick"],
prefix="oldnick!user@host",
)
out = encode_message(msg, "libera", OWN)
assert out.params[0] == "newnick/libera"
assert out.prefix == "oldnick/libera!user@host"
def test_join(self):
msg = IRCMessage(
command="JOIN",
params=["#test"],
prefix="user!ident@host",
)
out = encode_message(msg, "libera", OWN)
assert out.params[0] == "#test/libera"
def test_non_channel_target_untouched(self):
msg = IRCMessage(
command="PRIVMSG",
params=["someuser", "hi"],
prefix="other!ident@host",
)
out = encode_message(msg, "libera", OWN)
# Private message to a nick -- params[0] is not a channel, left as-is
assert out.params[0] == "someuser"
def test_no_raw_preserved(self):
msg = IRCMessage(
command="PRIVMSG",
params=["#test", "hello"],
prefix="user!ident@host",
raw=b":user!ident@host PRIVMSG #test :hello\r\n",
)
out = encode_message(msg, "libera", OWN)
assert out.raw is None # raw must not carry over