mirror of
https://github.com/progval/irctest.git
synced 2025-04-05 14:59:49 +00:00
Compare commits
203 Commits
master-ori
...
master-ora
Author | SHA1 | Date | |
---|---|---|---|
4ded96fbba | |||
9f68f12b3a | |||
85d14f3e12 | |||
81d5715465 | |||
13be312366 | |||
85f02c4626 | |||
efa5b5eb3b | |||
fe694487c7 | |||
c4a9592156 | |||
3d7a539d06 | |||
3932a40d74 | |||
0dfe0de549 | |||
c9c08c7f6f | |||
684c889304 | |||
c9dbba985c | |||
fe0d65f7c8 | |||
8d427c80c8 | |||
a74f893942 | |||
75dafa47fe | |||
d69c41756b | |||
9b2a6a063c | |||
de49571b1e | |||
b58fe44b5b | |||
307722fbec | |||
0b9087cc39 | |||
40ac45cdbe | |||
5aeb297de5 | |||
8c66157a9e | |||
3b489a2125 | |||
14435ce0e8 | |||
a723791942 | |||
d741ab86d5 | |||
b7975ada46 | |||
b43e127805 | |||
512b4bd74d | |||
d48cbc4287 | |||
215ed3171b | |||
95a26bfa57 | |||
706e794df6 | |||
62197e4c4d | |||
f0eb6e4e80 | |||
513c74a52b | |||
1ac1c1c6a7 | |||
d144dad001 | |||
0c069b7418 | |||
616785eae4 | |||
b2a1df41bd | |||
9eef97e615 | |||
a67cfea82f | |||
0287b83797 | |||
59eb7502f5 | |||
a37b454ee7 | |||
9cf318ed66 | |||
1614c5a888 | |||
873a304445 | |||
f9ccc4c824 | |||
d7c231ba9e | |||
a14ebf9ec2 | |||
c5e565ed27 | |||
8851083a3e | |||
61941e2be0 | |||
8922c0ef4e | |||
ceb2431134 | |||
f09ec7d9aa | |||
ed2b6148e5 | |||
0bdbe2ce24 | |||
347e4fd74a | |||
829cb38863 | |||
59e3b873ea | |||
23d6fecae9 | |||
d1f15fb05c | |||
b54a1b1455 | |||
4e9de71f7d | |||
bdefa32d3a | |||
a87416e5ee | |||
e5789f9a37 | |||
40364408a4 | |||
53f5ef711e | |||
b208baaa11 | |||
21b225f23d | |||
c12c44b993 | |||
d1d94646a7 | |||
d490f532c8 | |||
e89d394ce5 | |||
957e7ce1fd | |||
dcec0a48ce | |||
015eef0bfa | |||
41d63ff3cc | |||
0ad60477be | |||
2401f6a07f | |||
10070f3efd | |||
b35258f6ab | |||
1b372e996a | |||
7465c6432f | |||
7749407d6a | |||
8012b380e2 | |||
50bc578e0b | |||
c5708e5722 | |||
68d7813325 | |||
5073dd7a3d | |||
224cb4dde5 | |||
1109820f72 | |||
c8e4f1eaa2 | |||
493e1eba65 | |||
ab9e6788db | |||
47f94a8133 | |||
28048e319f | |||
020564bdcb | |||
0a875ed7de | |||
b98ca189c0 | |||
998035e17e | |||
d351b84b03 | |||
cb3c87cb84 | |||
b2890a2d10 | |||
b044d857a0 | |||
e6c2c0d619 | |||
63a45a6c07 | |||
3697ecbebf | |||
a72c9a74c0 | |||
3660e9b9a3 | |||
8ccf59c28a | |||
fddc395d43 | |||
5f566e7164 | |||
7d81888b44 | |||
f0c5cc5648 | |||
79f29a768a | |||
1d0c8687ee | |||
18d123357f | |||
f7d927cbc4 | |||
90f43d509d | |||
383a65d58e | |||
b184892a1c | |||
7b2efeb2d4 | |||
088d02e8ec | |||
852cd71ff6 | |||
85dc8a2636 | |||
17303fa7fe | |||
3914537315 | |||
2825454dfe | |||
1463d4b2c4 | |||
b9872018ad | |||
7f5a489cae | |||
dc3ac21f75 | |||
a3ad8a1038 | |||
83f5c924f0 | |||
9497bc72a9 | |||
ecfb0e327f | |||
73237237ef | |||
866ad726eb | |||
dd5bbc3dd5 | |||
a28e6e0f75 | |||
884f2010cd | |||
125b49f878 | |||
655a1b63c2 | |||
aabf89a737 | |||
60e24d34a6 | |||
721b9022e7 | |||
f4b65a453d | |||
8b1c484ac4 | |||
12a1046d5a | |||
12493b1eb5 | |||
019639ba88 | |||
6497f97951 | |||
b42bb1ade1 | |||
7b32961bc7 | |||
226fbd5ad4 | |||
bed3a4581a | |||
481c6a03b2 | |||
9824bb2296 | |||
afec48d26b | |||
a47e42f562 | |||
a9e0de4896 | |||
16a138828b | |||
b961f19922 | |||
6ff0c52442 | |||
97b3f4fb8b | |||
eda43b3cda | |||
38f7836fa5 | |||
e39f7be14c | |||
bceb5883cc | |||
39a90e5726 | |||
37ea5be753 | |||
754f9ad250 | |||
e4c3490787 | |||
59d5d2c76e | |||
463733c772 | |||
e670df8b56 | |||
5d1d3ce03b | |||
136e65eb13 | |||
dc8bca9436 | |||
a077f264a3 | |||
924d17b747 | |||
19394fdd09 | |||
717b557610 | |||
4cc60247da | |||
a0009a0267 | |||
f359feb8e2 | |||
2f95675348 | |||
dadf85c4a3 | |||
c6663bc9b6 | |||
ca8b3cf625 | |||
da25b59380 | |||
9ede9045ad |
8
Makefile
8
Makefile
@ -1,5 +1,9 @@
|
||||
.PHONY: oragono
|
||||
.PHONY: all flakes integration
|
||||
|
||||
oragono:
|
||||
all: flakes integration
|
||||
|
||||
flakes:
|
||||
pyflakes3 ./irctest/cases.py ./irctest/client_mock.py ./irctest/controllers/oragono.py irctest/server_tests/*.py
|
||||
|
||||
integration:
|
||||
./test.py irctest.controllers.oragono
|
||||
|
124
README.md
124
README.md
@ -23,50 +23,58 @@ Install irctest and dependencies:
|
||||
```
|
||||
git clone https://github.com/ProgVal/irctest.git
|
||||
cd irctest
|
||||
pip3 install --user -r requirements.txt
|
||||
pip3 install --user -r requirements.txt pyxmpp2-scram
|
||||
python3 setup.py install --user
|
||||
```
|
||||
|
||||
Add `~/.local/bin/` to your `PATH` if it is not.
|
||||
Add `~/.local/bin/` (and/or `~/.local/bin/` for Oragono)
|
||||
to your `PATH` if it is not.
|
||||
|
||||
```
|
||||
export PATH=$HOME/.local/bin/:$PATH
|
||||
export PATH=$HOME/.local/bin/:$HOME/go/bin/:$PATH
|
||||
```
|
||||
|
||||
## Using pytest
|
||||
|
||||
irctest is invoked using the pytest test runner / CLI.
|
||||
|
||||
You can usually invoke it with `python3 -m pytest` command; which can often
|
||||
be called by the `pytest` or `pytest-3` commands (if not, alias them if you
|
||||
are planning to use them often).
|
||||
|
||||
The rest of this README assumes `pytest` works.
|
||||
|
||||
## Test selection
|
||||
|
||||
A major feature of pytest that irctest heavily relies on is test selection.
|
||||
Using the `-k` option, you can select and deselect tests based on their names
|
||||
and/or markers (listed in `pytest.ini`).
|
||||
For example, you can run `LUSERS`-related tests with `-k lusers`.
|
||||
Or only tests based on RFC1459 with `-k rfc1459`.
|
||||
|
||||
By default, all tests run; even niche ones. So you probably always want to
|
||||
use these options: `-k 'not Oragono and not deprecated and not strict`.
|
||||
This excludes:
|
||||
|
||||
* `Oragono`-specific tests (included as Oragono uses irctest as its official
|
||||
integration test suite)
|
||||
* tests for deprecated specifications, such as the IRCv3 METADATA
|
||||
specification
|
||||
* tests that check for a strict interpretation of a specification, when
|
||||
the specification is ambiguous.
|
||||
|
||||
## Run tests
|
||||
|
||||
To run (client) tests on Limnoria:
|
||||
|
||||
```
|
||||
pip3 install --user limnoria
|
||||
python3 -m irctest irctest.controllers.limnoria
|
||||
```
|
||||
|
||||
To run (client) tests on Sopel:
|
||||
|
||||
```
|
||||
pip3 install --user sopel
|
||||
mkdir ~/.sopel/
|
||||
python3 -m irctest irctest.controllers.sopel
|
||||
```
|
||||
|
||||
To run (server) tests on InspIRCd:
|
||||
To run (server) tests on Oragono:
|
||||
|
||||
```
|
||||
cd /tmp/
|
||||
git clone https://github.com/inspircd/inspircd.git
|
||||
cd inspircd
|
||||
./configure --prefix=$HOME/.local/ --development
|
||||
make -j 4
|
||||
git clone https://github.com/oragono/oragono.git
|
||||
cd oragono/
|
||||
make build
|
||||
make install
|
||||
python3 -m irctest irctest.controllers.inspircd
|
||||
```
|
||||
|
||||
To run (server) tests on Mammon:
|
||||
|
||||
```
|
||||
pip3 install --user git+https://github.com/mammon-ircd/mammon.git
|
||||
python3 -m irctest irctest.controllers.mammon
|
||||
cd ~/irctest
|
||||
pytest --controller irctest.controllers.oragono -k 'not deprecated'
|
||||
```
|
||||
|
||||
To run (server) tests on Charybdis::
|
||||
@ -78,31 +86,46 @@ cd charybdis
|
||||
./configure --prefix=$HOME/.local/
|
||||
make -j 4
|
||||
make install
|
||||
python3 -m irctest irctest.controllers.charybdis
|
||||
cd ~/irctest
|
||||
pytest --controller irctest.controllers.charybdis -k 'not Oragono and not deprecated and not strict'
|
||||
```
|
||||
|
||||
## Full help
|
||||
To run (server) tests on InspIRCd:
|
||||
|
||||
```
|
||||
usage: python3 -m irctest [-h] [--show-io] [-v] [-s SPECIFICATION] [-l] module
|
||||
cd /tmp/
|
||||
git clone https://github.com/inspircd/inspircd.git
|
||||
cd inspircd
|
||||
./configure --prefix=$HOME/.local/ --development
|
||||
make -j 4
|
||||
make install
|
||||
cd ~/irctest
|
||||
pytest --controller irctest.controllers.inspircd -k 'not Oragono and not deprecated and not strict'
|
||||
```
|
||||
|
||||
positional arguments:
|
||||
module The module used to run the tested program.
|
||||
To run (server) tests on Mammon:
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--show-io Show input/outputs with the tested program.
|
||||
-v, --verbose Verbosity. Give this option multiple times to make it
|
||||
even more verbose.
|
||||
-s SPECIFICATION, --specification SPECIFICATION
|
||||
The set of specifications to test the program with.
|
||||
Valid values: RFC1459, RFC2812, IRCv3.1, IRCv3.2. Use
|
||||
this option multiple times to test with multiple
|
||||
specifications. If it is not given, defaults to all.
|
||||
-l, --loose Disables strict checks of conformity to the
|
||||
specification. Strict means the specification is
|
||||
unclear, and the most restrictive interpretation is
|
||||
choosen.
|
||||
```
|
||||
pip3 install --user git+https://github.com/mammon-ircd/mammon.git
|
||||
cd ~/irctest
|
||||
pytest --controller irctest.controllers.mammon -k 'not Oragono and not deprecated and not strict'
|
||||
```
|
||||
|
||||
To run (client) tests on Limnoria:
|
||||
|
||||
```
|
||||
pip3 install --user limnoria pyxmpp2-scram
|
||||
cd ~/irctest
|
||||
pytest --controller irctest.controllers.limnoria
|
||||
```
|
||||
|
||||
To run (client) tests on Sopel:
|
||||
|
||||
```
|
||||
pip3 install --user sopel
|
||||
mkdir ~/.sopel/
|
||||
cd ~/irctest
|
||||
pytest --controller irctest.controllers.sopel
|
||||
```
|
||||
|
||||
## What `irctest` is not
|
||||
@ -114,3 +137,4 @@ At best, `irctest` can help you find issues in your software, but it may
|
||||
still have false positives (because it does not implement itself a
|
||||
full-featured client/server, so it supports only “usual” behavior).
|
||||
Bug reports for false positives are welcome.
|
||||
|
||||
|
94
conftest.py
Normal file
94
conftest.py
Normal file
@ -0,0 +1,94 @@
|
||||
import importlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
import _pytest.unittest
|
||||
|
||||
from irctest.cases import _IrcTestCase, BaseClientTestCase, BaseServerTestCase
|
||||
from irctest.basecontrollers import BaseClientController, BaseServerController
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Called by pytest, registers CLI options passed to the pytest command."""
|
||||
parser.addoption("--controller", help="Which module to use to run the tested software.")
|
||||
parser.addoption('--openssl-bin', type=str, default='openssl',
|
||||
help='The openssl binary to use')
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Called by pytest, after it parsed the command-line."""
|
||||
module_name = config.getoption("controller")
|
||||
|
||||
if module_name is None:
|
||||
pytest.exit("--controller is required.", 1)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except ImportError:
|
||||
pytest.exit('Cannot import module {}'.format(module_name), 1)
|
||||
|
||||
controller_class = module.get_irctest_controller_class()
|
||||
if issubclass(controller_class, BaseClientController):
|
||||
from irctest import client_tests as module
|
||||
elif issubclass(controller_class, BaseServerController):
|
||||
from irctest import server_tests as module
|
||||
else:
|
||||
pytest.exit(
|
||||
r'{}.Controller should be a subclass of '
|
||||
r'irctest.basecontroller.Base{{Client,Server}}Controller'
|
||||
.format(module_name),
|
||||
1
|
||||
)
|
||||
_IrcTestCase.controllerClass = controller_class
|
||||
_IrcTestCase.controllerClass.openssl_bin = config.getoption("openssl_bin")
|
||||
_IrcTestCase.show_io = True # TODO
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
"""Called by pytest after finishing the test collection,
|
||||
and before actually running the tests.
|
||||
|
||||
This function filters out client tests if running with a server controller,
|
||||
and vice versa.
|
||||
"""
|
||||
|
||||
# First, check if we should run server tests or client tests
|
||||
if issubclass(_IrcTestCase.controllerClass, BaseServerController):
|
||||
server_tests = True
|
||||
elif issubclass(_IrcTestCase.controllerClass, BaseClientController):
|
||||
server_tests = False
|
||||
else:
|
||||
assert False, (
|
||||
f"{_IrcTestCase.controllerClass} inherits neither "
|
||||
f"BaseClientController or BaseServerController"
|
||||
)
|
||||
|
||||
filtered_items = []
|
||||
|
||||
# Iterate over each of the test functions (they are pytest "Nodes")
|
||||
for item in items:
|
||||
# we only use unittest-style test function here
|
||||
assert isinstance(item, _pytest.unittest.TestCaseFunction)
|
||||
|
||||
# unittest-style test functions have the node of UnitTest class as parent
|
||||
assert isinstance(item.parent, _pytest.unittest.UnitTestCase)
|
||||
|
||||
# and that node references the UnitTest class
|
||||
assert issubclass(item.parent.cls, unittest.TestCase)
|
||||
|
||||
# and in this project, TestCase classes all inherit either from BaseClientController
|
||||
# or BaseServerController.
|
||||
if issubclass(item.parent.cls, BaseServerTestCase):
|
||||
if server_tests:
|
||||
filtered_items.append(item)
|
||||
elif issubclass(item.parent.cls, BaseClientTestCase):
|
||||
if not server_tests:
|
||||
filtered_items.append(item)
|
||||
else:
|
||||
assert False, (
|
||||
f"{item}'s class inherits neither BaseServerTestCase "
|
||||
"or BaseClientTestCase"
|
||||
)
|
||||
|
||||
# Finally, rewrite in-place the list of tests pytest will run
|
||||
items[:] = filtered_items
|
@ -1,87 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
import argparse
|
||||
import unittest
|
||||
import functools
|
||||
import importlib
|
||||
from .cases import _IrcTestCase
|
||||
from .runner import TextTestRunner
|
||||
from .specifications import Specifications
|
||||
from .basecontrollers import BaseClientController, BaseServerController
|
||||
|
||||
def main(args):
|
||||
try:
|
||||
module = importlib.import_module(args.module)
|
||||
except ImportError:
|
||||
print('Cannot import module {}'.format(args.module), file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
controller_class = module.get_irctest_controller_class()
|
||||
if issubclass(controller_class, BaseClientController):
|
||||
import irctest.client_tests as module
|
||||
elif issubclass(controller_class, BaseServerController):
|
||||
import irctest.server_tests as module
|
||||
else:
|
||||
print(r'{}.Controller should be a subclass of '
|
||||
r'irctest.basecontroller.Base{{Client,Server}}Controller'
|
||||
.format(args.module),
|
||||
file=sys.stderr)
|
||||
exit(1)
|
||||
_IrcTestCase.controllerClass = controller_class
|
||||
_IrcTestCase.controllerClass.openssl_bin = args.openssl_bin
|
||||
_IrcTestCase.show_io = args.show_io
|
||||
_IrcTestCase.strictTests = not args.loose
|
||||
if args.specification:
|
||||
try:
|
||||
_IrcTestCase.testedSpecifications = frozenset(
|
||||
Specifications.of_name(x) for x in args.specification
|
||||
)
|
||||
except ValueError:
|
||||
print('Invalid set of specifications: {}'
|
||||
.format(', '.join(args.specification)))
|
||||
exit(1)
|
||||
else:
|
||||
_IrcTestCase.testedSpecifications = frozenset(
|
||||
Specifications)
|
||||
print('Testing {} on specification(s): {}'.format(
|
||||
controller_class.software_name,
|
||||
', '.join(sorted(map(lambda x:x.value,
|
||||
_IrcTestCase.testedSpecifications)))))
|
||||
ts = module.discover()
|
||||
testRunner = TextTestRunner(
|
||||
verbosity=args.verbose,
|
||||
descriptions=True,
|
||||
)
|
||||
testLoader = unittest.loader.defaultTestLoader
|
||||
result = testRunner.run(ts)
|
||||
if result.failures or result.errors:
|
||||
exit(1)
|
||||
else:
|
||||
exit(0)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='A script to test interoperability of IRC software.')
|
||||
parser.add_argument('module', type=str,
|
||||
help='The module used to run the tested program.')
|
||||
parser.add_argument('--openssl-bin', type=str, default='openssl',
|
||||
help='The openssl binary to use')
|
||||
parser.add_argument('--show-io', action='store_true',
|
||||
help='Show input/outputs with the tested program.')
|
||||
parser.add_argument('-v', '--verbose', action='count', default=1,
|
||||
help='Verbosity. Give this option multiple times to make '
|
||||
'it even more verbose.')
|
||||
parser.add_argument('-s', '--specification', type=str, action='append',
|
||||
help=('The set of specifications to test the program with. '
|
||||
'Valid values: {}. '
|
||||
'Use this option multiple times to test with multiple '
|
||||
'specifications. If it is not given, defaults to all.')
|
||||
.format(', '.join(x.value for x in Specifications)))
|
||||
parser.add_argument('-l', '--loose', action='store_true',
|
||||
help='Disables strict checks of conformity to the specification. '
|
||||
'Strict means the specification is unclear, and the most restrictive '
|
||||
'interpretation is choosen.')
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
main(args)
|
@ -13,13 +13,14 @@ class _BaseController:
|
||||
A software controller is an object that handles configuring and running
|
||||
a process (eg. a server or a client), as well as sending it instructions
|
||||
that are not part of the IRC specification."""
|
||||
pass
|
||||
def __init__(self, test_config):
|
||||
self.test_config = test_config
|
||||
|
||||
class DirectoryBasedController(_BaseController):
|
||||
"""Helper for controllers whose software configuration is based on an
|
||||
arbitrary directory."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, test_config):
|
||||
super().__init__(test_config)
|
||||
self.directory = None
|
||||
self.proc = None
|
||||
|
||||
@ -38,11 +39,6 @@ class DirectoryBasedController(_BaseController):
|
||||
self.kill_proc()
|
||||
if self.directory:
|
||||
shutil.rmtree(self.directory)
|
||||
def terminate(self):
|
||||
"""Stops the process gracefully, and does not clean its config."""
|
||||
self.proc.terminate()
|
||||
self.proc.wait()
|
||||
self.proc = None
|
||||
def open_file(self, name, mode='a'):
|
||||
"""Open a file in the configuration directory."""
|
||||
assert self.directory
|
||||
@ -53,13 +49,7 @@ class DirectoryBasedController(_BaseController):
|
||||
assert os.path.isdir(dir_)
|
||||
return open(os.path.join(self.directory, name), mode)
|
||||
def create_config(self):
|
||||
"""If there is no config dir, creates it and returns True.
|
||||
Else returns False."""
|
||||
if self.directory:
|
||||
return False
|
||||
else:
|
||||
self.directory = tempfile.mkdtemp()
|
||||
return True
|
||||
self.directory = tempfile.mkdtemp()
|
||||
|
||||
def gen_ssl(self):
|
||||
self.csr_path = os.path.join(self.directory, 'ssl.csr')
|
||||
@ -85,6 +75,7 @@ class BaseClientController(_BaseController):
|
||||
|
||||
class BaseServerController(_BaseController):
|
||||
"""Base controller for IRC server."""
|
||||
_port_wait_interval = .1
|
||||
port_open = False
|
||||
def run(self, hostname, port, password,
|
||||
valid_metadata_keys, invalid_metadata_keys):
|
||||
@ -93,9 +84,19 @@ class BaseServerController(_BaseController):
|
||||
raise NotImplementedByController('account registration')
|
||||
def wait_for_port(self):
|
||||
while not self.port_open:
|
||||
time.sleep(0.1)
|
||||
time.sleep(self._port_wait_interval)
|
||||
try:
|
||||
c = socket.create_connection(('localhost', self.port), timeout=1.0)
|
||||
c.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
# Make sure the server properly processes the disconnect.
|
||||
# Otherwise, it may still count it in LUSER and fail tests in
|
||||
# test_lusers.py (eg. this happens with Charybdis 3.5.0)
|
||||
c.send(b"QUIT :chkport\r\n")
|
||||
data = b""
|
||||
while b"chkport" not in data:
|
||||
data += c.recv(1024)
|
||||
|
||||
c.close()
|
||||
self.port_open = True
|
||||
except Exception as e:
|
||||
|
111
irctest/cases.py
111
irctest/cases.py
@ -5,31 +5,50 @@ import tempfile
|
||||
import unittest
|
||||
import functools
|
||||
|
||||
import supybot.utils
|
||||
import pytest
|
||||
|
||||
from . import runner
|
||||
from . import client_mock
|
||||
from .irc_utils import capabilities
|
||||
from .irc_utils import message_parser
|
||||
from .irc_utils.junkdrawer import normalizeWhitespace, random_name
|
||||
from .irc_utils.sasl import sasl_plain_blob
|
||||
from .exceptions import ConnectionClosed
|
||||
from .specifications import Specifications
|
||||
from .numerics import ERR_NOSUCHCHANNEL, ERR_TOOMANYCHANNELS, ERR_BADCHANNELKEY, ERR_INVITEONLYCHAN, ERR_BANNEDFROMCHAN, ERR_NEEDREGGEDNICK
|
||||
|
||||
CHANNEL_JOIN_FAIL_NUMERICS = frozenset([ERR_NOSUCHCHANNEL, ERR_TOOMANYCHANNELS, ERR_BADCHANNELKEY, ERR_INVITEONLYCHAN, ERR_BANNEDFROMCHAN, ERR_NEEDREGGEDNICK])
|
||||
|
||||
class ChannelJoinException(Exception):
|
||||
def __init__(self, code, params):
|
||||
super().__init__(f'Failed to join channel ({code}): {params}')
|
||||
self.code = code
|
||||
self.params = params
|
||||
|
||||
class _IrcTestCase(unittest.TestCase):
|
||||
"""Base class for test cases."""
|
||||
controllerClass = None # Will be set by __main__.py
|
||||
|
||||
@staticmethod
|
||||
def config():
|
||||
"""Some configuration to pass to the controllers.
|
||||
For example, Oragono only enables its MySQL support if
|
||||
config()["chathistory"]=True.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def description(self):
|
||||
method_doc = self._testMethodDoc
|
||||
if not method_doc:
|
||||
return ''
|
||||
return '\t'+supybot.utils.str.normalizeWhitespace(
|
||||
return '\t'+normalizeWhitespace(
|
||||
method_doc,
|
||||
removeNewline=False,
|
||||
).strip().replace('\n ', '\n\t')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.controller = self.controllerClass()
|
||||
self.controller = self.controllerClass(self.config())
|
||||
self.inbuffer = []
|
||||
if self.show_io:
|
||||
print('---- new test ----')
|
||||
@ -99,9 +118,8 @@ class BaseClientTestCase(_IrcTestCase):
|
||||
try:
|
||||
self.conn.sendall(b'QUIT :end of test.')
|
||||
except BrokenPipeError:
|
||||
pass # client disconnected before we did
|
||||
except OSError:
|
||||
pass # the conn was already closed by the test, or something
|
||||
# client already disconnected
|
||||
pass
|
||||
self.controller.kill()
|
||||
if self.conn:
|
||||
self.conn_file.close()
|
||||
@ -113,10 +131,9 @@ class BaseClientTestCase(_IrcTestCase):
|
||||
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server.bind(('', 0)) # Bind any free port
|
||||
self.server.listen(1)
|
||||
def acceptClient(self, tls_cert=None, tls_key=None, server=None):
|
||||
def acceptClient(self, tls_cert=None, tls_key=None):
|
||||
"""Make the server accept a client connection. Blocking."""
|
||||
server = server or self.server
|
||||
(self.conn, addr) = server.accept()
|
||||
(self.conn, addr) = self.server.accept()
|
||||
if tls_cert is None and tls_key is None:
|
||||
pass
|
||||
else:
|
||||
@ -154,9 +171,11 @@ class BaseClientTestCase(_IrcTestCase):
|
||||
if not filter_pred or filter_pred(msg):
|
||||
return msg
|
||||
def sendLine(self, line):
|
||||
self.conn.sendall(line.encode())
|
||||
ret = self.conn.sendall(line.encode())
|
||||
assert ret is None
|
||||
if not line.endswith('\r\n'):
|
||||
self.conn.sendall(b'\r\n')
|
||||
ret = self.conn.sendall(b'\r\n')
|
||||
assert ret is None
|
||||
if self.show_io:
|
||||
print('{:.3f} S: {}'.format(time.time(), line.strip()))
|
||||
|
||||
@ -307,15 +326,9 @@ class BaseServerTestCase(_IrcTestCase):
|
||||
|
||||
def assertDisconnected(self, client):
|
||||
try:
|
||||
self.getLines(client)
|
||||
self.sendLine(client, 'PING foo')
|
||||
while True:
|
||||
l = self.getLine(client)
|
||||
self.assertNotEqual(line, '')
|
||||
m = message_parser.parse_message(l)
|
||||
self.assertNotEqual(m.command, 'PONG',
|
||||
'Client not disconnected.')
|
||||
except socket.error:
|
||||
self.getMessages(client)
|
||||
self.getMessages(client)
|
||||
except (socket.error, ConnectionClosed):
|
||||
del self.clients[client]
|
||||
return
|
||||
else:
|
||||
@ -334,7 +347,7 @@ class BaseServerTestCase(_IrcTestCase):
|
||||
return result
|
||||
|
||||
def connectClient(self, nick, name=None, capabilities=None,
|
||||
skip_if_cap_nak=False, show_io=None):
|
||||
skip_if_cap_nak=False, show_io=None, password=None, ident='username'):
|
||||
client = self.addClient(name, show_io=show_io)
|
||||
if capabilities is not None and 0 < len(capabilities):
|
||||
self.sendLine(client, 'CAP REQ :{}'.format(' '.join(capabilities)))
|
||||
@ -351,8 +364,11 @@ class BaseServerTestCase(_IrcTestCase):
|
||||
else:
|
||||
raise
|
||||
self.sendLine(client, 'CAP END')
|
||||
if password is not None:
|
||||
self.sendLine(client, 'AUTHENTICATE PLAIN')
|
||||
self.sendLine(client, sasl_plain_blob(nick, password))
|
||||
self.sendLine(client, 'NICK {}'.format(nick))
|
||||
self.sendLine(client, 'USER username * * :Realname')
|
||||
self.sendLine(client, 'USER %s * * :Realname' % (ident,))
|
||||
|
||||
welcome = self.skipToWelcome(client)
|
||||
self.sendLine(client, 'PING foo')
|
||||
@ -387,10 +403,30 @@ class BaseServerTestCase(_IrcTestCase):
|
||||
joined = False
|
||||
while not joined:
|
||||
for msg in self.getMessages(client):
|
||||
# todo: also respond to cannot join channel numeric
|
||||
if msg.command.upper() == 'JOIN' and 0 < len(msg.params) and msg.params[0].lower() == channel.lower():
|
||||
if msg.command == 'JOIN' and 0 < len(msg.params) and msg.params[0].lower() == channel.lower():
|
||||
joined = True
|
||||
break
|
||||
elif msg.command in CHANNEL_JOIN_FAIL_NUMERICS:
|
||||
raise ChannelJoinException(msg.command, msg.params)
|
||||
|
||||
def getISupport(self):
|
||||
cn = random_name('bar')
|
||||
self.addClient(name=cn)
|
||||
self.sendLine(cn, 'NICK %s' % (cn,))
|
||||
self.sendLine(cn, 'USER u s e r')
|
||||
messages = self.getMessages(cn)
|
||||
isupport = {}
|
||||
for message in messages:
|
||||
if message.command != '005':
|
||||
continue
|
||||
# 005 nick <tokens...> :are supported by this server
|
||||
tokens = message.params[1:-1]
|
||||
for token in tokens:
|
||||
name, _, value = token.partition('=')
|
||||
isupport[name] = value
|
||||
self.sendLine(cn, 'QUIT')
|
||||
self.assertDisconnected(cn)
|
||||
return isupport
|
||||
|
||||
class OptionalityHelper:
|
||||
def checkSaslSupport(self):
|
||||
@ -419,20 +455,6 @@ class OptionalityHelper:
|
||||
return f(self)
|
||||
return newf
|
||||
|
||||
def checkCapabilitySupport(self, cap):
|
||||
if cap in self.controller.supported_capabilities:
|
||||
return
|
||||
raise runner.CapabilityNotSupported(cap)
|
||||
|
||||
def skipUnlessSupportsCapability(cap):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def newf(self):
|
||||
self.checkCapabilitySupport(cap)
|
||||
return f(self)
|
||||
return newf
|
||||
return decorator
|
||||
|
||||
class SpecificationSelector:
|
||||
|
||||
def requiredBySpecification(*specifications, strict=False):
|
||||
@ -443,12 +465,9 @@ class SpecificationSelector:
|
||||
raise ValueError('Invalid set of specifications: {}'
|
||||
.format(specifications))
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def newf(self):
|
||||
if specifications.isdisjoint(self.testedSpecifications):
|
||||
raise runner.NotRequiredBySpecifications()
|
||||
if strict and not self.strictTests:
|
||||
raise runner.SkipStrictTest()
|
||||
return f(self)
|
||||
return newf
|
||||
for specification in specifications:
|
||||
f = getattr(pytest.mark, specification.value)(f)
|
||||
if strict:
|
||||
f = pytest.mark.strict(f)
|
||||
return f
|
||||
return decorator
|
||||
|
@ -1,5 +1,4 @@
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
from .irc_utils import message_parser
|
||||
@ -25,7 +24,7 @@ class ClientMock:
|
||||
assert not self.ssl, 'SSL already active.'
|
||||
self.conn = ssl.wrap_socket(self.conn)
|
||||
self.ssl = True
|
||||
def getMessages(self, synchronize=True, assert_get_one=False):
|
||||
def getMessages(self, synchronize=True, assert_get_one=False, raw=False):
|
||||
if synchronize:
|
||||
token = 'synchronize{}'.format(time.monotonic())
|
||||
self.sendLine('PING {}'.format(token))
|
||||
@ -42,7 +41,7 @@ class ClientMock:
|
||||
# Received nothing
|
||||
return []
|
||||
if self.show_io:
|
||||
print('{:.3f} waiting…'.format(time.time()))
|
||||
print('{:.3f} {}: waiting…'.format(time.time(), self.name))
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
except ConnectionResetError:
|
||||
@ -65,12 +64,15 @@ class ClientMock:
|
||||
ssl=' (ssl)' if self.ssl else '',
|
||||
client=self.name,
|
||||
line=line))
|
||||
message = message_parser.parse_message(line + '\r\n')
|
||||
message = message_parser.parse_message(line)
|
||||
if message.command == 'PONG' and \
|
||||
token in message.params:
|
||||
got_pong = True
|
||||
else:
|
||||
messages.append(message)
|
||||
if raw:
|
||||
messages.append(line)
|
||||
else:
|
||||
messages.append(message)
|
||||
data = b''
|
||||
except ConnectionClosed:
|
||||
if messages:
|
||||
@ -79,31 +81,43 @@ class ClientMock:
|
||||
raise
|
||||
else:
|
||||
return messages
|
||||
def getMessage(self, filter_pred=None, synchronize=True):
|
||||
def getMessage(self, filter_pred=None, synchronize=True, raw=False):
|
||||
while True:
|
||||
if not self.inbuffer:
|
||||
self.inbuffer = self.getMessages(
|
||||
synchronize=synchronize, assert_get_one=True)
|
||||
synchronize=synchronize, assert_get_one=True, raw=raw)
|
||||
if not self.inbuffer:
|
||||
raise NoMessageException()
|
||||
message = self.inbuffer.pop(0) # TODO: use dequeue
|
||||
if not filter_pred or filter_pred(message):
|
||||
return message
|
||||
def sendLine(self, line):
|
||||
if not line.endswith('\r\n'):
|
||||
line += '\r\n'
|
||||
encoded_line = line.encode()
|
||||
if isinstance(line, str):
|
||||
encoded_line = line.encode()
|
||||
elif isinstance(line, bytes):
|
||||
encoded_line = line
|
||||
else:
|
||||
raise ValueError(line)
|
||||
if not encoded_line.endswith(b'\r\n'):
|
||||
encoded_line += b'\r\n'
|
||||
try:
|
||||
ret = self.conn.sendall(encoded_line)
|
||||
except BrokenPipeError:
|
||||
raise ConnectionClosed()
|
||||
if sys.version_info <= (3, 6) and self.ssl: # https://bugs.python.org/issue25951
|
||||
if self.ssl: # https://bugs.python.org/issue25951
|
||||
assert ret == len(encoded_line), (ret, repr(encoded_line))
|
||||
else:
|
||||
assert ret is None, ret
|
||||
if self.show_io:
|
||||
print('{time:.3f}{ssl} {client} -> S: {line}'.format(
|
||||
if isinstance(line, str):
|
||||
escaped_line = line
|
||||
escaped = ''
|
||||
else:
|
||||
escaped_line = repr(line)
|
||||
escaped = ' (escaped)'
|
||||
print('{time:.3f}{escaped}{ssl} {client} -> S: {line}'.format(
|
||||
time=time.time(),
|
||||
escaped=escaped,
|
||||
ssl=' (ssl)' if self.ssl else '',
|
||||
client=self.name,
|
||||
line=line.strip('\r\n')))
|
||||
line=escaped_line.strip('\r\n')))
|
||||
|
@ -11,4 +11,4 @@ class CapTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper):
|
||||
def testEmptyCapLs(self):
|
||||
"""Empty result to CAP LS. Client should send CAP END."""
|
||||
m = self.negotiateCapabilities([])
|
||||
self.assertEqual(m, Message([], None, 'CAP', ['END']))
|
||||
self.assertEqual(m, Message({}, None, 'CAP', ['END']))
|
||||
|
@ -1,35 +1,14 @@
|
||||
import hashlib
|
||||
|
||||
import ecdsa
|
||||
from ecdsa.util import sigencode_der, sigdecode_der
|
||||
import base64
|
||||
import pyxmpp2_scram as scram
|
||||
|
||||
try:
|
||||
import pyxmpp2_scram as scram
|
||||
except ImportError:
|
||||
scram = None
|
||||
|
||||
from irctest import cases
|
||||
from irctest import authentication
|
||||
from irctest.irc_utils.message_parser import Message
|
||||
|
||||
ECDSA_KEY = """
|
||||
-----BEGIN EC PARAMETERS-----
|
||||
BggqhkjOPQMBBw==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIIJueQ3W2IrGbe9wKdOI75yGS7PYZSj6W4tg854hlsvmoAoGCCqGSM49
|
||||
AwEHoUQDQgAEAZmaVhNSMmV5r8FXPvKuMnqDKyIA9pDHN5TNMfiF3mMeikGgK10W
|
||||
IRX9cyi2wdYg9mUUYyh9GKdBCYHGUJAiCA==
|
||||
-----END EC PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
CHALLENGE = bytes(range(32))
|
||||
assert len(CHALLENGE) == 32
|
||||
|
||||
class IdentityHash:
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
|
||||
def digest(self):
|
||||
return self._data
|
||||
|
||||
class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper,
|
||||
cases.OptionalityHelper):
|
||||
@cases.OptionalityHelper.skipUnlessHasMechanism('PLAIN')
|
||||
@ -41,15 +20,15 @@ class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper,
|
||||
password='sesame',
|
||||
)
|
||||
m = self.negotiateCapabilities(['sasl'], auth=auth)
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['PLAIN']))
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE', ['PLAIN']))
|
||||
self.sendLine('AUTHENTICATE +')
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE',
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE',
|
||||
['amlsbGVzAGppbGxlcwBzZXNhbWU=']))
|
||||
self.sendLine('900 * * jilles :You are now logged in.')
|
||||
self.sendLine('903 * :SASL authentication successful')
|
||||
m = self.negotiateCapabilities(['sasl'], False)
|
||||
self.assertEqual(m, Message([], None, 'CAP', ['END']))
|
||||
self.assertEqual(m, Message({}, None, 'CAP', ['END']))
|
||||
|
||||
@cases.OptionalityHelper.skipUnlessHasMechanism('PLAIN')
|
||||
def testPlainNotAvailable(self):
|
||||
@ -67,10 +46,15 @@ class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper,
|
||||
)
|
||||
m = self.negotiateCapabilities(['sasl=EXTERNAL'], auth=auth)
|
||||
self.assertEqual(self.acked_capabilities, {'sasl'})
|
||||
if m == Message([], None, 'CAP', ['END']):
|
||||
# IRCv3.2-style
|
||||
if m == Message({}, None, 'CAP', ['END']):
|
||||
# IRCv3.2-style, for clients that skip authentication
|
||||
# when unavailable (eg. Limnoria)
|
||||
return
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['PLAIN']))
|
||||
elif m.command == 'QUIT':
|
||||
# IRCv3.2-style, for clients that quit when unavailable
|
||||
# (eg. Sopel)
|
||||
return
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE', ['PLAIN']))
|
||||
self.sendLine('904 {} :SASL auth failed'.format(self.nick))
|
||||
m = self.getMessage()
|
||||
self.assertMessageEqual(m, command='CAP')
|
||||
@ -91,21 +75,21 @@ class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper,
|
||||
authstring = base64.b64encode(b'\x00'.join(
|
||||
[b'foo', b'foo', b'bar'*200])).decode()
|
||||
m = self.negotiateCapabilities(['sasl'], auth=auth)
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['PLAIN']))
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE', ['PLAIN']))
|
||||
self.sendLine('AUTHENTICATE +')
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE',
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE',
|
||||
[authstring[0:400]]), m)
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE',
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE',
|
||||
[authstring[400:800]]))
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE',
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE',
|
||||
[authstring[800:]]))
|
||||
self.sendLine('900 * * {} :You are now logged in.'.format('foo'))
|
||||
self.sendLine('903 * :SASL authentication successful')
|
||||
m = self.negotiateCapabilities(['sasl'], False)
|
||||
self.assertEqual(m, Message([], None, 'CAP', ['END']))
|
||||
self.assertEqual(m, Message({}, None, 'CAP', ['END']))
|
||||
|
||||
@cases.OptionalityHelper.skipUnlessHasMechanism('PLAIN')
|
||||
def testPlainLargeMultiple(self):
|
||||
@ -122,51 +106,21 @@ class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper,
|
||||
authstring = base64.b64encode(b'\x00'.join(
|
||||
[b'foo', b'foo', b'quux'*148])).decode()
|
||||
m = self.negotiateCapabilities(['sasl'], auth=auth)
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['PLAIN']))
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE', ['PLAIN']))
|
||||
self.sendLine('AUTHENTICATE +')
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE',
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE',
|
||||
[authstring[0:400]]), m)
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE',
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE',
|
||||
[authstring[400:800]]))
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE',
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE',
|
||||
['+']))
|
||||
self.sendLine('900 * * {} :You are now logged in.'.format('foo'))
|
||||
self.sendLine('903 * :SASL authentication successful')
|
||||
m = self.negotiateCapabilities(['sasl'], False)
|
||||
self.assertEqual(m, Message([], None, 'CAP', ['END']))
|
||||
|
||||
@cases.OptionalityHelper.skipUnlessHasMechanism('ECDSA-NIST256P-CHALLENGE')
|
||||
def testEcdsa(self):
|
||||
"""Test ECDSA authentication.
|
||||
"""
|
||||
auth = authentication.Authentication(
|
||||
mechanisms=[authentication.Mechanisms.ecdsa_nist256p_challenge],
|
||||
username='jilles',
|
||||
ecdsa_key=ECDSA_KEY,
|
||||
)
|
||||
m = self.negotiateCapabilities(['sasl'], auth=auth)
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['ECDSA-NIST256P-CHALLENGE']))
|
||||
self.sendLine('AUTHENTICATE +')
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE',
|
||||
['amlsbGVz'])) # jilles
|
||||
self.sendLine('AUTHENTICATE {}'.format(base64.b64encode(CHALLENGE).decode('ascii')))
|
||||
m = self.getMessage()
|
||||
self.assertMessageEqual(m, command='AUTHENTICATE')
|
||||
sk = ecdsa.SigningKey.from_pem(ECDSA_KEY)
|
||||
vk = sk.get_verifying_key()
|
||||
signature = base64.b64decode(m.params[0])
|
||||
try:
|
||||
vk.verify(signature, CHALLENGE, hashfunc=IdentityHash, sigdecode=sigdecode_der)
|
||||
except ecdsa.BadSignatureError:
|
||||
raise AssertionError('Bad signature')
|
||||
self.sendLine('900 * * foo :You are now logged in.')
|
||||
self.sendLine('903 * :SASL authentication successful')
|
||||
m = self.negotiateCapabilities(['sasl'], False)
|
||||
self.assertEqual(m, Message([], None, 'CAP', ['END']))
|
||||
self.assertEqual(m, Message({}, None, 'CAP', ['END']))
|
||||
|
||||
@cases.OptionalityHelper.skipUnlessHasMechanism('SCRAM-SHA-256')
|
||||
def testScram(self):
|
||||
@ -184,7 +138,7 @@ class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper,
|
||||
channel_binding=False, password_database=PasswdDb())
|
||||
|
||||
m = self.negotiateCapabilities(['sasl'], auth=auth)
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['SCRAM-SHA-256']))
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE', ['SCRAM-SHA-256']))
|
||||
self.sendLine('AUTHENTICATE +')
|
||||
|
||||
m = self.getMessage()
|
||||
@ -204,10 +158,6 @@ class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper,
|
||||
self.sendLine('AUTHENTICATE :' + base64.b64encode(response).decode())
|
||||
self.assertEqual(properties, {'authzid': None, 'username': 'jilles'})
|
||||
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m.command, 'AUTHENTICATE', m)
|
||||
self.assertEqual(m.params, ['+'], m)
|
||||
|
||||
@cases.OptionalityHelper.skipUnlessHasMechanism('SCRAM-SHA-256')
|
||||
def testScramBadPassword(self):
|
||||
"""Test SCRAM-SHA-256 authentication with a bad password.
|
||||
@ -224,7 +174,7 @@ class SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper,
|
||||
channel_binding=False, password_database=PasswdDb())
|
||||
|
||||
m = self.negotiateCapabilities(['sasl'], auth=auth)
|
||||
self.assertEqual(m, Message([], None, 'AUTHENTICATE', ['SCRAM-SHA-256']))
|
||||
self.assertEqual(m, Message({}, None, 'AUTHENTICATE', ['SCRAM-SHA-256']))
|
||||
self.sendLine('AUTHENTICATE +')
|
||||
|
||||
m = self.getMessage()
|
||||
@ -254,4 +204,10 @@ class Irc302SaslTestCase(cases.BaseClientTestCase, cases.ClientNegociationHelper
|
||||
)
|
||||
m = self.negotiateCapabilities(['sasl=EXTERNAL'], auth=auth)
|
||||
self.assertEqual(self.acked_capabilities, {'sasl'})
|
||||
self.assertEqual(m, Message([], None, 'CAP', ['END']))
|
||||
|
||||
if m.command == 'QUIT':
|
||||
# Some clients quit when it can't authenticate (eg. Sopel)
|
||||
pass
|
||||
else:
|
||||
# Others will just skip authentication (eg. Limnoria)
|
||||
self.assertEqual(m, Message({}, None, 'CAP', ['END']))
|
||||
|
@ -1,6 +1,3 @@
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from irctest import tls
|
||||
from irctest import cases
|
||||
from irctest.exceptions import ConnectionClosed
|
||||
@ -144,92 +141,3 @@ class TlsTestCase(cases.BaseClientTestCase):
|
||||
self.acceptClient(tls_cert=BAD_CERT, tls_key=BAD_KEY)
|
||||
with self.assertRaises((ConnectionClosed, ConnectionResetError)):
|
||||
m = self.getMessage()
|
||||
|
||||
|
||||
class StsTestCase(cases.BaseClientTestCase, cases.OptionalityHelper):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.insecure_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.insecure_server.bind(('', 0)) # Bind any free port
|
||||
self.insecure_server.listen(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.insecure_server.close()
|
||||
super().tearDown()
|
||||
|
||||
@cases.OptionalityHelper.skipUnlessSupportsCapability('sts')
|
||||
def testSts(self):
|
||||
tls_config = tls.TlsConfig(
|
||||
enable=False,
|
||||
trusted_fingerprints=[GOOD_FINGERPRINT])
|
||||
|
||||
# Connect client to insecure server
|
||||
(hostname, port) = self.insecure_server.getsockname()
|
||||
self.controller.run(
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
auth=None,
|
||||
tls_config=tls_config,
|
||||
)
|
||||
self.acceptClient(server=self.insecure_server)
|
||||
|
||||
# Send STS policy to client
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m.command, 'CAP',
|
||||
'First message is not CAP LS.')
|
||||
self.assertEqual(m.params[0], 'LS',
|
||||
'First message is not CAP LS.')
|
||||
self.sendLine('CAP * LS :sts=port={}'.format(self.server.getsockname()[1]))
|
||||
|
||||
# "If the client is not already connected securely to the server
|
||||
# at the requested hostname, it MUST close the insecure connection
|
||||
# and reconnect securely on the stated port."
|
||||
self.acceptClient(tls_cert=GOOD_CERT, tls_key=GOOD_KEY)
|
||||
|
||||
# Send the STS policy, over secure connection this time
|
||||
self.sendLine('CAP * LS :sts=duration=10,port={}'.format(
|
||||
self.server.getsockname()[1]))
|
||||
|
||||
# Make the client reconnect. It should reconnect to the secure server.
|
||||
self.sendLine('ERROR :closing link')
|
||||
self.acceptClient()
|
||||
|
||||
# Kill the client
|
||||
self.controller.terminate()
|
||||
|
||||
# Run the client, still configured to connect to the insecure server
|
||||
self.controller.run(
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
auth=None,
|
||||
tls_config=tls_config,
|
||||
)
|
||||
|
||||
# The client should remember the STS policy and connect to the secure
|
||||
# server
|
||||
self.acceptClient()
|
||||
|
||||
@cases.OptionalityHelper.skipUnlessSupportsCapability('sts')
|
||||
def testStsInvalidCertificate(self):
|
||||
# Connect client to insecure server
|
||||
(hostname, port) = self.insecure_server.getsockname()
|
||||
self.controller.run(
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
auth=None,
|
||||
)
|
||||
self.acceptClient(server=self.insecure_server)
|
||||
|
||||
# Send STS policy to client
|
||||
m = self.getMessage()
|
||||
self.assertEqual(m.command, 'CAP',
|
||||
'First message is not CAP LS.')
|
||||
self.assertEqual(m.params[0], 'LS',
|
||||
'First message is not CAP LS.')
|
||||
self.sendLine('CAP * LS :sts=port={}'.format(self.server.getsockname()[1]))
|
||||
|
||||
# The client will reconnect to the TLS port. Unfortunately, it does
|
||||
# not trust its fingerprint.
|
||||
|
||||
with self.assertRaises((ssl.SSLError, socket.error)):
|
||||
self.acceptClient(tls_cert=GOOD_CERT, tls_key=GOOD_KEY)
|
||||
|
@ -45,7 +45,6 @@ TEMPLATE_SSL_CONFIG = """
|
||||
class CharybdisController(BaseServerController, DirectoryBasedController):
|
||||
software_name = 'Charybdis'
|
||||
supported_sasl_mechanisms = set()
|
||||
supported_capabilities = set() # Not exhaustive
|
||||
def create_config(self):
|
||||
super().create_config()
|
||||
with self.open_file('server.conf'):
|
||||
|
@ -4,13 +4,12 @@ from irctest.basecontrollers import BaseClientController, NotImplementedByContro
|
||||
|
||||
class GircController(BaseClientController):
|
||||
software_name = 'gIRC'
|
||||
supported_sasl_mechanisms = ['PLAIN']
|
||||
supported_capabilities = set() # Not exhaustive
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.directory = None
|
||||
self.proc = None
|
||||
self.supported_sasl_mechanisms = ['PLAIN']
|
||||
|
||||
def kill(self):
|
||||
if self.proc:
|
||||
|
@ -43,8 +43,6 @@ TEMPLATE_SSL_CONFIG = """
|
||||
class HybridController(BaseServerController, DirectoryBasedController):
|
||||
software_name = 'Hybrid'
|
||||
supported_sasl_mechanisms = set()
|
||||
supported_capabilities = set() # Not exhaustive
|
||||
|
||||
def create_config(self):
|
||||
super().create_config()
|
||||
with self.open_file('server.conf'):
|
||||
|
@ -30,8 +30,6 @@ TEMPLATE_SSL_CONFIG = """
|
||||
class InspircdController(BaseServerController, DirectoryBasedController):
|
||||
software_name = 'InspIRCd'
|
||||
supported_sasl_mechanisms = set()
|
||||
supported_capabilities = set() # Not exhaustive
|
||||
|
||||
def create_config(self):
|
||||
super().create_config()
|
||||
with self.open_file('server.conf'):
|
||||
|
@ -2,7 +2,6 @@ import os
|
||||
import subprocess
|
||||
|
||||
from irctest import authentication
|
||||
from irctest import tls
|
||||
from irctest.basecontrollers import NotImplementedByController
|
||||
from irctest.basecontrollers import BaseClientController, DirectoryBasedController
|
||||
|
||||
@ -31,19 +30,14 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
|
||||
supported_sasl_mechanisms = {
|
||||
'PLAIN', 'ECDSA-NIST256P-CHALLENGE', 'SCRAM-SHA-256', 'EXTERNAL',
|
||||
}
|
||||
supported_capabilities = set(['sts']) # Not exhaustive
|
||||
|
||||
def create_config(self):
|
||||
create_config = super().create_config()
|
||||
if create_config:
|
||||
with self.open_file('bot.conf'):
|
||||
pass
|
||||
with self.open_file('conf/users.conf'):
|
||||
pass
|
||||
super().create_config()
|
||||
with self.open_file('bot.conf'):
|
||||
pass
|
||||
with self.open_file('conf/users.conf'):
|
||||
pass
|
||||
|
||||
def run(self, hostname, port, auth, tls_config=None):
|
||||
if tls_config is None:
|
||||
tls_config = tls.TlsConfig(enable=False, trusted_fingerprints=[])
|
||||
def run(self, hostname, port, auth, tls_config):
|
||||
# Runs a client with the config given as arguments
|
||||
assert self.proc is None
|
||||
self.create_config()
|
||||
@ -68,8 +62,7 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
|
||||
trusted_fingerprints=' '.join(tls_config.trusted_fingerprints) if tls_config else '',
|
||||
))
|
||||
self.proc = subprocess.Popen(['supybot',
|
||||
os.path.join(self.directory, 'bot.conf')],
|
||||
stderr=subprocess.STDOUT)
|
||||
os.path.join(self.directory, 'bot.conf')])
|
||||
|
||||
def get_irctest_controller_class():
|
||||
return LimnoriaController
|
||||
|
@ -66,8 +66,6 @@ class MammonController(BaseServerController, DirectoryBasedController):
|
||||
supported_sasl_mechanisms = {
|
||||
'PLAIN', 'ECDSA-NIST256P-CHALLENGE',
|
||||
}
|
||||
supported_capabilities = set() # Not exhaustive
|
||||
|
||||
def create_config(self):
|
||||
super().create_config()
|
||||
with self.open_file('server.conf'):
|
||||
|
@ -1,125 +1,211 @@
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from irctest.basecontrollers import NotImplementedByController
|
||||
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||
|
||||
TEMPLATE_CONFIG = """
|
||||
network:
|
||||
name: OragonoTest
|
||||
OPER_PWD = 'frenchfries'
|
||||
|
||||
server:
|
||||
name: oragono.test
|
||||
listen:
|
||||
- "{hostname}:{port}"
|
||||
{tls}
|
||||
BASE_CONFIG = {
|
||||
"network": {
|
||||
"name": "OragonoTest",
|
||||
},
|
||||
|
||||
check-ident: false
|
||||
"server": {
|
||||
"name": "oragono.test",
|
||||
"listeners": {},
|
||||
"max-sendq": "16k",
|
||||
"connection-limits": {
|
||||
"enabled": True,
|
||||
"cidr-len-ipv4": 32,
|
||||
"cidr-len-ipv6": 64,
|
||||
"ips-per-subnet": 1,
|
||||
"exempted": ["localhost"],
|
||||
},
|
||||
"connection-throttling": {
|
||||
"enabled": True,
|
||||
"cidr-len-ipv4": 32,
|
||||
"cidr-len-ipv6": 64,
|
||||
"ips-per-subnet": 16,
|
||||
"duration": "10m",
|
||||
"max-connections": 1,
|
||||
"ban-duration": "10m",
|
||||
"ban-message": "Try again later",
|
||||
"exempted": ["localhost"],
|
||||
},
|
||||
'enforce-utf8': True,
|
||||
'relaymsg': {
|
||||
'enabled': True,
|
||||
'separators': '/',
|
||||
'available-to-chanops': True,
|
||||
},
|
||||
},
|
||||
|
||||
max-sendq: 16k
|
||||
'accounts': {
|
||||
'authentication-enabled': True,
|
||||
'multiclient': {
|
||||
'allowed-by-default': True,
|
||||
'enabled': True,
|
||||
'always-on': 'disabled',
|
||||
},
|
||||
'registration': {
|
||||
'bcrypt-cost': 4,
|
||||
'enabled': True,
|
||||
'enabled-callbacks': ['none'],
|
||||
'verify-timeout': '120h',
|
||||
},
|
||||
'nick-reservation': {
|
||||
'enabled': True,
|
||||
'additional-nick-limit': 2,
|
||||
'method': 'strict',
|
||||
},
|
||||
},
|
||||
|
||||
connection-limits:
|
||||
cidr-len-ipv4: 24
|
||||
cidr-len-ipv6: 120
|
||||
ips-per-subnet: 16
|
||||
"channels": {
|
||||
"registration": {"enabled": True,},
|
||||
},
|
||||
|
||||
exempted:
|
||||
- "127.0.0.1/8"
|
||||
- "::1/128"
|
||||
"datastore": {
|
||||
"path": None,
|
||||
},
|
||||
|
||||
connection-throttling:
|
||||
enabled: true
|
||||
cidr-len-ipv4: 32
|
||||
cidr-len-ipv6: 128
|
||||
duration: 10m
|
||||
max-connections: 12
|
||||
ban-duration: 10m
|
||||
ban-message: You have attempted to connect too many times within a short duration. Wait a while, and you will be able to connect.
|
||||
'limits': {
|
||||
'awaylen': 200,
|
||||
'chan-list-modes': 60,
|
||||
'channellen': 64,
|
||||
'kicklen': 390,
|
||||
'linelen': {'rest': 2048,},
|
||||
'monitor-entries': 100,
|
||||
'nicklen': 32,
|
||||
'topiclen': 390,
|
||||
'whowas-entries': 100,
|
||||
'multiline': {'max-bytes': 4096, 'max-lines': 32,},
|
||||
},
|
||||
|
||||
exempted:
|
||||
- "127.0.0.1/8"
|
||||
- "::1/128"
|
||||
"history": {
|
||||
"enabled": True,
|
||||
"channel-length": 128,
|
||||
"client-length": 128,
|
||||
"chathistory-maxmessages": 100,
|
||||
"tagmsg-storage": {
|
||||
"default": False,
|
||||
"whitelist": ["+draft/persist", "+persist"],
|
||||
},
|
||||
},
|
||||
|
||||
accounts:
|
||||
registration:
|
||||
enabled: true
|
||||
verify-timeout: "120h"
|
||||
enabled-callbacks:
|
||||
- none # no verification needed, will instantly register successfully
|
||||
allow-multiple-per-connection: true
|
||||
'oper-classes': {
|
||||
'server-admin': {
|
||||
'title': 'Server Admin',
|
||||
'capabilities': [
|
||||
"oper:local_kill",
|
||||
"oper:local_ban",
|
||||
"oper:local_unban",
|
||||
"nofakelag",
|
||||
"oper:remote_kill",
|
||||
"oper:remote_ban",
|
||||
"oper:remote_unban",
|
||||
"oper:rehash",
|
||||
"oper:die",
|
||||
"accreg",
|
||||
"sajoin",
|
||||
"samode",
|
||||
"vhosts",
|
||||
"chanreg",
|
||||
"relaymsg",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
authentication-enabled: true
|
||||
'opers': {
|
||||
'root': {
|
||||
'class': 'server-admin',
|
||||
'whois-line': 'is a server admin',
|
||||
# OPER_PWD
|
||||
'password': '$2a$04$3GzUZB5JapaAbwn7sogpOu9NSiLOgnozVllm2e96LiNPrm61ZsZSq',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
channels:
|
||||
registration:
|
||||
enabled: true
|
||||
LOGGING_CONFIG = {
|
||||
"logging": [
|
||||
{
|
||||
"method": "stderr",
|
||||
"level": "debug",
|
||||
"type": "*",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
datastore:
|
||||
path: {directory}/ircd.db
|
||||
|
||||
limits:
|
||||
nicklen: 32
|
||||
channellen: 64
|
||||
awaylen: 200
|
||||
kicklen: 390
|
||||
topiclen: 390
|
||||
monitor-entries: 100
|
||||
whowas-entries: 100
|
||||
chan-list-modes: 60
|
||||
linelen:
|
||||
tags: 2048
|
||||
rest: 2048
|
||||
"""
|
||||
def hash_password(password):
|
||||
if isinstance(password, str):
|
||||
password = password.encode('utf-8')
|
||||
# simulate entry of password and confirmation:
|
||||
input_ = password + b'\n' + password + b'\n'
|
||||
p = subprocess.Popen(['oragono', 'genpasswd'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
out, _ = p.communicate(input_)
|
||||
return out.decode('utf-8')
|
||||
|
||||
class OragonoController(BaseServerController, DirectoryBasedController):
|
||||
software_name = 'Oragono'
|
||||
supported_sasl_mechanisms = {
|
||||
'PLAIN',
|
||||
}
|
||||
supported_capabilities = set() # Not exhaustive
|
||||
|
||||
def create_config(self):
|
||||
super().create_config()
|
||||
with self.open_file('ircd.yaml'):
|
||||
pass
|
||||
_port_wait_interval = .01
|
||||
|
||||
def kill_proc(self):
|
||||
self.proc.kill()
|
||||
|
||||
def run(self, hostname, port, password=None, ssl=False,
|
||||
restricted_metadata_keys=None,
|
||||
valid_metadata_keys=None, invalid_metadata_keys=None):
|
||||
valid_metadata_keys=None, invalid_metadata_keys=None, config=None):
|
||||
if valid_metadata_keys or invalid_metadata_keys:
|
||||
raise NotImplementedByController(
|
||||
'Defining valid and invalid METADATA keys.')
|
||||
if password is not None:
|
||||
#TODO(dan): fix dis
|
||||
raise NotImplementedByController('PASS command')
|
||||
|
||||
self.create_config()
|
||||
tls_config = ""
|
||||
if config is None:
|
||||
config = copy.deepcopy(BASE_CONFIG)
|
||||
|
||||
enable_chathistory = self.test_config.get("chathistory")
|
||||
enable_roleplay = self.test_config.get("oragono_roleplay")
|
||||
if enable_chathistory or enable_roleplay:
|
||||
config = self.addMysqlToConfig(config)
|
||||
|
||||
if enable_roleplay:
|
||||
config['roleplay'] = {
|
||||
'enabled': True,
|
||||
}
|
||||
|
||||
if 'oragono_config' in self.test_config:
|
||||
self.test_config['oragono_config'](config)
|
||||
|
||||
self.port = port
|
||||
bind_address = "127.0.0.1:%s" % (port,)
|
||||
listener_conf = None # plaintext
|
||||
if ssl:
|
||||
self.key_path = os.path.join(self.directory, 'ssl.key')
|
||||
self.pem_path = os.path.join(self.directory, 'ssl.pem')
|
||||
tls_config = 'tls-listeners:\n ":{port}":\n key: {key}\n cert: {pem}'.format(
|
||||
port=port,
|
||||
key=self.key_path,
|
||||
pem=self.pem_path,
|
||||
)
|
||||
listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path},}
|
||||
config['server']['listeners'][bind_address] = listener_conf
|
||||
|
||||
config['datastore']['path'] = os.path.join(self.directory, 'ircd.db')
|
||||
|
||||
if password is not None:
|
||||
config['server']['password'] = hash_password(password)
|
||||
|
||||
assert self.proc is None
|
||||
self.port = port
|
||||
with self.open_file('server.yml') as fd:
|
||||
fd.write(TEMPLATE_CONFIG.format(
|
||||
directory=self.directory,
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
tls=tls_config,
|
||||
))
|
||||
|
||||
self._config_path = os.path.join(self.directory, 'server.yml')
|
||||
self._config = config
|
||||
self._write_config()
|
||||
subprocess.call(['oragono', 'initdb',
|
||||
'--conf', os.path.join(self.directory, 'server.yml'), '--quiet'])
|
||||
'--conf', self._config_path, '--quiet'])
|
||||
subprocess.call(['oragono', 'mkcerts',
|
||||
'--conf', os.path.join(self.directory, 'server.yml'), '--quiet'])
|
||||
'--conf', self._config_path, '--quiet'])
|
||||
self.proc = subprocess.Popen(['oragono', 'run',
|
||||
'--conf', os.path.join(self.directory, 'server.yml'), '--quiet'])
|
||||
'--conf', self._config_path, '--quiet'])
|
||||
|
||||
def registerUser(self, case, username, password=None):
|
||||
# XXX: Move this somewhere else when
|
||||
@ -127,18 +213,76 @@ class OragonoController(BaseServerController, DirectoryBasedController):
|
||||
# part of the specification
|
||||
client = case.addClient(show_io=False)
|
||||
case.sendLine(client, 'CAP LS 302')
|
||||
case.sendLine(client, 'NICK registration_user')
|
||||
case.sendLine(client, 'NICK ' + username)
|
||||
case.sendLine(client, 'USER r e g :user')
|
||||
case.sendLine(client, 'CAP END')
|
||||
while case.getRegistrationMessage(client).command != '001':
|
||||
pass
|
||||
case.getMessages(client)
|
||||
case.sendLine(client, 'ACC REGISTER {} * {}'.format(
|
||||
username, password))
|
||||
case.sendLine(client, 'NS REGISTER ' + password)
|
||||
msg = case.getMessage(client)
|
||||
assert msg.command == '920', msg
|
||||
assert msg.params == [username, 'Account created']
|
||||
case.sendLine(client, 'QUIT')
|
||||
case.assertDisconnected(client)
|
||||
|
||||
def _write_config(self):
|
||||
with open(self._config_path, 'w') as fd:
|
||||
json.dump(self._config, fd)
|
||||
|
||||
def baseConfig(self):
|
||||
return copy.deepcopy(BASE_CONFIG)
|
||||
|
||||
def getConfig(self):
|
||||
return copy.deepcopy(self._config)
|
||||
|
||||
def addLoggingToConfig(self, config=None):
|
||||
if config is None:
|
||||
config = self.baseConfig()
|
||||
config.update(LOGGING_CONFIG)
|
||||
return config
|
||||
|
||||
def addMysqlToConfig(self, config=None):
|
||||
mysql_password = os.getenv('MYSQL_PASSWORD')
|
||||
if not mysql_password:
|
||||
return config
|
||||
if config is None:
|
||||
config = self.baseConfig()
|
||||
config['datastore']['mysql'] = {
|
||||
"enabled": True,
|
||||
"host": "localhost",
|
||||
"user": "oragono",
|
||||
"password": mysql_password,
|
||||
"history-database": "oragono_history",
|
||||
"timeout": "3s",
|
||||
}
|
||||
config['accounts']['multiclient'] = {
|
||||
'enabled': True,
|
||||
'allowed-by-default': True,
|
||||
'always-on': 'disabled',
|
||||
}
|
||||
config['history']['persistent'] = {
|
||||
"enabled": True,
|
||||
"unregistered-channels": True,
|
||||
"registered-channels": "opt-out",
|
||||
"direct-messages": "opt-out",
|
||||
}
|
||||
return config
|
||||
|
||||
def rehash(self, case, config):
|
||||
self._config = config
|
||||
self._write_config()
|
||||
client = 'operator_for_rehash'
|
||||
case.connectClient(nick=client, name=client)
|
||||
case.sendLine(client, 'OPER root %s' % (OPER_PWD,))
|
||||
case.sendLine(client, 'REHASH')
|
||||
case.getMessages(client)
|
||||
case.sendLine(client, 'QUIT')
|
||||
case.assertDisconnected(client)
|
||||
|
||||
def enable_debug_logging(self, case):
|
||||
config = self.getConfig()
|
||||
config.update(LOGGING_CONFIG)
|
||||
self.rehash(case, config)
|
||||
|
||||
def get_irctest_controller_class():
|
||||
return OragonoController
|
||||
|
@ -24,10 +24,8 @@ class SopelController(BaseClientController):
|
||||
supported_sasl_mechanisms = {
|
||||
'PLAIN',
|
||||
}
|
||||
supported_capabilities = set() # Not exhaustive
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, test_config):
|
||||
super().__init__(test_config)
|
||||
self.filename = next(tempfile._get_candidate_names()) + '.cfg'
|
||||
self.proc = None
|
||||
def kill(self):
|
||||
|
53
irctest/irc_utils/junkdrawer.py
Normal file
53
irctest/irc_utils/junkdrawer.py
Normal file
@ -0,0 +1,53 @@
|
||||
import datetime
|
||||
import re
|
||||
import secrets
|
||||
from collections import namedtuple
|
||||
|
||||
HistoryMessage = namedtuple('HistoryMessage', ['time', 'msgid', 'target', 'text'])
|
||||
|
||||
def to_history_message(msg):
|
||||
return HistoryMessage(time=msg.tags.get('time'), msgid=msg.tags.get('msgid'), target=msg.params[0], text=msg.params[1])
|
||||
|
||||
# thanks jess!
|
||||
IRCV3_FORMAT_STRFTIME = "%Y-%m-%dT%H:%M:%S.%f%z"
|
||||
|
||||
def ircv3_timestamp_to_unixtime(timestamp):
|
||||
return datetime.datetime.strptime(timestamp, IRCV3_FORMAT_STRFTIME).timestamp()
|
||||
|
||||
def random_name(base):
|
||||
return base + '-' + secrets.token_hex(8)
|
||||
|
||||
"""
|
||||
Stolen from supybot:
|
||||
"""
|
||||
|
||||
class MultipleReplacer:
|
||||
"""Return a callable that replaces all dict keys by the associated
|
||||
value. More efficient than multiple .replace()."""
|
||||
|
||||
# We use an object instead of a lambda function because it avoids the
|
||||
# need for using the staticmethod() on the lambda function if assigning
|
||||
# it to a class in Python 3.
|
||||
def __init__(self, dict_):
|
||||
self._dict = dict_
|
||||
dict_ = dict([(re.escape(key), val) for key,val in dict_.items()])
|
||||
self._matcher = re.compile('|'.join(dict_.keys()))
|
||||
def __call__(self, s):
|
||||
return self._matcher.sub(lambda m: self._dict[m.group(0)], s)
|
||||
|
||||
def normalizeWhitespace(s, removeNewline=True):
|
||||
r"""Normalizes the whitespace in a string; \s+ becomes one space."""
|
||||
if not s:
|
||||
return str(s) # not the same reference
|
||||
starts_with_space = (s[0] in ' \n\t\r')
|
||||
ends_with_space = (s[-1] in ' \n\t\r')
|
||||
if removeNewline:
|
||||
newline_re = re.compile('[\r\n]+')
|
||||
s = ' '.join(filter(bool, newline_re.split(s)))
|
||||
s = ' '.join(filter(bool, s.split('\t')))
|
||||
s = ' '.join(filter(bool, s.split(' ')))
|
||||
if starts_with_space:
|
||||
s = ' ' + s
|
||||
if ends_with_space:
|
||||
s += ' '
|
||||
return s
|
@ -1,6 +1,7 @@
|
||||
import re
|
||||
import collections
|
||||
import supybot.utils
|
||||
|
||||
from .junkdrawer import MultipleReplacer
|
||||
|
||||
# http://ircv3.net/specs/core/message-tags-3.2.html#escaping-values
|
||||
TAG_ESCAPE = [
|
||||
@ -10,11 +11,11 @@ TAG_ESCAPE = [
|
||||
('\r', r'\r'),
|
||||
('\n', r'\n'),
|
||||
]
|
||||
unescape_tag_value = supybot.utils.str.MultipleReplacer(
|
||||
unescape_tag_value = MultipleReplacer(
|
||||
dict(map(lambda x:(x[1],x[0]), TAG_ESCAPE)))
|
||||
|
||||
# TODO: validate host
|
||||
tag_key_validator = re.compile('(\S+/)?[a-zA-Z0-9-]+')
|
||||
tag_key_validator = re.compile(r'\+?(\S+/)?[a-zA-Z0-9-]+')
|
||||
|
||||
def parse_tags(s):
|
||||
tags = {}
|
||||
@ -36,8 +37,7 @@ def parse_message(s):
|
||||
http://tools.ietf.org/html/rfc1459#section-2.3.1
|
||||
and
|
||||
http://ircv3.net/specs/core/message-tags-3.2.html"""
|
||||
assert s.endswith('\r\n'), 'Message does not end with CR LF: {!r}'.format(s)
|
||||
s = s[0:-2]
|
||||
s = s.rstrip('\r\n')
|
||||
if s.startswith('@'):
|
||||
(tags, s) = s.split(' ', 1)
|
||||
tags = parse_tags(tags[1:])
|
||||
|
6
irctest/irc_utils/sasl.py
Normal file
6
irctest/irc_utils/sasl.py
Normal file
@ -0,0 +1,6 @@
|
||||
import base64
|
||||
|
||||
def sasl_plain_blob(username, passphrase):
|
||||
blob = base64.b64encode(b'\x00'.join((username.encode('utf-8'), username.encode('utf-8'), passphrase.encode('utf-8'))))
|
||||
blobstr = blob.decode('ascii')
|
||||
return f'AUTHENTICATE {blobstr}'
|
@ -47,6 +47,8 @@ RPL_ADMINEMAIL = "259"
|
||||
RPL_TRACELOG = "261"
|
||||
RPL_TRACEEND = "262"
|
||||
RPL_TRYAGAIN = "263"
|
||||
RPL_LOCALUSERS = "265"
|
||||
RPL_GLOBALUSERS = "266"
|
||||
RPL_WHOISCERTFP = "276"
|
||||
RPL_AWAY = "301"
|
||||
RPL_USERHOST = "302"
|
||||
@ -116,6 +118,7 @@ ERR_NOTEXTTOSEND = "412"
|
||||
ERR_NOTOPLEVEL = "413"
|
||||
ERR_WILDTOPLEVEL = "414"
|
||||
ERR_BADMASK = "415"
|
||||
ERR_INPUTTOOLONG = "417"
|
||||
ERR_UNKNOWNCOMMAND = "421"
|
||||
ERR_NOMOTD = "422"
|
||||
ERR_NOADMININFO = "423"
|
||||
@ -141,6 +144,7 @@ ERR_YOUREBANNEDCREEP = "465"
|
||||
ERR_YOUWILLBEBANNED = "466"
|
||||
ERR_KEYSET = "467"
|
||||
ERR_INVALIDUSERNAME = "468"
|
||||
ERR_LINKCHANNEL = "470"
|
||||
ERR_CHANNELISFULL = "471"
|
||||
ERR_UNKNOWNMODE = "472"
|
||||
ERR_INVITEONLYCHAN = "473"
|
||||
@ -148,6 +152,7 @@ ERR_BANNEDFROMCHAN = "474"
|
||||
ERR_BADCHANNELKEY = "475"
|
||||
ERR_BADCHANMASK = "476"
|
||||
ERR_NOCHANMODES = "477"
|
||||
ERR_NEEDREGGEDNICK = "477"
|
||||
ERR_BANLISTFULL = "478"
|
||||
ERR_NOPRIVILEGES = "481"
|
||||
ERR_CHANOPRIVSNEEDED = "482"
|
||||
@ -162,6 +167,7 @@ ERR_CANNOTSENDRP = "573"
|
||||
RPL_WHOISSECURE = "671"
|
||||
RPL_YOURLANGUAGESARE = "687"
|
||||
RPL_WHOISLANGUAGE = "690"
|
||||
ERR_INVALIDMODEPARAM = "696"
|
||||
RPL_HELPSTART = "704"
|
||||
RPL_HELPTXT = "705"
|
||||
RPL_ENDOFHELP = "706"
|
||||
|
@ -19,10 +19,6 @@ class OptionalSaslMechanismNotSupported(unittest.SkipTest):
|
||||
def __str__(self):
|
||||
return 'Unsupported SASL mechanism: {}'.format(self.args[0])
|
||||
|
||||
class CapabilityNotSupported(unittest.SkipTest):
|
||||
def __str__(self):
|
||||
return 'Unsupported capability: {}'.format(self.args[0])
|
||||
|
||||
class NotRequiredBySpecifications(unittest.SkipTest):
|
||||
def __str__(self):
|
||||
return 'Tests not required by the set of tested specification(s).'
|
||||
|
@ -45,32 +45,3 @@ class AccountTagTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||
fail_msg='PRIVMSG by logged in nick '
|
||||
'does not contain the correct account tag (should be '
|
||||
'“jilles”): {msg}')
|
||||
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.OptionalityHelper.skipUnlessHasMechanism('PLAIN')
|
||||
def testMonitor(self):
|
||||
self.connectClient('foo', capabilities=['account-tag'],
|
||||
skip_if_cap_nak=True)
|
||||
if 'MONITOR' not in self.server_support:
|
||||
raise NotImplementedByController('MONITOR')
|
||||
self.sendLine(1, 'MONITOR + bar')
|
||||
self.getMessages(1)
|
||||
self.controller.registerUser(self, 'jilles', 'sesame')
|
||||
self.connectRegisteredClient('bar')
|
||||
m = self.getMessage(1)
|
||||
self.assertMessageEqual(m, command='730', # RPL_MONONLINE
|
||||
fail_msg='Sent non-730 (RPL_MONONLINE) message after '
|
||||
'monitored nick “bar” connected: {msg}')
|
||||
self.assertEqual(len(m.params), 2, m,
|
||||
fail_msg='Invalid number of params of RPL_MONONLINE: {msg}')
|
||||
self.assertEqual(m.params[1].split('!')[0], 'bar',
|
||||
fail_msg='730 (RPL_MONONLINE) with bad target after “bar” '
|
||||
'connects: {msg}')
|
||||
self.assertIn('account', m.tags, m,
|
||||
fail_msg='730 (RPL_MONONLINE) sent because of logged in nick '
|
||||
'does not contain an account tag: {msg}')
|
||||
self.assertEqual(m.tags['account'], 'jilles', m,
|
||||
fail_msg='730 (RPL_MONONLINE) sent because of logged in nick '
|
||||
'does not contain the correct account tag (should be '
|
||||
'“jilles”): {msg}')
|
||||
|
52
irctest/server_tests/test_away_notify.py
Normal file
52
irctest/server_tests/test_away_notify.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
<https://ircv3.net/specs/extensions/away-notify-3.1>
|
||||
"""
|
||||
|
||||
from irctest import cases
|
||||
|
||||
class AwayNotifyTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.1')
|
||||
def testAwayNotify(self):
|
||||
"""Basic away-notify test."""
|
||||
self.connectClient('foo', capabilities=['away-notify'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.joinChannel(1, '#chan')
|
||||
|
||||
self.connectClient('bar')
|
||||
self.getMessages(2)
|
||||
self.joinChannel(2, '#chan')
|
||||
self.getMessages(2)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(2, "AWAY :i'm going away")
|
||||
self.getMessages(2)
|
||||
|
||||
messages = [msg for msg in self.getMessages(1) if msg.command == 'AWAY']
|
||||
self.assertEqual(len(messages), 1)
|
||||
awayNotify = messages[0]
|
||||
self.assertTrue(awayNotify.prefix.startswith('bar!'), 'Unexpected away-notify source: %s' % (awayNotify.prefix,))
|
||||
self.assertEqual(awayNotify.params, ["i'm going away"])
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testAwayNotifyOnJoin(self):
|
||||
"""The away-notify specification states:
|
||||
"Clients will be sent an AWAY message [...] when a user joins and has an away message set."
|
||||
"""
|
||||
self.connectClient('foo', capabilities=['away-notify'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.joinChannel(1, '#chan')
|
||||
|
||||
self.connectClient('bar')
|
||||
self.getMessages(2)
|
||||
self.sendLine(2, "AWAY :i'm already away")
|
||||
self.getMessages(2)
|
||||
|
||||
self.joinChannel(2, '#chan')
|
||||
self.getMessages(2)
|
||||
|
||||
messages = [msg for msg in self.getMessages(1) if msg.command == 'AWAY']
|
||||
self.assertEqual(len(messages), 1)
|
||||
awayNotify = messages[0]
|
||||
self.assertTrue(awayNotify.prefix.startswith('bar!'), 'Unexpected away-notify source: %s' % (awayNotify.prefix,))
|
||||
self.assertEqual(awayNotify.params, ["i'm already away"])
|
142
irctest/server_tests/test_bouncer.py
Normal file
142
irctest/server_tests/test_bouncer.py
Normal file
@ -0,0 +1,142 @@
|
||||
from irctest import cases
|
||||
from irctest.irc_utils.sasl import sasl_plain_blob
|
||||
|
||||
from irctest.numerics import RPL_WELCOME
|
||||
from irctest.numerics import ERR_NICKNAMEINUSE
|
||||
|
||||
class Bouncer(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testBouncer(self):
|
||||
"""Test basic bouncer functionality."""
|
||||
self.controller.registerUser(self, 'observer', 'observerpassword')
|
||||
self.controller.registerUser(self, 'testuser', 'mypassword')
|
||||
|
||||
self.connectClient('observer', password='observerpassword')
|
||||
self.joinChannel(1, '#chan')
|
||||
self.sendLine(1, 'CAP REQ :message-tags server-time')
|
||||
self.getMessages(1)
|
||||
|
||||
self.addClient()
|
||||
self.sendLine(2, 'CAP LS 302')
|
||||
self.sendLine(2, 'AUTHENTICATE PLAIN')
|
||||
self.sendLine(2, sasl_plain_blob('testuser', 'mypassword'))
|
||||
self.sendLine(2, 'NICK testnick')
|
||||
self.sendLine(2, 'USER a 0 * a')
|
||||
self.sendLine(2, 'CAP REQ :server-time message-tags')
|
||||
self.sendLine(2, 'CAP END')
|
||||
messages = self.getMessages(2)
|
||||
welcomes = [message for message in messages if message.command == RPL_WELCOME]
|
||||
self.assertEqual(len(welcomes), 1)
|
||||
# should see a regburst for testnick
|
||||
self.assertEqual(welcomes[0].params[0], 'testnick')
|
||||
self.joinChannel(2, '#chan')
|
||||
|
||||
self.addClient()
|
||||
self.sendLine(3, 'CAP LS 302')
|
||||
self.sendLine(3, 'AUTHENTICATE PLAIN')
|
||||
self.sendLine(3, sasl_plain_blob('testuser', 'mypassword'))
|
||||
self.sendLine(3, 'NICK testnick')
|
||||
self.sendLine(3, 'USER a 0 * a')
|
||||
self.sendLine(3, 'CAP REQ :server-time message-tags account-tag')
|
||||
self.sendLine(3, 'CAP END')
|
||||
messages = self.getMessages(3)
|
||||
welcomes = [message for message in messages if message.command == RPL_WELCOME]
|
||||
self.assertEqual(len(welcomes), 1)
|
||||
# should see the *same* regburst for testnick
|
||||
self.assertEqual(welcomes[0].params[0], 'testnick')
|
||||
joins = [message for message in messages if message.command == 'JOIN']
|
||||
# we should be automatically joined to #chan
|
||||
self.assertEqual(joins[0].params[0], '#chan')
|
||||
|
||||
# disable multiclient in nickserv
|
||||
self.sendLine(3, 'NS SET MULTICLIENT OFF')
|
||||
self.getMessages(3)
|
||||
|
||||
self.addClient()
|
||||
self.sendLine(4, 'CAP LS 302')
|
||||
self.sendLine(4, 'AUTHENTICATE PLAIN')
|
||||
self.sendLine(4, sasl_plain_blob('testuser', 'mypassword'))
|
||||
self.sendLine(4, 'NICK testnick')
|
||||
self.sendLine(4, 'USER a 0 * a')
|
||||
self.sendLine(4, 'CAP REQ :server-time message-tags')
|
||||
self.sendLine(4, 'CAP END')
|
||||
# with multiclient disabled, we should not be able to attach to the nick
|
||||
messages = self.getMessages(4)
|
||||
welcomes = [message for message in messages if message.command == RPL_WELCOME]
|
||||
self.assertEqual(len(welcomes), 0)
|
||||
errors = [message for message in messages if message.command == ERR_NICKNAMEINUSE]
|
||||
self.assertEqual(len(errors), 1)
|
||||
|
||||
self.sendLine(3, 'NS SET MULTICLIENT ON')
|
||||
self.getMessages(3)
|
||||
self.addClient()
|
||||
self.sendLine(5, 'CAP LS 302')
|
||||
self.sendLine(5, 'AUTHENTICATE PLAIN')
|
||||
self.sendLine(5, sasl_plain_blob('testuser', 'mypassword'))
|
||||
self.sendLine(5, 'NICK testnick')
|
||||
self.sendLine(5, 'USER a 0 * a')
|
||||
self.sendLine(5, 'CAP REQ server-time')
|
||||
self.sendLine(5, 'CAP END')
|
||||
messages = self.getMessages(5)
|
||||
welcomes = [message for message in messages if message.command == RPL_WELCOME]
|
||||
self.assertEqual(len(welcomes), 1)
|
||||
|
||||
self.sendLine(1, '@+clientOnlyTag=Value PRIVMSG #chan :hey')
|
||||
self.getMessages(1)
|
||||
messagesfortwo = [msg for msg in self.getMessages(2) if msg.command == 'PRIVMSG']
|
||||
messagesforthree = [msg for msg in self.getMessages(3) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual(len(messagesfortwo), 1)
|
||||
self.assertEqual(len(messagesforthree), 1)
|
||||
messagefortwo = messagesfortwo[0]
|
||||
messageforthree = messagesforthree[0]
|
||||
messageforfive = self.getMessage(5)
|
||||
self.assertEqual(messagefortwo.params, ['#chan', 'hey'])
|
||||
self.assertEqual(messageforthree.params, ['#chan', 'hey'])
|
||||
self.assertEqual(messageforfive.params, ['#chan', 'hey'])
|
||||
self.assertIn('time', messagefortwo.tags)
|
||||
self.assertIn('time', messageforthree.tags)
|
||||
self.assertIn('time', messageforfive.tags)
|
||||
# 3 has account-tag
|
||||
self.assertIn('account', messageforthree.tags)
|
||||
# should get same msgid
|
||||
self.assertEqual(messagefortwo.tags['msgid'], messageforthree.tags['msgid'])
|
||||
# 5 only has server-time, shouldn't get account or msgid tags
|
||||
self.assertNotIn('account', messageforfive.tags)
|
||||
self.assertNotIn('msgid', messageforfive.tags)
|
||||
|
||||
# test that copies of sent messages go out to other sessions
|
||||
self.sendLine(2, 'PRIVMSG observer :this is a direct message')
|
||||
self.getMessages(2)
|
||||
messageForRecipient = [msg for msg in self.getMessages(1) if msg.command == 'PRIVMSG'][0]
|
||||
copyForOtherSession = [msg for msg in self.getMessages(3) if msg.command == 'PRIVMSG'][0]
|
||||
self.assertEqual(messageForRecipient.params, copyForOtherSession.params)
|
||||
self.assertEqual(messageForRecipient.tags['msgid'], copyForOtherSession.tags['msgid'])
|
||||
|
||||
self.sendLine(2, 'QUIT :two out')
|
||||
quitLines = [msg for msg in self.getMessages(2) if msg.command == 'QUIT']
|
||||
self.assertEqual(len(quitLines), 1)
|
||||
self.assertIn('two out', quitLines[0].params[0])
|
||||
# neither the observer nor the other attached session should see a quit here
|
||||
quitLines = [msg for msg in self.getMessages(1) if msg.command == 'QUIT']
|
||||
self.assertEqual(quitLines, [])
|
||||
quitLines = [msg for msg in self.getMessages(3) if msg.command == 'QUIT']
|
||||
self.assertEqual(quitLines, [])
|
||||
|
||||
# session 3 should be untouched at this point
|
||||
self.sendLine(1, '@+clientOnlyTag=Value PRIVMSG #chan :hey again')
|
||||
self.getMessages(1)
|
||||
messagesforthree = [msg for msg in self.getMessages(3) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual(len(messagesforthree), 1)
|
||||
self.assertMessageEqual(messagesforthree[0], command='PRIVMSG', params=['#chan', 'hey again'])
|
||||
|
||||
self.sendLine(5, 'QUIT :five out')
|
||||
self.getMessages(5)
|
||||
self.sendLine(3, 'QUIT :three out')
|
||||
quitLines = [msg for msg in self.getMessages(3) if msg.command == 'QUIT']
|
||||
self.assertEqual(len(quitLines), 1)
|
||||
self.assertIn('three out', quitLines[0].params[0])
|
||||
# observer should see *this* quit
|
||||
quitLines = [msg for msg in self.getMessages(1) if msg.command == 'QUIT']
|
||||
self.assertEqual(len(quitLines), 1)
|
||||
self.assertIn('three out', quitLines[0].params[0])
|
@ -95,3 +95,33 @@ class CapTestCase(cases.BaseServerTestCase):
|
||||
subcommand='ACK', subparams=['multi-prefix'],
|
||||
fail_msg='Expected “CAP ACK :multi-prefix” after '
|
||||
'sending “CAP REQ :multi-prefix”, but got {msg}.')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testCapRemovalByClient(self):
|
||||
"""Test CAP LIST and removal of caps via CAP REQ :-tagname."""
|
||||
self.addClient(1)
|
||||
self.sendLine(1, 'CAP LS 302')
|
||||
self.assertIn('multi-prefix', self.getCapLs(1))
|
||||
self.sendLine(1, 'CAP REQ :echo-message server-time')
|
||||
self.sendLine(1, 'nick bar')
|
||||
self.sendLine(1, 'user user 0 * realname')
|
||||
self.sendLine(1, 'CAP END')
|
||||
self.skipToWelcome(1)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, 'CAP LIST')
|
||||
messages = self.getMessages(1)
|
||||
cap_list = [m for m in messages if m.command == 'CAP'][0]
|
||||
self.assertEqual(set(cap_list.params[2].split()), {'echo-message', 'server-time'})
|
||||
self.assertIn('time', cap_list.tags)
|
||||
|
||||
# remove the server-time cap
|
||||
self.sendLine(1, 'CAP REQ :-server-time')
|
||||
self.getMessages(1)
|
||||
|
||||
# server-time should be disabled
|
||||
self.sendLine(1, 'CAP LIST')
|
||||
messages = self.getMessages(1)
|
||||
cap_list = [m for m in messages if m.command == 'CAP'][0]
|
||||
self.assertEqual(set(cap_list.params[2].split()), {'echo-message'})
|
||||
self.assertNotIn('time', cap_list.tags)
|
||||
|
44
irctest/server_tests/test_channel_forward.py
Normal file
44
irctest/server_tests/test_channel_forward.py
Normal file
@ -0,0 +1,44 @@
|
||||
from irctest import cases
|
||||
from irctest.numerics import ERR_CHANOPRIVSNEEDED, ERR_INVALIDMODEPARAM, ERR_LINKCHANNEL
|
||||
|
||||
MODERN_CAPS = ['server-time', 'message-tags', 'batch', 'labeled-response', 'echo-message', 'account-tag']
|
||||
|
||||
class ChannelForwarding(cases.BaseServerTestCase):
|
||||
"""Test the +f channel forwarding mode."""
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testChannelForwarding(self):
|
||||
self.connectClient('bar', name='bar', capabilities=MODERN_CAPS)
|
||||
self.connectClient('baz', name='baz', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('bar', '#bar')
|
||||
self.joinChannel('bar', '#bar_two')
|
||||
self.joinChannel('baz', '#baz')
|
||||
|
||||
self.sendLine('bar', 'MODE #bar +f #nonexistent')
|
||||
msg = self.getMessage('bar')
|
||||
self.assertMessageEqual(msg, command=ERR_INVALIDMODEPARAM)
|
||||
|
||||
# need chanops in the target channel as well
|
||||
self.sendLine('bar', 'MODE #bar +f #baz')
|
||||
responses = set(msg.command for msg in self.getMessages('bar'))
|
||||
self.assertIn(ERR_CHANOPRIVSNEEDED, responses)
|
||||
|
||||
self.sendLine('bar', 'MODE #bar +f #bar_two')
|
||||
msg = self.getMessage('bar')
|
||||
self.assertMessageEqual(msg, command='MODE', params=['#bar', '+f', '#bar_two'])
|
||||
|
||||
# can still join the channel fine
|
||||
self.joinChannel('baz', '#bar')
|
||||
self.sendLine('baz', 'PART #bar')
|
||||
self.getMessages('baz')
|
||||
|
||||
# now make it invite-only, which should cause forwarding
|
||||
self.sendLine('bar', 'MODE #bar +i')
|
||||
self.getMessages('bar')
|
||||
|
||||
self.sendLine('baz', 'JOIN #bar')
|
||||
msgs = self.getMessages('baz')
|
||||
forward = [msg for msg in msgs if msg.command == ERR_LINKCHANNEL]
|
||||
self.assertEqual(forward[0].params[:3], ['baz', '#bar', '#bar_two'])
|
||||
join = [msg for msg in msgs if msg.command == 'JOIN']
|
||||
self.assertMessageEqual(join[0], params=['#bar_two'])
|
@ -7,7 +7,10 @@ from irctest import cases
|
||||
from irctest import client_mock
|
||||
from irctest import runner
|
||||
from irctest.irc_utils import ambiguities
|
||||
from irctest.numerics import RPL_NOTOPIC, RPL_NAMREPLY, RPL_INVITING, ERR_NOSUCHCHANNEL, ERR_NOTONCHANNEL, ERR_CHANOPRIVSNEEDED, ERR_NOSUCHNICK, ERR_INVITEONLYCHAN
|
||||
from irctest.numerics import RPL_TOPIC, RPL_TOPICTIME, RPL_NOTOPIC, RPL_NAMREPLY, RPL_INVITING
|
||||
from irctest.numerics import ERR_NOSUCHCHANNEL, ERR_NOTONCHANNEL, ERR_CHANOPRIVSNEEDED, ERR_NOSUCHNICK, ERR_INVITEONLYCHAN, ERR_CANNOTSENDTOCHAN, ERR_BADCHANNELKEY, ERR_INVALIDMODEPARAM, ERR_UNKNOWNERROR
|
||||
|
||||
MODERN_CAPS = ['server-time', 'message-tags', 'batch', 'labeled-response', 'echo-message', 'account-tag']
|
||||
|
||||
class JoinTestCase(cases.BaseServerTestCase):
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459', 'RFC2812',
|
||||
@ -34,8 +37,6 @@ class JoinTestCase(cases.BaseServerTestCase):
|
||||
self.assertTrue(expected_commands.issubset(received_commands),
|
||||
'Server sent {} commands, but at least {} were expected.'
|
||||
.format(received_commands, expected_commands))
|
||||
self.assertTrue(received_commands & {'331', '332'} != set(), # RPL_NOTOPIC, RPL_TOPIC
|
||||
'Server sent neither 331 (RPL_NOTOPIC) or 332 (RPL_TOPIC)')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC2812')
|
||||
def testJoinNamreply(self):
|
||||
@ -177,8 +178,7 @@ class JoinTestCase(cases.BaseServerTestCase):
|
||||
self.connectClient('bar')
|
||||
self.joinChannel(2, '#chan')
|
||||
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
# clear waiting msgs about cli 2 joining the channel
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
|
||||
@ -219,15 +219,8 @@ class JoinTestCase(cases.BaseServerTestCase):
|
||||
# TODO: check foo is opped
|
||||
|
||||
self.sendLine(1, 'MODE #chan +t')
|
||||
try:
|
||||
m = self.getMessage(1)
|
||||
if m.command == '482':
|
||||
raise runner.ImplementationChoice(
|
||||
'Channel creators are not opped by default.')
|
||||
self.assertMessageEqual(m, command='TOPIC')
|
||||
except client_mock.NoMessageException:
|
||||
# The RFCs do not say TOPIC must be echoed
|
||||
pass
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(2, 'TOPIC #chan :T0P1C')
|
||||
m = self.getMessage(2)
|
||||
self.assertMessageEqual(m, command='482',
|
||||
@ -308,10 +301,9 @@ class JoinTestCase(cases.BaseServerTestCase):
|
||||
self.getMessages(1)
|
||||
self.sendLine(2, 'LIST')
|
||||
m = self.getMessage(2)
|
||||
self.assertMessageEqual(m, command='321', # RPL_LISTSTART
|
||||
fail_msg='First reply to LIST is not 321 (RPL_LISTSTART), '
|
||||
'but: {msg}')
|
||||
m = self.getMessage(2)
|
||||
if m.command == '321':
|
||||
# skip RPL_LISTSTART
|
||||
m = self.getMessage(2)
|
||||
self.assertNotEqual(m.command, '322', # RPL_LIST
|
||||
'LIST response gives (at least) one channel, whereas there '
|
||||
'is none.')
|
||||
@ -331,10 +323,9 @@ class JoinTestCase(cases.BaseServerTestCase):
|
||||
self.getMessages(1)
|
||||
self.sendLine(2, 'LIST')
|
||||
m = self.getMessage(2)
|
||||
self.assertMessageEqual(m, command='321', # RPL_LISTSTART
|
||||
fail_msg='First reply to LIST is not 321 (RPL_LISTSTART), '
|
||||
'but: {msg}')
|
||||
m = self.getMessage(2)
|
||||
if m.command == '321':
|
||||
# skip RPL_LISTSTART
|
||||
m = self.getMessage(2)
|
||||
self.assertNotEqual(m.command, '323', # RPL_LISTEND
|
||||
fail_msg='LIST response ended (ie. 323, aka RPL_LISTEND) '
|
||||
'without listing any channel, whereas there is one.')
|
||||
@ -368,8 +359,6 @@ class JoinTestCase(cases.BaseServerTestCase):
|
||||
|
||||
# TODO: check foo is an operator
|
||||
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
self.getMessages(3)
|
||||
@ -485,15 +474,23 @@ class JoinTestCase(cases.BaseServerTestCase):
|
||||
# The RFCs do not say KICK must be echoed
|
||||
pass
|
||||
|
||||
# TODO: could be in the other order
|
||||
m = self.getMessage(4)
|
||||
self.assertMessageEqual(m, command='KICK',
|
||||
params=['#chan', 'bar', 'bye'])
|
||||
m = self.getMessage(4)
|
||||
self.assertMessageEqual(m, command='KICK',
|
||||
params=['#chan', 'baz', 'bye'])
|
||||
mgroup = self.getMessages(4)
|
||||
self.assertGreaterEqual(len(mgroup), 2)
|
||||
m1, m2 = mgroup[:2]
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459', 'RFC2812')
|
||||
for m in m1, m2:
|
||||
self.assertEqual(m.command, 'KICK')
|
||||
|
||||
self.assertEqual(len(m.params), 3)
|
||||
self.assertEqual(m.params[0], '#chan')
|
||||
self.assertEqual(m.params[2], 'bye')
|
||||
|
||||
if (m1.params[1] == 'bar' and m2.params[1] == 'baz') or (m1.params[1] == 'baz' and m2.params[1] == 'bar'):
|
||||
... # success
|
||||
else:
|
||||
raise AssertionError('Middle params [{}, {}] are not correct.'.format(m1.params[1], m2.params[1]))
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC-deprecated')
|
||||
def testInviteNonExistingChannelTransmitted(self):
|
||||
"""“There is no requirement that the channel the target user is being
|
||||
invited to must exist or be a valid channel.”
|
||||
@ -520,7 +517,7 @@ class JoinTestCase(cases.BaseServerTestCase):
|
||||
'#chan, “bar” should have received “INVITE #chan bar” but '
|
||||
'got this instead: {msg}')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459', 'RFC2812')
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC-deprecated')
|
||||
def testInviteNonExistingChannelEchoed(self):
|
||||
"""“There is no requirement that the channel the target user is being
|
||||
invited to must exist or be a valid channel.”
|
||||
@ -653,3 +650,394 @@ class ChannelQuitTestCase(cases.BaseServerTestCase):
|
||||
self.assertEqual(m.command, 'QUIT')
|
||||
self.assertTrue(m.prefix.startswith('qux')) # nickmask of quitter
|
||||
self.assertIn('qux out', m.params[0])
|
||||
|
||||
|
||||
class NoCTCPTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testQuit(self):
|
||||
self.connectClient('bar')
|
||||
self.joinChannel(1, '#chan')
|
||||
self.sendLine(1, 'MODE #chan +C')
|
||||
self.getMessages(1)
|
||||
|
||||
self.connectClient('qux')
|
||||
self.joinChannel(2, '#chan')
|
||||
self.getMessages(2)
|
||||
|
||||
self.sendLine(1, 'PRIVMSG #chan :\x01ACTION hi\x01')
|
||||
self.getMessages(1)
|
||||
ms = self.getMessages(2)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageEqual(ms[0], command='PRIVMSG', params=['#chan', '\x01ACTION hi\x01'])
|
||||
|
||||
self.sendLine(1, 'PRIVMSG #chan :\x01PING 1473523796 918320\x01')
|
||||
ms = self.getMessages(1)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageEqual(ms[0], command=ERR_CANNOTSENDTOCHAN)
|
||||
ms = self.getMessages(2)
|
||||
self.assertEqual(ms, [])
|
||||
|
||||
class KeyTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC2812')
|
||||
def testKeyNormal(self):
|
||||
self.connectClient('bar')
|
||||
self.joinChannel(1, '#chan')
|
||||
self.sendLine(1, 'MODE #chan +k beer')
|
||||
self.getMessages(1)
|
||||
|
||||
self.connectClient('qux')
|
||||
self.getMessages(2)
|
||||
self.sendLine(2, 'JOIN #chan')
|
||||
reply = self.getMessages(2)
|
||||
self.assertNotIn('JOIN', {msg.command for msg in reply})
|
||||
self.assertIn(ERR_BADCHANNELKEY, {msg.command for msg in reply})
|
||||
|
||||
self.sendLine(2, 'JOIN #chan beer')
|
||||
reply = self.getMessages(2)
|
||||
self.assertMessageEqual(reply[0], command='JOIN', params=['#chan'])
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testKeyValidation(self):
|
||||
# oragono issue #1021
|
||||
self.connectClient('bar')
|
||||
self.joinChannel(1, '#chan')
|
||||
self.sendLine(1, 'MODE #chan +k :invalid channel passphrase')
|
||||
reply = self.getMessages(1)
|
||||
self.assertNotIn(ERR_UNKNOWNERROR, {msg.command for msg in reply})
|
||||
self.assertIn(ERR_INVALIDMODEPARAM, {msg.command for msg in reply})
|
||||
|
||||
|
||||
class AuditoriumTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testAuditorium(self):
|
||||
self.connectClient('bar', name='bar', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('bar', '#auditorium')
|
||||
self.getMessages('bar')
|
||||
self.sendLine('bar', 'MODE #auditorium +u')
|
||||
modelines = [msg for msg in self.getMessages('bar') if msg.command == 'MODE']
|
||||
self.assertEqual(len(modelines), 1)
|
||||
self.assertMessageEqual(modelines[0], params=['#auditorium', '+u'])
|
||||
|
||||
self.connectClient('guest1', name='guest1', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('guest1', '#auditorium')
|
||||
self.getMessages('guest1')
|
||||
# chanop should get a JOIN message
|
||||
join_msgs = [msg for msg in self.getMessages('bar') if msg.command == 'JOIN']
|
||||
self.assertEqual(len(join_msgs), 1)
|
||||
self.assertMessageEqual(join_msgs[0], nick='guest1', params=['#auditorium'])
|
||||
|
||||
self.connectClient('guest2', name='guest2', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('guest2', '#auditorium')
|
||||
self.getMessages('guest2')
|
||||
# chanop should get a JOIN message
|
||||
join_msgs = [msg for msg in self.getMessages('bar') if msg.command == 'JOIN']
|
||||
self.assertEqual(len(join_msgs), 1)
|
||||
self.assertMessageEqual(join_msgs[0], nick='guest2', params=['#auditorium'])
|
||||
# fellow unvoiced participant should not
|
||||
unvoiced_join_msgs = [msg for msg in self.getMessages('guest1') if msg.command == 'JOIN']
|
||||
self.assertEqual(len(unvoiced_join_msgs), 0)
|
||||
|
||||
self.connectClient('guest3', name='guest3', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('guest3', '#auditorium')
|
||||
self.getMessages('guest3')
|
||||
|
||||
self.sendLine('bar', 'PRIVMSG #auditorium hi')
|
||||
echo_message = [msg for msg in self.getMessages('bar') if msg.command == 'PRIVMSG'][0]
|
||||
self.assertEqual(echo_message, self.getMessages('guest1')[0])
|
||||
self.assertEqual(echo_message, self.getMessages('guest2')[0])
|
||||
self.assertEqual(echo_message, self.getMessages('guest3')[0])
|
||||
|
||||
# unvoiced users can speak
|
||||
self.sendLine('guest1', 'PRIVMSG #auditorium :hi you')
|
||||
echo_message = [msg for msg in self.getMessages('guest1') if msg.command == 'PRIVMSG'][0]
|
||||
self.assertEqual(self.getMessages('bar'), [echo_message])
|
||||
self.assertEqual(self.getMessages('guest2'), [echo_message])
|
||||
self.assertEqual(self.getMessages('guest3'), [echo_message])
|
||||
|
||||
def names(client):
|
||||
self.sendLine(client, 'NAMES #auditorium')
|
||||
result = set()
|
||||
for msg in self.getMessages(client):
|
||||
if msg.command == RPL_NAMREPLY:
|
||||
result.update(msg.params[-1].split())
|
||||
return result
|
||||
|
||||
self.assertEqual(names('bar'), {'@bar', 'guest1', 'guest2', 'guest3'})
|
||||
self.assertEqual(names('guest1'), {'@bar',})
|
||||
self.assertEqual(names('guest2'), {'@bar',})
|
||||
self.assertEqual(names('guest3'), {'@bar',})
|
||||
|
||||
self.sendLine('bar', 'MODE #auditorium +v guest1')
|
||||
modeLine = [msg for msg in self.getMessages('bar') if msg.command == 'MODE'][0]
|
||||
self.assertEqual(self.getMessages('guest1'), [modeLine])
|
||||
self.assertEqual(self.getMessages('guest2'), [modeLine])
|
||||
self.assertEqual(self.getMessages('guest3'), [modeLine])
|
||||
self.assertEqual(names('bar'), {'@bar', '+guest1', 'guest2', 'guest3'})
|
||||
self.assertEqual(names('guest2'), {'@bar', '+guest1'})
|
||||
self.assertEqual(names('guest3'), {'@bar', '+guest1'})
|
||||
|
||||
self.sendLine('guest1', 'PART #auditorium')
|
||||
part = [msg for msg in self.getMessages('guest1') if msg.command == 'PART'][0]
|
||||
# everyone should see voiced PART
|
||||
self.assertEqual(self.getMessages('bar')[0], part)
|
||||
self.assertEqual(self.getMessages('guest2')[0], part)
|
||||
self.assertEqual(self.getMessages('guest3')[0], part)
|
||||
|
||||
self.joinChannel('guest1', '#auditorium')
|
||||
self.getMessages('guest1')
|
||||
self.getMessages('bar')
|
||||
|
||||
self.sendLine('guest2', 'PART #auditorium')
|
||||
part = [msg for msg in self.getMessages('guest2') if msg.command == 'PART'][0]
|
||||
self.assertEqual(self.getMessages('bar'), [part])
|
||||
# part should be hidden from unvoiced participants
|
||||
self.assertEqual(self.getMessages('guest1'), [])
|
||||
self.assertEqual(self.getMessages('guest3'), [])
|
||||
|
||||
self.sendLine('guest3', 'QUIT')
|
||||
self.assertDisconnected('guest3')
|
||||
# quit should be hidden from unvoiced participants
|
||||
self.assertEqual(len([msg for msg in self.getMessages('bar') if msg.command =='QUIT']), 1)
|
||||
self.assertEqual(len([msg for msg in self.getMessages('guest1') if msg.command =='QUIT']), 0)
|
||||
|
||||
|
||||
class TopicPrivileges(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC2812')
|
||||
def testTopicPrivileges(self):
|
||||
# test the +t channel mode, which prevents unprivileged users from changing the topic
|
||||
self.connectClient('bar', name='bar')
|
||||
self.joinChannel('bar', '#chan')
|
||||
self.getMessages('bar')
|
||||
self.sendLine('bar', 'MODE #chan +t')
|
||||
replies = {msg.command for msg in self.getMessages('bar')}
|
||||
# success response is undefined, may be MODE or may be 324 RPL_CHANNELMODEIS,
|
||||
# depending on whether this was a no-op
|
||||
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||
self.sendLine('bar', 'TOPIC #chan :new topic')
|
||||
replies = {msg.command for msg in self.getMessages('bar')}
|
||||
self.assertIn('TOPIC', replies)
|
||||
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||
|
||||
self.connectClient('qux', name='qux')
|
||||
self.joinChannel('qux', '#chan')
|
||||
self.getMessages('qux')
|
||||
self.sendLine('qux', 'TOPIC #chan :new topic')
|
||||
replies = {msg.command for msg in self.getMessages('qux')}
|
||||
self.assertIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||
self.assertNotIn('TOPIC', replies)
|
||||
|
||||
self.sendLine('bar', 'MODE #chan +v qux')
|
||||
replies = {msg.command for msg in self.getMessages('bar')}
|
||||
self.assertIn('MODE', replies)
|
||||
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||
|
||||
# regression test: +v cannot change the topic of a +t channel
|
||||
self.sendLine('qux', 'TOPIC #chan :new topic')
|
||||
replies = {msg.command for msg in self.getMessages('qux')}
|
||||
self.assertIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||
self.assertNotIn('TOPIC', replies)
|
||||
|
||||
# test that RPL_TOPIC and RPL_TOPICTIME are sent on join
|
||||
self.connectClient('buzz', name='buzz')
|
||||
self.sendLine('buzz', 'JOIN #chan')
|
||||
replies = self.getMessages('buzz')
|
||||
rpl_topic = [msg for msg in replies if msg.command == RPL_TOPIC][0]
|
||||
self.assertMessageEqual(rpl_topic, command=RPL_TOPIC, params=['buzz', '#chan', 'new topic'])
|
||||
self.assertEqual(len([msg for msg in replies if msg.command == RPL_TOPICTIME]), 1)
|
||||
|
||||
|
||||
class ModeratedMode(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC2812')
|
||||
def testModeratedMode(self):
|
||||
# test the +m channel mode
|
||||
self.connectClient('chanop', name='chanop')
|
||||
self.joinChannel('chanop', '#chan')
|
||||
self.getMessages('chanop')
|
||||
self.sendLine('chanop', 'MODE #chan +m')
|
||||
replies = self.getMessages('chanop')
|
||||
modeLines = [line for line in replies if line.command == 'MODE']
|
||||
self.assertMessageEqual(modeLines[0], command='MODE', params=['#chan', '+m'])
|
||||
|
||||
self.connectClient('baz', name='baz')
|
||||
self.joinChannel('baz', '#chan')
|
||||
self.getMessages('chanop')
|
||||
# this message should be suppressed completely by +m
|
||||
self.sendLine('baz', 'PRIVMSG #chan :hi from baz')
|
||||
replies = self.getMessages('baz')
|
||||
reply_cmds = {reply.command for reply in replies}
|
||||
self.assertIn(ERR_CANNOTSENDTOCHAN, reply_cmds)
|
||||
self.assertEqual(self.getMessages('chanop'), [])
|
||||
|
||||
# grant +v, user should be able to send messages
|
||||
self.sendLine('chanop', 'MODE #chan +v baz')
|
||||
self.getMessages('chanop')
|
||||
self.getMessages('baz')
|
||||
self.sendLine('baz', 'PRIVMSG #chan :hi again from baz')
|
||||
self.getMessages('baz')
|
||||
relays = self.getMessages('chanop')
|
||||
relay = relays[0]
|
||||
self.assertMessageEqual(relay, command='PRIVMSG', params=['#chan', 'hi again from baz'])
|
||||
|
||||
|
||||
class OpModerated(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testOpModerated(self):
|
||||
# test the +U channel mode
|
||||
self.connectClient('chanop', name='chanop', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('chanop', '#chan')
|
||||
self.getMessages('chanop')
|
||||
self.sendLine('chanop', 'MODE #chan +U')
|
||||
replies = {msg.command for msg in self.getMessages('chanop')}
|
||||
self.assertIn('MODE', replies)
|
||||
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||
|
||||
self.connectClient('baz', name='baz', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('baz', '#chan')
|
||||
self.sendLine('baz', 'PRIVMSG #chan :hi from baz')
|
||||
echo = self.getMessages('baz')[0]
|
||||
self.assertMessageEqual(echo, command='PRIVMSG', params=['#chan', 'hi from baz'])
|
||||
self.assertEqual([msg for msg in self.getMessages('chanop') if msg.command == 'PRIVMSG'], [echo])
|
||||
|
||||
self.connectClient('qux', name='qux', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('qux', '#chan')
|
||||
self.sendLine('qux', 'PRIVMSG #chan :hi from qux')
|
||||
echo = self.getMessages('qux')[0]
|
||||
self.assertMessageEqual(echo, command='PRIVMSG', params=['#chan', 'hi from qux'])
|
||||
# message is relayed to chanop but not to unprivileged
|
||||
self.assertEqual([msg for msg in self.getMessages('chanop') if msg.command == 'PRIVMSG'], [echo])
|
||||
self.assertEqual([msg for msg in self.getMessages('baz') if msg.command == 'PRIVMSG'], [])
|
||||
|
||||
self.sendLine('chanop', 'MODE #chan +v qux')
|
||||
self.getMessages('chanop')
|
||||
self.sendLine('qux', 'PRIVMSG #chan :hi again from qux')
|
||||
echo = [msg for msg in self.getMessages('qux') if msg.command == 'PRIVMSG'][0]
|
||||
self.assertMessageEqual(echo, command='PRIVMSG', params=['#chan', 'hi again from qux'])
|
||||
self.assertEqual([msg for msg in self.getMessages('chanop') if msg.command == 'PRIVMSG'], [echo])
|
||||
self.assertEqual([msg for msg in self.getMessages('baz') if msg.command == 'PRIVMSG'], [echo])
|
||||
|
||||
|
||||
class MuteExtban(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testISupport(self):
|
||||
isupport = self.getISupport()
|
||||
token = isupport['EXTBAN']
|
||||
prefix, comma, types = token.partition(',')
|
||||
self.assertEqual(prefix, '')
|
||||
self.assertEqual(comma, ',')
|
||||
self.assertIn('m', types)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testMuteExtban(self):
|
||||
clients = ('chanop', 'bar', 'qux')
|
||||
|
||||
self.connectClient('chanop', name='chanop', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('chanop', '#chan')
|
||||
self.getMessages('chanop')
|
||||
self.sendLine('chanop', 'MODE #chan +b m:bar!*@*')
|
||||
self.sendLine('chanop', 'MODE #chan +b m:qux!*@*')
|
||||
replies = {msg.command for msg in self.getMessages('chanop')}
|
||||
self.assertIn('MODE', replies)
|
||||
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||
|
||||
self.connectClient('bar', name='bar', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('bar', '#chan')
|
||||
self.connectClient('qux', name='qux', capabilities=MODERN_CAPS, ident='evan')
|
||||
self.joinChannel('qux', '#chan')
|
||||
|
||||
for client in clients:
|
||||
self.getMessages(client)
|
||||
|
||||
self.sendLine('bar', 'PRIVMSG #chan :hi from bar')
|
||||
replies = self.getMessages('bar')
|
||||
replies_cmds = {msg.command for msg in replies}
|
||||
self.assertNotIn('PRIVMSG', replies_cmds)
|
||||
self.assertIn(ERR_CANNOTSENDTOCHAN, replies_cmds)
|
||||
self.assertEqual(self.getMessages('chanop'), [])
|
||||
|
||||
self.sendLine('qux', 'PRIVMSG #chan :hi from qux')
|
||||
replies = self.getMessages('qux')
|
||||
replies_cmds = {msg.command for msg in replies}
|
||||
self.assertNotIn('PRIVMSG', replies_cmds)
|
||||
self.assertIn(ERR_CANNOTSENDTOCHAN, replies_cmds)
|
||||
self.assertEqual(self.getMessages('chanop'), [])
|
||||
|
||||
# remove mute with -b
|
||||
self.sendLine('chanop', 'MODE #chan -b m:bar!*@*')
|
||||
self.getMessages('chanop')
|
||||
self.sendLine('bar', 'PRIVMSG #chan :hi again from bar')
|
||||
replies = self.getMessages('bar')
|
||||
replies_cmds = {msg.command for msg in replies}
|
||||
self.assertIn('PRIVMSG', replies_cmds)
|
||||
self.assertNotIn(ERR_CANNOTSENDTOCHAN, replies_cmds)
|
||||
self.assertEqual(self.getMessages('chanop'), [msg for msg in replies if msg.command == 'PRIVMSG'])
|
||||
|
||||
for client in clients:
|
||||
self.getMessages(client)
|
||||
|
||||
# +v grants an exemption to +b
|
||||
self.sendLine('chanop', 'MODE #chan +v qux')
|
||||
self.getMessages('chanop')
|
||||
self.sendLine('qux', 'PRIVMSG #chan :hi again from qux')
|
||||
replies = self.getMessages('qux')
|
||||
replies_cmds = {msg.command for msg in replies}
|
||||
self.assertIn('PRIVMSG', replies_cmds)
|
||||
self.assertNotIn(ERR_CANNOTSENDTOCHAN, replies_cmds)
|
||||
self.assertEqual(self.getMessages('chanop'), [msg for msg in replies if msg.command == 'PRIVMSG'])
|
||||
|
||||
self.sendLine('qux', 'PART #chan')
|
||||
self.sendLine('qux', 'JOIN #chan')
|
||||
self.getMessages('qux')
|
||||
self.sendLine('chanop', 'MODE #chan +e m:*!~evan@*')
|
||||
self.getMessages('chanop')
|
||||
|
||||
# +e grants an exemption to +b
|
||||
self.sendLine('qux', 'PRIVMSG #chan :thanks for mute-excepting me')
|
||||
replies = self.getMessages('qux')
|
||||
replies_cmds = {msg.command for msg in replies}
|
||||
self.assertIn('PRIVMSG', replies_cmds)
|
||||
self.assertNotIn(ERR_CANNOTSENDTOCHAN, replies_cmds)
|
||||
self.assertEqual(self.getMessages('chanop'), [msg for msg in replies if msg.command == 'PRIVMSG'])
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testIssue1370(self):
|
||||
# regression test for oragono #1370: mutes not correctly enforced against
|
||||
# users with capital letters in their NUH
|
||||
clients = ('chanop', 'bar')
|
||||
|
||||
self.connectClient('chanop', name='chanop', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('chanop', '#chan')
|
||||
self.getMessages('chanop')
|
||||
self.sendLine('chanop', 'MODE #chan +b m:BAR!*@*')
|
||||
replies = {msg.command for msg in self.getMessages('chanop')}
|
||||
self.assertIn('MODE', replies)
|
||||
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||
|
||||
self.connectClient('Bar', name='bar', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('bar', '#chan')
|
||||
|
||||
for client in clients:
|
||||
self.getMessages(client)
|
||||
|
||||
self.sendLine('bar', 'PRIVMSG #chan :hi from bar')
|
||||
replies = self.getMessages('bar')
|
||||
replies_cmds = {msg.command for msg in replies}
|
||||
self.assertNotIn('PRIVMSG', replies_cmds)
|
||||
self.assertIn(ERR_CANNOTSENDTOCHAN, replies_cmds)
|
||||
self.assertEqual(self.getMessages('chanop'), [])
|
||||
|
||||
# remove mute with -b
|
||||
self.sendLine('chanop', 'MODE #chan -b m:bar!*@*')
|
||||
self.getMessages('chanop')
|
||||
self.sendLine('bar', 'PRIVMSG #chan :hi again from bar')
|
||||
replies = self.getMessages('bar')
|
||||
replies_cmds = {msg.command for msg in replies}
|
||||
self.assertIn('PRIVMSG', replies_cmds)
|
||||
self.assertNotIn(ERR_CANNOTSENDTOCHAN, replies_cmds)
|
||||
self.assertEqual(self.getMessages('chanop'), [msg for msg in replies if msg.command == 'PRIVMSG'])
|
||||
|
27
irctest/server_tests/test_channel_rename.py
Normal file
27
irctest/server_tests/test_channel_rename.py
Normal file
@ -0,0 +1,27 @@
|
||||
from irctest import cases
|
||||
from irctest.numerics import ERR_CHANOPRIVSNEEDED
|
||||
|
||||
MODERN_CAPS = ['server-time', 'message-tags', 'batch', 'labeled-response', 'echo-message', 'account-tag']
|
||||
RENAME_CAP = 'draft/channel-rename'
|
||||
|
||||
class ChannelRename(cases.BaseServerTestCase):
|
||||
"""Basic tests for channel-rename."""
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testChannelRename(self):
|
||||
self.connectClient('bar', name='bar', capabilities=MODERN_CAPS+[RENAME_CAP])
|
||||
self.connectClient('baz', name='baz', capabilities=MODERN_CAPS)
|
||||
self.joinChannel('bar', '#bar')
|
||||
self.joinChannel('baz', '#bar')
|
||||
self.getMessages('bar')
|
||||
self.getMessages('baz')
|
||||
|
||||
self.sendLine('bar', 'RENAME #bar #qux :no reason')
|
||||
self.assertMessageEqual(self.getMessage('bar'), command='RENAME', params=['#bar', '#qux', 'no reason'])
|
||||
legacy_responses = self.getMessages('baz')
|
||||
self.assertEqual(1, len([msg for msg in legacy_responses if msg.command == 'PART' and msg.params[0] == '#bar']))
|
||||
self.assertEqual(1, len([msg for msg in legacy_responses if msg.command == 'JOIN' and msg.params == ['#qux']]))
|
||||
|
||||
self.joinChannel('baz', '#bar')
|
||||
self.sendLine('baz', 'MODE #bar +k beer')
|
||||
self.assertNotIn(ERR_CHANOPRIVSNEEDED, [msg.command for msg in self.getMessages('baz')])
|
380
irctest/server_tests/test_chathistory.py
Normal file
380
irctest/server_tests/test_chathistory.py
Normal file
@ -0,0 +1,380 @@
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from irctest import cases
|
||||
from irctest.irc_utils.junkdrawer import to_history_message, random_name
|
||||
|
||||
CHATHISTORY_CAP = 'draft/chathistory'
|
||||
EVENT_PLAYBACK_CAP = 'draft/event-playback'
|
||||
|
||||
|
||||
MYSQL_PASSWORD = ""
|
||||
|
||||
def validate_chathistory_batch(msgs):
|
||||
batch_tag = None
|
||||
closed_batch_tag = None
|
||||
result = []
|
||||
for msg in msgs:
|
||||
if msg.command == "BATCH":
|
||||
batch_param = msg.params[0]
|
||||
if batch_tag is None and batch_param[0] == '+':
|
||||
batch_tag = batch_param[1:]
|
||||
elif batch_param[0] == '-':
|
||||
closed_batch_tag = batch_param[1:]
|
||||
elif msg.command == "PRIVMSG" and batch_tag is not None and msg.tags.get("batch") == batch_tag:
|
||||
result.append(to_history_message(msg))
|
||||
assert batch_tag == closed_batch_tag
|
||||
return result
|
||||
|
||||
class ChathistoryTestCase(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"chathistory": True,
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testInvalidTargets(self):
|
||||
bar, pw = random_name('bar'), random_name('pw')
|
||||
self.controller.registerUser(self, bar, pw)
|
||||
self.connectClient(bar, name=bar, capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP], password=pw)
|
||||
self.getMessages(bar)
|
||||
|
||||
qux = random_name('qux')
|
||||
real_chname = random_name('#real_channel')
|
||||
self.connectClient(qux, name=qux)
|
||||
self.joinChannel(qux, real_chname)
|
||||
self.getMessages(qux)
|
||||
|
||||
# test a nonexistent channel
|
||||
self.sendLine(bar, 'CHATHISTORY LATEST #nonexistent_channel * 10')
|
||||
msgs = self.getMessages(bar)
|
||||
self.assertEqual(msgs[0].command, 'FAIL')
|
||||
self.assertEqual(msgs[0].params[:2], ['CHATHISTORY', 'INVALID_TARGET'])
|
||||
|
||||
# as should a real channel to which one is not joined:
|
||||
self.sendLine(bar, 'CHATHISTORY LATEST %s * 10' % (real_chname,))
|
||||
msgs = self.getMessages(bar)
|
||||
self.assertEqual(msgs[0].command, 'FAIL')
|
||||
self.assertEqual(msgs[0].params[:2], ['CHATHISTORY', 'INVALID_TARGET'])
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testMessagesToSelf(self):
|
||||
bar, pw = random_name('bar'), random_name('pw')
|
||||
self.controller.registerUser(self, bar, pw)
|
||||
self.connectClient(bar, name=bar, capabilities=['batch', 'labeled-response', 'message-tags', 'server-time'], password=pw)
|
||||
self.getMessages(bar)
|
||||
|
||||
messages = []
|
||||
|
||||
self.sendLine(bar, 'PRIVMSG %s :this is a privmsg sent to myself' % (bar,))
|
||||
replies = [msg for msg in self.getMessages(bar) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual(len(replies), 1)
|
||||
msg = replies[0]
|
||||
self.assertEqual(msg.params, [bar, 'this is a privmsg sent to myself'])
|
||||
messages.append(to_history_message(msg))
|
||||
|
||||
self.sendLine(bar, 'CAP REQ echo-message')
|
||||
self.getMessages(bar)
|
||||
self.sendLine(bar, 'PRIVMSG %s :this is a second privmsg sent to myself' % (bar,))
|
||||
replies = [msg for msg in self.getMessages(bar) if msg.command == 'PRIVMSG']
|
||||
# two messages, the echo and the delivery
|
||||
self.assertEqual(len(replies), 2)
|
||||
self.assertEqual(replies[0].params, [bar, 'this is a second privmsg sent to myself'])
|
||||
messages.append(to_history_message(replies[0]))
|
||||
# messages should be otherwise identical
|
||||
self.assertEqual(to_history_message(replies[0]), to_history_message(replies[1]))
|
||||
|
||||
self.sendLine(bar, '@label=xyz PRIVMSG %s :this is a third privmsg sent to myself' % (bar,))
|
||||
replies = [msg for msg in self.getMessages(bar) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual(len(replies), 2)
|
||||
# exactly one of the replies MUST be labeled
|
||||
echo = [msg for msg in replies if msg.tags.get('label') == 'xyz'][0]
|
||||
delivery = [msg for msg in replies if msg.tags.get('label') is None][0]
|
||||
self.assertEqual(echo.params, [bar, 'this is a third privmsg sent to myself'])
|
||||
messages.append(to_history_message(echo))
|
||||
self.assertEqual(to_history_message(echo), to_history_message(delivery))
|
||||
|
||||
# should receive exactly 3 messages in the correct order, no duplicates
|
||||
self.sendLine(bar, 'CHATHISTORY LATEST * * 10')
|
||||
replies = [msg for msg in self.getMessages(bar) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual([to_history_message(msg) for msg in replies], messages)
|
||||
|
||||
self.sendLine(bar, 'CHATHISTORY LATEST %s * 10' % (bar,))
|
||||
replies = [msg for msg in self.getMessages(bar) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual([to_history_message(msg) for msg in replies], messages)
|
||||
|
||||
def validate_echo_messages(self, num_messages, echo_messages):
|
||||
# sanity checks: should have received the correct number of echo messages,
|
||||
# all with distinct time tags (because we slept) and msgids
|
||||
self.assertEqual(len(echo_messages), num_messages)
|
||||
self.assertEqual(len(set(msg.msgid for msg in echo_messages)), num_messages)
|
||||
self.assertEqual(len(set(msg.time for msg in echo_messages)), num_messages)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testChathistory(self):
|
||||
self.connectClient('bar', capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP])
|
||||
chname = '#' + secrets.token_hex(12)
|
||||
self.joinChannel(1, chname)
|
||||
self.getMessages(1)
|
||||
|
||||
NUM_MESSAGES = 10
|
||||
echo_messages = []
|
||||
for i in range(NUM_MESSAGES):
|
||||
self.sendLine(1, 'PRIVMSG %s :this is message %d' % (chname, i))
|
||||
echo_messages.extend(to_history_message(msg) for msg in self.getMessages(1))
|
||||
time.sleep(0.002)
|
||||
|
||||
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
||||
self.validate_chathistory(echo_messages, 1, chname)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testChathistoryDMs(self):
|
||||
c1 = secrets.token_hex(12)
|
||||
c2 = secrets.token_hex(12)
|
||||
self.controller.registerUser(self, c1, 'sesame1')
|
||||
self.controller.registerUser(self, c2, 'sesame2')
|
||||
self.connectClient(c1, capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP], password='sesame1')
|
||||
self.connectClient(c2, capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP], password='sesame2')
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
|
||||
NUM_MESSAGES = 10
|
||||
echo_messages = []
|
||||
for i in range(NUM_MESSAGES):
|
||||
user = (i % 2) + 1
|
||||
if user == 1:
|
||||
target = c2
|
||||
else:
|
||||
target = c1
|
||||
self.getMessages(user)
|
||||
self.sendLine(user, 'PRIVMSG %s :this is message %d' % (target, i))
|
||||
echo_messages.extend(to_history_message(msg) for msg in self.getMessages(user))
|
||||
time.sleep(0.002)
|
||||
|
||||
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
||||
self.validate_chathistory(echo_messages, 1, c2)
|
||||
self.validate_chathistory(echo_messages, 1, '*')
|
||||
self.validate_chathistory(echo_messages, 2, c1)
|
||||
self.validate_chathistory(echo_messages, 2, '*')
|
||||
|
||||
c3 = secrets.token_hex(12)
|
||||
self.connectClient(c3, capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP])
|
||||
self.sendLine(1, 'PRIVMSG %s :this is a message in a separate conversation' % (c3,))
|
||||
self.getMessages(1)
|
||||
self.sendLine(3, 'PRIVMSG %s :i agree that this is a separate conversation' % (c1,))
|
||||
# 3 received the first message as a delivery and the second as an echo
|
||||
new_convo = [to_history_message(msg) for msg in self.getMessages(3) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual([msg.text for msg in new_convo], ['this is a message in a separate conversation', 'i agree that this is a separate conversation'])
|
||||
|
||||
# messages should be stored and retrievable by c1, even though c3 is not registered
|
||||
self.getMessages(1)
|
||||
self.sendLine(1, 'CHATHISTORY LATEST %s * 10' % (c3,))
|
||||
results = [to_history_message(msg) for msg in self.getMessages(1) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual(results, new_convo)
|
||||
|
||||
# additional messages with c3 should not show up in the c1-c2 history:
|
||||
self.validate_chathistory(echo_messages, 1, c2)
|
||||
self.validate_chathistory(echo_messages, 2, c1)
|
||||
self.validate_chathistory(echo_messages, 2, c1.upper())
|
||||
|
||||
# regression test for #833
|
||||
self.sendLine(3, 'QUIT')
|
||||
self.assertDisconnected(3)
|
||||
# register c3 as an account, then attempt to retrieve the conversation history with c1
|
||||
self.controller.registerUser(self, c3, 'sesame3')
|
||||
self.connectClient(c3, name=c3, capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP], password='sesame3')
|
||||
self.getMessages(c3)
|
||||
self.sendLine(c3, 'CHATHISTORY LATEST %s * 10' % (c1,))
|
||||
results = [to_history_message(msg) for msg in self.getMessages(c3) if msg.command == 'PRIVMSG']
|
||||
# should get nothing
|
||||
self.assertEqual(results, [])
|
||||
|
||||
def validate_chathistory(self, echo_messages, user, chname):
|
||||
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
||||
|
||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages, result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[-5:], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[-1:], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY LATEST %s msgid=%s %d" % (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[5:], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY LATEST %s timestamp=%s %d" % (chname, echo_messages[4].time, INCLUSIVE_LIMIT))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[5:], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY BEFORE %s msgid=%s %d" % (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[:6], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY BEFORE %s timestamp=%s %d" % (chname, echo_messages[6].time, INCLUSIVE_LIMIT))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[:6], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY BEFORE %s timestamp=%s %d" % (chname, echo_messages[6].time, 2))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[4:6], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY AFTER %s msgid=%s %d" % (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[4:], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, INCLUSIVE_LIMIT))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[4:], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[4:7], result)
|
||||
|
||||
# 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 = validate_chathistory_batch(self.getMessages(user))
|
||||
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 = validate_chathistory_batch(self.getMessages(user))
|
||||
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 = validate_chathistory_batch(self.getMessages(user))
|
||||
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 = validate_chathistory_batch(self.getMessages(user))
|
||||
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 = validate_chathistory_batch(self.getMessages(user))
|
||||
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 = validate_chathistory_batch(self.getMessages(user))
|
||||
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 = validate_chathistory_batch(self.getMessages(user))
|
||||
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 = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[-4:-1], result)
|
||||
|
||||
# AROUND
|
||||
self.sendLine(user, "CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual([echo_messages[7]], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertEqual(echo_messages[6:9], result)
|
||||
|
||||
self.sendLine(user, "CHATHISTORY AROUND %s timestamp=%s %d" % (chname, echo_messages[7].time, 3))
|
||||
result = validate_chathistory_batch(self.getMessages(user))
|
||||
self.assertIn(echo_messages[7], result)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testChathistoryTagmsg(self):
|
||||
c1 = secrets.token_hex(12)
|
||||
c2 = secrets.token_hex(12)
|
||||
chname = '#' + secrets.token_hex(12)
|
||||
self.controller.registerUser(self, c1, 'sesame1')
|
||||
self.controller.registerUser(self, c2, 'sesame2')
|
||||
self.connectClient(c1, capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP], password='sesame1')
|
||||
self.connectClient(c2, capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP,], password='sesame2')
|
||||
self.joinChannel(1, chname)
|
||||
self.joinChannel(2, chname)
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
|
||||
self.sendLine(1, '@+client-only-tag-test=success;+draft/persist TAGMSG %s' % (chname,))
|
||||
echo = self.getMessages(1)[0]
|
||||
msgid = echo.tags['msgid']
|
||||
|
||||
def validate_tagmsg(msg, target, msgid):
|
||||
self.assertEqual(msg.command, 'TAGMSG')
|
||||
self.assertEqual(msg.tags['+client-only-tag-test'], 'success')
|
||||
self.assertEqual(msg.tags['msgid'], msgid)
|
||||
self.assertEqual(msg.params, [target])
|
||||
|
||||
validate_tagmsg(echo, chname, msgid)
|
||||
|
||||
relay = self.getMessages(2)
|
||||
self.assertEqual(len(relay), 1)
|
||||
validate_tagmsg(relay[0], chname, msgid)
|
||||
|
||||
self.sendLine(1, 'CHATHISTORY LATEST %s * 10' % (chname,))
|
||||
history_tagmsgs = [msg for msg in self.getMessages(1) if msg.command == 'TAGMSG']
|
||||
self.assertEqual(len(history_tagmsgs), 1)
|
||||
validate_tagmsg(history_tagmsgs[0], chname, msgid)
|
||||
|
||||
# c2 doesn't have event-playback and MUST NOT receive replayed tagmsg
|
||||
self.sendLine(2, 'CHATHISTORY LATEST %s * 10' % (chname,))
|
||||
history_tagmsgs = [msg for msg in self.getMessages(2) if msg.command == 'TAGMSG']
|
||||
self.assertEqual(len(history_tagmsgs), 0)
|
||||
|
||||
# now try a DM
|
||||
self.sendLine(1, '@+client-only-tag-test=success;+draft/persist TAGMSG %s' % (c2,))
|
||||
echo = self.getMessages(1)[0]
|
||||
msgid = echo.tags['msgid']
|
||||
validate_tagmsg(echo, c2, msgid)
|
||||
|
||||
relay = self.getMessages(2)
|
||||
self.assertEqual(len(relay), 1)
|
||||
validate_tagmsg(relay[0], c2, msgid)
|
||||
|
||||
self.sendLine(1, 'CHATHISTORY LATEST %s * 10' % (c2,))
|
||||
history_tagmsgs = [msg for msg in self.getMessages(1) if msg.command == 'TAGMSG']
|
||||
self.assertEqual(len(history_tagmsgs), 1)
|
||||
validate_tagmsg(history_tagmsgs[0], c2, msgid)
|
||||
|
||||
# c2 doesn't have event-playback and MUST NOT receive replayed tagmsg
|
||||
self.sendLine(2, 'CHATHISTORY LATEST %s * 10' % (c1,))
|
||||
history_tagmsgs = [msg for msg in self.getMessages(2) if msg.command == 'TAGMSG']
|
||||
self.assertEqual(len(history_tagmsgs), 0)
|
||||
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testChathistoryDMClientOnlyTags(self):
|
||||
# regression test for Oragono #1411
|
||||
c1 = secrets.token_hex(12)
|
||||
c2 = secrets.token_hex(12)
|
||||
self.controller.registerUser(self, c1, 'sesame1')
|
||||
self.controller.registerUser(self, c2, 'sesame2')
|
||||
self.connectClient(c1, capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP], password='sesame1')
|
||||
self.connectClient(c2, capabilities=['message-tags', 'server-time', 'echo-message', 'batch', 'labeled-response', CHATHISTORY_CAP,], password='sesame2')
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
|
||||
echo_msgid = None
|
||||
def validate_msg(msg):
|
||||
self.assertEqual(msg.command, 'PRIVMSG')
|
||||
self.assertEqual(msg.tags['+client-only-tag-test'], 'success')
|
||||
self.assertEqual(msg.tags['msgid'], echo_msgid)
|
||||
self.assertEqual(msg.params, [c2, 'hi'])
|
||||
|
||||
self.sendLine(1, '@+client-only-tag-test=success;+draft/persist PRIVMSG %s hi' % (c2,))
|
||||
echo = self.getMessage(1)
|
||||
echo_msgid = echo.tags['msgid']
|
||||
validate_msg(echo)
|
||||
relay = self.getMessage(2)
|
||||
validate_msg(relay)
|
||||
|
||||
self.sendLine(1, 'CHATHISTORY LATEST * * 10')
|
||||
hist = [msg for msg in self.getMessages(1) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual(len(hist), 1)
|
||||
validate_msg(hist[0])
|
||||
|
||||
self.sendLine(2, 'CHATHISTORY LATEST * * 10')
|
||||
hist = [msg for msg in self.getMessages(2) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual(len(hist), 1)
|
||||
validate_msg(hist[0])
|
31
irctest/server_tests/test_confusables.py
Normal file
31
irctest/server_tests/test_confusables.py
Normal file
@ -0,0 +1,31 @@
|
||||
from irctest import cases
|
||||
from irctest.numerics import RPL_WELCOME, ERR_NICKNAMEINUSE
|
||||
|
||||
class ConfusablesTestCase(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"oragono_config": lambda config: config['accounts'].update(
|
||||
{'nick-reservation': {'enabled': True, 'method': 'strict'}}
|
||||
)
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testConfusableNicks(self):
|
||||
self.controller.registerUser(self, 'evan', 'sesame')
|
||||
|
||||
self.addClient(1)
|
||||
# U+0435 in place of e:
|
||||
self.sendLine(1, 'NICK еvan')
|
||||
self.sendLine(1, 'USER a 0 * a')
|
||||
messages = self.getMessages(1)
|
||||
commands = set(msg.command for msg in messages)
|
||||
self.assertNotIn(RPL_WELCOME, commands)
|
||||
self.assertIn(ERR_NICKNAMEINUSE, commands)
|
||||
|
||||
self.connectClient('evan', name='evan', password='sesame')
|
||||
# should be able to switch to the confusable nick
|
||||
self.sendLine('evan', 'NICK еvan')
|
||||
messages = self.getMessages('evan')
|
||||
commands = set(msg.command for msg in messages)
|
||||
self.assertIn('NICK', commands)
|
@ -38,7 +38,7 @@ class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
||||
self.assertNotEqual(m.command, '001',
|
||||
msg='Got 001 after NICK+USER but incorrect PASS')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459', 'RFC2812')
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459', 'RFC2812', strict=True)
|
||||
def testPassAfterNickuser(self):
|
||||
"""“The password can and must be set before any attempt to register
|
||||
the connection is made.”
|
||||
|
@ -4,6 +4,30 @@
|
||||
|
||||
from irctest import cases
|
||||
from irctest.basecontrollers import NotImplementedByController
|
||||
from irctest.irc_utils.junkdrawer import random_name
|
||||
|
||||
class DMEchoMessageTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testDirectMessageEcho(self):
|
||||
bar = random_name('bar')
|
||||
self.connectClient(bar, name=bar, capabilities=['batch', 'labeled-response', 'echo-message', 'message-tags', 'server-time'])
|
||||
self.getMessages(bar)
|
||||
|
||||
qux = random_name('qux')
|
||||
self.connectClient(qux, name=qux, capabilities=['batch', 'labeled-response', 'echo-message', 'message-tags', 'server-time'])
|
||||
self.getMessages(qux)
|
||||
|
||||
self.sendLine(bar, '@label=xyz;+example-client-tag=example-value PRIVMSG %s :hi there' % (qux,))
|
||||
echo = self.getMessages(bar)[0]
|
||||
delivery = self.getMessages(qux)[0]
|
||||
|
||||
self.assertEqual(delivery.params, [qux, 'hi there'])
|
||||
self.assertEqual(delivery.params, echo.params)
|
||||
self.assertEqual(delivery.tags['msgid'], echo.tags['msgid'])
|
||||
self.assertEqual(echo.tags['label'], 'xyz')
|
||||
self.assertEqual(delivery.tags['+example-client-tag'], 'example-value')
|
||||
self.assertEqual(delivery.tags['+example-client-tag'], echo.tags['+example-client-tag'])
|
||||
|
||||
class EchoMessageTestCase(cases.BaseServerTestCase):
|
||||
def _testEchoMessage(command, solo, server_time):
|
||||
|
@ -2,22 +2,23 @@
|
||||
<https://ircv3.net/specs/extensions/labeled-response.html>
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from irctest import cases
|
||||
from irctest.basecontrollers import NotImplementedByController
|
||||
|
||||
class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledPrivmsgResponsesToMultipleClients(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(2)
|
||||
self.connectClient('carl', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('carl', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(3)
|
||||
self.connectClient('alice', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('alice', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(4)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345 PRIVMSG bar,carl,alice :hi')
|
||||
self.sendLine(1, '@label=12345 PRIVMSG bar,carl,alice :hi')
|
||||
m = self.getMessage(1)
|
||||
m2 = self.getMessage(2)
|
||||
m3 = self.getMessage(3)
|
||||
@ -25,38 +26,38 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
|
||||
|
||||
# ensure the label isn't sent to recipients
|
||||
self.assertMessageEqual(m2, command='PRIVMSG', fail_msg='No PRIVMSG received by target 1 after sending one out')
|
||||
self.assertNotIn('draft/label', m2.tags, m2, fail_msg="When sending a PRIVMSG with a label, the target users shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', m2.tags, m2, fail_msg="When sending a PRIVMSG with a label, the target users shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertMessageEqual(m3, command='PRIVMSG', fail_msg='No PRIVMSG received by target 1 after sending one out')
|
||||
self.assertNotIn('draft/label', m3.tags, m3, fail_msg="When sending a PRIVMSG with a label, the target users shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', m3.tags, m3, fail_msg="When sending a PRIVMSG with a label, the target users shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertMessageEqual(m4, command='PRIVMSG', fail_msg='No PRIVMSG received by target 1 after sending one out')
|
||||
self.assertNotIn('draft/label', m4.tags, m4, fail_msg="When sending a PRIVMSG with a label, the target users shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', m4.tags, m4, fail_msg="When sending a PRIVMSG with a label, the target users shouldn't receive the label (only the sending user should): {msg}")
|
||||
|
||||
self.assertMessageEqual(m, command='BATCH', fail_msg='No BATCH echo received after sending one out')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledPrivmsgResponsesToClient(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(2)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345 PRIVMSG bar :hi')
|
||||
self.sendLine(1, '@label=12345 PRIVMSG bar :hi')
|
||||
m = self.getMessage(1)
|
||||
m2 = self.getMessage(2)
|
||||
|
||||
# ensure the label isn't sent to recipient
|
||||
self.assertMessageEqual(m2, command='PRIVMSG', fail_msg='No PRIVMSG received by the target after sending one out')
|
||||
self.assertNotIn('draft/label', m2.tags, m2, fail_msg="When sending a PRIVMSG with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', m2.tags, m2, fail_msg="When sending a PRIVMSG with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
|
||||
self.assertMessageEqual(m, command='PRIVMSG', fail_msg='No PRIVMSG echo received after sending one out')
|
||||
self.assertIn('draft/label', m.tags, m, fail_msg="When sending a PRIVMSG with a label, the echo'd message didn't contain the label at all: {msg}")
|
||||
self.assertEqual(m.tags['draft/label'], '12345', m, fail_msg="Echo'd PRIVMSG to a client did not contain the same label we sent it with(should be '12345'): {msg}")
|
||||
self.assertIn('label', m.tags, m, fail_msg="When sending a PRIVMSG with a label, the echo'd message didn't contain the label at all: {msg}")
|
||||
self.assertEqual(m.tags['label'], '12345', m, fail_msg="Echo'd PRIVMSG to a client did not contain the same label we sent it with(should be '12345'): {msg}")
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledPrivmsgResponsesToChannel(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(2)
|
||||
|
||||
# join channels
|
||||
@ -66,61 +67,61 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
|
||||
self.getMessages(2)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345;+draft/reply=123;+draft/react=l😃l PRIVMSG #test :hi')
|
||||
self.sendLine(1, '@label=12345;+draft/reply=123;+draft/react=l😃l PRIVMSG #test :hi')
|
||||
ms = self.getMessage(1)
|
||||
mt = self.getMessage(2)
|
||||
|
||||
# ensure the label isn't sent to recipient
|
||||
self.assertMessageEqual(mt, command='PRIVMSG', fail_msg='No PRIVMSG received by the target after sending one out')
|
||||
self.assertNotIn('draft/label', mt.tags, mt, fail_msg="When sending a PRIVMSG with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', mt.tags, mt, fail_msg="When sending a PRIVMSG with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
|
||||
# ensure sender correctly receives msg
|
||||
self.assertMessageEqual(ms, command='PRIVMSG', fail_msg="Got a message back that wasn't a PRIVMSG")
|
||||
self.assertIn('draft/label', ms.tags, ms, fail_msg="When sending a PRIVMSG with a label, the source user should receive the label but didn't: {msg}")
|
||||
self.assertEqual(ms.tags['draft/label'], '12345', ms, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
self.assertIn('label', ms.tags, ms, fail_msg="When sending a PRIVMSG with a label, the source user should receive the label but didn't: {msg}")
|
||||
self.assertEqual(ms.tags['label'], '12345', ms, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledPrivmsgResponsesToSelf(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345 PRIVMSG foo :hi')
|
||||
self.sendLine(1, '@label=12345 PRIVMSG foo :hi')
|
||||
m1 = self.getMessage(1)
|
||||
m2 = self.getMessage(1)
|
||||
|
||||
number_of_labels = 0
|
||||
for m in [m1, m2]:
|
||||
self.assertMessageEqual(m, command='PRIVMSG', fail_msg="Got a message back that wasn't a PRIVMSG")
|
||||
if 'draft/label' in m.tags:
|
||||
if 'label' in m.tags:
|
||||
number_of_labels += 1
|
||||
self.assertEqual(m.tags['draft/label'], '12345', m, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
self.assertEqual(m.tags['label'], '12345', m, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
|
||||
self.assertEqual(number_of_labels, 1, m1, fail_msg="When sending a PRIVMSG to self with echo-message, we only expect one message to contain the label. Instead, {} messages had the label".format(number_of_labels))
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledNoticeResponsesToClient(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(2)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345 NOTICE bar :hi')
|
||||
self.sendLine(1, '@label=12345 NOTICE bar :hi')
|
||||
m = self.getMessage(1)
|
||||
m2 = self.getMessage(2)
|
||||
|
||||
# ensure the label isn't sent to recipient
|
||||
self.assertMessageEqual(m2, command='NOTICE', fail_msg='No NOTICE received by the target after sending one out')
|
||||
self.assertNotIn('draft/label', m2.tags, m2, fail_msg="When sending a NOTICE with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', m2.tags, m2, fail_msg="When sending a NOTICE with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
|
||||
self.assertMessageEqual(m, command='NOTICE', fail_msg='No NOTICE echo received after sending one out')
|
||||
self.assertIn('draft/label', m.tags, m, fail_msg="When sending a NOTICE with a label, the echo'd message didn't contain the label at all: {msg}")
|
||||
self.assertEqual(m.tags['draft/label'], '12345', m, fail_msg="Echo'd NOTICE to a client did not contain the same label we sent it with(should be '12345'): {msg}")
|
||||
self.assertIn('label', m.tags, m, fail_msg="When sending a NOTICE with a label, the echo'd message didn't contain the label at all: {msg}")
|
||||
self.assertEqual(m.tags['label'], '12345', m, fail_msg="Echo'd NOTICE to a client did not contain the same label we sent it with(should be '12345'): {msg}")
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledNoticeResponsesToChannel(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(2)
|
||||
|
||||
# join channels
|
||||
@ -130,59 +131,59 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
|
||||
self.getMessages(2)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345;+draft/reply=123;+draft/react=l😃l NOTICE #test :hi')
|
||||
self.sendLine(1, '@label=12345;+draft/reply=123;+draft/react=l😃l NOTICE #test :hi')
|
||||
ms = self.getMessage(1)
|
||||
mt = self.getMessage(2)
|
||||
|
||||
# ensure the label isn't sent to recipient
|
||||
self.assertMessageEqual(mt, command='NOTICE', fail_msg='No NOTICE received by the target after sending one out')
|
||||
self.assertNotIn('draft/label', mt.tags, mt, fail_msg="When sending a NOTICE with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', mt.tags, mt, fail_msg="When sending a NOTICE with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
|
||||
# ensure sender correctly receives msg
|
||||
self.assertMessageEqual(ms, command='NOTICE', fail_msg="Got a message back that wasn't a NOTICE")
|
||||
self.assertIn('draft/label', ms.tags, ms, fail_msg="When sending a NOTICE with a label, the source user should receive the label but didn't: {msg}")
|
||||
self.assertEqual(ms.tags['draft/label'], '12345', ms, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
self.assertIn('label', ms.tags, ms, fail_msg="When sending a NOTICE with a label, the source user should receive the label but didn't: {msg}")
|
||||
self.assertEqual(ms.tags['label'], '12345', ms, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledNoticeResponsesToSelf(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345 NOTICE foo :hi')
|
||||
self.sendLine(1, '@label=12345 NOTICE foo :hi')
|
||||
m1 = self.getMessage(1)
|
||||
m2 = self.getMessage(1)
|
||||
|
||||
number_of_labels = 0
|
||||
for m in [m1, m2]:
|
||||
self.assertMessageEqual(m, command='NOTICE', fail_msg="Got a message back that wasn't a NOTICE")
|
||||
if 'draft/label' in m.tags:
|
||||
if 'label' in m.tags:
|
||||
number_of_labels += 1
|
||||
self.assertEqual(m.tags['draft/label'], '12345', m, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
self.assertEqual(m.tags['label'], '12345', m, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
|
||||
self.assertEqual(number_of_labels, 1, m1, fail_msg="When sending a NOTICE to self with echo-message, we only expect one message to contain the label. Instead, {} messages had the label".format(number_of_labels))
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledTagMsgResponsesToClient(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response', 'draft/message-tags-0.2'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response', 'message-tags'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'draft/labeled-response', 'draft/message-tags-0.2'], skip_if_cap_nak=True)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response', 'message-tags'], skip_if_cap_nak=True)
|
||||
self.getMessages(2)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG bar')
|
||||
self.sendLine(1, '@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG bar')
|
||||
m = self.getMessage(1)
|
||||
m2 = self.getMessage(2)
|
||||
|
||||
# ensure the label isn't sent to recipient
|
||||
self.assertMessageEqual(m2, command='TAGMSG', fail_msg='No TAGMSG received by the target after sending one out')
|
||||
self.assertNotIn('draft/label', m2.tags, m2, fail_msg="When sending a TAGMSG with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', m2.tags, m2, fail_msg="When sending a TAGMSG with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertIn('+draft/reply', m2.tags, m2, fail_msg="Reply tag wasn't present on the target user's TAGMSG: {msg}")
|
||||
self.assertEqual(m2.tags['+draft/reply'], '123', m2, fail_msg="Reply tag wasn't the same on the target user's TAGMSG: {msg}")
|
||||
self.assertIn('+draft/react', m2.tags, m2, fail_msg="React tag wasn't present on the target user's TAGMSG: {msg}")
|
||||
self.assertEqual(m2.tags['+draft/react'], 'l😃l', m2, fail_msg="React tag wasn't the same on the target user's TAGMSG: {msg}")
|
||||
|
||||
self.assertMessageEqual(m, command='TAGMSG', fail_msg='No TAGMSG echo received after sending one out')
|
||||
self.assertIn('draft/label', m.tags, m, fail_msg="When sending a TAGMSG with a label, the echo'd message didn't contain the label at all: {msg}")
|
||||
self.assertEqual(m.tags['draft/label'], '12345', m, fail_msg="Echo'd TAGMSG to a client did not contain the same label we sent it with(should be '12345'): {msg}")
|
||||
self.assertIn('label', m.tags, m, fail_msg="When sending a TAGMSG with a label, the echo'd message didn't contain the label at all: {msg}")
|
||||
self.assertEqual(m.tags['label'], '12345', m, fail_msg="Echo'd TAGMSG to a client did not contain the same label we sent it with(should be '12345'): {msg}")
|
||||
self.assertIn('+draft/reply', m.tags, m, fail_msg="Reply tag wasn't present on the source user's TAGMSG: {msg}")
|
||||
self.assertEqual(m2.tags['+draft/reply'], '123', m, fail_msg="Reply tag wasn't the same on the source user's TAGMSG: {msg}")
|
||||
self.assertIn('+draft/react', m.tags, m, fail_msg="React tag wasn't present on the source user's TAGMSG: {msg}")
|
||||
@ -190,9 +191,9 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledTagMsgResponsesToChannel(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response', 'draft/message-tags-0.2'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response', 'message-tags'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'draft/labeled-response', 'draft/message-tags-0.2'], skip_if_cap_nak=True)
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response', 'message-tags'], skip_if_cap_nak=True)
|
||||
self.getMessages(2)
|
||||
|
||||
# join channels
|
||||
@ -202,33 +203,96 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper
|
||||
self.getMessages(2)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG #test')
|
||||
self.sendLine(1, '@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG #test')
|
||||
ms = self.getMessage(1)
|
||||
mt = self.getMessage(2)
|
||||
|
||||
# ensure the label isn't sent to recipient
|
||||
self.assertMessageEqual(mt, command='TAGMSG', fail_msg='No TAGMSG received by the target after sending one out')
|
||||
self.assertNotIn('draft/label', mt.tags, mt, fail_msg="When sending a TAGMSG with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
self.assertNotIn('label', mt.tags, mt, fail_msg="When sending a TAGMSG with a label, the target user shouldn't receive the label (only the sending user should): {msg}")
|
||||
|
||||
# ensure sender correctly receives msg
|
||||
self.assertMessageEqual(ms, command='TAGMSG', fail_msg="Got a message back that wasn't a TAGMSG")
|
||||
self.assertIn('draft/label', ms.tags, ms, fail_msg="When sending a TAGMSG with a label, the source user should receive the label but didn't: {msg}")
|
||||
self.assertEqual(ms.tags['draft/label'], '12345', ms, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
self.assertIn('label', ms.tags, ms, fail_msg="When sending a TAGMSG with a label, the source user should receive the label but didn't: {msg}")
|
||||
self.assertEqual(ms.tags['label'], '12345', ms, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testLabeledTagMsgResponsesToSelf(self):
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'draft/labeled-response', 'draft/message-tags-0.2'], skip_if_cap_nak=True)
|
||||
self.connectClient('foo', capabilities=['batch', 'echo-message', 'labeled-response', 'message-tags'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, '@draft/label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG foo')
|
||||
self.sendLine(1, '@label=12345;+draft/reply=123;+draft/react=l😃l TAGMSG foo')
|
||||
m1 = self.getMessage(1)
|
||||
m2 = self.getMessage(1)
|
||||
|
||||
number_of_labels = 0
|
||||
for m in [m1, m2]:
|
||||
self.assertMessageEqual(m, command='TAGMSG', fail_msg="Got a message back that wasn't a TAGMSG")
|
||||
if 'draft/label' in m.tags:
|
||||
if 'label' in m.tags:
|
||||
number_of_labels += 1
|
||||
self.assertEqual(m.tags['draft/label'], '12345', m, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
self.assertEqual(m.tags['label'], '12345', m, fail_msg="Echo'd label doesn't match the label we sent (should be '12345'): {msg}")
|
||||
|
||||
self.assertEqual(number_of_labels, 1, m1, fail_msg="When sending a TAGMSG to self with echo-message, we only expect one message to contain the label. Instead, {} messages had the label".format(number_of_labels))
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testBatchedJoinMessages(self):
|
||||
self.connectClient('bar', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time'], skip_if_cap_nak=True)
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, '@label=12345 JOIN #xyz')
|
||||
m = self.getMessages(1)
|
||||
|
||||
# we expect at least join and names lines, which must be batched
|
||||
self.assertGreaterEqual(len(m), 3)
|
||||
|
||||
# valid BATCH start line:
|
||||
batch_start = m[0]
|
||||
self.assertMessageEqual(batch_start, command='BATCH')
|
||||
self.assertEqual(len(batch_start.params), 2)
|
||||
self.assertTrue(batch_start.params[0].startswith('+'), 'batch start param must begin with +, got %s' % (batch_start.params[0],))
|
||||
batch_id = batch_start.params[0][1:]
|
||||
# batch id MUST be alphanumerics and hyphens
|
||||
self.assertTrue(re.match(r'^[A-Za-z0-9\-]+$', batch_id) is not None, 'batch id must be alphanumerics and hyphens, got %r' % (batch_id,))
|
||||
self.assertEqual(batch_start.params[1], 'labeled-response')
|
||||
self.assertEqual(batch_start.tags.get('label'), '12345')
|
||||
|
||||
# valid BATCH end line
|
||||
batch_end = m[-1]
|
||||
self.assertMessageEqual(batch_end, command='BATCH', params=['-' + batch_id])
|
||||
|
||||
# messages must have the BATCH tag
|
||||
for message in m[1:-1]:
|
||||
self.assertEqual(message.tags.get('batch'), batch_id)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testNoBatchForSingleMessage(self):
|
||||
self.connectClient('bar', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time'])
|
||||
self.getMessages(1)
|
||||
|
||||
self.sendLine(1, '@label=98765 PING adhoctestline')
|
||||
# no BATCH should be initiated for a one-line response, it should just be labeled
|
||||
ms = self.getMessages(1)
|
||||
self.assertEqual(len(ms), 1)
|
||||
m = ms[0]
|
||||
self.assertEqual(m.command, 'PONG')
|
||||
self.assertEqual(m.params[-1], 'adhoctestline')
|
||||
# check the label
|
||||
self.assertEqual(m.tags.get('label'), '98765')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testEmptyBatchForNoResponse(self):
|
||||
self.connectClient('bar', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time'])
|
||||
self.getMessages(1)
|
||||
|
||||
# PONG never receives a response
|
||||
self.sendLine(1, '@label=98765 PONG adhoctestline')
|
||||
|
||||
# labeled-response: "Servers MUST respond with a labeled
|
||||
# `ACK` message when a client sends a labeled command that normally
|
||||
# produces no response."
|
||||
ms = self.getMessages(1)
|
||||
self.assertEqual(len(ms), 1)
|
||||
ack = ms[0]
|
||||
|
||||
self.assertEqual(ack.command, 'ACK')
|
||||
self.assertEqual(ack.tags.get('label'), '98765')
|
||||
|
347
irctest/server_tests/test_lusers.py
Normal file
347
irctest/server_tests/test_lusers.py
Normal file
@ -0,0 +1,347 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from irctest import cases
|
||||
|
||||
from irctest.numerics import RPL_LUSERCLIENT, RPL_LUSEROP, RPL_LUSERUNKNOWN, RPL_LUSERCHANNELS, RPL_LUSERME, RPL_LOCALUSERS, RPL_GLOBALUSERS, ERR_NOTREGISTERED
|
||||
from irctest.numerics import RPL_YOUREOPER
|
||||
|
||||
# 3 numbers, delimited by spaces, possibly negative (eek)
|
||||
LUSERCLIENT_REGEX = re.compile(r'^.*( [-0-9]* ).*( [-0-9]* ).*( [-0-9]* ).*$')
|
||||
# 2 numbers
|
||||
LUSERME_REGEX = re.compile(r'^.*( [-0-9]* ).*( [-0-9]* ).*$')
|
||||
|
||||
@dataclass
|
||||
class LusersResult:
|
||||
GlobalVisible: int = None
|
||||
GlobalInvisible: int = None
|
||||
Servers: int = None
|
||||
Opers: int = None
|
||||
Unregistered: int = None
|
||||
Channels: int = None
|
||||
LocalTotal: int = None
|
||||
LocalMax: int = None
|
||||
GlobalTotal: int = None
|
||||
GlobalMax: int = None
|
||||
|
||||
class LusersTestCase(cases.BaseServerTestCase):
|
||||
|
||||
def getLusers(self, client):
|
||||
self.sendLine(client, 'LUSERS')
|
||||
messages = self.getMessages(client)
|
||||
by_numeric = dict((msg.command, msg) for msg in messages)
|
||||
|
||||
result = LusersResult()
|
||||
|
||||
# all of these take the nick as first param
|
||||
for message in messages:
|
||||
self.assertEqual(client, message.params[0])
|
||||
|
||||
luserclient = by_numeric[RPL_LUSERCLIENT] # 251
|
||||
self.assertEqual(len(luserclient.params), 2)
|
||||
luserclient_param = luserclient.params[1]
|
||||
try:
|
||||
match = LUSERCLIENT_REGEX.match(luserclient_param)
|
||||
result.GlobalVisible = int(match.group(1))
|
||||
result.GlobalInvisible = int(match.group(2))
|
||||
result.Servers = int(match.group(3))
|
||||
except:
|
||||
raise ValueError("corrupt reply for 251 RPL_LUSERCLIENT", luserclient_param)
|
||||
|
||||
if RPL_LUSEROP in by_numeric:
|
||||
result.Opers = int(by_numeric[RPL_LUSEROP].params[1])
|
||||
if RPL_LUSERUNKNOWN in by_numeric:
|
||||
result.Unregistered = int(by_numeric[RPL_LUSERUNKNOWN].params[1])
|
||||
if RPL_LUSERCHANNELS in by_numeric:
|
||||
result.Channels = int(by_numeric[RPL_LUSERCHANNELS].params[1])
|
||||
localusers = by_numeric[RPL_LOCALUSERS]
|
||||
result.LocalTotal = int(localusers.params[1])
|
||||
result.LocalMax = int(localusers.params[2])
|
||||
globalusers = by_numeric[RPL_GLOBALUSERS]
|
||||
result.GlobalTotal = int(globalusers.params[1])
|
||||
result.GlobalMax = int(globalusers.params[2])
|
||||
|
||||
luserme = by_numeric[RPL_LUSERME]
|
||||
self.assertEqual(len(luserme.params), 2)
|
||||
luserme_param = luserme.params[1]
|
||||
try:
|
||||
match = LUSERME_REGEX.match(luserme_param)
|
||||
localTotalFromUserme = int(match.group(1))
|
||||
serversFromUserme = int(match.group(2))
|
||||
except:
|
||||
raise ValueError("corrupt reply for 255 RPL_LUSERME", luserme_param)
|
||||
self.assertEqual(result.LocalTotal, localTotalFromUserme)
|
||||
# serversFromUserme is "servers i'm currently connected to", generally undefined
|
||||
self.assertGreaterEqual(serversFromUserme, 0)
|
||||
|
||||
return result
|
||||
|
||||
class BasicLusersTest(LusersTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC2812')
|
||||
def testLusers(self):
|
||||
self.connectClient('bar', name='bar')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertIn(lusers.Unregistered, (0, None))
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 1)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 1)
|
||||
|
||||
self.connectClient('qux', name='qux')
|
||||
lusers = self.getLusers('qux')
|
||||
self.assertIn(lusers.Unregistered, (0, None))
|
||||
self.assertEqual(lusers.GlobalTotal, 2)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 2)
|
||||
self.assertEqual(lusers.LocalTotal, 2)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
|
||||
self.sendLine('qux', 'QUIT')
|
||||
self.assertDisconnected('qux')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertIn(lusers.Unregistered, (0, None))
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
|
||||
|
||||
class LusersUnregisteredTestCase(LusersTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC2812')
|
||||
def testLusers(self):
|
||||
self.doLusersTest()
|
||||
|
||||
def _synchronize(self, client_name):
|
||||
"""Synchronizes using a PING, but accept ERR_NOTREGISTERED as a response."""
|
||||
self.sendLine(client_name, 'PING')
|
||||
for _ in range(1000):
|
||||
msg = self.getRegistrationMessage(client_name)
|
||||
if msg.command in (ERR_NOTREGISTERED, 'PONG'):
|
||||
break
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
assert False, (
|
||||
'Sent a PING before registration, '
|
||||
'got neither PONG or ERR_NOTREGISTERED'
|
||||
)
|
||||
|
||||
def doLusersTest(self):
|
||||
self.connectClient('bar', name='bar')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertIn(lusers.Unregistered, (0, None))
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 1)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 1)
|
||||
|
||||
self.addClient('qux')
|
||||
self.sendLine('qux', 'NICK qux')
|
||||
self._synchronize('qux')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.Unregistered, 1)
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 1)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 1)
|
||||
|
||||
self.addClient('bat')
|
||||
self.sendLine('bat', 'NICK bat')
|
||||
self._synchronize('bat')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.Unregistered, 2)
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 1)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 1)
|
||||
|
||||
# complete registration on one client
|
||||
self.sendLine('qux', 'USER u s e r')
|
||||
self.getMessages('qux')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.Unregistered, 1)
|
||||
self.assertEqual(lusers.GlobalTotal, 2)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 2)
|
||||
self.assertEqual(lusers.LocalTotal, 2)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
|
||||
# QUIT the other without registering
|
||||
self.sendLine('bat', 'QUIT')
|
||||
self.assertDisconnected('bat')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertIn(lusers.Unregistered, (0, None))
|
||||
self.assertEqual(lusers.GlobalTotal, 2)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 2)
|
||||
self.assertEqual(lusers.LocalTotal, 2)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
|
||||
|
||||
class LusersUnregisteredDefaultInvisibleTest(LusersUnregisteredTestCase):
|
||||
"""Same as above but with +i as the default."""
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"oragono_config": lambda config: config['accounts'].update(
|
||||
{'default-user-modes': '+i'}
|
||||
)
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testLusers(self):
|
||||
self.doLusersTest()
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.Unregistered, 0)
|
||||
self.assertEqual(lusers.GlobalTotal, 2)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertEqual(lusers.GlobalInvisible, 2)
|
||||
self.assertEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.LocalTotal, 2)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
|
||||
|
||||
class LuserOpersTest(LusersTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testLuserOpers(self):
|
||||
self.connectClient('bar', name='bar')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 1)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 1)
|
||||
self.assertIn(lusers.Opers, (0, None))
|
||||
|
||||
# add 1 oper
|
||||
self.sendLine('bar', 'OPER root frenchfries')
|
||||
msgs = self.getMessages('bar')
|
||||
self.assertIn(RPL_YOUREOPER, {msg.command for msg in msgs})
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 1)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 1)
|
||||
self.assertEqual(lusers.Opers, 1)
|
||||
|
||||
# now 2 opers
|
||||
self.connectClient('qux', name='qux')
|
||||
self.sendLine('qux', 'OPER root frenchfries')
|
||||
self.getMessages('qux')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.GlobalTotal, 2)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 2)
|
||||
self.assertEqual(lusers.LocalTotal, 2)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
self.assertEqual(lusers.Opers, 2)
|
||||
|
||||
# remove oper with MODE
|
||||
self.sendLine('bar', 'MODE bar -o')
|
||||
msgs = self.getMessages('bar')
|
||||
self.assertIn('MODE', {msg.command for msg in msgs})
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.GlobalTotal, 2)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 2)
|
||||
self.assertEqual(lusers.LocalTotal, 2)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
self.assertEqual(lusers.Opers, 1)
|
||||
|
||||
# remove oper by quit
|
||||
self.sendLine('qux', 'QUIT')
|
||||
self.assertDisconnected('qux')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertGreaterEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertGreaterEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.GlobalInvisible + lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
self.assertEqual(lusers.Opers, 0)
|
||||
|
||||
|
||||
class OragonoInvisibleDefaultTest(LusersTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"oragono_config": lambda config: config['accounts'].update(
|
||||
{'default-user-modes': '+i'}
|
||||
)
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testLusers(self):
|
||||
self.connectClient('bar', name='bar')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 1)
|
||||
self.assertEqual(lusers.GlobalInvisible, 1)
|
||||
self.assertEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 1)
|
||||
|
||||
self.connectClient('qux', name='qux')
|
||||
lusers = self.getLusers('qux')
|
||||
self.assertEqual(lusers.GlobalTotal, 2)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertEqual(lusers.GlobalInvisible, 2)
|
||||
self.assertEqual(lusers.GlobalVisible, 0)
|
||||
self.assertEqual(lusers.LocalTotal, 2)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
|
||||
# remove +i with MODE
|
||||
self.sendLine('bar', 'MODE bar -i')
|
||||
msgs = self.getMessages('bar')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertIn('MODE', {msg.command for msg in msgs})
|
||||
self.assertEqual(lusers.GlobalTotal, 2)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertEqual(lusers.GlobalInvisible, 1)
|
||||
self.assertEqual(lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 2)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
||||
|
||||
# disconnect invisible user
|
||||
self.sendLine('qux', 'QUIT')
|
||||
self.assertDisconnected('qux')
|
||||
lusers = self.getLusers('bar')
|
||||
self.assertEqual(lusers.GlobalTotal, 1)
|
||||
self.assertEqual(lusers.GlobalMax, 2)
|
||||
self.assertEqual(lusers.GlobalInvisible, 0)
|
||||
self.assertEqual(lusers.GlobalVisible, 1)
|
||||
self.assertEqual(lusers.LocalTotal, 1)
|
||||
self.assertEqual(lusers.LocalMax, 2)
|
147
irctest/server_tests/test_message_tags.py
Normal file
147
irctest/server_tests/test_message_tags.py
Normal file
@ -0,0 +1,147 @@
|
||||
"""
|
||||
https://ircv3.net/specs/extensions/message-tags.html
|
||||
"""
|
||||
|
||||
from irctest import cases
|
||||
from irctest.irc_utils.message_parser import parse_message
|
||||
from irctest.numerics import ERR_INPUTTOOLONG
|
||||
|
||||
class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('message-tags')
|
||||
def testBasic(self):
|
||||
def getAllMessages():
|
||||
for name in ['alice', 'bob', 'carol', 'dave']:
|
||||
self.getMessages(name)
|
||||
|
||||
def assertNoTags(line):
|
||||
# tags start with '@', without tags we start with the prefix,
|
||||
# which begins with ':'
|
||||
self.assertEqual(line[0], ':')
|
||||
msg = parse_message(line)
|
||||
self.assertEqual(msg.tags, {})
|
||||
return msg
|
||||
|
||||
self.connectClient(
|
||||
'alice',
|
||||
name='alice',
|
||||
capabilities=['message-tags'],
|
||||
skip_if_cap_nak=True
|
||||
)
|
||||
self.joinChannel('alice', '#test')
|
||||
self.connectClient('bob', name='bob', capabilities=['message-tags', 'echo-message'])
|
||||
self.joinChannel('bob', '#test')
|
||||
self.connectClient('carol', name='carol')
|
||||
self.joinChannel('carol', '#test')
|
||||
self.connectClient('dave', name='dave', capabilities=['server-time'])
|
||||
self.joinChannel('dave', '#test')
|
||||
getAllMessages()
|
||||
|
||||
self.sendLine('alice', '@+baz=bat;fizz=buzz PRIVMSG #test hi')
|
||||
self.getMessages('alice')
|
||||
bob_msg = self.getMessage('bob')
|
||||
carol_line = self.getMessage('carol', raw=True)
|
||||
self.assertMessageEqual(bob_msg, command='PRIVMSG', params=['#test', 'hi'])
|
||||
self.assertEqual(bob_msg.tags['+baz'], "bat")
|
||||
self.assertIn('msgid', bob_msg.tags)
|
||||
# should not relay a non-client-only tag
|
||||
self.assertNotIn('fizz', bob_msg.tags)
|
||||
# carol MUST NOT receive tags
|
||||
carol_msg = assertNoTags(carol_line)
|
||||
self.assertMessageEqual(carol_msg, command='PRIVMSG', params=['#test', 'hi'])
|
||||
# dave SHOULD receive server-time tag
|
||||
dave_msg = self.getMessage('dave')
|
||||
self.assertIn('time', dave_msg.tags)
|
||||
# dave MUST NOT receive client-only tags
|
||||
self.assertNotIn('+baz', dave_msg.tags)
|
||||
getAllMessages()
|
||||
|
||||
self.sendLine('bob', '@+bat=baz;+fizz=buzz PRIVMSG #test :hi yourself')
|
||||
bob_msg = self.getMessage('bob') # bob has echo-message
|
||||
alice_msg = self.getMessage('alice')
|
||||
carol_line = self.getMessage('carol', raw=True)
|
||||
carol_msg = assertNoTags(carol_line)
|
||||
for msg in [alice_msg, bob_msg, carol_msg]:
|
||||
self.assertMessageEqual(msg, command='PRIVMSG', params=['#test', 'hi yourself'])
|
||||
for msg in [alice_msg, bob_msg]:
|
||||
self.assertEqual(msg.tags['+bat'], 'baz')
|
||||
self.assertEqual(msg.tags['+fizz'], 'buzz')
|
||||
self.assertTrue(alice_msg.tags['msgid'])
|
||||
self.assertEqual(alice_msg.tags['msgid'], bob_msg.tags['msgid'])
|
||||
getAllMessages()
|
||||
|
||||
# test TAGMSG and basic escaping
|
||||
self.sendLine('bob', '@+buzz=fizz\:buzz;cat=dog;+steel=wootz TAGMSG #test')
|
||||
bob_msg = self.getMessage('bob') # bob has echo-message
|
||||
alice_msg = self.getMessage('alice')
|
||||
# carol MUST NOT receive TAGMSG at all
|
||||
self.assertEqual(self.getMessages('carol'), [])
|
||||
# dave MUST NOT receive TAGMSG either, despite having server-time
|
||||
self.assertEqual(self.getMessages('dave'), [])
|
||||
for msg in [alice_msg, bob_msg]:
|
||||
self.assertMessageEqual(alice_msg, command='TAGMSG', params=['#test'])
|
||||
self.assertEqual(msg.tags['+buzz'], 'fizz;buzz')
|
||||
self.assertEqual(msg.tags['+steel'], 'wootz')
|
||||
self.assertNotIn('cat', msg.tags)
|
||||
self.assertTrue(alice_msg.tags['msgid'])
|
||||
self.assertEqual(alice_msg.tags['msgid'], bob_msg.tags['msgid'])
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('message-tags')
|
||||
def testLengthLimits(self):
|
||||
self.connectClient(
|
||||
'alice',
|
||||
name='alice',
|
||||
capabilities=['message-tags', 'echo-message'],
|
||||
skip_if_cap_nak=True
|
||||
)
|
||||
self.joinChannel('alice', '#test')
|
||||
self.connectClient('bob', name='bob', capabilities=['message-tags'])
|
||||
self.joinChannel('bob', '#test')
|
||||
self.getMessages('alice')
|
||||
self.getMessages('bob')
|
||||
|
||||
# this is right at the limit of 4094 bytes of tag data,
|
||||
# 4096 bytes of tag section (including the starting '@' and the final ' ')
|
||||
max_tagmsg = '@foo=bar;+baz=%s TAGMSG #test' % ('a' * 4081,)
|
||||
self.assertEqual(max_tagmsg.index('TAGMSG'), 4096)
|
||||
self.sendLine('alice', max_tagmsg)
|
||||
echo = self.getMessage('alice')
|
||||
relay = self.getMessage('bob')
|
||||
self.assertMessageEqual(echo, command='TAGMSG', params=['#test'])
|
||||
self.assertMessageEqual(relay, command='TAGMSG', params=['#test'])
|
||||
self.assertNotEqual(echo.tags['msgid'], '')
|
||||
self.assertEqual(echo.tags['msgid'], relay.tags['msgid'])
|
||||
self.assertEqual(echo.tags['+baz'], 'a' * 4081)
|
||||
self.assertEqual(relay.tags['+baz'], echo.tags['+baz'])
|
||||
|
||||
excess_tagmsg = '@foo=bar;+baz=%s TAGMSG #test' % ('a' * 4082,)
|
||||
self.assertEqual(excess_tagmsg.index('TAGMSG'), 4097)
|
||||
self.sendLine('alice', excess_tagmsg)
|
||||
reply = self.getMessage('alice')
|
||||
self.assertEqual(reply.command, ERR_INPUTTOOLONG)
|
||||
self.assertEqual(self.getMessages('bob'), [])
|
||||
|
||||
max_privmsg = '@foo=bar;+baz=%s PRIVMSG #test %s' % ('a' * 4081, 'b' * 496)
|
||||
# irctest adds the '\r\n' for us, this is right at the limit
|
||||
self.assertEqual(len(max_privmsg), 4096 + (512 - 2))
|
||||
self.sendLine('alice', max_privmsg)
|
||||
echo = self.getMessage('alice')
|
||||
relay = self.getMessage('bob')
|
||||
self.assertNotEqual(echo.tags['msgid'], '')
|
||||
self.assertEqual(echo.tags['msgid'], relay.tags['msgid'])
|
||||
self.assertEqual(echo.tags['+baz'], 'a' * 4081)
|
||||
self.assertEqual(relay.tags['+baz'], echo.tags['+baz'])
|
||||
# message may have been truncated
|
||||
self.assertIn('b' * 400, echo.params[1])
|
||||
self.assertEqual(echo.params[1].rstrip('b'), '')
|
||||
self.assertIn('b' * 400, relay.params[1])
|
||||
self.assertEqual(relay.params[1].rstrip('b'), '')
|
||||
|
||||
excess_privmsg = '@foo=bar;+baz=%s PRIVMSG #test %s' % ('a' * 4082, 'b' * 495)
|
||||
# TAGMSG data is over the limit, but we're within the overall limit for a line
|
||||
self.assertEqual(excess_privmsg.index('PRIVMSG'), 4097)
|
||||
self.assertEqual(len(excess_privmsg), 4096 + (512 - 2))
|
||||
self.sendLine('alice', excess_privmsg)
|
||||
reply = self.getMessage('alice')
|
||||
self.assertEqual(reply.command, ERR_INPUTTOOLONG)
|
||||
self.assertEqual(self.getMessages('bob'), [])
|
@ -4,6 +4,7 @@ Section 3.2 of RFC 2812
|
||||
"""
|
||||
|
||||
from irctest import cases
|
||||
from irctest.numerics import ERR_INPUTTOOLONG
|
||||
|
||||
class PrivmsgTestCase(cases.BaseServerTestCase):
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459', 'RFC2812')
|
||||
@ -55,9 +56,22 @@ class NoticeTestCase(cases.BaseServerTestCase):
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459', 'RFC2812')
|
||||
def testNoticeNonexistentChannel(self):
|
||||
"""
|
||||
'automatic replies MUST NEVER be sent in response to a NOTICE message'
|
||||
'automatic replies MUST NEVER be sent in response to a NOTICE message.
|
||||
This rule applies to servers too - they MUST NOT send any error repl
|
||||
back to the client on receipt of a notice.'
|
||||
https://tools.ietf.org/html/rfc2812#section-3.3.2>
|
||||
"""
|
||||
self.connectClient('foo')
|
||||
self.sendLine(1, 'NOTICE #nonexistent :hello there')
|
||||
self.assertEqual(self.getMessages(1), [])
|
||||
|
||||
|
||||
class TagsTestCase(cases.BaseServerTestCase):
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testLineTooLong(self):
|
||||
self.connectClient('bar')
|
||||
self.joinChannel(1, '#xyz')
|
||||
monsterMessage = '@+clientOnlyTagExample=' + 'a'*4096 + ' PRIVMSG #xyz hi!'
|
||||
self.sendLine(1, monsterMessage)
|
||||
replies = self.getMessages(1)
|
||||
self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies))
|
||||
|
@ -8,7 +8,7 @@ from irctest import cases
|
||||
class MetadataTestCase(cases.BaseServerTestCase):
|
||||
valid_metadata_keys = {'valid_key1', 'valid_key2'}
|
||||
invalid_metadata_keys = {'invalid_key1', 'invalid_key2'}
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testInIsupport(self):
|
||||
"""“If METADATA is supported, it MUST be specified in RPL_ISUPPORT
|
||||
using the METADATA key.”
|
||||
@ -28,7 +28,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
||||
fail_msg='{item} missing from RPL_ISUPPORT')
|
||||
self.getMessages(1)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testGetOneUnsetValid(self):
|
||||
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
|
||||
"""
|
||||
@ -39,7 +39,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
||||
fail_msg='Did not reply with 766 (ERR_NOMATCHINGKEY) to a '
|
||||
'request to an unset valid METADATA key.')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testGetTwoUnsetValid(self):
|
||||
"""“Multiple keys may be given. The response will be either RPL_KEYVALUE,
|
||||
ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.”
|
||||
@ -62,7 +62,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
||||
fail_msg='Response to “METADATA * GET valid_key1 valid_key2” '
|
||||
'did not respond to valid_key2 as second response: {msg}')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testListNoSet(self):
|
||||
"""“This subcommand MUST list all currently-set metadata keys along
|
||||
with their values. The response will be zero or more RPL_KEYVALUE
|
||||
@ -76,7 +76,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
||||
fail_msg='Response to “METADATA * LIST” was not '
|
||||
'762 (RPL_METADATAEND) but: {msg}')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testListInvalidTarget(self):
|
||||
"""“In case of invalid target RPL_METADATAEND MUST NOT be sent.”
|
||||
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
|
||||
@ -130,14 +130,14 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
||||
self.assertSetValue(target, key, value, displayable_value)
|
||||
self.assertGetValue(target, key, value, displayable_value)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testSetGetValid(self):
|
||||
"""<http://ircv3.net/specs/core/metadata-3.2.html>
|
||||
"""
|
||||
self.connectClient('foo')
|
||||
self.assertSetGetValue('*', 'valid_key1', 'myvalue')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testSetGetZeroCharInValue(self):
|
||||
"""“Values are unrestricted, except that they MUST be UTF-8.”
|
||||
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
|
||||
@ -146,7 +146,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
||||
self.assertSetGetValue('*', 'valid_key1', 'zero->\0<-zero',
|
||||
'zero->\\0<-zero')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testSetGetHeartInValue(self):
|
||||
"""“Values are unrestricted, except that they MUST be UTF-8.”
|
||||
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
|
||||
@ -156,7 +156,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
||||
self.assertSetGetValue('*', 'valid_key1', '->{}<-'.format(heart),
|
||||
'zero->{}<-zero'.format(heart.encode()))
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2-deprecated')
|
||||
def testSetInvalidUtf8(self):
|
||||
"""“Values are unrestricted, except that they MUST be UTF-8.”
|
||||
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
|
||||
|
@ -5,9 +5,9 @@
|
||||
from irctest import cases
|
||||
from irctest.client_mock import NoMessageException
|
||||
from irctest.basecontrollers import NotImplementedByController
|
||||
from irctest.numerics import RPL_MONLIST, RPL_ENDOFMONLIST
|
||||
from irctest.numerics import RPL_MONLIST, RPL_ENDOFMONLIST, RPL_MONONLINE, RPL_MONOFFLINE
|
||||
|
||||
class EchoMessageTestCase(cases.BaseServerTestCase):
|
||||
class MonitorTestCase(cases.BaseServerTestCase):
|
||||
def check_server_support(self):
|
||||
if 'MONITOR' not in self.server_support:
|
||||
raise NotImplementedByController('MONITOR')
|
||||
@ -255,3 +255,38 @@ class EchoMessageTestCase(cases.BaseServerTestCase):
|
||||
self.getMessages(1)
|
||||
self.sendLine(1, 'MONITOR L')
|
||||
checkMonitorSubjects(self.getMessages(1), 'bar', {'bazbat',})
|
||||
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testNickChange(self):
|
||||
# see oragono issue #1076: nickname changes must trigger RPL_MONOFFLINE
|
||||
self.connectClient('bar')
|
||||
self.check_server_support()
|
||||
self.sendLine(1, 'MONITOR + qux')
|
||||
self.getMessages(1)
|
||||
|
||||
self.connectClient('baz')
|
||||
self.getMessages(2)
|
||||
self.assertEqual(self.getMessages(1), [])
|
||||
|
||||
self.sendLine(2, 'NICK qux')
|
||||
self.getMessages(2)
|
||||
mononline = self.getMessages(1)[0]
|
||||
self.assertEqual(mononline.command, RPL_MONONLINE)
|
||||
self.assertEqual(len(mononline.params), 2, mononline.params)
|
||||
self.assertIn(mononline.params[0], ('bar', '*'))
|
||||
self.assertEqual(mononline.params[1].split('!')[0], 'qux')
|
||||
|
||||
# no numerics for a case change
|
||||
self.sendLine(2, 'NICK QUX')
|
||||
self.getMessages(2)
|
||||
self.assertEqual(self.getMessages(1), [])
|
||||
|
||||
self.sendLine(2, 'NICK bazbat')
|
||||
self.getMessages(2)
|
||||
monoffline = self.getMessages(1)[0]
|
||||
# should get RPL_MONOFFLINE with the current unfolded nick
|
||||
self.assertEqual(monoffline.command, RPL_MONOFFLINE)
|
||||
self.assertEqual(len(monoffline.params), 2, monoffline.params)
|
||||
self.assertIn(monoffline.params[0], ('bar', '*'))
|
||||
self.assertEqual(monoffline.params[1].split('!')[0], 'QUX')
|
||||
|
121
irctest/server_tests/test_multiline.py
Normal file
121
irctest/server_tests/test_multiline.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""
|
||||
draft/multiline
|
||||
"""
|
||||
|
||||
from irctest import cases
|
||||
|
||||
CAP_NAME = 'draft/multiline'
|
||||
BATCH_TYPE = 'draft/multiline'
|
||||
CONCAT_TAG = 'draft/multiline-concat'
|
||||
|
||||
base_caps = ['message-tags', 'batch', 'echo-message', 'server-time', 'labeled-response']
|
||||
|
||||
class MultilineTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('multiline')
|
||||
def testBasic(self):
|
||||
self.connectClient(
|
||||
'alice', capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=True
|
||||
)
|
||||
self.joinChannel(1, '#test')
|
||||
self.connectClient('bob', capabilities=(base_caps + [CAP_NAME]))
|
||||
self.joinChannel(2, '#test')
|
||||
self.connectClient('charlie', capabilities=base_caps)
|
||||
self.joinChannel(3, '#test')
|
||||
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
self.getMessages(3)
|
||||
|
||||
self.sendLine(1, '@label=xyz BATCH +123 %s #test' % (BATCH_TYPE,))
|
||||
self.sendLine(1, '@batch=123 PRIVMSG #test hello')
|
||||
self.sendLine(1, '@batch=123 PRIVMSG #test :#how is ')
|
||||
self.sendLine(1, '@batch=123;%s PRIVMSG #test :everyone?' % (CONCAT_TAG,))
|
||||
self.sendLine(1, 'BATCH -123')
|
||||
|
||||
echo = self.getMessages(1)
|
||||
batchStart, batchEnd = echo[0], echo[-1]
|
||||
self.assertEqual(batchStart.command, 'BATCH')
|
||||
self.assertEqual(batchStart.tags.get('label'), 'xyz')
|
||||
self.assertEqual(len(batchStart.params), 3)
|
||||
self.assertEqual(batchStart.params[1], CAP_NAME)
|
||||
self.assertEqual(batchStart.params[2], "#test")
|
||||
self.assertEqual(batchEnd.command, 'BATCH')
|
||||
self.assertEqual(batchStart.params[0][1:], batchEnd.params[0][1:])
|
||||
msgid = batchStart.tags.get('msgid')
|
||||
time = batchStart.tags.get('time')
|
||||
assert msgid
|
||||
assert time
|
||||
privmsgs = echo[1:-1]
|
||||
for msg in privmsgs:
|
||||
self.assertMessageEqual(msg, command='PRIVMSG')
|
||||
self.assertNotIn('msgid', msg.tags)
|
||||
self.assertNotIn('time', msg.tags)
|
||||
self.assertIn(CONCAT_TAG, echo[3].tags)
|
||||
|
||||
relay = self.getMessages(2)
|
||||
batchStart, batchEnd = relay[0], relay[-1]
|
||||
self.assertEqual(batchStart.command, 'BATCH')
|
||||
self.assertEqual(batchEnd.command, 'BATCH')
|
||||
batchTag = batchStart.params[0][1:]
|
||||
self.assertEqual(batchStart.params[0], '+'+batchTag)
|
||||
self.assertEqual(batchEnd.params[0], '-'+batchTag)
|
||||
self.assertEqual(batchStart.tags.get('msgid'), msgid)
|
||||
self.assertEqual(batchStart.tags.get('time'), time)
|
||||
privmsgs = relay[1:-1]
|
||||
for msg in privmsgs:
|
||||
self.assertMessageEqual(msg, command='PRIVMSG')
|
||||
self.assertNotIn('msgid', msg.tags)
|
||||
self.assertNotIn('time', msg.tags)
|
||||
self.assertEqual(msg.tags.get('batch'), batchTag)
|
||||
self.assertIn(CONCAT_TAG, relay[3].tags)
|
||||
|
||||
fallback_relay = self.getMessages(3)
|
||||
relayed_fmsgids = []
|
||||
for msg in fallback_relay:
|
||||
self.assertMessageEqual(msg, command='PRIVMSG')
|
||||
relayed_fmsgids.append(msg.tags.get('msgid'))
|
||||
self.assertEqual(msg.tags.get('time'), time)
|
||||
self.assertNotIn(CONCAT_TAG, msg.tags)
|
||||
self.assertEqual(relayed_fmsgids, [msgid] + [None]*(len(fallback_relay)-1))
|
||||
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('multiline')
|
||||
def testBlankLines(self):
|
||||
self.connectClient(
|
||||
'alice', capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=True
|
||||
)
|
||||
self.joinChannel(1, '#test')
|
||||
self.connectClient('bob', capabilities=(base_caps + [CAP_NAME]))
|
||||
self.joinChannel(2, '#test')
|
||||
self.connectClient('charlie', capabilities=base_caps)
|
||||
self.joinChannel(3, '#test')
|
||||
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
self.getMessages(3)
|
||||
|
||||
self.sendLine(1, '@label=xyz;+client-only-tag BATCH +123 %s #test' % (BATCH_TYPE,))
|
||||
self.sendLine(1, '@batch=123 PRIVMSG #test :')
|
||||
self.sendLine(1, '@batch=123 PRIVMSG #test :#how is ')
|
||||
self.sendLine(1, '@batch=123;%s PRIVMSG #test :everyone?' % (CONCAT_TAG,))
|
||||
self.sendLine(1, 'BATCH -123')
|
||||
self.getMessages(1)
|
||||
|
||||
relay = self.getMessages(2)
|
||||
batch_start = relay[0]
|
||||
privmsgs = relay[1:-1]
|
||||
self.assertEqual(len(privmsgs), 3)
|
||||
self.assertMessageEqual(privmsgs[0], command='PRIVMSG', params=['#test', ''])
|
||||
self.assertMessageEqual(privmsgs[1], command='PRIVMSG', params=['#test', '#how is '])
|
||||
self.assertMessageEqual(privmsgs[2], command='PRIVMSG', params=['#test', 'everyone?'])
|
||||
self.assertIn('+client-only-tag', batch_start.tags)
|
||||
msgid = batch_start.tags['msgid']
|
||||
|
||||
fallback_relay = self.getMessages(3)
|
||||
self.assertEqual(len(fallback_relay), 2)
|
||||
self.assertMessageEqual(fallback_relay[0], command='PRIVMSG', params=['#test', '#how is '])
|
||||
self.assertMessageEqual(fallback_relay[1], command='PRIVMSG', params=['#test', 'everyone?'])
|
||||
self.assertIn('+client-only-tag', fallback_relay[0].tags)
|
||||
self.assertIn('+client-only-tag', fallback_relay[1].tags)
|
||||
self.assertEqual(fallback_relay[0].tags['msgid'], msgid)
|
19
irctest/server_tests/test_readq.py
Normal file
19
irctest/server_tests/test_readq.py
Normal file
@ -0,0 +1,19 @@
|
||||
from irctest import cases
|
||||
|
||||
|
||||
class ReadqTestCase(cases.BaseServerTestCase):
|
||||
"""Test responses to DoS attacks using long lines."""
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testReadqTags(self):
|
||||
self.connectClient('mallory', name='mallory', capabilities=['message-tags'])
|
||||
self.joinChannel('mallory', '#test')
|
||||
self.sendLine('mallory', 'PRIVMSG #test ' + 'a' * 16384)
|
||||
self.assertDisconnected('mallory')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testReadqNoTags(self):
|
||||
self.connectClient('mallory', name='mallory')
|
||||
self.joinChannel('mallory', '#test')
|
||||
self.sendLine('mallory', 'PRIVMSG #test ' + 'a' * 16384)
|
||||
self.assertDisconnected('mallory')
|
108
irctest/server_tests/test_register_verify.py
Normal file
108
irctest/server_tests/test_register_verify.py
Normal file
@ -0,0 +1,108 @@
|
||||
from irctest import cases
|
||||
|
||||
REGISTER_CAP_NAME = 'draft/register'
|
||||
|
||||
class TestRegisterBeforeConnect(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"oragono_config": lambda config: config['accounts']['registration'].update(
|
||||
{'allow-before-connect': True}
|
||||
)
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testBeforeConnect(self):
|
||||
self.addClient('bar')
|
||||
self.sendLine('bar', 'CAP LS 302')
|
||||
caps = self.getCapLs('bar')
|
||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
||||
self.assertIn('before-connect', caps[REGISTER_CAP_NAME])
|
||||
self.sendLine('bar', 'NICK bar')
|
||||
self.sendLine('bar', 'REGISTER * shivarampassphrase')
|
||||
msgs = self.getMessages('bar')
|
||||
register_response = [msg for msg in msgs if msg.command == 'REGISTER'][0]
|
||||
self.assertEqual(register_response.params[0], 'SUCCESS')
|
||||
|
||||
class TestRegisterBeforeConnectDisallowed(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"oragono_config": lambda config: config['accounts']['registration'].update(
|
||||
{'allow-before-connect': False}
|
||||
)
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testBeforeConnect(self):
|
||||
self.addClient('bar')
|
||||
self.sendLine('bar', 'CAP LS 302')
|
||||
caps = self.getCapLs('bar')
|
||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
||||
self.assertEqual(caps[REGISTER_CAP_NAME], None)
|
||||
self.sendLine('bar', 'NICK bar')
|
||||
self.sendLine('bar', 'REGISTER * shivarampassphrase')
|
||||
msgs = self.getMessages('bar')
|
||||
fail_response = [msg for msg in msgs if msg.command == 'FAIL'][0]
|
||||
self.assertEqual(fail_response.params[:2], ['REGISTER', 'DISALLOWED'])
|
||||
|
||||
class TestRegisterEmailVerified(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"oragono_config": lambda config: config['accounts']['registration'].update(
|
||||
{
|
||||
'email-verification': {
|
||||
'enabled': True,
|
||||
'sender': 'test@example.com',
|
||||
'require-tls': True,
|
||||
'helo-domain': 'example.com',
|
||||
},
|
||||
'allow-before-connect': True,
|
||||
}
|
||||
)
|
||||
}
|
||||
return config
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testBeforeConnect(self):
|
||||
self.addClient('bar')
|
||||
self.sendLine('bar', 'CAP LS 302')
|
||||
caps = self.getCapLs('bar')
|
||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
||||
self.assertEqual(set(caps[REGISTER_CAP_NAME].split(',')), {'before-connect', 'email-required'})
|
||||
self.sendLine('bar', 'NICK bar')
|
||||
self.sendLine('bar', 'REGISTER * shivarampassphrase')
|
||||
msgs = self.getMessages('bar')
|
||||
fail_response = [msg for msg in msgs if msg.command == 'FAIL'][0]
|
||||
self.assertEqual(fail_response.params[:2], ['REGISTER', 'INVALID_EMAIL'])
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testAfterConnect(self):
|
||||
self.connectClient('bar', name='bar')
|
||||
self.sendLine('bar', 'REGISTER * shivarampassphrase')
|
||||
msgs = self.getMessages('bar')
|
||||
fail_response = [msg for msg in msgs if msg.command == 'FAIL'][0]
|
||||
self.assertEqual(fail_response.params[:2], ['REGISTER', 'INVALID_EMAIL'])
|
||||
|
||||
class TestRegisterNoLandGrabs(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"oragono_config": lambda config: config['accounts']['registration'].update(
|
||||
{'allow-before-connect': True}
|
||||
)
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testBeforeConnect(self):
|
||||
# have an anonymous client take the 'root' username:
|
||||
self.connectClient('root', name='root')
|
||||
|
||||
# cannot register it out from under the anonymous nick holder:
|
||||
self.addClient('bar')
|
||||
self.sendLine('bar', 'NICK root')
|
||||
self.sendLine('bar', 'REGISTER * shivarampassphrase')
|
||||
msgs = self.getMessages('bar')
|
||||
fail_response = [msg for msg in msgs if msg.command == 'FAIL'][0]
|
||||
self.assertEqual(fail_response.params[:2], ['REGISTER', 'USERNAME_EXISTS'])
|
@ -4,6 +4,8 @@ Regression tests for bugs in oragono.
|
||||
|
||||
from irctest import cases
|
||||
|
||||
from irctest.numerics import ERR_ERRONEUSNICKNAME, ERR_NICKNAMEINUSE, RPL_WELCOME
|
||||
|
||||
class RegressionsTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459')
|
||||
@ -16,7 +18,7 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
||||
self.sendLine(2, 'NICK alice')
|
||||
ms = self.getMessages(2)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageEqual(ms[0], command='433') # ERR_NICKNAMEINUSE
|
||||
self.assertMessageEqual(ms[0], command=ERR_NICKNAMEINUSE)
|
||||
|
||||
# bob MUST still own the bob nick, and be able to receive PRIVMSG as bob
|
||||
self.sendLine(1, 'PRIVMSG bob hi')
|
||||
@ -44,7 +46,124 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageEqual(ms[0], command='NICK', params=['Alice'])
|
||||
|
||||
# bob should not get notified on no-op nick change
|
||||
# no responses, either to the user or to friends, from a no-op nick change
|
||||
self.sendLine(1, 'NICK Alice')
|
||||
ms = self.getMessages(1)
|
||||
self.assertEqual(ms, [])
|
||||
ms = self.getMessages(2)
|
||||
self.assertEqual(ms, [])
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('IRCv3.2')
|
||||
def testTagCap(self):
|
||||
# regression test for oragono #754
|
||||
self.connectClient(
|
||||
'alice',
|
||||
capabilities=['message-tags', 'batch', 'echo-message', 'server-time'],
|
||||
skip_if_cap_nak=True
|
||||
)
|
||||
self.connectClient('bob')
|
||||
self.getMessages(1)
|
||||
self.getMessages(2)
|
||||
|
||||
self.sendLine(1, '@+draft/reply=ct95w3xemz8qj9du2h74wp8pee PRIVMSG bob :hey yourself')
|
||||
ms = self.getMessages(1)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageEqual(ms[0], command='PRIVMSG', params=['bob', 'hey yourself'])
|
||||
self.assertEqual(ms[0].tags.get('+draft/reply'), 'ct95w3xemz8qj9du2h74wp8pee')
|
||||
|
||||
ms = self.getMessages(2)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageEqual(ms[0], command='PRIVMSG', params=['bob', 'hey yourself'])
|
||||
self.assertEqual(ms[0].tags, {})
|
||||
|
||||
self.sendLine(2, 'CAP REQ :message-tags server-time')
|
||||
self.getMessages(2)
|
||||
self.sendLine(1, '@+draft/reply=tbxqauh9nykrtpa3n6icd9whan PRIVMSG bob :hey again')
|
||||
self.getMessages(1)
|
||||
ms = self.getMessages(2)
|
||||
# now bob has the tags cap, so he should receive the tags
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertMessageEqual(ms[0], command='PRIVMSG', params=['bob', 'hey again'])
|
||||
self.assertEqual(ms[0].tags.get('+draft/reply'), 'tbxqauh9nykrtpa3n6icd9whan')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459')
|
||||
def testStarNick(self):
|
||||
self.addClient(1)
|
||||
self.sendLine(1, 'NICK *')
|
||||
self.sendLine(1, 'USER u s e r')
|
||||
replies = {'NOTICE'}
|
||||
while replies == {'NOTICE'}:
|
||||
replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
|
||||
self.assertIn(ERR_ERRONEUSNICKNAME, replies)
|
||||
self.assertNotIn(RPL_WELCOME, replies)
|
||||
|
||||
self.sendLine(1, 'NICK valid')
|
||||
replies = {'NOTICE'}
|
||||
while replies <= {'NOTICE'}:
|
||||
replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
|
||||
self.assertNotIn(ERR_ERRONEUSNICKNAME, replies)
|
||||
self.assertIn(RPL_WELCOME, replies)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459')
|
||||
def testEmptyNick(self):
|
||||
self.addClient(1)
|
||||
self.sendLine(1, 'NICK :')
|
||||
self.sendLine(1, 'USER u s e r')
|
||||
replies = {'NOTICE'}
|
||||
while replies == {'NOTICE'}:
|
||||
replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
|
||||
self.assertNotIn(RPL_WELCOME, replies)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459')
|
||||
def testNickRelease(self):
|
||||
# regression test for oragono #1252
|
||||
self.connectClient('alice')
|
||||
self.getMessages(1)
|
||||
self.sendLine(1, 'NICK malice')
|
||||
nick_msgs = [msg for msg in self.getMessages(1) if msg.command == 'NICK']
|
||||
self.assertEqual(len(nick_msgs), 1)
|
||||
self.assertMessageEqual(nick_msgs[0], command='NICK', params=['malice'])
|
||||
|
||||
self.addClient(2)
|
||||
self.sendLine(2, 'NICK alice')
|
||||
self.sendLine(2, 'USER u s e r')
|
||||
replies = set(msg.command for msg in self.getMessages(2))
|
||||
self.assertNotIn(ERR_NICKNAMEINUSE, replies)
|
||||
self.assertIn(RPL_WELCOME, replies)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459')
|
||||
def testNickReleaseQuit(self):
|
||||
self.connectClient('alice')
|
||||
self.getMessages(1)
|
||||
self.sendLine(1, 'QUIT')
|
||||
self.assertDisconnected(1)
|
||||
|
||||
self.addClient(2)
|
||||
self.sendLine(2, 'NICK alice')
|
||||
self.sendLine(2, 'USER u s e r')
|
||||
replies = set(msg.command for msg in self.getMessages(2))
|
||||
self.assertNotIn(ERR_NICKNAMEINUSE, replies)
|
||||
self.assertIn(RPL_WELCOME, replies)
|
||||
self.sendLine(2, 'QUIT')
|
||||
self.assertDisconnected(2)
|
||||
|
||||
self.addClient(3)
|
||||
self.sendLine(3, 'NICK ALICE')
|
||||
self.sendLine(3, 'USER u s e r')
|
||||
replies = set(msg.command for msg in self.getMessages(3))
|
||||
self.assertNotIn(ERR_NICKNAMEINUSE, replies)
|
||||
self.assertIn(RPL_WELCOME, replies)
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC1459')
|
||||
def testNickReleaseUnregistered(self):
|
||||
self.addClient(1)
|
||||
self.sendLine(1, 'NICK alice')
|
||||
self.sendLine(1, 'QUIT')
|
||||
self.assertDisconnected(1)
|
||||
|
||||
self.addClient(2)
|
||||
self.sendLine(2, 'NICK alice')
|
||||
self.sendLine(2, 'USER u s e r')
|
||||
replies = set(msg.command for msg in self.getMessages(2))
|
||||
self.assertNotIn(ERR_NICKNAMEINUSE, replies)
|
||||
self.assertIn(RPL_WELCOME, replies)
|
||||
|
72
irctest/server_tests/test_relaymsg.py
Normal file
72
irctest/server_tests/test_relaymsg.py
Normal file
@ -0,0 +1,72 @@
|
||||
from irctest import cases
|
||||
from irctest.irc_utils.junkdrawer import random_name
|
||||
from irctest.server_tests.test_chathistory import CHATHISTORY_CAP, EVENT_PLAYBACK_CAP
|
||||
|
||||
|
||||
RELAYMSG_CAP = 'draft/relaymsg'
|
||||
RELAYMSG_TAG_NAME = 'draft/relaymsg'
|
||||
|
||||
class RelaymsgTestCase(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"chathistory": True,
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testRelaymsg(self):
|
||||
self.connectClient('baz', name='baz', capabilities=['server-time', 'message-tags', 'batch', 'labeled-response', 'echo-message', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP])
|
||||
self.connectClient('qux', name='qux', capabilities=['server-time', 'message-tags', 'batch', 'labeled-response', 'echo-message', CHATHISTORY_CAP, EVENT_PLAYBACK_CAP])
|
||||
chname = random_name('#relaymsg')
|
||||
self.joinChannel('baz', chname)
|
||||
self.joinChannel('qux', chname)
|
||||
self.getMessages('baz')
|
||||
self.getMessages('qux')
|
||||
|
||||
self.sendLine('baz', 'RELAYMSG %s invalid!nick/discord hi' % (chname,))
|
||||
response = self.getMessages('baz')[0]
|
||||
self.assertEqual(response.command, 'FAIL')
|
||||
self.assertEqual(response.params[:2], ['RELAYMSG', 'INVALID_NICK'])
|
||||
|
||||
self.sendLine('baz', 'RELAYMSG %s regular_nick hi' % (chname,))
|
||||
response = self.getMessages('baz')[0]
|
||||
self.assertEqual(response.command, 'FAIL')
|
||||
self.assertEqual(response.params[:2], ['RELAYMSG', 'INVALID_NICK'])
|
||||
|
||||
self.sendLine('baz', 'RELAYMSG %s smt/discord hi' % (chname,))
|
||||
response = self.getMessages('baz')[0]
|
||||
self.assertMessageEqual(response, nick='smt/discord', command='PRIVMSG', params=[chname, 'hi'])
|
||||
relayed_msg = self.getMessages('qux')[0]
|
||||
self.assertMessageEqual(relayed_msg, nick='smt/discord', command='PRIVMSG', params=[chname, 'hi'])
|
||||
|
||||
# labeled-response
|
||||
self.sendLine('baz', '@label=x RELAYMSG %s smt/discord :hi again' % (chname,))
|
||||
response = self.getMessages('baz')[0]
|
||||
self.assertMessageEqual(response, nick='smt/discord', command='PRIVMSG', params=[chname, 'hi again'])
|
||||
self.assertEqual(response.tags.get('label'), 'x')
|
||||
relayed_msg = self.getMessages('qux')[0]
|
||||
self.assertMessageEqual(relayed_msg, nick='smt/discord', command='PRIVMSG', params=[chname, 'hi again'])
|
||||
|
||||
self.sendLine('qux', 'RELAYMSG %s smt/discord :hi a third time' % (chname,))
|
||||
response = self.getMessages('qux')[0]
|
||||
self.assertEqual(response.command, 'FAIL')
|
||||
self.assertEqual(response.params[:2], ['RELAYMSG', 'PRIVS_NEEDED'])
|
||||
|
||||
# grant qux chanop, allowing relaymsg
|
||||
self.sendLine('baz', 'MODE %s +o qux' % (chname,))
|
||||
self.getMessages('baz')
|
||||
self.getMessages('qux')
|
||||
# give baz the relaymsg cap
|
||||
self.sendLine('baz', 'CAP REQ %s' % (RELAYMSG_CAP))
|
||||
self.assertMessageEqual(self.getMessages('baz')[0], command='CAP', params=['baz', 'ACK', RELAYMSG_CAP])
|
||||
|
||||
self.sendLine('qux', 'RELAYMSG %s smt/discord :hi a third time' % (chname,))
|
||||
response = self.getMessages('qux')[0]
|
||||
self.assertMessageEqual(response, nick='smt/discord', command='PRIVMSG', params=[chname, 'hi a third time'])
|
||||
relayed_msg = self.getMessages('baz')[0]
|
||||
self.assertMessageEqual(relayed_msg, nick='smt/discord', command='PRIVMSG', params=[chname, 'hi a third time'])
|
||||
self.assertEqual(relayed_msg.tags.get(RELAYMSG_TAG_NAME), 'qux')
|
||||
|
||||
self.sendLine('baz', 'CHATHISTORY LATEST %s * 10' % (chname,))
|
||||
messages = self.getMessages('baz')
|
||||
self.assertEqual([msg.params[-1] for msg in messages if msg.command == 'PRIVMSG'], ['hi', 'hi again', 'hi a third time'])
|
176
irctest/server_tests/test_resume.py
Normal file
176
irctest/server_tests/test_resume.py
Normal file
@ -0,0 +1,176 @@
|
||||
"""
|
||||
<https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md>
|
||||
"""
|
||||
|
||||
import secrets
|
||||
|
||||
from irctest import cases
|
||||
|
||||
from irctest.numerics import RPL_AWAY
|
||||
|
||||
ANCIENT_TIMESTAMP = '2006-01-02T15:04:05.999Z'
|
||||
|
||||
class ResumeTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testNoResumeByDefault(self):
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response'])
|
||||
ms = self.getMessages(1)
|
||||
resume_messages = [m for m in ms if m.command == 'RESUME']
|
||||
self.assertEqual(resume_messages, [], 'should not see RESUME messages unless explicitly negotiated')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testResume(self):
|
||||
chname = '#' + secrets.token_hex(12)
|
||||
self.connectClient('bar', capabilities=['batch', 'labeled-response', 'server-time'])
|
||||
ms = self.getMessages(1)
|
||||
|
||||
welcome = self.connectClient('baz', capabilities=['batch', 'labeled-response', 'server-time', 'draft/resume-0.5'])
|
||||
resume_messages = [m for m in welcome if m.command == 'RESUME']
|
||||
self.assertEqual(len(resume_messages), 1)
|
||||
self.assertEqual(resume_messages[0].params[0], 'TOKEN')
|
||||
token = resume_messages[0].params[1]
|
||||
|
||||
self.joinChannel(1, chname)
|
||||
self.joinChannel(2, chname)
|
||||
self.sendLine(1, 'PRIVMSG %s :hello friends' % (chname,))
|
||||
self.sendLine(1, 'PRIVMSG baz :hello friend singular')
|
||||
self.getMessages(1)
|
||||
# should receive these messages
|
||||
privmsgs = [m for m in self.getMessages(2) if m.command == 'PRIVMSG']
|
||||
self.assertEqual(len(privmsgs), 2)
|
||||
privmsgs.sort(key=lambda m: m.params[0])
|
||||
self.assertMessageEqual(privmsgs[0], command='PRIVMSG', params=[chname, 'hello friends'])
|
||||
self.assertMessageEqual(privmsgs[1], command='PRIVMSG', params=['baz', 'hello friend singular'])
|
||||
channelMsgTime = privmsgs[0].tags.get('time')
|
||||
|
||||
# tokens MUST be cryptographically secure; therefore, this token should be invalid
|
||||
# with probability at least 1 - 1/(2**128)
|
||||
bad_token = 'a' * len(token)
|
||||
self.addClient()
|
||||
self.sendLine(3, 'CAP LS')
|
||||
self.sendLine(3, 'CAP REQ :batch labeled-response server-time draft/resume-0.5')
|
||||
self.sendLine(3, 'NICK tempnick')
|
||||
self.sendLine(3, 'USER tempuser 0 * tempuser')
|
||||
self.sendLine(3, ' '.join(('RESUME', bad_token, ANCIENT_TIMESTAMP)))
|
||||
|
||||
# resume with a bad token MUST fail
|
||||
ms = self.getMessages(3)
|
||||
resume_err_messages = [m for m in ms if m.command == 'FAIL' and m.params[:2] == ['RESUME', 'INVALID_TOKEN']]
|
||||
self.assertEqual(len(resume_err_messages), 1)
|
||||
# however, registration should proceed with the alternative nick
|
||||
self.sendLine(3, 'CAP END')
|
||||
welcome_msgs = [m for m in self.getMessages(3) if m.command == '001'] # RPL_WELCOME
|
||||
self.assertEqual(welcome_msgs[0].params[0], 'tempnick')
|
||||
|
||||
self.addClient()
|
||||
self.sendLine(4, 'CAP LS')
|
||||
self.sendLine(4, 'CAP REQ :batch labeled-response server-time draft/resume-0.5')
|
||||
self.sendLine(4, 'NICK tempnick_')
|
||||
self.sendLine(4, 'USER tempuser 0 * tempuser')
|
||||
# resume with a timestamp in the distant past
|
||||
self.sendLine(4, ' '.join(('RESUME', token, ANCIENT_TIMESTAMP)))
|
||||
# successful resume does not require CAP END:
|
||||
# https://github.com/ircv3/ircv3-specifications/pull/306/files#r255318883
|
||||
ms = self.getMessages(4)
|
||||
|
||||
# now, do a valid resume with the correct token
|
||||
resume_messages = [m for m in ms if m.command == 'RESUME']
|
||||
self.assertEqual(len(resume_messages), 2)
|
||||
self.assertEqual(resume_messages[0].params[0], 'TOKEN')
|
||||
new_token = resume_messages[0].params[1]
|
||||
self.assertNotEqual(token, new_token, 'should receive a new, strong resume token; instead got ' + new_token)
|
||||
# success message
|
||||
self.assertMessageEqual(resume_messages[1], command='RESUME', params=['SUCCESS', 'baz'])
|
||||
|
||||
# test replay of messages
|
||||
privmsgs = [m for m in ms if m.command == 'PRIVMSG' and m.prefix.startswith('bar')]
|
||||
self.assertEqual(len(privmsgs), 2)
|
||||
privmsgs.sort(key=lambda m: m.params[0])
|
||||
self.assertMessageEqual(privmsgs[0], command='PRIVMSG', params=[chname, 'hello friends'])
|
||||
self.assertMessageEqual(privmsgs[1], command='PRIVMSG', params=['baz', 'hello friend singular'])
|
||||
# should replay with the original server-time
|
||||
# TODO this probably isn't testing anything because the timestamp only has second resolution,
|
||||
# hence will typically match by accident
|
||||
self.assertEqual(privmsgs[0].tags.get('time'), channelMsgTime)
|
||||
|
||||
# legacy client should receive a QUIT and a JOIN
|
||||
quit, join = [m for m in self.getMessages(1) if m.command in ('QUIT', 'JOIN')]
|
||||
self.assertEqual(quit.command, 'QUIT')
|
||||
self.assertTrue(quit.prefix.startswith('baz'))
|
||||
self.assertMessageEqual(join, command='JOIN', params=[chname])
|
||||
self.assertTrue(join.prefix.startswith('baz'))
|
||||
|
||||
# original client should have been disconnected
|
||||
self.assertDisconnected(2)
|
||||
# new client should be receiving PRIVMSG sent to baz
|
||||
self.sendLine(1, 'PRIVMSG baz :hello again')
|
||||
self.getMessages(1)
|
||||
self.assertMessageEqual(self.getMessage(4), command='PRIVMSG', params=['baz', 'hello again'])
|
||||
|
||||
# test chain-resuming (resuming the resumed connection, using the new token)
|
||||
self.addClient()
|
||||
self.sendLine(5, 'CAP LS')
|
||||
self.sendLine(5, 'CAP REQ :batch labeled-response server-time draft/resume-0.5')
|
||||
self.sendLine(5, 'NICK tempnick_')
|
||||
self.sendLine(5, 'USER tempuser 0 * tempuser')
|
||||
self.sendLine(5, 'RESUME ' + new_token)
|
||||
ms = self.getMessages(5)
|
||||
|
||||
resume_messages = [m for m in ms if m.command == 'RESUME']
|
||||
self.assertEqual(len(resume_messages), 2)
|
||||
self.assertEqual(resume_messages[0].params[0], 'TOKEN')
|
||||
new_new_token = resume_messages[0].params[1]
|
||||
self.assertNotEqual(token, new_new_token, 'should receive a new, strong resume token; instead got ' + new_new_token)
|
||||
self.assertNotEqual(new_token, new_new_token, 'should receive a new, strong resume token; instead got ' + new_new_token)
|
||||
# success message
|
||||
self.assertMessageEqual(resume_messages[1], command='RESUME', params=['SUCCESS', 'baz'])
|
||||
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testBRB(self):
|
||||
chname = '#' + secrets.token_hex(12)
|
||||
self.connectClient('bar', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', 'draft/resume-0.5'])
|
||||
ms = self.getMessages(1)
|
||||
self.joinChannel(1, chname)
|
||||
|
||||
welcome = self.connectClient('baz', capabilities=['batch', 'labeled-response', 'server-time', 'draft/resume-0.5'])
|
||||
resume_messages = [m for m in welcome if m.command == 'RESUME']
|
||||
self.assertEqual(len(resume_messages), 1)
|
||||
self.assertEqual(resume_messages[0].params[0], 'TOKEN')
|
||||
token = resume_messages[0].params[1]
|
||||
self.joinChannel(2, chname)
|
||||
|
||||
self.getMessages(1)
|
||||
self.sendLine(2, 'BRB :software upgrade')
|
||||
# should receive, e.g., `BRB 210` (number of seconds)
|
||||
ms = [m for m in self.getMessages(2) if m.command == 'BRB']
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertGreater(int(ms[0].params[0]), 1)
|
||||
# BRB disconnects you
|
||||
self.assertDisconnected(2)
|
||||
# without sending a QUIT line to friends
|
||||
self.assertEqual(self.getMessages(1), [])
|
||||
|
||||
self.sendLine(1, 'PRIVMSG baz :hey there')
|
||||
# BRB message should be sent as an away message
|
||||
self.assertMessageEqual(self.getMessage(1), command=RPL_AWAY, params=['bar', 'baz', 'software upgrade'])
|
||||
|
||||
self.addClient(3)
|
||||
self.sendLine(3, 'CAP REQ :batch account-tag message-tags draft/resume-0.5')
|
||||
self.sendLine(3, ' '.join(('RESUME', token, ANCIENT_TIMESTAMP)))
|
||||
ms = self.getMessages(3)
|
||||
|
||||
resume_messages = [m for m in ms if m.command == 'RESUME']
|
||||
self.assertEqual(len(resume_messages), 2)
|
||||
self.assertEqual(resume_messages[0].params[0], 'TOKEN')
|
||||
self.assertMessageEqual(resume_messages[1], command='RESUME', params=['SUCCESS', 'baz'])
|
||||
|
||||
privmsgs = [m for m in ms if m.command == 'PRIVMSG' and m.prefix.startswith('bar')]
|
||||
self.assertEqual(len(privmsgs), 1)
|
||||
self.assertMessageEqual(privmsgs[0], params=['baz', 'hey there'])
|
||||
|
||||
# friend with the resume cap should receive a RESUMED message
|
||||
resumed_messages = [m for m in self.getMessages(1) if m.command == 'RESUMED']
|
||||
self.assertEqual(len(resumed_messages), 1)
|
||||
self.assertTrue(resumed_messages[0].prefix.startswith('baz'))
|
66
irctest/server_tests/test_roleplay.py
Normal file
66
irctest/server_tests/test_roleplay.py
Normal file
@ -0,0 +1,66 @@
|
||||
from irctest import cases
|
||||
from irctest.numerics import ERR_CANNOTSENDRP
|
||||
from irctest.irc_utils.junkdrawer import random_name
|
||||
|
||||
class RoleplayTestCase(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"oragono_roleplay": True,
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testRoleplay(self):
|
||||
bar = random_name('bar')
|
||||
qux = random_name('qux')
|
||||
chan = random_name('#chan')
|
||||
self.connectClient(bar, name=bar, capabilities=['batch', 'labeled-response', 'message-tags', 'server-time'])
|
||||
self.connectClient(qux, name=qux, capabilities=['batch', 'labeled-response', 'message-tags', 'server-time'])
|
||||
self.joinChannel(bar, chan)
|
||||
self.joinChannel(qux, chan)
|
||||
self.getMessages(bar)
|
||||
|
||||
# roleplay should be forbidden because we aren't +E yet
|
||||
self.sendLine(bar, 'NPC %s bilbo too much bread' % (chan,))
|
||||
reply = self.getMessages(bar)[0]
|
||||
self.assertEqual(reply.command, ERR_CANNOTSENDRP)
|
||||
|
||||
self.sendLine(bar, 'MODE %s +E' % (chan,))
|
||||
reply = self.getMessages(bar)[0]
|
||||
self.assertEqual(reply.command, 'MODE')
|
||||
self.assertMessageEqual(reply, command='MODE', params=[chan, '+E'])
|
||||
self.getMessages(qux)
|
||||
|
||||
self.sendLine(bar, 'NPC %s bilbo too much bread' % (chan,))
|
||||
reply = self.getMessages(bar)[0]
|
||||
self.assertEqual(reply.command, 'PRIVMSG')
|
||||
self.assertEqual(reply.params[0], chan)
|
||||
self.assertTrue(reply.prefix.startswith('*bilbo*!'))
|
||||
self.assertIn('too much bread', reply.params[1])
|
||||
|
||||
reply = self.getMessages(qux)[0]
|
||||
self.assertEqual(reply.command, 'PRIVMSG')
|
||||
self.assertEqual(reply.params[0], chan)
|
||||
self.assertTrue(reply.prefix.startswith('*bilbo*!'))
|
||||
self.assertIn('too much bread', reply.params[1])
|
||||
|
||||
self.sendLine(bar, 'SCENE %s dark and stormy night' % (chan,))
|
||||
reply = self.getMessages(bar)[0]
|
||||
self.assertEqual(reply.command, 'PRIVMSG')
|
||||
self.assertEqual(reply.params[0], chan)
|
||||
self.assertTrue(reply.prefix.startswith('=Scene=!'))
|
||||
self.assertIn('dark and stormy night', reply.params[1])
|
||||
|
||||
reply = self.getMessages(qux)[0]
|
||||
self.assertEqual(reply.command, 'PRIVMSG')
|
||||
self.assertEqual(reply.params[0], chan)
|
||||
self.assertTrue(reply.prefix.startswith('=Scene=!'))
|
||||
self.assertIn('dark and stormy night', reply.params[1])
|
||||
|
||||
# test history storage
|
||||
self.sendLine(qux, 'CHATHISTORY LATEST %s * 10' % (chan,))
|
||||
reply = [msg for msg in self.getMessages(qux) if msg.command == 'PRIVMSG' and 'bilbo' in msg.prefix][0]
|
||||
self.assertEqual(reply.command, 'PRIVMSG')
|
||||
self.assertEqual(reply.params[0], chan)
|
||||
self.assertTrue(reply.prefix.startswith('*bilbo*!'))
|
||||
self.assertIn('too much bread', reply.params[1])
|
41
irctest/server_tests/test_statusmsg.py
Normal file
41
irctest/server_tests/test_statusmsg.py
Normal file
@ -0,0 +1,41 @@
|
||||
from irctest import cases
|
||||
from irctest.numerics import RPL_NAMREPLY
|
||||
|
||||
class StatusmsgTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testInIsupport(self):
|
||||
"""Check that the expected STATUSMSG parameter appears in our isupport list."""
|
||||
isupport = self.getISupport()
|
||||
self.assertEqual(isupport['STATUSMSG'], '~&@%+')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testStatusmsg(self):
|
||||
"""Test that STATUSMSG are sent to the intended recipients, with the intended prefixes."""
|
||||
self.connectClient('chanop')
|
||||
self.joinChannel(1, '#chan')
|
||||
self.getMessages(1)
|
||||
self.connectClient('joe')
|
||||
self.joinChannel(2, '#chan')
|
||||
self.getMessages(2)
|
||||
|
||||
self.connectClient('schmoe')
|
||||
self.sendLine(3, 'join #chan')
|
||||
messages = self.getMessages(3)
|
||||
names = set()
|
||||
for message in messages:
|
||||
if message.command == RPL_NAMREPLY:
|
||||
names.update(set(message.params[-1].split()))
|
||||
# chanop should be opped
|
||||
self.assertEqual(names, {'@chanop', 'joe', 'schmoe'}, f'unexpected names: {names}')
|
||||
|
||||
self.sendLine(3, 'privmsg @#chan :this message is for operators')
|
||||
self.getMessages(3)
|
||||
|
||||
# check the operator's messages
|
||||
statusMsg = self.getMessage(1, filter_pred=lambda m:m.command == 'PRIVMSG')
|
||||
self.assertMessageEqual(statusMsg, params=['@#chan', 'this message is for operators'])
|
||||
|
||||
# check the non-operator's messages
|
||||
unprivilegedMessages = [msg for msg in self.getMessages(2) if msg.command == 'PRIVMSG']
|
||||
self.assertEqual(len(unprivilegedMessages), 0)
|
@ -12,7 +12,7 @@ class WhoisTestCase(cases.BaseServerTestCase):
|
||||
def testWhoisUser(self):
|
||||
"""Test basic WHOIS behavior"""
|
||||
nick = 'myCoolNickname'
|
||||
username = 'myCoolUsername'
|
||||
username = 'myUsernam' # may be truncated if longer than this
|
||||
realname = 'My Real Name'
|
||||
self.addClient()
|
||||
self.sendLine(1, f'NICK {nick}')
|
||||
@ -27,11 +27,88 @@ class WhoisTestCase(cases.BaseServerTestCase):
|
||||
self.assertEqual(whois_user.command, RPL_WHOISUSER)
|
||||
# "<client> <nick> <username> <host> * :<realname>"
|
||||
self.assertEqual(whois_user.params[1], nick)
|
||||
self.assertIn(whois_user.params[2], ('~' + username, '~' + username[0:9]))
|
||||
self.assertIn(whois_user.params[2], ('~' + username, username))
|
||||
# dumb regression test for oragono/oragono#355:
|
||||
self.assertNotIn(whois_user.params[3], [nick, username, '~' + username, realname])
|
||||
self.assertEqual(whois_user.params[5], realname)
|
||||
|
||||
|
||||
class InvisibleTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testInvisibleWhois(self):
|
||||
"""Test interaction between MODE +i and RPL_WHOISCHANNELS."""
|
||||
self.connectClient('userOne')
|
||||
self.joinChannel(1, '#xyz')
|
||||
|
||||
self.connectClient('userTwo')
|
||||
self.getMessages(2)
|
||||
self.sendLine(2, 'WHOIS userOne')
|
||||
commands = {m.command for m in self.getMessages(2)}
|
||||
self.assertIn(RPL_WHOISCHANNELS, commands,
|
||||
'RPL_WHOISCHANNELS should be sent for a non-invisible nick')
|
||||
|
||||
self.getMessages(1)
|
||||
self.sendLine(1, 'MODE userOne +i')
|
||||
message = self.getMessage(1)
|
||||
self.assertEqual(message.command, 'MODE',
|
||||
'Expected MODE reply, but received {}'.format(message.command))
|
||||
self.assertEqual(message.params, ['userOne', '+i'],
|
||||
'Expected user set +i, but received {}'.format(message.params))
|
||||
|
||||
self.getMessages(2)
|
||||
self.sendLine(2, 'WHOIS userOne')
|
||||
commands = {m.command for m in self.getMessages(2)}
|
||||
self.assertNotIn(RPL_WHOISCHANNELS, commands,
|
||||
'RPL_WHOISCHANNELS should not be sent for an invisible nick'
|
||||
'unless the user is also a member of the channel')
|
||||
|
||||
self.sendLine(2, 'JOIN #xyz')
|
||||
self.sendLine(2, 'WHOIS userOne')
|
||||
commands = {m.command for m in self.getMessages(2)}
|
||||
self.assertIn(RPL_WHOISCHANNELS, commands,
|
||||
'RPL_WHOISCHANNELS should be sent for an invisible nick'
|
||||
'if the user is also a member of the channel')
|
||||
|
||||
self.sendLine(2, 'PART #xyz')
|
||||
self.getMessages(2)
|
||||
self.getMessages(1)
|
||||
self.sendLine(1, 'MODE userOne -i')
|
||||
message = self.getMessage(1)
|
||||
self.assertEqual(message.command, 'MODE',
|
||||
'Expected MODE reply, but received {}'.format(message.command))
|
||||
self.assertEqual(message.params, ['userOne', '-i'],
|
||||
'Expected user set -i, but received {}'.format(message.params))
|
||||
|
||||
self.sendLine(2, 'WHOIS userOne')
|
||||
commands = {m.command for m in self.getMessages(2)}
|
||||
self.assertIn(RPL_WHOISCHANNELS, commands,
|
||||
'RPL_WHOISCHANNELS should be sent for a non-invisible nick')
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testWhoisAccount(self):
|
||||
"""Test numeric 330, RPL_WHOISACCOUNT."""
|
||||
self.controller.registerUser(self, 'shivaram', 'sesame')
|
||||
self.connectClient('netcat')
|
||||
self.sendLine(1, 'NS IDENTIFY shivaram sesame')
|
||||
self.getMessages(1)
|
||||
|
||||
self.connectClient('curious')
|
||||
self.sendLine(2, 'WHOIS netcat')
|
||||
messages = self.getMessages(2)
|
||||
# 330 RPL_WHOISACCOUNT
|
||||
whoisaccount = [message for message in messages if message.command == '330']
|
||||
self.assertEqual(len(whoisaccount), 1)
|
||||
params = whoisaccount[0].params
|
||||
# <client> <nick> <authname> :<info>
|
||||
self.assertEqual(len(params), 4)
|
||||
self.assertEqual(params[:3], ['curious', 'netcat', 'shivaram'])
|
||||
|
||||
self.sendLine(1, 'WHOIS curious')
|
||||
messages = self.getMessages(2)
|
||||
whoisaccount = [message for message in messages if message.command == '330']
|
||||
self.assertEqual(len(whoisaccount), 0)
|
||||
|
||||
class AwayTestCase(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('RFC2812')
|
||||
@ -55,3 +132,32 @@ class AwayTestCase(cases.BaseServerTestCase):
|
||||
self.sendLine(2, "PRIVMSG bar :what's up")
|
||||
replies = self.getMessages(2)
|
||||
self.assertEqual(len(replies), 0)
|
||||
|
||||
class TestNoCTCPMode(cases.BaseServerTestCase):
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testNoCTCPMode(self):
|
||||
self.connectClient('bar', 'bar')
|
||||
self.connectClient('qux', 'qux')
|
||||
# CTCP is not blocked by default:
|
||||
self.sendLine('qux', 'PRIVMSG bar :\x01VERSION\x01')
|
||||
self.getMessages('qux')
|
||||
relay = [msg for msg in self.getMessages('bar') if msg.command == 'PRIVMSG'][0]
|
||||
self.assertEqual(relay.params[-1], '\x01VERSION\x01')
|
||||
|
||||
# set the no-CTCP user mode on bar:
|
||||
self.sendLine('bar', 'MODE bar +T')
|
||||
replies = self.getMessages('bar')
|
||||
umode_line = [msg for msg in replies if msg.command == 'MODE'][0]
|
||||
self.assertMessageEqual(umode_line, command='MODE', params=['bar', '+T'])
|
||||
|
||||
# CTCP is now blocked:
|
||||
self.sendLine('qux', 'PRIVMSG bar :\x01VERSION\x01')
|
||||
self.getMessages('qux')
|
||||
self.assertEqual(self.getMessages('bar'), [])
|
||||
|
||||
# normal PRIVMSG go through:
|
||||
self.sendLine('qux', 'PRIVMSG bar :please just tell me your client version')
|
||||
self.getMessages('qux')
|
||||
relay = self.getMessages('bar')[0]
|
||||
self.assertMessageEqual(relay, command='PRIVMSG', nick='qux', params=['bar', 'please just tell me your client version'])
|
||||
|
23
irctest/server_tests/test_utf8.py
Normal file
23
irctest/server_tests/test_utf8.py
Normal file
@ -0,0 +1,23 @@
|
||||
from irctest import cases
|
||||
|
||||
class Utf8TestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testUtf8Validation(self):
|
||||
self.connectClient('bar', capabilities=['batch', 'echo-message', 'labeled-response', 'message-tags'])
|
||||
self.joinChannel(1, '#qux')
|
||||
self.sendLine(1, 'PRIVMSG #qux hi')
|
||||
ms = self.getMessages(1)
|
||||
self.assertMessageEqual([m for m in ms if m.command == 'PRIVMSG'][0], params=['#qux', 'hi'])
|
||||
|
||||
self.sendLine(1, b'PRIVMSG #qux hi\xaa')
|
||||
ms = self.getMessages(1)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertEqual(ms[0].command, 'FAIL')
|
||||
self.assertEqual(ms[0].params[:2], ['PRIVMSG', 'INVALID_UTF8'])
|
||||
|
||||
self.sendLine(1, b'@label=xyz PRIVMSG #qux hi\xaa')
|
||||
ms = self.getMessages(1)
|
||||
self.assertEqual(len(ms), 1)
|
||||
self.assertEqual(ms[0].command, 'FAIL')
|
||||
self.assertEqual(ms[0].params[:2], ['PRIVMSG', 'INVALID_UTF8'])
|
||||
self.assertEqual(ms[0].tags.get('label'), 'xyz')
|
112
irctest/server_tests/test_znc_playback.py
Normal file
112
irctest/server_tests/test_znc_playback.py
Normal file
@ -0,0 +1,112 @@
|
||||
import time
|
||||
|
||||
from irctest import cases
|
||||
from irctest.irc_utils.junkdrawer import ircv3_timestamp_to_unixtime
|
||||
from irctest.irc_utils.junkdrawer import to_history_message
|
||||
from irctest.irc_utils.junkdrawer import random_name
|
||||
|
||||
|
||||
def extract_playback_privmsgs(messages):
|
||||
# convert the output of a playback command, drop the echo message
|
||||
result = []
|
||||
for msg in messages:
|
||||
if msg.command == 'PRIVMSG' and msg.params[0].lower() != '*playback':
|
||||
result.append(to_history_message(msg))
|
||||
return result
|
||||
|
||||
|
||||
class ZncPlaybackTestCase(cases.BaseServerTestCase):
|
||||
@staticmethod
|
||||
def config():
|
||||
return {
|
||||
"chathistory": True,
|
||||
}
|
||||
|
||||
@cases.SpecificationSelector.requiredBySpecification('Oragono')
|
||||
def testZncPlayback(self):
|
||||
early_time = int(time.time() - 60)
|
||||
|
||||
chname = random_name('#znc_channel')
|
||||
bar, pw = random_name('bar'), random_name('pass')
|
||||
self.controller.registerUser(self, bar, pw)
|
||||
self.connectClient(bar, name=bar, capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', 'echo-message'], password=pw)
|
||||
self.joinChannel(bar, chname)
|
||||
|
||||
qux = random_name('qux')
|
||||
self.connectClient(qux, name=qux, capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', 'echo-message'])
|
||||
self.joinChannel(qux, chname)
|
||||
|
||||
self.sendLine(qux, 'PRIVMSG %s :hi there' % (bar,))
|
||||
dm = to_history_message([msg for msg in self.getMessages(qux) if msg.command == 'PRIVMSG'][0])
|
||||
self.assertEqual(dm.text, 'hi there')
|
||||
|
||||
NUM_MESSAGES = 10
|
||||
echo_messages = []
|
||||
for i in range(NUM_MESSAGES):
|
||||
self.sendLine(qux, 'PRIVMSG %s :this is message %d' % (chname, i))
|
||||
echo_messages.extend(to_history_message(msg) for msg in self.getMessages(qux) if msg.command == 'PRIVMSG')
|
||||
time.sleep(0.003)
|
||||
self.assertEqual(len(echo_messages), NUM_MESSAGES)
|
||||
|
||||
self.getMessages(bar)
|
||||
|
||||
# reattach to 'bar'
|
||||
self.connectClient(bar, name='viewer', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', 'echo-message'], password=pw)
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play * %d' % (early_time,))
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
self.assertEqual(set(messages), set([dm] + echo_messages))
|
||||
self.sendLine('viewer', 'QUIT')
|
||||
self.assertDisconnected('viewer')
|
||||
|
||||
# reattach to 'bar', play back selectively
|
||||
self.connectClient(bar, name='viewer', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', 'echo-message'], password=pw)
|
||||
mid_timestamp = ircv3_timestamp_to_unixtime(echo_messages[5].time)
|
||||
# exclude message 5 itself (oragono's CHATHISTORY implementation corrects for this, but znc.in/playback does not because whatever)
|
||||
mid_timestamp += .001
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play * %s' % (mid_timestamp,))
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
self.assertEqual(messages, echo_messages[6:])
|
||||
self.sendLine('viewer', 'QUIT')
|
||||
self.assertDisconnected('viewer')
|
||||
|
||||
# reattach to 'bar', play back selectively (pass a parameter and 2 timestamps)
|
||||
self.connectClient(bar, name='viewer', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', 'echo-message'], password=pw)
|
||||
start_timestamp = ircv3_timestamp_to_unixtime(echo_messages[2].time)
|
||||
start_timestamp += .001
|
||||
end_timestamp = ircv3_timestamp_to_unixtime(echo_messages[7].time)
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play %s %s %s' % (chname, start_timestamp, end_timestamp,))
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
self.assertEqual(messages, echo_messages[3:7])
|
||||
# test nicknames as targets
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play %s %d' % (qux, early_time,))
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
self.assertEqual(messages, [dm])
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play %s %d' % (qux.upper(), early_time,))
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
self.assertEqual(messages, [dm])
|
||||
self.sendLine('viewer', 'QUIT')
|
||||
self.assertDisconnected('viewer')
|
||||
|
||||
# test 2-argument form
|
||||
self.connectClient(bar, name='viewer', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', 'echo-message'], password=pw)
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play %s' % (chname,))
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
self.assertEqual(messages, echo_messages)
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play *self')
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
self.assertEqual(messages, [dm])
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play *')
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
self.assertEqual(set(messages), set([dm] + echo_messages))
|
||||
self.sendLine('viewer', 'QUIT')
|
||||
self.assertDisconnected('viewer')
|
||||
|
||||
# test limiting behavior
|
||||
config = self.controller.getConfig()
|
||||
config['history']['znc-maxmessages'] = 5
|
||||
self.controller.rehash(self, config)
|
||||
self.connectClient(bar, name='viewer', capabilities=['batch', 'labeled-response', 'message-tags', 'server-time', 'echo-message'], password=pw)
|
||||
self.sendLine('viewer', 'PRIVMSG *playback :play %s %d' % (chname, int(time.time() - 60)))
|
||||
messages = extract_playback_privmsgs(self.getMessages('viewer'))
|
||||
# should receive the latest 5 messages
|
||||
self.assertEqual(messages, echo_messages[5:])
|
@ -4,8 +4,13 @@ import enum
|
||||
class Specifications(enum.Enum):
|
||||
RFC1459 = 'RFC1459'
|
||||
RFC2812 = 'RFC2812'
|
||||
RFCDeprecated = 'RFC-deprecated'
|
||||
IRC301 = 'IRCv3.1'
|
||||
IRC302 = 'IRCv3.2'
|
||||
IRC302Deprecated = 'IRCv3.2-deprecated'
|
||||
Oragono = 'Oragono'
|
||||
Multiline = 'multiline'
|
||||
MessageTags = 'message-tags'
|
||||
|
||||
@classmethod
|
||||
def of_name(cls, name):
|
||||
|
12
pytest.ini
Normal file
12
pytest.ini
Normal file
@ -0,0 +1,12 @@
|
||||
[pytest]
|
||||
markers =
|
||||
RFC1459
|
||||
RFC2812
|
||||
RFC-deprecated
|
||||
IRCv3.1
|
||||
IRCv3.2
|
||||
IRCv3.2-deprecated
|
||||
message-tags
|
||||
multiline
|
||||
Oragono
|
||||
strict
|
@ -1,3 +0,0 @@
|
||||
limnoria > 2012.08.04 # Needs MultipleReplacer, from 1a64f105
|
||||
ecdsa
|
||||
pyxmpp2_scram
|
54
setup.py
54
setup.py
@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
from setuptools import setup
|
||||
|
||||
if sys.version_info < (3, 4, 0):
|
||||
sys.stderr.write("This script requires Python 3.4 or newer.")
|
||||
sys.stderr.write(os.linesep)
|
||||
sys.exit(-1)
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), 'requirements.txt')) as fd:
|
||||
requirements = [x.split('#')[0]
|
||||
for x in fd.readlines()]
|
||||
|
||||
setup(
|
||||
name='irctest',
|
||||
version='0.1.2',
|
||||
author='Valentin Lorentz',
|
||||
url='https://github.com/ProgVal/irctest/',
|
||||
author_email='progval+irctest@progval.net',
|
||||
description='A script to test interoperability of IRC software.',
|
||||
platforms=['linux', 'linux2'],
|
||||
long_description="""This script aims at testing interoperability of
|
||||
software using the IRC protocol, by running them against test suites
|
||||
and making different software communicate with each other.""",
|
||||
classifiers = [
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Natural Language :: English',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python :: 3.2',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Topic :: Communications :: Chat :: Internet Relay Chat',
|
||||
'Topic :: Software Development :: Testing',
|
||||
],
|
||||
|
||||
# Installation data
|
||||
packages=[
|
||||
'irctest',
|
||||
'irctest.client_tests',
|
||||
'irctest.controllers',
|
||||
'irctest.irc_utils',
|
||||
'irctest.server_tests',
|
||||
],
|
||||
install_requires=requirements,
|
||||
)
|
||||
|
||||
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
Reference in New Issue
Block a user