mirror of
https://github.com/progval/irctest.git
synced 2025-04-07 07:49:52 +00:00
Compare commits
5 Commits
testLineTo
...
named-mode
Author | SHA1 | Date | |
---|---|---|---|
302dd33990 | |||
50b9358ed0 | |||
2a62040b4f | |||
93f70c54d2 | |||
da8a6d1f98 |
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@ -13,10 +13,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.7
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.7
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
|
490
.github/workflows/test-devel.yml
vendored
490
.github/workflows/test-devel.yml
vendored
File diff suppressed because it is too large
Load Diff
88
.github/workflows/test-devel_release.yml
vendored
88
.github/workflows/test-devel_release.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
|||||||
- name: Create directories
|
- name: Create directories
|
||||||
run: cd ~/; mkdir -p .local/ go/
|
run: cd ~/; mkdir -p .local/ go/
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
key: 3-${{ runner.os }}-anope-devel_release
|
key: 3-${{ runner.os }}-anope-devel_release
|
||||||
path: '~/.cache
|
path: '~/.cache
|
||||||
@ -16,28 +16,28 @@ jobs:
|
|||||||
${ github.workspace }/anope
|
${ github.workspace }/anope
|
||||||
|
|
||||||
'
|
'
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Checkout Anope
|
- name: Checkout Anope
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
path: anope
|
path: anope
|
||||||
ref: '2.0'
|
ref: 2.0.9
|
||||||
repository: anope/anope
|
repository: anope/anope
|
||||||
- name: Build Anope
|
- name: Build Anope
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE/anope/
|
cd $GITHUB_WORKSPACE/anope/
|
||||||
sudo apt-get install ninja-build --no-install-recommends
|
cp $GITHUB_WORKSPACE/data/anope/* .
|
||||||
mkdir build && cd build
|
CFLAGS=-O0 ./Config -quick
|
||||||
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local/ -DPROGRAM_NAME=anope -DUSE_PCH=ON -GNinja ..
|
make -C build -j 4
|
||||||
ninja install
|
make -C build install
|
||||||
- name: Make artefact tarball
|
- name: Make artefact tarball
|
||||||
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
|
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
|
||||||
- name: Upload build artefacts
|
- name: Upload build artefacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: installed-anope
|
name: installed-anope
|
||||||
path: ~/artefacts-*.tar.gz
|
path: ~/artefacts-*.tar.gz
|
||||||
@ -47,13 +47,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create directories
|
- name: Create directories
|
||||||
run: cd ~/; mkdir -p .local/ go/
|
run: cd ~/; mkdir -p .local/ go/
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Checkout InspIRCd
|
- name: Checkout InspIRCd
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
path: inspircd
|
path: inspircd
|
||||||
ref: insp3
|
ref: insp3
|
||||||
@ -61,13 +61,19 @@ jobs:
|
|||||||
- name: Build InspIRCd
|
- name: Build InspIRCd
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE/inspircd/
|
cd $GITHUB_WORKSPACE/inspircd/
|
||||||
|
|
||||||
|
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
|
||||||
|
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
|
||||||
|
|
||||||
|
wget https://raw.githubusercontent.com/progval/inspircd-contrib/namedmodes/4.0/m_ircv3_namedmodes.cpp -O src/modules/m_ircv3_namedmodes.cpp
|
||||||
./configure --prefix=$HOME/.local/inspircd --development
|
./configure --prefix=$HOME/.local/inspircd --development
|
||||||
|
|
||||||
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
||||||
make install
|
make install
|
||||||
- name: Make artefact tarball
|
- name: Make artefact tarball
|
||||||
run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/
|
run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/
|
||||||
- name: Upload build artefacts
|
- name: Upload build artefacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: ~/artefacts-*.tar.gz
|
path: ~/artefacts-*.tar.gz
|
||||||
@ -81,9 +87,9 @@ jobs:
|
|||||||
- test-inspircd-atheme
|
- test-inspircd-atheme
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Install dashboard dependencies
|
- name: Install dashboard dependencies
|
||||||
@ -108,13 +114,13 @@ jobs:
|
|||||||
- build-inspircd
|
- build-inspircd
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
@ -125,16 +131,14 @@ jobs:
|
|||||||
- name: Install irctest dependencies
|
- name: Install irctest dependencies
|
||||||
run: |-
|
run: |-
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install pytest pytest-xdist pytest-timeout -r requirements.txt
|
pip install pytest pytest-xdist -r requirements.txt
|
||||||
- env:
|
- name: Test with pytest
|
||||||
IRCTEST_DEBUG_LOGS: ${{ runner.debug }}
|
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
|
||||||
name: Test with pytest
|
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
|
|
||||||
make inspircd
|
make inspircd
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
- if: always()
|
- if: always()
|
||||||
name: Publish results
|
name: Publish results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd_devel_release
|
name: pytest-results_inspircd_devel_release
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
@ -144,18 +148,18 @@ jobs:
|
|||||||
- build-anope
|
- build-anope
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: installed-anope
|
name: installed-anope
|
||||||
path: '~'
|
path: '~'
|
||||||
@ -166,16 +170,14 @@ jobs:
|
|||||||
- name: Install irctest dependencies
|
- name: Install irctest dependencies
|
||||||
run: |-
|
run: |-
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install pytest pytest-xdist pytest-timeout -r requirements.txt
|
pip install pytest pytest-xdist -r requirements.txt
|
||||||
- env:
|
- name: Test with pytest
|
||||||
IRCTEST_DEBUG_LOGS: ${{ runner.debug }}
|
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make
|
||||||
name: Test with pytest
|
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make
|
|
||||||
inspircd-anope
|
inspircd-anope
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
- if: always()
|
- if: always()
|
||||||
name: Publish results
|
name: Publish results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd-anope_devel_release
|
name: pytest-results_inspircd-anope_devel_release
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
@ -184,13 +186,13 @@ jobs:
|
|||||||
- build-inspircd
|
- build-inspircd
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
@ -201,16 +203,14 @@ jobs:
|
|||||||
- name: Install irctest dependencies
|
- name: Install irctest dependencies
|
||||||
run: |-
|
run: |-
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install pytest pytest-xdist pytest-timeout -r requirements.txt
|
pip install pytest pytest-xdist -r requirements.txt
|
||||||
- env:
|
- name: Test with pytest
|
||||||
IRCTEST_DEBUG_LOGS: ${{ runner.debug }}
|
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
|
||||||
name: Test with pytest
|
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
|
|
||||||
make inspircd-atheme
|
make inspircd-atheme
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
- if: always()
|
- if: always()
|
||||||
name: Publish results
|
name: Publish results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd-atheme_devel_release
|
name: pytest-results_inspircd-atheme_devel_release
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
|
536
.github/workflows/test-stable.yml
vendored
536
.github/workflows/test-stable.yml
vendored
File diff suppressed because it is too large
Load Diff
9
Makefile
9
Makefile
@ -83,17 +83,11 @@ LIMNORIA_SELECTORS := \
|
|||||||
(foo or not foo) \
|
(foo or not foo) \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
# Tests marked with arbitrary_client_tags or react_tag can't pass because Sable does not support client tags yet
|
|
||||||
# Tests marked with private_chathistory can't pass because Sable does not implement CHATHISTORY for DMs
|
|
||||||
|
|
||||||
SABLE_SELECTORS := \
|
SABLE_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
and not arbitrary_client_tags \
|
and not whowas and not list and not lusers and not userhost and not time and not info \
|
||||||
and not react_tag \
|
|
||||||
and not private_chathistory \
|
|
||||||
and not list and not lusers and not time and not info \
|
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
SOLANUM_SELECTORS := \
|
SOLANUM_SELECTORS := \
|
||||||
@ -262,6 +256,7 @@ sable:
|
|||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.sable \
|
--controller=irctest.controllers.sable \
|
||||||
-n 20 \
|
-n 20 \
|
||||||
|
-m 'not services' \
|
||||||
-k '$(SABLE_SELECTORS)'
|
-k '$(SABLE_SELECTORS)'
|
||||||
|
|
||||||
solanum:
|
solanum:
|
||||||
|
64
README.md
64
README.md
@ -3,30 +3,16 @@
|
|||||||
This project aims at testing interoperability of software using the
|
This project aims at testing interoperability of software using the
|
||||||
IRC protocol, by running them against common test suites.
|
IRC protocol, by running them against common test suites.
|
||||||
|
|
||||||
It is also used while editing [the "Modern" specification](https://modern.ircdocs.horse/)
|
|
||||||
to check behavior of a large selection of servers at once.
|
|
||||||
|
|
||||||
## The big picture
|
## The big picture
|
||||||
|
|
||||||
This project contains:
|
This project contains:
|
||||||
|
|
||||||
* IRC protocol test cases, primarily checking conformance to
|
* IRC protocol test cases
|
||||||
[the "Modern" specification](https://modern.ircdocs.horse/) and
|
* small wrappers around existing software to run tests on them
|
||||||
[IRCv3 extensions](https://ircv3.net/irc/), but also
|
|
||||||
[RFC 1459](https://datatracker.ietf.org/doc/html/rfc1459) and
|
|
||||||
[RFC 2812](https://datatracker.ietf.org/doc/html/rfc2812).
|
|
||||||
Most of them are for servers but also some for clients.
|
|
||||||
Only the client-server protocol is tested; server-server protocols are out of scope.
|
|
||||||
* Small wrappers around existing software to run tests on them.
|
|
||||||
So far this is restricted to headless software (servers, service packages,
|
|
||||||
and clients bots).
|
|
||||||
|
|
||||||
Wrappers run software in temporary directories, so running `irctest` should
|
Wrappers run software in temporary directories, so running `irctest` should
|
||||||
have no side effect.
|
have no side effect.
|
||||||
|
|
||||||
Test results for the latest version of each supported software, and respective logs,
|
|
||||||
are [published daily](https://dashboard.irctest.limnoria.net/).
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Install irctest and dependencies:
|
Install irctest and dependencies:
|
||||||
@ -34,7 +20,7 @@ Install irctest and dependencies:
|
|||||||
```
|
```
|
||||||
sudo apt install faketime # Optional, but greatly speeds up irctest/server_tests/list.py
|
sudo apt install faketime # Optional, but greatly speeds up irctest/server_tests/list.py
|
||||||
cd ~
|
cd ~
|
||||||
git clone https://github.com/progval/irctest.git
|
git clone https://github.com/ProgVal/irctest.git
|
||||||
cd irctest
|
cd irctest
|
||||||
pip3 install --user -r requirements.txt
|
pip3 install --user -r requirements.txt
|
||||||
```
|
```
|
||||||
@ -54,23 +40,18 @@ 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
|
be called by the `pytest` or `pytest-3` commands (if not, alias them if you
|
||||||
are planning to use them often).
|
are planning to use them often).
|
||||||
|
|
||||||
After installing `pytest-xdist`, you can also pass `pytest` the `-n 10` option
|
|
||||||
to run `10` tests in parallel.
|
|
||||||
|
|
||||||
The rest of this README assumes `pytest` works.
|
The rest of this README assumes `pytest` works.
|
||||||
|
|
||||||
## Test selection
|
## Test selection
|
||||||
|
|
||||||
A major feature of pytest that irctest heavily relies on is 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
|
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`.
|
For example, you can run `LUSERS`-related tests with `-k lusers`.
|
||||||
|
Or only tests based on RFC1459 with `-k rfc1459`.
|
||||||
Using the `-m` option, you can select and deselect and them based on their markers
|
|
||||||
(listed in `pytest.ini`).
|
|
||||||
For example, you can run only tests based on RFC1459 with `-m rfc1459`.
|
|
||||||
|
|
||||||
By default, all tests run; even niche ones. So you probably always want to
|
By default, all tests run; even niche ones. So you probably always want to
|
||||||
use these options: `-m 'not Ergo and not deprecated and not strict`.
|
use these options: `-k 'not Ergo and not deprecated and not strict`.
|
||||||
This excludes:
|
This excludes:
|
||||||
|
|
||||||
* `Ergo`-specific tests (included as Ergo uses irctest as its official
|
* `Ergo`-specific tests (included as Ergo uses irctest as its official
|
||||||
@ -82,10 +63,6 @@ This excludes:
|
|||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
This list is non-exhaustive, see `workflows.yml` for software not listed here.
|
|
||||||
If software you want to test is not listed their either, please open an issue
|
|
||||||
or pull request to add support for it.
|
|
||||||
|
|
||||||
### Servers
|
### Servers
|
||||||
|
|
||||||
#### Ergo:
|
#### Ergo:
|
||||||
@ -112,6 +89,20 @@ make install
|
|||||||
pytest --controller irctest.controllers.solanum -k 'not Ergo and not deprecated and not strict'
|
pytest --controller irctest.controllers.solanum -k 'not Ergo and not deprecated and not strict'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Charybdis:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /tmp/
|
||||||
|
git clone https://github.com/atheme/charybdis.git
|
||||||
|
cd charybdis
|
||||||
|
./autogen.sh
|
||||||
|
./configure --prefix=$HOME/.local/
|
||||||
|
make -j 4
|
||||||
|
make install
|
||||||
|
cd ~/irctest
|
||||||
|
pytest --controller irctest.controllers.charybdis -k 'not Ergo and not deprecated and not strict'
|
||||||
|
```
|
||||||
|
|
||||||
#### InspIRCd:
|
#### InspIRCd:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -125,6 +116,9 @@ patch src/inspircd.cpp < ~/irctest/patches/inspircd_mainloop.patch
|
|||||||
# on Insp3 >= 3.17.0 and Insp4 >= 4.0.0a22:
|
# on Insp3 >= 3.17.0 and Insp4 >= 4.0.0a22:
|
||||||
export CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP
|
export CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP
|
||||||
|
|
||||||
|
# third-party module, used in named-modes tests because the spec is not implemented upstream
|
||||||
|
wget https://raw.githubusercontent.com/progval/inspircd-contrib/namedmodes/4.0/m_ircv3_namedmodes.cpp -O src/modules/m_ircv3_namedmodes.cpp
|
||||||
|
|
||||||
./configure --prefix=$HOME/.local/ --development
|
./configure --prefix=$HOME/.local/ --development
|
||||||
make -j 4
|
make -j 4
|
||||||
make install
|
make install
|
||||||
@ -132,6 +126,14 @@ cd ~/irctest
|
|||||||
pytest --controller irctest.controllers.inspircd -k 'not Ergo and not deprecated and not strict'
|
pytest --controller irctest.controllers.inspircd -k 'not Ergo and not deprecated and not strict'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Mammon:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip3 install --user git+https://github.com/mammon-ircd/mammon.git
|
||||||
|
cd ~/irctest
|
||||||
|
pytest --controller irctest.controllers.mammon -k 'not Ergo and not deprecated and not strict'
|
||||||
|
```
|
||||||
|
|
||||||
#### UnrealIRCd:
|
#### UnrealIRCd:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -148,8 +150,8 @@ pytest --controller irctest.controllers.unreal -k 'not Ergo and not deprecated a
|
|||||||
|
|
||||||
### Servers with services
|
### Servers with services
|
||||||
|
|
||||||
Besides Ergo (that has built-in services) and Sable (that ships its own services),
|
Besides Ergo (that has built-in services), most server controllers can optionally run
|
||||||
most server controllers can optionally run service packages.
|
service packages.
|
||||||
|
|
||||||
#### Atheme:
|
#### Atheme:
|
||||||
|
|
||||||
|
8
data/anope/config.cache
Normal file
8
data/anope/config.cache
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
INSTDIR="$HOME/.local/"
|
||||||
|
RUNGROUP=""
|
||||||
|
UMASK=077
|
||||||
|
DEBUG="yes"
|
||||||
|
USE_PCH="yes"
|
||||||
|
EXTRA_INCLUDE_DIRS=""
|
||||||
|
EXTRA_LIB_DIRS=""
|
||||||
|
EXTRA_CONFIG_ARGS=""
|
@ -11,20 +11,7 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
import textwrap
|
||||||
import time
|
import time
|
||||||
from typing import (
|
from typing import IO, Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Type
|
||||||
IO,
|
|
||||||
Any,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Iterator,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
Set,
|
|
||||||
Tuple,
|
|
||||||
Type,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
import irctest
|
import irctest
|
||||||
|
|
||||||
@ -51,14 +38,6 @@ class TestCaseControllerConfig:
|
|||||||
chathistory: bool = False
|
chathistory: bool = False
|
||||||
"""Whether to enable chathistory features."""
|
"""Whether to enable chathistory features."""
|
||||||
|
|
||||||
account_registration_before_connect: bool = False
|
|
||||||
"""Whether draft/account-registration should be allowed before completing
|
|
||||||
connection registration (NICK + USER + CAP END)"""
|
|
||||||
|
|
||||||
account_registration_requires_email: bool = False
|
|
||||||
"""Whether an email address must be provided when using draft/account-registration.
|
|
||||||
This does not imply servers must validate it."""
|
|
||||||
|
|
||||||
ergo_roleplay: bool = False
|
ergo_roleplay: bool = False
|
||||||
"""Whether to enable the Ergo role-play commands."""
|
"""Whether to enable the Ergo role-play commands."""
|
||||||
|
|
||||||
@ -87,7 +66,6 @@ class _BaseController:
|
|||||||
_port_lock = FileLock(Path(tempfile.gettempdir()) / "irctest_ports.json.lock")
|
_port_lock = FileLock(Path(tempfile.gettempdir()) / "irctest_ports.json.lock")
|
||||||
|
|
||||||
def __init__(self, test_config: TestCaseControllerConfig):
|
def __init__(self, test_config: TestCaseControllerConfig):
|
||||||
self.debug_mode = os.getenv("IRCTEST_DEBUG_LOGS", "0").lower() in ("true", "1")
|
|
||||||
self.test_config = test_config
|
self.test_config = test_config
|
||||||
self.proc = None
|
self.proc = None
|
||||||
self._own_ports: Set[Tuple[str, int]] = set()
|
self._own_ports: Set[Tuple[str, int]] = set()
|
||||||
@ -144,12 +122,6 @@ class _BaseController:
|
|||||||
used_ports.remove((hostname, port))
|
used_ports.remove((hostname, port))
|
||||||
self._own_ports.remove((hostname, port))
|
self._own_ports.remove((hostname, port))
|
||||||
|
|
||||||
def execute(
|
|
||||||
self, command: Sequence[Union[str, Path]], **kwargs: Any
|
|
||||||
) -> subprocess.Popen:
|
|
||||||
output_to = None if self.debug_mode else subprocess.DEVNULL
|
|
||||||
return subprocess.Popen(command, stderr=output_to, stdout=output_to, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class DirectoryBasedController(_BaseController):
|
class DirectoryBasedController(_BaseController):
|
||||||
"""Helper for controllers whose software configuration is based on an
|
"""Helper for controllers whose software configuration is based on an
|
||||||
@ -392,8 +364,6 @@ class BaseServicesController(_BaseController):
|
|||||||
pass
|
pass
|
||||||
elif msg.command in ("MODE", "221"): # RPL_UMODEIS
|
elif msg.command in ("MODE", "221"): # RPL_UMODEIS
|
||||||
pass
|
pass
|
||||||
elif msg.command == "396": # RPL_VISIBLEHOST
|
|
||||||
pass
|
|
||||||
elif msg.command == "NOTICE":
|
elif msg.command == "NOTICE":
|
||||||
assert msg.prefix is not None
|
assert msg.prefix is not None
|
||||||
if "!" not in msg.prefix and "." in msg.prefix:
|
if "!" not in msg.prefix and "." in msg.prefix:
|
||||||
|
@ -160,7 +160,6 @@ class _IrcTestCase(Generic[TController]):
|
|||||||
def messageDiffers(
|
def messageDiffers(
|
||||||
self,
|
self,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
command: Union[str, None, patma.Operator] = None,
|
|
||||||
params: Optional[List[Union[str, None, patma.Operator]]] = None,
|
params: Optional[List[Union[str, None, patma.Operator]]] = None,
|
||||||
target: Optional[str] = None,
|
target: Optional[str] = None,
|
||||||
tags: Optional[
|
tags: Optional[
|
||||||
@ -187,14 +186,6 @@ class _IrcTestCase(Generic[TController]):
|
|||||||
msg=msg,
|
msg=msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
if command is not None and not patma.match_string(msg.command, command):
|
|
||||||
fail_msg = (
|
|
||||||
fail_msg or "expected command to match {expects}, got {got}: {msg}"
|
|
||||||
)
|
|
||||||
return fail_msg.format(
|
|
||||||
*extra_format, got=msg.command, expects=command, msg=msg
|
|
||||||
)
|
|
||||||
|
|
||||||
if prefix is not None and not patma.match_string(msg.prefix, prefix):
|
if prefix is not None and not patma.match_string(msg.prefix, prefix):
|
||||||
fail_msg = (
|
fail_msg = (
|
||||||
fail_msg or "expected prefix to match {expects}, got {got}: {msg}"
|
fail_msg or "expected prefix to match {expects}, got {got}: {msg}"
|
||||||
@ -223,7 +214,7 @@ class _IrcTestCase(Generic[TController]):
|
|||||||
or "expected nick to be {expects}, got {got} instead: {msg}"
|
or "expected nick to be {expects}, got {got} instead: {msg}"
|
||||||
)
|
)
|
||||||
return fail_msg.format(
|
return fail_msg.format(
|
||||||
*extra_format, got=got_nick, expects=nick, msg=msg
|
*extra_format, got=got_nick, expects=nick, param=key, msg=msg
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import functools
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Tuple, Type
|
from typing import Type
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServicesController, DirectoryBasedController
|
from irctest.basecontrollers import BaseServicesController, DirectoryBasedController
|
||||||
|
|
||||||
@ -49,8 +48,6 @@ module {{
|
|||||||
client = "NickServ"
|
client = "NickServ"
|
||||||
forceemail = no
|
forceemail = no
|
||||||
passlen = 1000 # Some tests need long passwords
|
passlen = 1000 # Some tests need long passwords
|
||||||
maxpasslen = 1000
|
|
||||||
minpasslen = 1
|
|
||||||
}}
|
}}
|
||||||
command {{ service = "NickServ"; name = "HELP"; command = "generic/help"; }}
|
command {{ service = "NickServ"; name = "HELP"; command = "generic/help"; }}
|
||||||
|
|
||||||
@ -66,28 +63,17 @@ options {{
|
|||||||
warningtimeout = 4h
|
warningtimeout = 4h
|
||||||
}}
|
}}
|
||||||
|
|
||||||
module {{ name = "{module_prefix}sasl" }}
|
module {{ name = "m_sasl" }}
|
||||||
module {{ name = "enc_bcrypt" }}
|
module {{ name = "enc_sha256" }}
|
||||||
module {{ name = "ns_cert" }}
|
module {{ name = "ns_cert" }}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
|
||||||
def installed_version() -> Tuple[int, ...]:
|
|
||||||
output = subprocess.run(
|
|
||||||
["anope", "--version"], stdout=subprocess.PIPE, universal_newlines=True
|
|
||||||
).stdout
|
|
||||||
(anope, version, *trailing) = output.split()[0].split("-")
|
|
||||||
assert anope == "Anope"
|
|
||||||
return tuple(map(int, version.split(".")))
|
|
||||||
|
|
||||||
|
|
||||||
class AnopeController(BaseServicesController, DirectoryBasedController):
|
class AnopeController(BaseServicesController, DirectoryBasedController):
|
||||||
"""Collaborator for server controllers that rely on Anope"""
|
"""Collaborator for server controllers that rely on Anope"""
|
||||||
|
|
||||||
software_name = "Anope"
|
software_name = "Anope"
|
||||||
software_version = None
|
|
||||||
|
|
||||||
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
||||||
self.create_config()
|
self.create_config()
|
||||||
@ -102,53 +88,36 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
|
|||||||
"ngircd",
|
"ngircd",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert self.directory
|
|
||||||
services_path = shutil.which("anope")
|
|
||||||
assert services_path
|
|
||||||
|
|
||||||
# Rewrite Anope 2.0 module names for 2.1
|
|
||||||
if not self.software_version:
|
|
||||||
self.software_version = installed_version()
|
|
||||||
if self.software_version >= (2, 1, 0):
|
|
||||||
if protocol == "charybdis":
|
|
||||||
protocol = "solanum"
|
|
||||||
elif protocol == "inspircd3":
|
|
||||||
protocol = "inspircd"
|
|
||||||
elif protocol == "unreal4":
|
|
||||||
protocol = "unrealircd"
|
|
||||||
|
|
||||||
with self.open_file("conf/services.conf") as fd:
|
with self.open_file("conf/services.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
protocol=protocol,
|
protocol=protocol,
|
||||||
server_hostname=server_hostname,
|
server_hostname=server_hostname,
|
||||||
server_port=server_port,
|
server_port=server_port,
|
||||||
module_prefix="" if self.software_version >= (2, 1, 2) else "m_",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.open_file("conf/empty_file") as fd:
|
with self.open_file("conf/empty_file") as fd:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
assert self.directory
|
||||||
|
services_path = shutil.which("services")
|
||||||
|
assert services_path
|
||||||
|
|
||||||
# Config and code need to be in the same directory, *obviously*
|
# Config and code need to be in the same directory, *obviously*
|
||||||
(self.directory / "lib").symlink_to(Path(services_path).parent.parent / "lib")
|
(self.directory / "lib").symlink_to(Path(services_path).parent.parent / "lib")
|
||||||
(self.directory / "modules").symlink_to(
|
|
||||||
Path(services_path).parent.parent / "modules"
|
|
||||||
)
|
|
||||||
|
|
||||||
extra_args = []
|
self.proc = subprocess.Popen(
|
||||||
if self.debug_mode:
|
|
||||||
extra_args.append("--debug")
|
|
||||||
|
|
||||||
self.proc = self.execute(
|
|
||||||
[
|
[
|
||||||
"anope",
|
"services",
|
||||||
"--config=services.conf", # can't be an absolute path in 2.0
|
"-n", # don't fork
|
||||||
"--nofork", # don't fork
|
"--config=services.conf", # can't be an absolute path
|
||||||
"--nopid", # don't write a pid
|
# "--logdir",
|
||||||
*extra_args,
|
# f"/tmp/services-{server_port}.log",
|
||||||
],
|
],
|
||||||
cwd=self.directory,
|
cwd=self.directory,
|
||||||
|
# stdout=subprocess.DEVNULL,
|
||||||
|
# stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
import irctest
|
import irctest
|
||||||
@ -74,7 +75,7 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert self.directory
|
assert self.directory
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
"atheme-services",
|
"atheme-services",
|
||||||
"-n", # don't fork
|
"-n", # don't fork
|
||||||
@ -87,6 +88,8 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
|
|||||||
"-D",
|
"-D",
|
||||||
self.directory,
|
self.directory,
|
||||||
],
|
],
|
||||||
|
# stdout=subprocess.DEVNULL,
|
||||||
|
# stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
def registerUser(
|
def registerUser(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from typing import Optional, Set, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||||
@ -149,7 +150,7 @@ class BahamutController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ircd",
|
"ircd",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||||
@ -51,8 +51,6 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ssl_config = ""
|
ssl_config = ""
|
||||||
binary_path = shutil.which(self.binary_name)
|
|
||||||
assert binary_path, f"Could not find '{binary_path}' executable"
|
|
||||||
with self.open_file("server.conf") as fd:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
(self.template_config).format(
|
(self.template_config).format(
|
||||||
@ -62,7 +60,6 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
services_port=services_port,
|
services_port=services_port,
|
||||||
password_field=password_field,
|
password_field=password_field,
|
||||||
ssl_config=ssl_config,
|
ssl_config=ssl_config,
|
||||||
install_prefix=Path(binary_path).parent.parent,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
@ -73,7 +70,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
self.binary_name,
|
self.binary_name,
|
||||||
@ -83,6 +80,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
"-pidfile",
|
"-pidfile",
|
||||||
self.directory / "server.pid",
|
self.directory / "server.pid",
|
||||||
],
|
],
|
||||||
|
# stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
if run_services:
|
if run_services:
|
||||||
|
@ -200,7 +200,7 @@ class DlkController(BaseServicesController, DirectoryBasedController):
|
|||||||
fd.write(TEMPLATE_DLK_WP_CONFIG.format(**template_vars))
|
fd.write(TEMPLATE_DLK_WP_CONFIG.format(**template_vars))
|
||||||
(dlk_conf_dir / "modules.conf").symlink_to(self.dlk_path / "conf/modules.conf")
|
(dlk_conf_dir / "modules.conf").symlink_to(self.dlk_path / "conf/modules.conf")
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
"php",
|
"php",
|
||||||
"src/dalek",
|
"src/dalek",
|
||||||
|
@ -14,7 +14,6 @@ BASE_CONFIG = {
|
|||||||
"name": "My.Little.Server",
|
"name": "My.Little.Server",
|
||||||
"listeners": {},
|
"listeners": {},
|
||||||
"max-sendq": "16k",
|
"max-sendq": "16k",
|
||||||
"casemapping": "ascii",
|
|
||||||
"connection-limits": {
|
"connection-limits": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"cidr-len-ipv4": 32,
|
"cidr-len-ipv4": 32,
|
||||||
@ -58,11 +57,6 @@ BASE_CONFIG = {
|
|||||||
"enabled": True,
|
"enabled": True,
|
||||||
"method": "strict",
|
"method": "strict",
|
||||||
},
|
},
|
||||||
"login-throttling": {
|
|
||||||
"enabled": True,
|
|
||||||
"duration": "1m",
|
|
||||||
"max-attempts": 3,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"channels": {"registration": {"enabled": True}},
|
"channels": {"registration": {"enabled": True}},
|
||||||
"datastore": {"path": None},
|
"datastore": {"path": None},
|
||||||
@ -172,16 +166,6 @@ class ErgoController(BaseServerController, DirectoryBasedController):
|
|||||||
if enable_roleplay:
|
if enable_roleplay:
|
||||||
config["roleplay"] = {"enabled": True}
|
config["roleplay"] = {"enabled": True}
|
||||||
|
|
||||||
if self.test_config.account_registration_before_connect:
|
|
||||||
config["accounts"]["registration"]["allow-before-connect"] = True # type: ignore
|
|
||||||
if self.test_config.account_registration_requires_email:
|
|
||||||
config["accounts"]["registration"]["email-verification"] = { # type: ignore
|
|
||||||
"enabled": True,
|
|
||||||
"sender": "test@example.com",
|
|
||||||
"require-tls": True,
|
|
||||||
"helo-domain": "example.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.test_config.ergo_config:
|
if self.test_config.ergo_config:
|
||||||
self.test_config.ergo_config(config)
|
self.test_config.ergo_config(config)
|
||||||
|
|
||||||
@ -213,7 +197,7 @@ class ErgoController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"]
|
[*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest import authentication, tls
|
from irctest import authentication, tls
|
||||||
@ -30,7 +31,7 @@ class GircController(BaseClientController, DirectoryBasedController):
|
|||||||
args += ["--sasl-fail-is-ok"]
|
args += ["--sasl-fail-is-ok"]
|
||||||
|
|
||||||
# Runs a client with the config given as arguments
|
# Runs a client with the config given as arguments
|
||||||
self.proc = self.execute(["girc_test", "connect"] + args)
|
self.proc = subprocess.Popen(["girc_test", "connect"] + args)
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[GircController]:
|
def get_irctest_controller_class() -> Type[GircController]:
|
||||||
|
@ -3,9 +3,6 @@ from typing import Set, Type
|
|||||||
from .base_hybrid import BaseHybridController
|
from .base_hybrid import BaseHybridController
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
module_base_path = "{install_prefix}/lib/ircd-hybrid/modules";
|
|
||||||
.include "./reference.modules.conf"
|
|
||||||
|
|
||||||
serverinfo {{
|
serverinfo {{
|
||||||
name = "My.Little.Server";
|
name = "My.Little.Server";
|
||||||
sid = "42X";
|
sid = "42X";
|
||||||
|
@ -48,7 +48,9 @@ TEMPLATE_CONFIG = """
|
|||||||
sendpass="password"
|
sendpass="password"
|
||||||
>
|
>
|
||||||
<module name="spanningtree">
|
<module name="spanningtree">
|
||||||
|
<module name="services_account">
|
||||||
<module name="hidechans"> # Anope errors when missing
|
<module name="hidechans"> # Anope errors when missing
|
||||||
|
<module name="svshold"> # Atheme raises a warning when missing
|
||||||
<sasl requiressl="no"
|
<sasl requiressl="no"
|
||||||
target="services.example.org">
|
target="services.example.org">
|
||||||
|
|
||||||
@ -66,13 +68,18 @@ TEMPLATE_CONFIG = """
|
|||||||
<module name="ircv3_invitenotify">
|
<module name="ircv3_invitenotify">
|
||||||
<module name="ircv3_labeledresponse">
|
<module name="ircv3_labeledresponse">
|
||||||
<module name="ircv3_msgid">
|
<module name="ircv3_msgid">
|
||||||
|
<module name="ircv3_namedmodes"> # third-party, https://github.com/progval/inspircd-contrib/blob/namedmodes/4.0/m_ircv3_namedmodes.cpp
|
||||||
<module name="ircv3_servertime">
|
<module name="ircv3_servertime">
|
||||||
<module name="monitor">
|
<module name="monitor">
|
||||||
<module name="m_muteban"> # for testing mute extbans
|
<module name="m_muteban"> # for testing mute extbans
|
||||||
|
<module name="namesx"> # For multi-prefix
|
||||||
<module name="sasl">
|
<module name="sasl">
|
||||||
<module name="uhnames"> # For userhost-in-names
|
<module name="uhnames"> # For userhost-in-names
|
||||||
|
|
||||||
|
# HELP/HELPOP
|
||||||
<module name="alias"> # for the HELP alias
|
<module name="alias"> # for the HELP alias
|
||||||
{version_config}
|
<module name="{help_module_name}">
|
||||||
|
<include file="examples/{help_module_name}.conf.example">
|
||||||
|
|
||||||
# Misc:
|
# Misc:
|
||||||
<log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log">
|
<log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log">
|
||||||
@ -84,26 +91,6 @@ TEMPLATE_SSL_CONFIG = """
|
|||||||
<openssl certfile="{pem_path}" keyfile="{key_path}" dhfile="{dh_path}" hash="sha1">
|
<openssl certfile="{pem_path}" keyfile="{key_path}" dhfile="{dh_path}" hash="sha1">
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TEMPLATE_V3_CONFIG = """
|
|
||||||
<module name="namesx"> # For multi-prefix
|
|
||||||
<module name="services_account">
|
|
||||||
<module name="svshold"> # Atheme raises a warning when missing
|
|
||||||
|
|
||||||
# HELP/HELPOP
|
|
||||||
<module name="helpop">
|
|
||||||
<include file="examples/helpop.conf.example">
|
|
||||||
"""
|
|
||||||
|
|
||||||
TEMPLATE_V4_CONFIG = """
|
|
||||||
<module name="account">
|
|
||||||
<module name="multiprefix"> # For multi-prefix
|
|
||||||
<module name="services">
|
|
||||||
|
|
||||||
# HELP/HELPOP
|
|
||||||
<module name="help">
|
|
||||||
<include file="examples/help.example.conf">
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache()
|
||||||
def installed_version() -> int:
|
def installed_version() -> int:
|
||||||
@ -112,14 +99,12 @@ def installed_version() -> int:
|
|||||||
return 3
|
return 3
|
||||||
if output.startswith("InspIRCd-4"):
|
if output.startswith("InspIRCd-4"):
|
||||||
return 4
|
return 4
|
||||||
if output.startswith("InspIRCd-5"):
|
else:
|
||||||
return 5
|
|
||||||
assert False, f"unexpected version: {output}"
|
assert False, f"unexpected version: {output}"
|
||||||
|
|
||||||
|
|
||||||
class InspircdController(BaseServerController, DirectoryBasedController):
|
class InspircdController(BaseServerController, DirectoryBasedController):
|
||||||
software_name = "InspIRCd"
|
software_name = "InspIRCd"
|
||||||
software_version = installed_version()
|
|
||||||
supported_sasl_mechanisms = {"PLAIN"}
|
supported_sasl_mechanisms = {"PLAIN"}
|
||||||
supports_sts = False
|
supports_sts = False
|
||||||
extban_mute_char = "m"
|
extban_mute_char = "m"
|
||||||
@ -156,9 +141,9 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
ssl_config = ""
|
ssl_config = ""
|
||||||
|
|
||||||
if installed_version() == 3:
|
if installed_version() == 3:
|
||||||
version_config = TEMPLATE_V3_CONFIG
|
help_module_name = "helpop"
|
||||||
elif installed_version() >= 4:
|
elif installed_version() == 4:
|
||||||
version_config = TEMPLATE_V4_CONFIG
|
help_module_name = "help"
|
||||||
else:
|
else:
|
||||||
assert False, f"unexpected version: {installed_version()}"
|
assert False, f"unexpected version: {installed_version()}"
|
||||||
|
|
||||||
@ -171,7 +156,7 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
services_port=services_port,
|
services_port=services_port,
|
||||||
password_field=password_field,
|
password_field=password_field,
|
||||||
ssl_config=ssl_config,
|
ssl_config=ssl_config,
|
||||||
version_config=version_config,
|
help_module_name=help_module_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
@ -182,22 +167,15 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
extra_args = []
|
self.proc = subprocess.Popen(
|
||||||
if self.debug_mode:
|
|
||||||
if installed_version() >= 4:
|
|
||||||
extra_args.append("--protocoldebug")
|
|
||||||
else:
|
|
||||||
extra_args.append("--debug")
|
|
||||||
|
|
||||||
self.proc = self.execute(
|
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"inspircd",
|
"inspircd",
|
||||||
"--nofork",
|
"--nofork",
|
||||||
"--config",
|
"--config",
|
||||||
self.directory / "server.conf",
|
self.directory / "server.conf",
|
||||||
*extra_args,
|
|
||||||
],
|
],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
if run_services:
|
if run_services:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
@ -77,7 +78,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ircd",
|
"ircd",
|
||||||
@ -87,6 +88,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
"-f",
|
"-f",
|
||||||
self.directory / "server.conf",
|
self.directory / "server.conf",
|
||||||
],
|
],
|
||||||
|
# stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
@ -96,7 +97,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ircd",
|
"ircd",
|
||||||
@ -106,6 +107,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
"-x",
|
"-x",
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
],
|
],
|
||||||
|
# stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest import authentication, tls
|
from irctest import authentication, tls
|
||||||
@ -83,7 +84,7 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
self.proc = self.execute(["supybot", self.directory / "bot.conf"])
|
self.proc = subprocess.Popen(["supybot", self.directory / "bot.conf"])
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[LimnoriaController]:
|
def get_irctest_controller_class() -> Type[LimnoriaController]:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from typing import Optional, Set, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
@ -115,7 +116,7 @@ class MammonController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"mammond",
|
"mammond",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from typing import Optional, Set, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||||
@ -22,14 +23,10 @@ TEMPLATE_CONFIG = """
|
|||||||
|
|
||||||
[Options]
|
[Options]
|
||||||
MorePrivacy = no # by default, always replies to WHOWAS with ERR_WASNOSUCHNICK
|
MorePrivacy = no # by default, always replies to WHOWAS with ERR_WASNOSUCHNICK
|
||||||
PAM = no
|
|
||||||
|
|
||||||
[Operator]
|
[Operator]
|
||||||
Name = operuser
|
Name = operuser
|
||||||
Password = operpassword
|
Password = operpassword
|
||||||
|
|
||||||
[Limits]
|
|
||||||
MaxNickLength = 32 # defaults to 9
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -94,7 +91,7 @@ class NgircdController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ngircd",
|
"ngircd",
|
||||||
@ -102,6 +99,7 @@ class NgircdController(BaseServerController, DirectoryBasedController):
|
|||||||
"--config",
|
"--config",
|
||||||
self.directory / "server.conf",
|
self.directory / "server.conf",
|
||||||
],
|
],
|
||||||
|
# stdout=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
if run_services:
|
if run_services:
|
||||||
|
@ -116,7 +116,20 @@ NETWORK_CONFIG_CONFIG = """
|
|||||||
],
|
],
|
||||||
|
|
||||||
"alias_users": [
|
"alias_users": [
|
||||||
%(services_alias_users)s
|
{
|
||||||
|
"nick": "ChanServ",
|
||||||
|
"user": "ChanServ",
|
||||||
|
"host": "services.",
|
||||||
|
"realname": "Channel services compatibility layer",
|
||||||
|
"command_alias": "CS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nick": "NickServ",
|
||||||
|
"user": "NickServ",
|
||||||
|
"host": "services.",
|
||||||
|
"realname": "Account services compatibility layer",
|
||||||
|
"command_alias": "NS"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"default_roles": {
|
"default_roles": {
|
||||||
@ -147,23 +160,6 @@ NETWORK_CONFIG_CONFIG = """
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SERVICES_ALIAS_USERS = """
|
|
||||||
{
|
|
||||||
"nick": "ChanServ",
|
|
||||||
"user": "ChanServ",
|
|
||||||
"host": "services.",
|
|
||||||
"realname": "Channel services compatibility layer",
|
|
||||||
"command_alias": "CS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nick": "NickServ",
|
|
||||||
"user": "NickServ",
|
|
||||||
"host": "services.",
|
|
||||||
"realname": "Account services compatibility layer",
|
|
||||||
"command_alias": "NS"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
SERVER_CONFIG = """
|
SERVER_CONFIG = """
|
||||||
{
|
{
|
||||||
"server_id": 1,
|
"server_id": 1,
|
||||||
@ -260,13 +256,7 @@ SERVICES_CONFIG = """
|
|||||||
"builtin:voice": [
|
"builtin:voice": [
|
||||||
"always_send", "voice_self", "receive_voice"
|
"always_send", "voice_self", "receive_voice"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
|
||||||
"password_hash": {
|
|
||||||
"algorithm": "bcrypt", // Only "bcrypt" is supported for now
|
|
||||||
"cost": 4, // Exponentially faster than the default 12
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"event_log": {
|
"event_log": {
|
||||||
@ -324,11 +314,6 @@ class SableController(BaseServerController, DirectoryBasedController):
|
|||||||
raise NotImplementedByController("PASS command")
|
raise NotImplementedByController("PASS command")
|
||||||
if ssl:
|
if ssl:
|
||||||
raise NotImplementedByController("SSL")
|
raise NotImplementedByController("SSL")
|
||||||
if self.test_config.account_registration_before_connect:
|
|
||||||
raise NotImplementedByController("account-registration with before-connect")
|
|
||||||
if self.test_config.account_registration_requires_email:
|
|
||||||
raise NotImplementedByController("account-registration with email-required")
|
|
||||||
|
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.create_config()
|
self.create_config()
|
||||||
@ -378,7 +363,6 @@ class SableController(BaseServerController, DirectoryBasedController):
|
|||||||
.strip(),
|
.strip(),
|
||||||
services_management_hostname=services_management_hostname,
|
services_management_hostname=services_management_hostname,
|
||||||
services_management_port=services_management_port,
|
services_management_port=services_management_port,
|
||||||
services_alias_users=SERVICES_ALIAS_USERS if run_services else "",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.open_file("configs/network.conf") as fd:
|
with self.open_file("configs/network.conf") as fd:
|
||||||
@ -394,7 +378,7 @@ class SableController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"sable_ircd",
|
"sable_ircd",
|
||||||
@ -408,7 +392,6 @@ class SableController(BaseServerController, DirectoryBasedController):
|
|||||||
],
|
],
|
||||||
cwd=self.directory,
|
cwd=self.directory,
|
||||||
preexec_fn=os.setsid,
|
preexec_fn=os.setsid,
|
||||||
env={"RUST_BACKTRACE": "1", **os.environ},
|
|
||||||
)
|
)
|
||||||
self.pgroup_id = os.getpgid(self.proc.pid)
|
self.pgroup_id = os.getpgid(self.proc.pid)
|
||||||
|
|
||||||
@ -475,7 +458,7 @@ class SableServicesController(BaseServicesController):
|
|||||||
with self.server_controller.open_file("configs/services.conf") as fd:
|
with self.server_controller.open_file("configs/services.conf") as fd:
|
||||||
fd.write(SERVICES_CONFIG % self.server_controller.template_vars)
|
fd.write(SERVICES_CONFIG % self.server_controller.template_vars)
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
"sable_services",
|
"sable_services",
|
||||||
"--foreground",
|
"--foreground",
|
||||||
@ -486,7 +469,6 @@ class SableServicesController(BaseServicesController):
|
|||||||
],
|
],
|
||||||
cwd=self.server_controller.directory,
|
cwd=self.server_controller.directory,
|
||||||
preexec_fn=os.setsid,
|
preexec_fn=os.setsid,
|
||||||
env={"RUST_BACKTRACE": "1", **os.environ},
|
|
||||||
)
|
)
|
||||||
self.pgroup_id = os.getpgid(self.proc.pid)
|
self.pgroup_id = os.getpgid(self.proc.pid)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
@ -95,7 +96,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ircd",
|
"ircd",
|
||||||
@ -105,6 +106,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
|
|||||||
"-x",
|
"-x",
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
],
|
],
|
||||||
|
# stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import Optional, TextIO, Type, cast
|
from typing import Optional, TextIO, Type, cast
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ class SopelController(BaseClientController):
|
|||||||
auth_method="auth_method = sasl" if auth else "",
|
auth_method="auth_method = sasl" if auth else "",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.proc = self.execute(["sopel", "-c", self.filename])
|
self.proc = subprocess.Popen(["sopel", "-c", self.filename])
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[SopelController]:
|
def get_irctest_controller_class() -> Type[SopelController]:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest import authentication, tls
|
from irctest import authentication, tls
|
||||||
@ -95,7 +96,7 @@ class TheLoungeController(BaseClientController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
with self.open_file("users/testuser.json", "r") as fd:
|
with self.open_file("users/testuser.json", "r") as fd:
|
||||||
print("config", json.load(fd)["networks"][0]["saslPassword"])
|
print("config", json.load(fd)["networks"][0]["saslPassword"])
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[os.environ.get("THELOUNGE_BIN", "thelounge"), "start"],
|
[os.environ.get("THELOUNGE_BIN", "thelounge"), "start"],
|
||||||
env={**os.environ, "THELOUNGE_HOME": str(self.directory)},
|
env={**os.environ, "THELOUNGE_HOME": str(self.directory)},
|
||||||
)
|
)
|
||||||
|
@ -261,7 +261,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
|
|||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
with _STARTSTOP_LOCK():
|
with _STARTSTOP_LOCK():
|
||||||
self.proc = self.execute(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"unrealircd",
|
"unrealircd",
|
||||||
@ -270,6 +270,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
|
|||||||
"-f",
|
"-f",
|
||||||
self.directory / "unrealircd.conf",
|
self.directory / "unrealircd.conf",
|
||||||
],
|
],
|
||||||
|
# stdout=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
self.wait_for_port()
|
self.wait_for_port()
|
||||||
|
|
||||||
|
@ -245,19 +245,8 @@ def build_test_table(
|
|||||||
# TODO: only hash test parameter
|
# TODO: only hash test parameter
|
||||||
row_anchor = md5sum(row_anchor)
|
row_anchor = md5sum(row_anchor)
|
||||||
|
|
||||||
doc = docstring(
|
|
||||||
getattr(getattr(module, class_name), test_name.split("[")[0])
|
|
||||||
)
|
|
||||||
row = HTML.tr(
|
row = HTML.tr(
|
||||||
HTML.th(
|
HTML.th(HTML.a(test_name, href=f"#{row_anchor}"), class_="test-name"),
|
||||||
HTML.details(
|
|
||||||
HTML.summary(HTML.a(test_name, href=f"#{row_anchor}")),
|
|
||||||
doc,
|
|
||||||
)
|
|
||||||
if doc
|
|
||||||
else HTML.a(test_name, href=f"#{row_anchor}"),
|
|
||||||
class_="test-name",
|
|
||||||
),
|
|
||||||
id=row_anchor,
|
id=row_anchor,
|
||||||
)
|
)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
23
irctest/irc_utils/ambiguities.py
Normal file
23
irctest/irc_utils/ambiguities.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Handles ambiguities of RFCs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_namreply_params(params: List[str]) -> List[str]:
|
||||||
|
# So… RFC 2812 says:
|
||||||
|
# "( "=" / "*" / "@" ) <channel>
|
||||||
|
# :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
|
||||||
|
# but spaces seem to be missing (eg. before the colon), so we
|
||||||
|
# don't know if there should be one before the <channel> and its
|
||||||
|
# prefix.
|
||||||
|
# So let's normalize this to “with space”, and strip spaces at the
|
||||||
|
# end of the nick list.
|
||||||
|
params = list(params) # copy the list
|
||||||
|
if len(params) == 3:
|
||||||
|
assert params[1][0] in "=*@", params
|
||||||
|
params.insert(1, params[1][0])
|
||||||
|
params[2] = params[2][1:]
|
||||||
|
params[3] = params[3].rstrip()
|
||||||
|
return params
|
@ -4,15 +4,16 @@ commonly packaged by Linux distributions but might not be available
|
|||||||
in some environments.
|
in some environments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import os
|
import os
|
||||||
from typing import Any, ContextManager
|
from typing import ContextManager
|
||||||
|
|
||||||
if os.getenv("PYTEST_XDIST_WORKER"):
|
if os.getenv("PYTEST_XDIST_WORKER"):
|
||||||
# running under pytest-xdist; filelock is required for reliability
|
# running under pytest-xdist; filelock is required for reliability
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
else:
|
else:
|
||||||
# normal test execution, no port races
|
# normal test execution, no port races
|
||||||
|
import contextlib
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
def FileLock(*args: Any, **kwargs: Any) -> ContextManager[None]:
|
def FileLock(*args: Any, **kwargs: Any) -> ContextManager[None]:
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
|
@ -15,7 +15,7 @@ TAG_ESCAPE = [
|
|||||||
unescape_tag_value = MultipleReplacer(dict(map(lambda x: (x[1], x[0]), TAG_ESCAPE)))
|
unescape_tag_value = MultipleReplacer(dict(map(lambda x: (x[1], x[0]), TAG_ESCAPE)))
|
||||||
|
|
||||||
# TODO: validate host
|
# TODO: validate host
|
||||||
tag_key_validator = re.compile(r"^\+?(\S+/)?[a-zA-Z0-9-]+$")
|
tag_key_validator = re.compile(r"\+?(\S+/)?[a-zA-Z0-9-]+")
|
||||||
|
|
||||||
|
|
||||||
def parse_tags(s: str) -> Dict[str, Optional[str]]:
|
def parse_tags(s: str) -> Dict[str, Optional[str]]:
|
||||||
|
@ -204,5 +204,11 @@ ERR_ACCOUNT_INVALID_VERIFY_CODE = "925"
|
|||||||
RPL_REG_VERIFICATION_REQUIRED = "927"
|
RPL_REG_VERIFICATION_REQUIRED = "927"
|
||||||
ERR_REG_INVALID_CRED_TYPE = "928"
|
ERR_REG_INVALID_CRED_TYPE = "928"
|
||||||
ERR_REG_INVALID_CALLBACK = "929"
|
ERR_REG_INVALID_CALLBACK = "929"
|
||||||
|
RPL_ENDOFPROPLIST = "960"
|
||||||
|
RPL_PROPLIST = "961"
|
||||||
|
RPL_ENDOFLISTPROPLIST = "962"
|
||||||
|
RPL_LISTPROPLIST = "963"
|
||||||
|
RPL_CHMODELIST = "964"
|
||||||
|
RPL_UMODELIST = "965"
|
||||||
ERR_TOOMANYLANGUAGES = "981"
|
ERR_TOOMANYLANGUAGES = "981"
|
||||||
ERR_NOLANGUAGE = "982"
|
ERR_NOLANGUAGE = "982"
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""Pattern-matching utilities"""
|
"""Pattern-matching utilities"""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import itertools
|
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
@ -28,14 +27,6 @@ class _AnyOptStr(Operator):
|
|||||||
return "ANYOPTSTR"
|
return "ANYOPTSTR"
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
|
||||||
class OptStrRe(Operator):
|
|
||||||
regexp: str
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"OptStrRe(r'{self.regexp}')"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class StrRe(Operator):
|
class StrRe(Operator):
|
||||||
regexp: str
|
regexp: str
|
||||||
@ -106,15 +97,10 @@ def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bo
|
|||||||
elif isinstance(expected, _AnyStr) and got is not None:
|
elif isinstance(expected, _AnyStr) and got is not None:
|
||||||
return True
|
return True
|
||||||
elif isinstance(expected, StrRe):
|
elif isinstance(expected, StrRe):
|
||||||
if got is None or not re.match(expected.regexp + "$", got):
|
if got is None or not re.match(expected.regexp, got):
|
||||||
return False
|
|
||||||
elif isinstance(expected, OptStrRe):
|
|
||||||
if got is None:
|
|
||||||
return True
|
|
||||||
if not re.match(expected.regexp + "$", got):
|
|
||||||
return False
|
return False
|
||||||
elif isinstance(expected, NotStrRe):
|
elif isinstance(expected, NotStrRe):
|
||||||
if got is None or re.match(expected.regexp + "$", got):
|
if got is None or re.match(expected.regexp, got):
|
||||||
return False
|
return False
|
||||||
elif isinstance(expected, InsensitiveStr):
|
elif isinstance(expected, InsensitiveStr):
|
||||||
if got is None or got.lower() != expected.string.lower():
|
if got is None or got.lower() != expected.string.lower():
|
||||||
@ -142,19 +128,11 @@ def match_list(
|
|||||||
nb_remaining_items = len(got) - len(expected)
|
nb_remaining_items = len(got) - len(expected)
|
||||||
expected += [remainder.item] * max(nb_remaining_items, remainder.min_length)
|
expected += [remainder.item] * max(nb_remaining_items, remainder.min_length)
|
||||||
|
|
||||||
nb_optionals = 0
|
if len(got) != len(expected):
|
||||||
for expected_value in expected:
|
|
||||||
if isinstance(expected_value, (_AnyOptStr, OptStrRe)):
|
|
||||||
nb_optionals += 1
|
|
||||||
else:
|
|
||||||
if nb_optionals > 0:
|
|
||||||
raise NotImplementedError("Optional values in non-final position")
|
|
||||||
|
|
||||||
if not (len(expected) - nb_optionals <= len(got) <= len(expected)):
|
|
||||||
return False
|
return False
|
||||||
return all(
|
return all(
|
||||||
match_string(got_value, expected_value)
|
match_string(got_value, expected_value)
|
||||||
for (got_value, expected_value) in itertools.zip_longest(got, expected)
|
for (got_value, expected_value) in zip(got, expected)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ from irctest.patma import (
|
|||||||
ANYSTR,
|
ANYSTR,
|
||||||
ListRemainder,
|
ListRemainder,
|
||||||
NotStrRe,
|
NotStrRe,
|
||||||
OptStrRe,
|
|
||||||
RemainingKeys,
|
RemainingKeys,
|
||||||
StrRe,
|
StrRe,
|
||||||
)
|
)
|
||||||
@ -173,7 +172,7 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
],
|
],
|
||||||
# and they each error with:
|
# and they each error with:
|
||||||
[
|
[
|
||||||
"expected command to match PRIVMSG, got PRIVMG",
|
"expected command to be PRIVMSG, got PRIVMG",
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
|
"expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
|
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
|
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
|
||||||
@ -206,7 +205,7 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
],
|
],
|
||||||
# and they each error with:
|
# and they each error with:
|
||||||
[
|
[
|
||||||
"expected command to match PRIVMSG, got PRIVMG",
|
"expected command to be PRIVMSG, got PRIVMG",
|
||||||
"expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
|
"expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
|
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
|
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
|
||||||
@ -235,34 +234,12 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
],
|
],
|
||||||
# and they each error with:
|
# and they each error with:
|
||||||
[
|
[
|
||||||
"expected command to match PRIVMSG, got PRIVMG",
|
"expected command to be PRIVMSG, got PRIVMG",
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'value1'}",
|
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'value1'}",
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': ''}",
|
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': ''}",
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': 'baz'}",
|
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': 'baz'}",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
|
||||||
# the specification:
|
|
||||||
dict(
|
|
||||||
command="004",
|
|
||||||
params=["nick", "...", OptStrRe("[a-zA-Z]+")],
|
|
||||||
),
|
|
||||||
# matches:
|
|
||||||
[
|
|
||||||
"004 nick ... abc",
|
|
||||||
"004 nick ...",
|
|
||||||
],
|
|
||||||
# and does not match:
|
|
||||||
[
|
|
||||||
"004 nick ... 123",
|
|
||||||
"004 nick ... :",
|
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected params to match ['nick', '...', OptStrRe(r'[a-zA-Z]+')], got ['nick', '...', '123']",
|
|
||||||
"expected params to match ['nick', '...', OptStrRe(r'[a-zA-Z]+')], got ['nick', '...', '']",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
# the specification:
|
# the specification:
|
||||||
dict(
|
dict(
|
||||||
@ -345,7 +322,7 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
],
|
],
|
||||||
# and they each error with:
|
# and they each error with:
|
||||||
[
|
[
|
||||||
"expected command to match PING, got PONG"
|
"expected command to be PING, got PONG"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -9,75 +9,15 @@ from irctest.patma import ANYSTR
|
|||||||
REGISTER_CAP_NAME = "draft/account-registration"
|
REGISTER_CAP_NAME = "draft/account-registration"
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
class RegisterTestCase(cases.BaseServerTestCase):
|
|
||||||
def testRegisterDefaultName(self):
|
|
||||||
"""
|
|
||||||
"If <account> is *, then this value is the user’s current nickname."
|
|
||||||
"""
|
|
||||||
self.connectClient(
|
|
||||||
"bar", name="bar", capabilities=[REGISTER_CAP_NAME], skip_if_cap_nak=True
|
|
||||||
)
|
|
||||||
self.sendLine("bar", "CAP LS 302")
|
|
||||||
caps = self.getCapLs("bar")
|
|
||||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
|
||||||
self.assertNotIn("email-required", (caps[REGISTER_CAP_NAME] or "").split(","))
|
|
||||||
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
|
||||||
msgs = self.getMessages("bar")
|
|
||||||
register_response = [msg for msg in msgs if msg.command == "REGISTER"][0]
|
|
||||||
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
|
|
||||||
|
|
||||||
def testRegisterSameName(self):
|
|
||||||
"""
|
|
||||||
Requested account name is the same as the nick
|
|
||||||
"""
|
|
||||||
self.connectClient(
|
|
||||||
"bar", name="bar", capabilities=[REGISTER_CAP_NAME], skip_if_cap_nak=True
|
|
||||||
)
|
|
||||||
self.sendLine("bar", "CAP LS 302")
|
|
||||||
caps = self.getCapLs("bar")
|
|
||||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
|
||||||
self.assertNotIn("email-required", (caps[REGISTER_CAP_NAME] or "").split(","))
|
|
||||||
self.sendLine("bar", "REGISTER bar * shivarampassphrase")
|
|
||||||
msgs = self.getMessages("bar")
|
|
||||||
register_response = [msg for msg in msgs if msg.command == "REGISTER"][0]
|
|
||||||
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
|
|
||||||
|
|
||||||
def testRegisterDifferentName(self):
|
|
||||||
"""
|
|
||||||
Requested account name differs from the nick
|
|
||||||
"""
|
|
||||||
self.connectClient(
|
|
||||||
"bar", name="bar", capabilities=[REGISTER_CAP_NAME], skip_if_cap_nak=True
|
|
||||||
)
|
|
||||||
self.sendLine("bar", "CAP LS 302")
|
|
||||||
caps = self.getCapLs("bar")
|
|
||||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
|
||||||
self.assertNotIn("email-required", (caps[REGISTER_CAP_NAME] or "").split(","))
|
|
||||||
self.sendLine("bar", "REGISTER foo * shivarampassphrase")
|
|
||||||
if "custom-account-name" in (caps[REGISTER_CAP_NAME] or "").split(","):
|
|
||||||
msgs = self.getMessages("bar")
|
|
||||||
register_response = [msg for msg in msgs if msg.command == "REGISTER"][0]
|
|
||||||
self.assertMessageMatch(
|
|
||||||
register_response, params=["SUCCESS", ANYSTR, ANYSTR]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage("bar"),
|
|
||||||
command="FAIL",
|
|
||||||
params=["REGISTER", "ACCOUNT_NAME_MUST_BE_NICK", "foo", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
@cases.mark_services
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
|
class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(
|
return cases.TestCaseControllerConfig(
|
||||||
account_registration_requires_email=False,
|
ergo_config=lambda config: config["accounts"]["registration"].update(
|
||||||
account_registration_before_connect=True,
|
{"allow-before-connect": True}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBeforeConnect(self):
|
def testBeforeConnect(self):
|
||||||
@ -86,7 +26,7 @@ class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
|
|||||||
self.sendLine("bar", "CAP LS 302")
|
self.sendLine("bar", "CAP LS 302")
|
||||||
caps = self.getCapLs("bar")
|
caps = self.getCapLs("bar")
|
||||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
self.assertIn(REGISTER_CAP_NAME, caps)
|
||||||
self.assertIn("before-connect", caps[REGISTER_CAP_NAME] or "")
|
self.assertIn("before-connect", caps[REGISTER_CAP_NAME])
|
||||||
self.sendLine("bar", "NICK bar")
|
self.sendLine("bar", "NICK bar")
|
||||||
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
||||||
msgs = self.getMessages("bar")
|
msgs = self.getMessages("bar")
|
||||||
@ -100,8 +40,9 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(
|
return cases.TestCaseControllerConfig(
|
||||||
account_registration_requires_email=False,
|
ergo_config=lambda config: config["accounts"]["registration"].update(
|
||||||
account_registration_before_connect=False,
|
{"allow-before-connect": False}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBeforeConnect(self):
|
def testBeforeConnect(self):
|
||||||
@ -110,7 +51,7 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
|||||||
self.sendLine("bar", "CAP LS 302")
|
self.sendLine("bar", "CAP LS 302")
|
||||||
caps = self.getCapLs("bar")
|
caps = self.getCapLs("bar")
|
||||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
self.assertIn(REGISTER_CAP_NAME, caps)
|
||||||
self.assertNotIn("before-connect", caps[REGISTER_CAP_NAME] or "")
|
self.assertEqual(caps[REGISTER_CAP_NAME], None)
|
||||||
self.sendLine("bar", "NICK bar")
|
self.sendLine("bar", "NICK bar")
|
||||||
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
||||||
msgs = self.getMessages("bar")
|
msgs = self.getMessages("bar")
|
||||||
@ -123,12 +64,21 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@cases.mark_services
|
@cases.mark_services
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
class RegisterEmailVerifiedBeforeConnectTestCase(cases.BaseServerTestCase):
|
class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(
|
return cases.TestCaseControllerConfig(
|
||||||
account_registration_requires_email=True,
|
ergo_config=lambda config: config["accounts"]["registration"].update(
|
||||||
account_registration_before_connect=True,
|
{
|
||||||
|
"email-verification": {
|
||||||
|
"enabled": True,
|
||||||
|
"sender": "test@example.com",
|
||||||
|
"require-tls": True,
|
||||||
|
"helo-domain": "example.com",
|
||||||
|
},
|
||||||
|
"allow-before-connect": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBeforeConnect(self):
|
def testBeforeConnect(self):
|
||||||
@ -139,8 +89,10 @@ class RegisterEmailVerifiedBeforeConnectTestCase(cases.BaseServerTestCase):
|
|||||||
self.sendLine("bar", "CAP LS 302")
|
self.sendLine("bar", "CAP LS 302")
|
||||||
caps = self.getCapLs("bar")
|
caps = self.getCapLs("bar")
|
||||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
self.assertIn(REGISTER_CAP_NAME, caps)
|
||||||
self.assertIn("email-required", caps[REGISTER_CAP_NAME] or "")
|
self.assertEqual(
|
||||||
self.assertIn("before-connect", caps[REGISTER_CAP_NAME] or "")
|
set(caps[REGISTER_CAP_NAME].split(",")),
|
||||||
|
{"before-connect", "email-required"},
|
||||||
|
)
|
||||||
self.sendLine("bar", "NICK bar")
|
self.sendLine("bar", "NICK bar")
|
||||||
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
||||||
msgs = self.getMessages("bar")
|
msgs = self.getMessages("bar")
|
||||||
@ -149,25 +101,10 @@ class RegisterEmailVerifiedBeforeConnectTestCase(cases.BaseServerTestCase):
|
|||||||
fail_response, params=["REGISTER", "INVALID_EMAIL", ANYSTR, ANYSTR]
|
fail_response, params=["REGISTER", "INVALID_EMAIL", ANYSTR, ANYSTR]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
class RegisterEmailVerifiedAfterConnectTestCase(cases.BaseServerTestCase):
|
|
||||||
@staticmethod
|
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
|
||||||
return cases.TestCaseControllerConfig(
|
|
||||||
account_registration_before_connect=False,
|
|
||||||
account_registration_requires_email=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def testAfterConnect(self):
|
def testAfterConnect(self):
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
"bar", name="bar", capabilities=[REGISTER_CAP_NAME], skip_if_cap_nak=True
|
"bar", name="bar", capabilities=[REGISTER_CAP_NAME], skip_if_cap_nak=True
|
||||||
)
|
)
|
||||||
self.sendLine("bar", "CAP LS 302")
|
|
||||||
caps = self.getCapLs("bar")
|
|
||||||
self.assertIn(REGISTER_CAP_NAME, caps)
|
|
||||||
self.assertIn("email-required", caps[REGISTER_CAP_NAME] or "")
|
|
||||||
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
self.sendLine("bar", "REGISTER * * shivarampassphrase")
|
||||||
msgs = self.getMessages("bar")
|
msgs = self.getMessages("bar")
|
||||||
fail_response = [msg for msg in msgs if msg.command == "FAIL"][0]
|
fail_response = [msg for msg in msgs if msg.command == "FAIL"][0]
|
||||||
@ -182,8 +119,9 @@ class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(
|
return cases.TestCaseControllerConfig(
|
||||||
account_registration_requires_email=False,
|
ergo_config=lambda config: config["accounts"]["registration"].update(
|
||||||
account_registration_before_connect=True,
|
{"allow-before-connect": True}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBeforeConnect(self):
|
def testBeforeConnect(self):
|
||||||
|
@ -86,10 +86,10 @@ class BufferingTestCase(cases.BaseServerTestCase):
|
|||||||
if messages and ERR_INPUTTOOLONG in (m.command for m in messages):
|
if messages and ERR_INPUTTOOLONG in (m.command for m in messages):
|
||||||
# https://defs.ircdocs.horse/defs/numerics.html#err-inputtoolong-417
|
# https://defs.ircdocs.horse/defs/numerics.html#err-inputtoolong-417
|
||||||
self.assertGreater(
|
self.assertGreater(
|
||||||
len((line + payload + "\r\n").encode()),
|
len(line + payload + "\r\n"),
|
||||||
512 - overhead,
|
512 - overhead,
|
||||||
"Got ERR_INPUTTOOLONG for a message that should fit "
|
"Got ERR_INPUTTOOLONG for a messag that should fit "
|
||||||
"within 512 characters.",
|
"withing 512 characters.",
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -125,24 +125,11 @@ class BufferingTestCase(cases.BaseServerTestCase):
|
|||||||
f"expected payload to be a prefix of {payload!r}, "
|
f"expected payload to be a prefix of {payload!r}, "
|
||||||
f"but got {payload!r}",
|
f"but got {payload!r}",
|
||||||
)
|
)
|
||||||
if self.controller.software_name == "Ergo":
|
|
||||||
self.assertTrue(
|
|
||||||
payload_intact,
|
|
||||||
f"Ergo should not truncate messages: {repr(line + payload)}, {repr(received_line)}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_overhead(self, client1, client2, colon):
|
def get_overhead(self, client1, client2, colon):
|
||||||
"""Compute the overhead added to client1's message:
|
self.sendLine(client1, f"PRIVMSG nick2 {colon}a\r\n")
|
||||||
PRIVMSG nick2 a\r\n
|
|
||||||
:nick1!~user@host PRIVMSG nick2 :a\r\n
|
|
||||||
So typically client1's NUH length plus either 2 or 3 bytes
|
|
||||||
(the initial colon, the space between source and command, and possibly
|
|
||||||
a colon preceding the trailing).
|
|
||||||
"""
|
|
||||||
outgoing = f"PRIVMSG nick2 {colon}a\r\n"
|
|
||||||
self.sendLine(client1, outgoing)
|
|
||||||
line = self._getLine(client2)
|
line = self._getLine(client2)
|
||||||
return len(line) - len(outgoing.encode())
|
return len(line) - len(f"PRIVMSG nick2 {colon}a\r\n")
|
||||||
|
|
||||||
def _getLine(self, client) -> bytes:
|
def _getLine(self, client) -> bytes:
|
||||||
line = b""
|
line = b""
|
||||||
|
@ -56,6 +56,10 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.xfailIfSoftware(
|
||||||
|
["Sable"],
|
||||||
|
"does not support multi-prefix",
|
||||||
|
)
|
||||||
def testReqOne(self):
|
def testReqOne(self):
|
||||||
"""Tests requesting a single capability"""
|
"""Tests requesting a single capability"""
|
||||||
self.addClient(1)
|
self.addClient(1)
|
||||||
@ -89,7 +93,7 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.xfailIfSoftware(
|
@cases.xfailIfSoftware(
|
||||||
["ngIRCd"],
|
["ngIRCd", "Sable"],
|
||||||
"does not support userhost-in-names",
|
"does not support userhost-in-names",
|
||||||
)
|
)
|
||||||
def testReqTwo(self):
|
def testReqTwo(self):
|
||||||
@ -131,7 +135,7 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.xfailIfSoftware(
|
@cases.xfailIfSoftware(
|
||||||
["ngIRCd"],
|
["ngIRCd", "Sable"],
|
||||||
"does not support userhost-in-names",
|
"does not support userhost-in-names",
|
||||||
)
|
)
|
||||||
def testReqOneThenOne(self):
|
def testReqOneThenOne(self):
|
||||||
@ -183,7 +187,7 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.xfailIfSoftware(
|
@cases.xfailIfSoftware(
|
||||||
["ngIRCd"],
|
["ngIRCd", "Sable"],
|
||||||
"does not support userhost-in-names",
|
"does not support userhost-in-names",
|
||||||
)
|
)
|
||||||
def testReqPostRegistration(self):
|
def testReqPostRegistration(self):
|
||||||
|
@ -58,16 +58,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(chathistory=True)
|
return cases.TestCaseControllerConfig(chathistory=True)
|
||||||
|
|
||||||
def _supports_msgid(self):
|
|
||||||
return "msgid" in self.server_support.get(
|
|
||||||
"MSGREFTYPES", "msgid,timestamp"
|
|
||||||
).split(",")
|
|
||||||
|
|
||||||
def _supports_timestamp(self):
|
|
||||||
return "timestamp" in self.server_support.get(
|
|
||||||
"MSGREFTYPES", "msgid,timestamp"
|
|
||||||
).split(",")
|
|
||||||
|
|
||||||
@skip_ngircd
|
@skip_ngircd
|
||||||
def testInvalidTargets(self):
|
def testInvalidTargets(self):
|
||||||
bar, pw = random_name("bar"), random_name("pw")
|
bar, pw = random_name("bar"), random_name("pw")
|
||||||
@ -470,7 +460,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[-1:], result)
|
self.assertEqual(echo_messages[-1:], result)
|
||||||
|
|
||||||
if self._supports_msgid():
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY LATEST %s msgid=%s %d"
|
"CHATHISTORY LATEST %s msgid=%s %d"
|
||||||
@ -479,7 +468,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[5:], result)
|
self.assertEqual(echo_messages[5:], result)
|
||||||
|
|
||||||
if self._supports_timestamp():
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY LATEST %s timestamp=%s %d"
|
"CHATHISTORY LATEST %s timestamp=%s %d"
|
||||||
@ -490,7 +478,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
def _validate_chathistory_BEFORE(self, echo_messages, user, chname):
|
def _validate_chathistory_BEFORE(self, echo_messages, user, chname):
|
||||||
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
||||||
if self._supports_msgid():
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BEFORE %s msgid=%s %d"
|
"CHATHISTORY BEFORE %s msgid=%s %d"
|
||||||
@ -499,7 +486,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[:6], result)
|
self.assertEqual(echo_messages[:6], result)
|
||||||
|
|
||||||
if self._supports_timestamp():
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
||||||
@ -518,7 +504,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
def _validate_chathistory_AFTER(self, echo_messages, user, chname):
|
def _validate_chathistory_AFTER(self, echo_messages, user, chname):
|
||||||
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
||||||
if self._supports_msgid():
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AFTER %s msgid=%s %d"
|
"CHATHISTORY AFTER %s msgid=%s %d"
|
||||||
@ -527,7 +512,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[4:], result)
|
self.assertEqual(echo_messages[4:], result)
|
||||||
|
|
||||||
if self._supports_timestamp():
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AFTER %s timestamp=%s %d"
|
"CHATHISTORY AFTER %s timestamp=%s %d"
|
||||||
@ -538,15 +522,13 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AFTER %s timestamp=%s %d"
|
"CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3),
|
||||||
% (chname, echo_messages[3].time, 3),
|
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[4:7], result)
|
self.assertEqual(echo_messages[4:7], result)
|
||||||
|
|
||||||
def _validate_chathistory_BETWEEN(self, echo_messages, user, chname):
|
def _validate_chathistory_BETWEEN(self, echo_messages, user, chname):
|
||||||
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
||||||
if self._supports_msgid():
|
|
||||||
# BETWEEN forwards and backwards
|
# BETWEEN forwards and backwards
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
@ -592,29 +574,18 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[-4:-1], result)
|
self.assertEqual(echo_messages[-4:-1], result)
|
||||||
|
|
||||||
if self._supports_timestamp():
|
|
||||||
# same stuff again but with timestamps
|
# same stuff again but with timestamps
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
% (
|
% (chname, echo_messages[0].time, echo_messages[-1].time, INCLUSIVE_LIMIT),
|
||||||
chname,
|
|
||||||
echo_messages[0].time,
|
|
||||||
echo_messages[-1].time,
|
|
||||||
INCLUSIVE_LIMIT,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
% (
|
% (chname, echo_messages[-1].time, echo_messages[0].time, INCLUSIVE_LIMIT),
|
||||||
chname,
|
|
||||||
echo_messages[-1].time,
|
|
||||||
echo_messages[0].time,
|
|
||||||
INCLUSIVE_LIMIT,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
@ -634,24 +605,20 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertEqual(echo_messages[-4:-1], result)
|
self.assertEqual(echo_messages[-4:-1], result)
|
||||||
|
|
||||||
def _validate_chathistory_AROUND(self, echo_messages, user, chname):
|
def _validate_chathistory_AROUND(self, echo_messages, user, chname):
|
||||||
if self._supports_msgid():
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AROUND %s msgid=%s %d"
|
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1),
|
||||||
% (chname, echo_messages[7].msgid, 1),
|
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual([echo_messages[7]], result)
|
self.assertEqual([echo_messages[7]], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AROUND %s msgid=%s %d"
|
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3),
|
||||||
% (chname, echo_messages[7].msgid, 3),
|
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[6:9], result)
|
self.assertEqual(echo_messages[6:9], result)
|
||||||
|
|
||||||
if self._supports_timestamp():
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AROUND %s timestamp=%s %d"
|
"CHATHISTORY AROUND %s timestamp=%s %d"
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
from irctest import cases
|
|
||||||
from irctest.numerics import RPL_CHANNELCREATED, RPL_CHANNELMODEIS
|
|
||||||
from irctest.patma import ANYSTR, ListRemainder, StrRe
|
|
||||||
|
|
||||||
|
|
||||||
class RplChannelModeIsTestCase(cases.BaseServerTestCase):
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testChannelModeIs(self):
|
|
||||||
"""Test RPL_CHANNELMODEIS and RPL_CHANNELCREATED as responses to
|
|
||||||
`MODE #channel`:
|
|
||||||
<https://modern.ircdocs.horse/#rplcreationtime-329>
|
|
||||||
<https://modern.ircdocs.horse/#rplchannelmodeis-324>
|
|
||||||
"""
|
|
||||||
expected_numerics = {RPL_CHANNELMODEIS, RPL_CHANNELCREATED}
|
|
||||||
if self.controller.software_name in ("irc2", "Sable"):
|
|
||||||
# irc2 and Sable don't use timestamps for conflict resolution,
|
|
||||||
# consequently they don't store the channel creation timestamp
|
|
||||||
# and don't send RPL_CHANNELCREATED
|
|
||||||
expected_numerics = {RPL_CHANNELMODEIS}
|
|
||||||
|
|
||||||
self.connectClient("chanop", name="chanop")
|
|
||||||
self.joinChannel("chanop", "#chan")
|
|
||||||
# i, n, and t are specified by RFC1459; some of them may be on by default,
|
|
||||||
# but after this, at least those three should be enabled:
|
|
||||||
self.sendLine("chanop", "MODE #chan +int")
|
|
||||||
self.getMessages("chanop")
|
|
||||||
|
|
||||||
self.sendLine("chanop", "MODE #chan")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
self.assertEqual(expected_numerics, {msg.command for msg in messages})
|
|
||||||
for message in messages:
|
|
||||||
if message.command == RPL_CHANNELMODEIS:
|
|
||||||
# the final parameters are the mode string (e.g. `+int`),
|
|
||||||
# and then optionally any mode parameters (in case the ircd
|
|
||||||
# lists a mode that takes a parameter)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
message,
|
|
||||||
command=RPL_CHANNELMODEIS,
|
|
||||||
params=["chanop", "#chan", ListRemainder(ANYSTR, min_length=1)],
|
|
||||||
)
|
|
||||||
final_param = message.params[2]
|
|
||||||
self.assertEqual(final_param[0], "+")
|
|
||||||
enabled_modes = list(final_param[1:])
|
|
||||||
break
|
|
||||||
|
|
||||||
self.assertLessEqual({"i", "n", "t"}, set(enabled_modes))
|
|
||||||
|
|
||||||
# remove all the modes listed by RPL_CHANNELMODEIS
|
|
||||||
self.sendLine("chanop", f"MODE #chan -{''.join(enabled_modes)}")
|
|
||||||
response = self.getMessage("chanop")
|
|
||||||
# we should get something like: MODE #chan -int
|
|
||||||
self.assertMessageMatch(
|
|
||||||
response, command="MODE", params=["#chan", StrRe("^-.*")]
|
|
||||||
)
|
|
||||||
self.assertEqual(set(response.params[1][1:]), set(enabled_modes))
|
|
||||||
|
|
||||||
self.sendLine("chanop", "MODE #chan")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
self.assertEqual(expected_numerics, {msg.command for msg in messages})
|
|
||||||
# all modes have been disabled; the correct representation of this is `+`
|
|
||||||
for message in messages:
|
|
||||||
if message.command == RPL_CHANNELMODEIS:
|
|
||||||
self.assertMessageMatch(
|
|
||||||
message,
|
|
||||||
command=RPL_CHANNELMODEIS,
|
|
||||||
params=["chanop", "#chan", "+"],
|
|
||||||
)
|
|
@ -1,159 +0,0 @@
|
|||||||
from irctest import cases
|
|
||||||
from irctest.numerics import (
|
|
||||||
ERR_CHANOPRIVSNEEDED,
|
|
||||||
ERR_NOSUCHCHANNEL,
|
|
||||||
ERR_NOSUCHNICK,
|
|
||||||
ERR_NOTONCHANNEL,
|
|
||||||
ERR_USERNOTINCHANNEL,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelOperatorModeTestCase(cases.BaseServerTestCase):
|
|
||||||
"""Test various error and success cases around the channel operator mode:
|
|
||||||
<https://modern.ircdocs.horse/#channel-operators>
|
|
||||||
<https://modern.ircdocs.horse/#mode-message>
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setupNicks(self):
|
|
||||||
"""Set up a standard set of three nicknames and two channels
|
|
||||||
for testing channel-user MODE interactions."""
|
|
||||||
# first nick to join the channel is privileged:
|
|
||||||
self.connectClient("chanop", name="chanop")
|
|
||||||
self.joinChannel("chanop", "#chan")
|
|
||||||
|
|
||||||
self.connectClient("unprivileged", name="unprivileged")
|
|
||||||
self.joinChannel("unprivileged", "#chan")
|
|
||||||
self.getMessages("chanop")
|
|
||||||
|
|
||||||
self.connectClient("unrelated", name="unrelated")
|
|
||||||
self.joinChannel("unrelated", "#unrelated")
|
|
||||||
self.joinChannel("unprivileged", "#unrelated")
|
|
||||||
self.getMessages("unrelated")
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.xfailIfSoftware(["irc2"], "broken in irc2")
|
|
||||||
def testChannelOperatorModeSenderPrivsNeeded(self):
|
|
||||||
"""Test that +o from a channel member without the necessary privileges
|
|
||||||
fails as expected."""
|
|
||||||
self.setupNicks()
|
|
||||||
# sender is a channel member but without the necessary privileges:
|
|
||||||
self.sendLine("unprivileged", "MODE #chan +o unprivileged")
|
|
||||||
messages = self.getMessages("unprivileged")
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertMessageMatch(messages[0], command=ERR_CHANOPRIVSNEEDED)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testChannelOperatorModeTargetNotInChannel(self):
|
|
||||||
"""Test that +o targeting a user not present in the channel fails
|
|
||||||
as expected."""
|
|
||||||
self.setupNicks()
|
|
||||||
# sender is a chanop, but target nick is not in the channel:
|
|
||||||
self.sendLine("chanop", "MODE #chan +o unrelated")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertMessageMatch(messages[0], command=ERR_USERNOTINCHANNEL)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testChannelOperatorModeTargetDoesNotExist(self):
|
|
||||||
"""Test that +o targeting a nonexistent nick fails as expected."""
|
|
||||||
self.setupNicks()
|
|
||||||
# sender is a chanop, but target nick does not exist:
|
|
||||||
self.sendLine("chanop", "MODE #chan +o nobody")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
# ERR_NOSUCHNICK is typical, Bahamut additionally sends ERR_USERNOTINCHANNEL
|
|
||||||
if self.controller.software_name != "Bahamut":
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertMessageMatch(messages[0], command=ERR_NOSUCHNICK)
|
|
||||||
else:
|
|
||||||
self.assertLessEqual(len(messages), 2)
|
|
||||||
commands = {message.command for message in messages}
|
|
||||||
self.assertLessEqual({ERR_NOSUCHNICK}, commands)
|
|
||||||
self.assertLessEqual(commands, {ERR_NOSUCHNICK, ERR_USERNOTINCHANNEL})
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.xfailIf(
|
|
||||||
lambda self: bool(
|
|
||||||
self.controller.software_name == "UnrealIRCd"
|
|
||||||
and self.controller.software_version == 5
|
|
||||||
),
|
|
||||||
"UnrealIRCd <6.1.7 returns ERR_NOSUCHNICK on non-existent channel",
|
|
||||||
)
|
|
||||||
def testChannelOperatorModeChannelDoesNotExist(self):
|
|
||||||
"""Test that +o targeting a nonexistent channel fails as expected.
|
|
||||||
|
|
||||||
"If <target> is a channel that does not exist on the network,
|
|
||||||
# the ERR_NOSUCHCHANNEL (403) numeric is returned."
|
|
||||||
"""
|
|
||||||
self.setupNicks()
|
|
||||||
# target channel does not exist, but target nick does:
|
|
||||||
self.sendLine("chanop", "MODE #nonexistentchan +o chanop")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertMessageMatch(messages[0], command=ERR_NOSUCHCHANNEL)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.xfailIf(
|
|
||||||
lambda self: bool(
|
|
||||||
self.controller.software_name == "UnrealIRCd"
|
|
||||||
and self.controller.software_version == 5
|
|
||||||
),
|
|
||||||
"UnrealIRCd <6.1.7 returns ERR_NOSUCHNICK on non-existent channel",
|
|
||||||
)
|
|
||||||
def testChannelOperatorModeChannelAndTargetDoNotExist(self):
|
|
||||||
"""Test that +o targeting a nonexistent channel and nickname
|
|
||||||
fails as expected."""
|
|
||||||
self.setupNicks()
|
|
||||||
# neither target channel nor target nick exist:
|
|
||||||
self.sendLine("chanop", "MODE #nonexistentchan +o nobody")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertIn(
|
|
||||||
messages[0].command,
|
|
||||||
[ERR_NOSUCHCHANNEL, ERR_NOTONCHANNEL, ERR_USERNOTINCHANNEL],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testChannelOperatorModeSenderNonMember(self):
|
|
||||||
"""Test that +o where the sender is not a channel member
|
|
||||||
fails as expected."""
|
|
||||||
self.setupNicks()
|
|
||||||
# sender is not a channel member, target nick exists and is a channel member:
|
|
||||||
self.sendLine("chanop", "MODE #unrelated +o unprivileged")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertIn(messages[0].command, [ERR_NOTONCHANNEL, ERR_CHANOPRIVSNEEDED])
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testChannelOperatorModeSenderAndTargetNonMembers(self):
|
|
||||||
"""Test that +o where neither the sender nor the target is a channel
|
|
||||||
member fails as expected."""
|
|
||||||
self.setupNicks()
|
|
||||||
# sender is not a channel member, target nick exists but is not a channel member:
|
|
||||||
self.sendLine("chanop", "MODE #unrelated +o chanop")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertIn(
|
|
||||||
messages[0].command,
|
|
||||||
[ERR_NOTONCHANNEL, ERR_CHANOPRIVSNEEDED, ERR_USERNOTINCHANNEL],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testChannelOperatorModeSuccess(self):
|
|
||||||
"""Tests a successful grant of +o in a channel."""
|
|
||||||
self.setupNicks()
|
|
||||||
|
|
||||||
self.sendLine("chanop", "MODE #chan +o unprivileged")
|
|
||||||
messages = self.getMessages("chanop")
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[0],
|
|
||||||
command="MODE",
|
|
||||||
params=["#chan", "+o", "unprivileged"],
|
|
||||||
)
|
|
||||||
messages = self.getMessages("unprivileged")
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[0],
|
|
||||||
command="MODE",
|
|
||||||
params=["#chan", "+o", "unprivileged"],
|
|
||||||
)
|
|
@ -12,8 +12,8 @@ class ConfusablesTestCase(cases.BaseServerTestCase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(
|
return cases.TestCaseControllerConfig(
|
||||||
ergo_config=lambda config: config["server"].update(
|
ergo_config=lambda config: config["accounts"].update(
|
||||||
{"casemapping": "precis"},
|
{"nick-reservation": {"enabled": True, "method": "strict"}}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import time
|
|||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.client_mock import ConnectionClosed
|
from irctest.client_mock import ConnectionClosed
|
||||||
from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_PASSWDMISMATCH
|
from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_PASSWDMISMATCH
|
||||||
from irctest.patma import ANYLIST, ANYSTR, OptStrRe, StrRe
|
from irctest.patma import ANYSTR, StrRe
|
||||||
|
|
||||||
|
|
||||||
class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
||||||
@ -85,92 +85,6 @@ class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
||||||
def testConnectionRegistration(self):
|
|
||||||
self.addClient()
|
|
||||||
self.sendLine(1, "NICK foo")
|
|
||||||
self.sendLine(1, "USER foo * * :foo")
|
|
||||||
|
|
||||||
for numeric in ("001", "002", "003"):
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getRegistrationMessage(1),
|
|
||||||
command=numeric,
|
|
||||||
params=["foo", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getRegistrationMessage(1),
|
|
||||||
command="004", # RPL_MYINFO
|
|
||||||
params=[
|
|
||||||
"foo",
|
|
||||||
"My.Little.Server",
|
|
||||||
ANYSTR, # version
|
|
||||||
StrRe("[a-zA-Z]+"), # user modes
|
|
||||||
StrRe("[a-zA-Z]+"), # channel modes
|
|
||||||
OptStrRe("[a-zA-Z]+"), # channel modes with parameter
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# ISUPPORT
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
while True:
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="005",
|
|
||||||
params=["foo", *ANYLIST],
|
|
||||||
)
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
if m.command != "005":
|
|
||||||
break
|
|
||||||
|
|
||||||
if m.command in ("042", "396"): # RPL_YOURID / RPL_VISIBLEHOST, non-standard
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
|
|
||||||
# LUSERS
|
|
||||||
while m.command in ("250", "251", "252", "253", "254", "255", "265", "266"):
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
|
|
||||||
if m.command == "375": # RPL_MOTDSTART
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="375",
|
|
||||||
params=["foo", ANYSTR],
|
|
||||||
)
|
|
||||||
while (m := self.getRegistrationMessage(1)).command == "372":
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="372", # RPL_MOTD
|
|
||||||
params=["foo", ANYSTR],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="376", # RPL_ENDOFMOTD
|
|
||||||
params=["foo", ANYSTR],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="422", # ERR_NOMOTD
|
|
||||||
params=["foo", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
# User mode
|
|
||||||
if m.command == "MODE":
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="MODE",
|
|
||||||
params=["foo", ANYSTR, *ANYLIST],
|
|
||||||
)
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
elif m.command == "221": # RPL_UMODEIS
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="221",
|
|
||||||
params=["foo", ANYSTR, *ANYLIST],
|
|
||||||
)
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
else:
|
|
||||||
print("Warning: missing MODE")
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459")
|
@cases.mark_specifications("RFC1459")
|
||||||
def testQuitDisconnects(self):
|
def testQuitDisconnects(self):
|
||||||
"""“The server must close the connection to a client which sends a
|
"""“The server must close the connection to a client which sends a
|
||||||
|
@ -9,38 +9,6 @@ from irctest import cases, runner
|
|||||||
|
|
||||||
|
|
||||||
class IsupportTestCase(cases.BaseServerTestCase):
|
class IsupportTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.mark_isupport("PREFIX")
|
|
||||||
def testParameters(self):
|
|
||||||
"""https://modern.ircdocs.horse/#rplisupport-005"""
|
|
||||||
|
|
||||||
# <https://modern.ircdocs.horse/#connection-registration>
|
|
||||||
# "Upon successful completion of the registration process,
|
|
||||||
# the server MUST send, in this order:
|
|
||||||
# [...]
|
|
||||||
# 5. at least one RPL_ISUPPORT (005) numeric to the client."
|
|
||||||
welcome_005s = [
|
|
||||||
msg for msg in self.connectClient("foo") if msg.command == "005"
|
|
||||||
]
|
|
||||||
self.assertGreaterEqual(len(welcome_005s), 1)
|
|
||||||
for msg in welcome_005s:
|
|
||||||
# first parameter is the client's nickname;
|
|
||||||
# last parameter is a human-readable trailing, typically
|
|
||||||
# "are supported by this server"
|
|
||||||
self.assertGreaterEqual(len(msg.params), 3)
|
|
||||||
self.assertEqual(msg.params[0], "foo")
|
|
||||||
# "As the maximum number of message parameters to any reply is 15,
|
|
||||||
# the maximum number of RPL_ISUPPORT tokens that can be advertised
|
|
||||||
# is 13."
|
|
||||||
self.assertLessEqual(len(msg.params), 15)
|
|
||||||
for param in msg.params[1:-1]:
|
|
||||||
self.validateIsupportParam(param)
|
|
||||||
|
|
||||||
def validateIsupportParam(self, param):
|
|
||||||
if not param.isascii():
|
|
||||||
raise ValueError("Invalid non-ASCII 005 parameter", param)
|
|
||||||
# TODO add more validation
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
@cases.mark_isupport("PREFIX")
|
@cases.mark_isupport("PREFIX")
|
||||||
def testPrefix(self):
|
def testPrefix(self):
|
||||||
@ -56,8 +24,7 @@ class IsupportTestCase(cases.BaseServerTestCase):
|
|||||||
return
|
return
|
||||||
|
|
||||||
m = re.match(
|
m = re.match(
|
||||||
r"^\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)$",
|
r"\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)", self.server_support["PREFIX"]
|
||||||
self.server_support["PREFIX"],
|
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
m,
|
m,
|
||||||
@ -118,5 +85,5 @@ class IsupportTestCase(cases.BaseServerTestCase):
|
|||||||
parts = self.server_support["TARGMAX"].split(",")
|
parts = self.server_support["TARGMAX"].split(",")
|
||||||
for part in parts:
|
for part in parts:
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
re.match("^[A-Z]+:[0-9]*$", part), "Invalid TARGMAX key:value: %r", part
|
re.match("[A-Z]+:[0-9]*", part), "Invalid TARGMAX key:value: %r", part
|
||||||
)
|
)
|
||||||
|
@ -6,6 +6,7 @@ The JOIN command (`RFC 1459
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases, runner
|
||||||
|
from irctest.irc_utils import ambiguities
|
||||||
from irctest.numerics import (
|
from irctest.numerics import (
|
||||||
ERR_BADCHANMASK,
|
ERR_BADCHANMASK,
|
||||||
ERR_FORBIDDENCHANNEL,
|
ERR_FORBIDDENCHANNEL,
|
||||||
@ -60,7 +61,6 @@ class JoinTestCase(cases.BaseServerTestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.xfailIfSoftware(["Bahamut", "irc2"], "trailing space on RPL_NAMREPLY")
|
|
||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
def testJoinNamreply(self):
|
def testJoinNamreply(self):
|
||||||
"""“353 RPL_NAMREPLY
|
"""“353 RPL_NAMREPLY
|
||||||
@ -75,23 +75,33 @@ class JoinTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
for m in self.getMessages(1):
|
for m in self.getMessages(1):
|
||||||
if m.command == "353":
|
if m.command == "353":
|
||||||
self.assertMessageMatch(
|
self.assertIn(
|
||||||
m, params=["foo", StrRe(r"[=\*@]"), "#chan", StrRe("[@+]?foo")]
|
len(m.params),
|
||||||
)
|
(3, 4),
|
||||||
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.sendLine(2, "JOIN #chan")
|
|
||||||
|
|
||||||
for m in self.getMessages(2):
|
|
||||||
if m.command == "353":
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
m,
|
||||||
params=[
|
fail_msg="RPL_NAM_REPLY with number of arguments "
|
||||||
"bar",
|
"<3 or >4: {msg}",
|
||||||
StrRe(r"[=\*@]"),
|
)
|
||||||
|
params = ambiguities.normalize_namreply_params(m.params)
|
||||||
|
self.assertIn(
|
||||||
|
params[1],
|
||||||
|
"=*@",
|
||||||
|
m,
|
||||||
|
fail_msg="Bad channel prefix: {item} not in {list}: {msg}",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
params[2],
|
||||||
"#chan",
|
"#chan",
|
||||||
StrRe("([@+]?foo bar|bar [@+]?foo)"),
|
m,
|
||||||
],
|
fail_msg="Bad channel name: {got} instead of " "{expects}: {msg}",
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
params[3],
|
||||||
|
{"foo", "@foo", "+foo"},
|
||||||
|
m,
|
||||||
|
fail_msg="Bad user list: should contain only user "
|
||||||
|
'"foo" with an optional "+" or "@" prefix, but got: '
|
||||||
|
"{msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def testJoinTwice(self):
|
def testJoinTwice(self):
|
||||||
@ -105,8 +115,34 @@ class JoinTestCase(cases.BaseServerTestCase):
|
|||||||
# if the join is successful, or has an error among the given set.
|
# if the join is successful, or has an error among the given set.
|
||||||
for m in self.getMessages(1):
|
for m in self.getMessages(1):
|
||||||
if m.command == "353":
|
if m.command == "353":
|
||||||
self.assertMessageMatch(
|
self.assertIn(
|
||||||
m, params=["foo", StrRe(r"[=\*@]"), "#chan", StrRe("[@+]?foo")]
|
len(m.params),
|
||||||
|
(3, 4),
|
||||||
|
m,
|
||||||
|
fail_msg="RPL_NAM_REPLY with number of arguments "
|
||||||
|
"<3 or >4: {msg}",
|
||||||
|
)
|
||||||
|
params = ambiguities.normalize_namreply_params(m.params)
|
||||||
|
self.assertIn(
|
||||||
|
params[1],
|
||||||
|
"=*@",
|
||||||
|
m,
|
||||||
|
fail_msg="Bad channel prefix: {item} not in {list}: {msg}",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
params[2],
|
||||||
|
"#chan",
|
||||||
|
m,
|
||||||
|
fail_msg="Bad channel name: {got} instead of " "{expects}: {msg}",
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
params[3],
|
||||||
|
{"foo", "@foo", "+foo"},
|
||||||
|
m,
|
||||||
|
fail_msg='Bad user list after user "foo" joined twice '
|
||||||
|
"the same channel: should contain only user "
|
||||||
|
'"foo" with an optional "+" or "@" prefix, but got: '
|
||||||
|
"{msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def testJoinPartiallyInvalid(self):
|
def testJoinPartiallyInvalid(self):
|
||||||
@ -200,78 +236,3 @@ class JoinTestCase(cases.BaseServerTestCase):
|
|||||||
fail_msg="Expected 1 error when joining channels '#valid' and 'inv@lid', "
|
fail_msg="Expected 1 error when joining channels '#valid' and 'inv@lid', "
|
||||||
"got {got}",
|
"got {got}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
|
||||||
def testJoinKey(self):
|
|
||||||
"""Joins a single channel with a key"""
|
|
||||||
self.connectClient("chanop")
|
|
||||||
self.joinChannel(1, "#chan")
|
|
||||||
self.sendLine(1, "MODE #chan +k key")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.connectClient("joiner")
|
|
||||||
self.sendLine(2, "JOIN #chan key")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(2),
|
|
||||||
command="JOIN",
|
|
||||||
params=["#chan"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
|
||||||
def testJoinKeys(self):
|
|
||||||
"""Joins two channels, both with keys"""
|
|
||||||
self.connectClient("chanop")
|
|
||||||
if self.targmax.get("JOIN", "1000") == "1":
|
|
||||||
raise runner.OptionalExtensionNotSupported("Multi-target JOIN")
|
|
||||||
self.joinChannel(1, "#chan1")
|
|
||||||
self.sendLine(1, "MODE #chan1 +k key1")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.joinChannel(1, "#chan2")
|
|
||||||
self.sendLine(1, "MODE #chan2 +k key2")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.connectClient("joiner")
|
|
||||||
self.sendLine(2, "JOIN #chan1,#chan2 key1,key2")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(2),
|
|
||||||
command="JOIN",
|
|
||||||
params=["#chan1"],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
[
|
|
||||||
msg
|
|
||||||
for msg in self.getMessages(2)
|
|
||||||
if msg.command not in {RPL_NAMREPLY, RPL_ENDOFNAMES}
|
|
||||||
][0],
|
|
||||||
command="JOIN",
|
|
||||||
params=["#chan2"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
|
||||||
def testJoinManySingleKey(self):
|
|
||||||
"""Joins two channels, the first one has a key."""
|
|
||||||
self.connectClient("chanop")
|
|
||||||
if self.targmax.get("JOIN", "1000") == "1":
|
|
||||||
raise runner.OptionalExtensionNotSupported("Multi-target JOIN")
|
|
||||||
self.joinChannel(1, "#chan1")
|
|
||||||
self.sendLine(1, "MODE #chan1 +k key1")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.joinChannel(1, "#chan2")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.connectClient("joiner")
|
|
||||||
self.sendLine(2, "JOIN #chan1,#chan2 key1")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(2),
|
|
||||||
command="JOIN",
|
|
||||||
params=["#chan1"],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
[
|
|
||||||
msg
|
|
||||||
for msg in self.getMessages(2)
|
|
||||||
if msg.command not in {RPL_NAMREPLY, RPL_ENDOFNAMES}
|
|
||||||
][0],
|
|
||||||
command="JOIN",
|
|
||||||
params=["#chan2"],
|
|
||||||
)
|
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
The PRIVMSG and NOTICE commands.
|
The PRIVMSG and NOTICE commands.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_INPUTTOOLONG
|
from irctest.numerics import ERR_INPUTTOOLONG
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
@ -125,30 +123,25 @@ class NoticeTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TagsTestCase(cases.BaseServerTestCase):
|
class TagsTestCase(cases.BaseServerTestCase):
|
||||||
@pytest.mark.parametrize("tag_length", [4096, 10000])
|
|
||||||
@cases.mark_capabilities("message-tags")
|
@cases.mark_capabilities("message-tags")
|
||||||
@cases.xfailIf(
|
@cases.xfailIf(
|
||||||
lambda self, tag_length: bool(
|
lambda self: bool(
|
||||||
self.controller.software_name == "UnrealIRCd"
|
self.controller.software_name == "UnrealIRCd"
|
||||||
and self.controller.software_version == 5
|
and self.controller.software_version == 5
|
||||||
),
|
),
|
||||||
"UnrealIRCd <6.0.7 dropped messages with excessively large tags: "
|
"UnrealIRCd <6.0.7 dropped messages with excessively large tags: "
|
||||||
"https://bugs.unrealircd.org/view.php?id=5947",
|
"https://bugs.unrealircd.org/view.php?id=5947",
|
||||||
)
|
)
|
||||||
def testLineTooLong(self, tag_length):
|
def testLineTooLong(self):
|
||||||
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
|
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
"recver", capabilities=["message-tags"], skip_if_cap_nak=True
|
"recver", capabilities=["message-tags"], skip_if_cap_nak=True
|
||||||
)
|
)
|
||||||
self.joinChannel(1, "#xyz")
|
self.joinChannel(1, "#xyz")
|
||||||
|
monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!"
|
||||||
monsterMessage = (
|
|
||||||
"@+clientOnlyTagExample=" + "a" * tag_length + " PRIVMSG #xyz hi!"
|
|
||||||
)
|
|
||||||
self.sendLine(1, monsterMessage)
|
self.sendLine(1, monsterMessage)
|
||||||
replies = self.getMessages(1)
|
|
||||||
self.assertEqual(self.getMessages(2), [], "overflowing message was relayed")
|
self.assertEqual(self.getMessages(2), [], "overflowing message was relayed")
|
||||||
if len(replies) > 0:
|
replies = self.getMessages(1)
|
||||||
self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies))
|
self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies))
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import pytest
|
|||||||
from irctest import cases, runner
|
from irctest import cases, runner
|
||||||
from irctest.client_mock import NoMessageException
|
from irctest.client_mock import NoMessageException
|
||||||
from irctest.numerics import (
|
from irctest.numerics import (
|
||||||
ERR_ERRONEUSNICKNAME,
|
|
||||||
RPL_ENDOFMONLIST,
|
RPL_ENDOFMONLIST,
|
||||||
RPL_MONLIST,
|
RPL_MONLIST,
|
||||||
RPL_MONOFFLINE,
|
RPL_MONOFFLINE,
|
||||||
@ -191,15 +190,14 @@ class MonitorTestCase(_BaseMonitorTestCase):
|
|||||||
self.check_server_support()
|
self.check_server_support()
|
||||||
self.sendLine(1, "MONITOR + *!username@localhost")
|
self.sendLine(1, "MONITOR + *!username@localhost")
|
||||||
self.sendLine(1, "MONITOR + *!username@127.0.0.1")
|
self.sendLine(1, "MONITOR + *!username@127.0.0.1")
|
||||||
expected_command = StrRe(f"({RPL_MONOFFLINE}|{ERR_ERRONEUSNICKNAME})")
|
|
||||||
try:
|
try:
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(m, command=expected_command)
|
self.assertMessageMatch(m, command="731")
|
||||||
except NoMessageException:
|
except NoMessageException:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(m, command=expected_command)
|
self.assertMessageMatch(m, command="731")
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
try:
|
try:
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
|
@ -24,6 +24,11 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
self.sendLine(1, "NAMES #chan")
|
self.sendLine(1, "NAMES #chan")
|
||||||
reply = self.getMessage(1)
|
reply = self.getMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
reply,
|
||||||
|
command="353",
|
||||||
|
fail_msg="Expected NAMES response (353) with @+foo, got: {msg}",
|
||||||
|
)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
reply,
|
reply,
|
||||||
command="353",
|
command="353",
|
||||||
@ -42,57 +47,9 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
|
|||||||
8,
|
8,
|
||||||
"Expected WHO response (352) with 8 params, got: {msg}".format(msg=msg),
|
"Expected WHO response (352) with 8 params, got: {msg}".format(msg=msg),
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertTrue(
|
||||||
"@+",
|
"@+" in msg.params[6],
|
||||||
msg.params[6],
|
|
||||||
'Expected WHO response (352) with "@+" in param 7, got: {msg}'.format(
|
'Expected WHO response (352) with "@+" in param 7, got: {msg}'.format(
|
||||||
msg=msg
|
msg=msg
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["irc2", "Bahamut"], "irc2 and Bahamut send a trailing space"
|
|
||||||
)
|
|
||||||
def testNoMultiPrefix(self):
|
|
||||||
"""When not requested, only the highest prefix should be sent"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
self.joinChannel(1, "#chan")
|
|
||||||
self.sendLine(1, "MODE #chan +v foo")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
# TODO(dan): Make sure +v is voice
|
|
||||||
|
|
||||||
self.sendLine(1, "NAMES #chan")
|
|
||||||
reply = self.getMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
reply,
|
|
||||||
command="353",
|
|
||||||
params=["foo", ANYSTR, "#chan", "@foo"],
|
|
||||||
fail_msg="Expected NAMES response (353) with @foo, got: {msg}",
|
|
||||||
)
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "WHO #chan")
|
|
||||||
msg = self.getMessage(1)
|
|
||||||
self.assertEqual(
|
|
||||||
msg.command, "352", msg, fail_msg="Expected WHO response (352), got: {msg}"
|
|
||||||
)
|
|
||||||
self.assertGreaterEqual(
|
|
||||||
len(msg.params),
|
|
||||||
8,
|
|
||||||
"Expected WHO response (352) with 8 params, got: {msg}".format(msg=msg),
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"@",
|
|
||||||
msg.params[6],
|
|
||||||
'Expected WHO response (352) with "@" in param 7, got: {msg}'.format(
|
|
||||||
msg=msg
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertNotIn(
|
|
||||||
"+",
|
|
||||||
msg.params[6],
|
|
||||||
'Expected WHO response (352) with no "+" in param 7, got: {msg}'.format(
|
|
||||||
msg=msg
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.patma import ANYDICT, ANYSTR, StrRe
|
from irctest.patma import ANYDICT, StrRe
|
||||||
|
|
||||||
CAP_NAME = "draft/multiline"
|
CAP_NAME = "draft/multiline"
|
||||||
BATCH_TYPE = "draft/multiline"
|
BATCH_TYPE = "draft/multiline"
|
||||||
@ -135,86 +135,3 @@ class MultilineTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertIn("+client-only-tag", fallback_relay[0].tags)
|
self.assertIn("+client-only-tag", fallback_relay[0].tags)
|
||||||
self.assertIn("+client-only-tag", fallback_relay[1].tags)
|
self.assertIn("+client-only-tag", fallback_relay[1].tags)
|
||||||
self.assertEqual(fallback_relay[0].tags["msgid"], msgid)
|
self.assertEqual(fallback_relay[0].tags["msgid"], msgid)
|
||||||
|
|
||||||
@cases.mark_capabilities("draft/multiline")
|
|
||||||
def testInvalidBatchTag(self):
|
|
||||||
"""Test that an unexpected change of batch tag results in
|
|
||||||
FAIL BATCH MULTILINE_INVALID."""
|
|
||||||
|
|
||||||
self.connectClient(
|
|
||||||
"alice", capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=True
|
|
||||||
)
|
|
||||||
self.joinChannel(1, "#test")
|
|
||||||
|
|
||||||
# invalid batch tag:
|
|
||||||
self.sendLine(1, "BATCH +123 %s #test" % (BATCH_TYPE,))
|
|
||||||
self.sendLine(1, "@batch=231 PRIVMSG #test :hi")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command="FAIL",
|
|
||||||
params=["BATCH", "MULTILINE_INVALID", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_capabilities("draft/multiline")
|
|
||||||
def testInvalidBlankConcatTag(self):
|
|
||||||
"""Test that the concat tag on a blank message results in
|
|
||||||
FAIL BATCH MULTILINE_INVALID."""
|
|
||||||
|
|
||||||
self.connectClient(
|
|
||||||
"alice", capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=True
|
|
||||||
)
|
|
||||||
self.joinChannel(1, "#test")
|
|
||||||
|
|
||||||
# cannot send the concat tag with a blank message:
|
|
||||||
self.sendLine(1, "BATCH +123 %s #test" % (BATCH_TYPE,))
|
|
||||||
self.sendLine(1, "@batch=123 PRIVMSG #test :hi")
|
|
||||||
self.sendLine(1, "@batch=123;%s PRIVMSG #test :" % (CONCAT_TAG,))
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command="FAIL",
|
|
||||||
params=["BATCH", "MULTILINE_INVALID", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Ergo")
|
|
||||||
def testLineLimit(self):
|
|
||||||
"""This is an Ergo-specific test for line limit enforcement
|
|
||||||
in multiline messages. Right now it hardcodes the same limits as in
|
|
||||||
the Ergo controller; we can generalize it in future for other multiline
|
|
||||||
implementations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.connectClient(
|
|
||||||
"alice", capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=False
|
|
||||||
)
|
|
||||||
self.joinChannel(1, "#test")
|
|
||||||
|
|
||||||
# line limit exceeded
|
|
||||||
self.sendLine(1, "BATCH +123 %s #test" % (BATCH_TYPE,))
|
|
||||||
for i in range(33):
|
|
||||||
self.sendLine(1, "@batch=123 PRIVMSG #test hi")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command="FAIL",
|
|
||||||
params=["BATCH", "MULTILINE_MAX_LINES", "32", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Ergo")
|
|
||||||
def testByteLimit(self):
|
|
||||||
"""This is an Ergo-specific test for line limit enforcement
|
|
||||||
in multiline messages (see testLineLimit).
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.connectClient(
|
|
||||||
"alice", capabilities=(base_caps + [CAP_NAME]), skip_if_cap_nak=False
|
|
||||||
)
|
|
||||||
self.joinChannel(1, "#test")
|
|
||||||
|
|
||||||
# byte limit exceeded
|
|
||||||
self.sendLine(1, "BATCH +234 %s #test" % (BATCH_TYPE,))
|
|
||||||
for i in range(11):
|
|
||||||
self.sendLine(1, "@batch=234 PRIVMSG #test " + ("x" * 400))
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command="FAIL",
|
|
||||||
params=["BATCH", "MULTILINE_MAX_BYTES", "4096", ANYSTR],
|
|
||||||
)
|
|
||||||
|
505
irctest/server_tests/named_modes.py
Normal file
505
irctest/server_tests/named_modes.py
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from irctest import cases
|
||||||
|
from irctest.numerics import (
|
||||||
|
ERR_BANNEDFROMCHAN,
|
||||||
|
ERR_CANNOTSENDTOCHAN,
|
||||||
|
ERR_INVITEONLYCHAN,
|
||||||
|
RPL_CHMODELIST,
|
||||||
|
RPL_ENDOFLISTPROPLIST,
|
||||||
|
RPL_ENDOFPROPLIST,
|
||||||
|
RPL_LISTPROPLIST,
|
||||||
|
RPL_PROPLIST,
|
||||||
|
RPL_UMODELIST,
|
||||||
|
)
|
||||||
|
from irctest.patma import ANYLIST, ANYSTR, ListRemainder, StrRe
|
||||||
|
from irctest.runner import NotImplementedByController
|
||||||
|
|
||||||
|
CHMODES = {
|
||||||
|
"op",
|
||||||
|
"voice",
|
||||||
|
"ban",
|
||||||
|
"inviteonly",
|
||||||
|
"limit",
|
||||||
|
"moderated",
|
||||||
|
"noextmsg",
|
||||||
|
"key",
|
||||||
|
"private",
|
||||||
|
"topiclock",
|
||||||
|
"secret",
|
||||||
|
"banex",
|
||||||
|
"invex",
|
||||||
|
"admin",
|
||||||
|
"halfop",
|
||||||
|
"noctcp",
|
||||||
|
"owner",
|
||||||
|
"permanent",
|
||||||
|
"regonly",
|
||||||
|
"secureonly",
|
||||||
|
"mute",
|
||||||
|
}
|
||||||
|
|
||||||
|
UMODES = {
|
||||||
|
"invisible",
|
||||||
|
"oper",
|
||||||
|
"snomask",
|
||||||
|
"wallops",
|
||||||
|
"bot",
|
||||||
|
"hidechans",
|
||||||
|
"cloak",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _NamedModeTestMixin:
|
||||||
|
ALLOW_MODE_REPLY: bool
|
||||||
|
|
||||||
|
def assertNewBans(self, msgs, expected_masks):
|
||||||
|
"""Checks ``msgs`` is a set of PROP messages (and/or MODE if
|
||||||
|
``self.ALLOW_MODE_REPLY`` is True) that ban exactly the given set of masks."""
|
||||||
|
banned_masks = set()
|
||||||
|
for msg in msgs:
|
||||||
|
if self.ALLOW_MODE_REPLY and msg.command == "MODE":
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command="MODE", params=["#chan", StrRe(r"\+?b+"), *ANYLIST]
|
||||||
|
)
|
||||||
|
(_, chars, *args) = msg.params
|
||||||
|
chars = chars.lstrip("+")
|
||||||
|
self.assertEqual(
|
||||||
|
len(chars), len(args), "Mismatched number of +b and args"
|
||||||
|
)
|
||||||
|
banned_masks.update(args)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg,
|
||||||
|
command="PROP",
|
||||||
|
params=["#chan", ListRemainder(StrRe(r"\+ban=.+"), min_length=1)],
|
||||||
|
)
|
||||||
|
banned_masks.update(param.split("=")[1] for param in msg.params[1:])
|
||||||
|
|
||||||
|
self.assertEqual(banned_masks, expected_masks)
|
||||||
|
|
||||||
|
def assertNewUnbans(self, msgs, expected_masks):
|
||||||
|
"""Same as ``assertNewBans`` but for unbans."""
|
||||||
|
banned_masks = set()
|
||||||
|
for msg in msgs:
|
||||||
|
if self.ALLOW_MODE_REPLY and msg.command == "MODE":
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command="MODE", params=["#chan", StrRe(r"-b+"), *ANYLIST]
|
||||||
|
)
|
||||||
|
(_, chars, *args) = msg.params
|
||||||
|
chars = chars.lstrip("-")
|
||||||
|
self.assertEqual(
|
||||||
|
len(chars), len(args), "Mismatched number of -b and args"
|
||||||
|
)
|
||||||
|
banned_masks.update(args)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg,
|
||||||
|
command="PROP",
|
||||||
|
params=["#chan", ListRemainder(StrRe(r"-ban=.+"), min_length=1)],
|
||||||
|
)
|
||||||
|
banned_masks.update(param.split("=")[1] for param in msg.params[1:])
|
||||||
|
|
||||||
|
self.assertEqual(banned_masks, expected_masks)
|
||||||
|
|
||||||
|
@cases.mark_capabilities("draft/named-modes")
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testListMode(self):
|
||||||
|
"""Checks list modes (type 1), using 'ban' as example."""
|
||||||
|
self.connectClient(
|
||||||
|
"foo", name="user", capabilities=["draft/named-modes"], skip_if_cap_nak=True
|
||||||
|
)
|
||||||
|
self.connectClient("chanop", name="chanop", capabilities=["draft/named-modes"])
|
||||||
|
self.joinChannel("chanop", "#chan")
|
||||||
|
self.getMessages("chanop")
|
||||||
|
|
||||||
|
# Set ban
|
||||||
|
self.sendLine("chanop", "PROP #chan +ban=foo!*@*")
|
||||||
|
self.assertNewBans(self.getMessages("chanop"), {"foo!*@*"})
|
||||||
|
|
||||||
|
# Should not appear in the main list
|
||||||
|
self.sendLine("chanop", "PROP #chan")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST]
|
||||||
|
)
|
||||||
|
self.assertNotIn("ban", msg.params[2:])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFPROPLIST,
|
||||||
|
params=["chanop", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check banned
|
||||||
|
self.sendLine("chanop", "PROP #chan ban")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_LISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", "foo!*@*", *ANYLIST],
|
||||||
|
)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFLISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", ANYSTR],
|
||||||
|
)
|
||||||
|
self.sendLine("user", "JOIN #chan")
|
||||||
|
self.assertMessageMatch(self.getMessage("user"), command=ERR_BANNEDFROMCHAN)
|
||||||
|
|
||||||
|
# Unset ban
|
||||||
|
self.sendLine("chanop", "PROP #chan -ban=foo!*@*")
|
||||||
|
self.assertNewUnbans(self.getMessages("chanop"), {"foo!*@*"})
|
||||||
|
|
||||||
|
# Check unbanned
|
||||||
|
self.sendLine("chanop", "PROP #chan ban")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFLISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", ANYSTR],
|
||||||
|
)
|
||||||
|
self.sendLine("user", "JOIN #chan")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("user"), command="JOIN", params=["#chan"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_capabilities("draft/named-modes")
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testFlagModeDefaultOn(self):
|
||||||
|
"""Checks flag modes (type 4), using 'noextmsg' as example."""
|
||||||
|
self.connectClient(
|
||||||
|
"foo", name="user", capabilities=["draft/named-modes"], skip_if_cap_nak=True
|
||||||
|
)
|
||||||
|
self.connectClient("chanop", name="chanop", capabilities=["draft/named-modes"])
|
||||||
|
self.joinChannel("chanop", "#chan")
|
||||||
|
self.getMessages("chanop")
|
||||||
|
|
||||||
|
# Check set
|
||||||
|
self.sendLine("chanop", "PROP #chan")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST]
|
||||||
|
)
|
||||||
|
self.assertIn("noextmsg", msg.params[2:])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFPROPLIST,
|
||||||
|
params=["chanop", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
self.sendLine("user", "PRIVMSG #chan :hi")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("user"),
|
||||||
|
command=ERR_CANNOTSENDTOCHAN,
|
||||||
|
params=["foo", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages("chanop"), [])
|
||||||
|
|
||||||
|
# Unset
|
||||||
|
self.sendLine("chanop", "PROP #chan -noextmsg")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
if self.ALLOW_MODE_REPLY and msg.command == "MODE":
|
||||||
|
self.assertMessageMatch(msg, command="MODE", params=["#chan", "-noextmsg"])
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(msg, command="PROP", params=["#chan", "-noextmsg"])
|
||||||
|
|
||||||
|
# Check unset
|
||||||
|
self.sendLine("chanop", "PROP #chan")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST]
|
||||||
|
)
|
||||||
|
self.assertNotIn("noextmsg", msg.params[2:])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFPROPLIST,
|
||||||
|
params=["chanop", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
self.sendLine("user", "PRIVMSG #chan :hi")
|
||||||
|
self.assertEqual(self.getMessages("user"), [])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"), command="PRIVMSG", params=["#chan", "hi"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set
|
||||||
|
self.sendLine("chanop", "PROP #chan +noextmsg")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
if self.ALLOW_MODE_REPLY and msg.command == "MODE":
|
||||||
|
self.assertMessageMatch(msg, command="MODE", params=["#chan", "+noextmsg"])
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(msg, command="PROP", params=["#chan", "+noextmsg"])
|
||||||
|
|
||||||
|
# Check set again
|
||||||
|
self.sendLine("chanop", "PROP #chan")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST]
|
||||||
|
)
|
||||||
|
self.assertIn("noextmsg", msg.params[2:])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFPROPLIST,
|
||||||
|
params=["chanop", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
self.sendLine("user", "PRIVMSG #chan :hi")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("user"),
|
||||||
|
command=ERR_CANNOTSENDTOCHAN,
|
||||||
|
params=["foo", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages("chanop"), [])
|
||||||
|
|
||||||
|
@cases.mark_capabilities("draft/named-modes")
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testFlagModeDefaultOff(self):
|
||||||
|
"""Checks flag modes (type 4), using 'inviteonly' as example."""
|
||||||
|
self.connectClient(
|
||||||
|
"foo", name="user", capabilities=["draft/named-modes"], skip_if_cap_nak=True
|
||||||
|
)
|
||||||
|
self.connectClient("chanop", name="chanop", capabilities=["draft/named-modes"])
|
||||||
|
self.joinChannel("chanop", "#chan")
|
||||||
|
self.getMessages("chanop")
|
||||||
|
|
||||||
|
# Check unset
|
||||||
|
self.sendLine("chanop", "PROP #chan")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST]
|
||||||
|
)
|
||||||
|
self.assertNotIn("inviteonly", msg.params[2:])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFPROPLIST,
|
||||||
|
params=["chanop", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
self.sendLine("user", "JOIn #chan")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("user"), command="JOIN", params=["#chan"]
|
||||||
|
)
|
||||||
|
self.sendLine("user", "PART #chan :bye")
|
||||||
|
self.getMessages("user")
|
||||||
|
self.getMessages("chanop")
|
||||||
|
|
||||||
|
# Set
|
||||||
|
self.sendLine("chanop", "PROP #chan +inviteonly")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
if self.ALLOW_MODE_REPLY and msg.command == "MODE":
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command="MODE", params=["#chan", "+inviteonly"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command="PROP", params=["#chan", "+inviteonly"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check set
|
||||||
|
self.sendLine("chanop", "PROP #chan")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST]
|
||||||
|
)
|
||||||
|
self.assertIn("inviteonly", msg.params[2:])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFPROPLIST,
|
||||||
|
params=["chanop", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
self.sendLine("user", "JOIN #chan")
|
||||||
|
self.assertMessageMatch(self.getMessage("user"), command=ERR_INVITEONLYCHAN)
|
||||||
|
|
||||||
|
# Unset
|
||||||
|
self.sendLine("chanop", "PROP #chan -inviteonly")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
if self.ALLOW_MODE_REPLY and msg.command == "MODE":
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command="MODE", params=["#chan", "-inviteonly"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command="PROP", params=["#chan", "-inviteonly"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check unset again
|
||||||
|
self.sendLine("chanop", "PROP #chan")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST]
|
||||||
|
)
|
||||||
|
self.assertNotIn("inviteonly", msg.params[2:])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFPROPLIST,
|
||||||
|
params=["chanop", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
self.sendLine("user", "JOIn #chan")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("user"), command="JOIN", params=["#chan"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_capabilities("draft/named-modes")
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testManyListModes(self):
|
||||||
|
"""Checks setting three list modes (type 1) at once, using 'ban' as example."""
|
||||||
|
self.connectClient("chanop", name="chanop", capabilities=["draft/named-modes"])
|
||||||
|
self.joinChannel("chanop", "#chan")
|
||||||
|
self.getMessages("chanop")
|
||||||
|
|
||||||
|
if int(self.server_support.get("MAXMODES") or "1") < 3:
|
||||||
|
raise NotImplementedByController("MAXMODES is not >= 3.")
|
||||||
|
|
||||||
|
# Set ban
|
||||||
|
self.sendLine("chanop", "PROP #chan +ban=foo1!*@* +ban=foo2!*@* +ban=foo3!*@*")
|
||||||
|
msgs = self.getMessages("chanop")
|
||||||
|
self.assertNewBans(msgs, {"foo1!*@*", "foo2!*@*", "foo3!*@*"})
|
||||||
|
|
||||||
|
# Should not appear in the main list
|
||||||
|
self.sendLine("chanop", "PROP #chan")
|
||||||
|
msg = self.getMessage("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_PROPLIST, params=["chanop", "#chan", *ANYLIST]
|
||||||
|
)
|
||||||
|
self.assertNotIn("ban", msg.params[2:])
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFPROPLIST,
|
||||||
|
params=["chanop", "#chan", ANYSTR],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check banned
|
||||||
|
self.sendLine("chanop", "PROP #chan ban")
|
||||||
|
# TODO: make it so the order doesn't matter
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_LISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", "foo1!*@*", *ANYLIST],
|
||||||
|
)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_LISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", "foo2!*@*", *ANYLIST],
|
||||||
|
)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_LISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", "foo3!*@*", *ANYLIST],
|
||||||
|
)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFLISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", ANYSTR],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unset two bans
|
||||||
|
self.sendLine("chanop", "PROP #chan -ban=foo2!*@* -ban=foo3!*@*")
|
||||||
|
msgs = self.getMessages("chanop")
|
||||||
|
self.assertNewUnbans(msgs, {"foo2!*@*", "foo3!*@*"})
|
||||||
|
|
||||||
|
# Check unbanned
|
||||||
|
self.sendLine("chanop", "PROP #chan ban")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_LISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", "foo1!*@*", *ANYLIST],
|
||||||
|
)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("chanop"),
|
||||||
|
command=RPL_ENDOFLISTPROPLIST,
|
||||||
|
params=["chanop", "#chan", "ban", ANYSTR],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NamedModesTestCase(_NamedModeTestMixin, cases.BaseServerTestCase):
|
||||||
|
"""Normal testing of the named-modes spec."""
|
||||||
|
|
||||||
|
ALLOW_MODE_REPLY = True
|
||||||
|
|
||||||
|
@cases.mark_capabilities("draft/named-modes")
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testConnectionNumerics(self):
|
||||||
|
"""Tests RPL_CHMODELIST and RPL_UMODELIST."""
|
||||||
|
self.connectClient(
|
||||||
|
"capchk",
|
||||||
|
name="capchk",
|
||||||
|
capabilities=["draft/named-modes"],
|
||||||
|
skip_if_cap_nak=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.addClient(1)
|
||||||
|
self.sendLine(1, "CAP LS 302")
|
||||||
|
self.getCapLs(1)
|
||||||
|
self.sendLine(1, "USER user user user :user")
|
||||||
|
self.sendLine(1, "NICK user")
|
||||||
|
self.sendLine(1, "CAP END")
|
||||||
|
self.skipToWelcome(1)
|
||||||
|
msgs = self.getMessages(1)
|
||||||
|
|
||||||
|
seen_chmodes = set()
|
||||||
|
seen_umodes = set()
|
||||||
|
|
||||||
|
got_last_chmode = False
|
||||||
|
got_last_umode = False
|
||||||
|
capturing_re = r"[12345]:(?P<name>(\S+/)?[a-zA-Z0-9-]+)(=[a-zA-Z]+)?"
|
||||||
|
# fmt: off
|
||||||
|
chmode_re = r"^[12345]:(\S+/)?[a-zA-Z0-9-]+(=[a-zA-Z]+)?$"
|
||||||
|
umode_re = r"^[34]:(\S+/)?[a-zA-Z0-9-]+(=[a-zA-Z]+)?$" # noqa
|
||||||
|
# fmt: on
|
||||||
|
chmode_pat = [ListRemainder(StrRe(chmode_re), min_length=1)]
|
||||||
|
umode_pat = [ListRemainder(StrRe(umode_re), min_length=1)]
|
||||||
|
for msg in msgs:
|
||||||
|
if msg.command == RPL_CHMODELIST:
|
||||||
|
self.assertFalse(
|
||||||
|
got_last_chmode, "Got RPL_CHMODELIST after the list ended."
|
||||||
|
)
|
||||||
|
if msg.params[1] == "*":
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_CHMODELIST, params=["user", "*", *chmode_pat]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_CHMODELIST, params=["user", *chmode_pat]
|
||||||
|
)
|
||||||
|
got_last_chmode = True
|
||||||
|
|
||||||
|
for token in msg.params[-1].split(" "):
|
||||||
|
name = re.match(capturing_re, token).group("name")
|
||||||
|
self.assertNotIn(name, seen_chmodes, f"Duplicate chmode {name}")
|
||||||
|
seen_chmodes.add(name)
|
||||||
|
|
||||||
|
elif msg.command == RPL_UMODELIST:
|
||||||
|
self.assertFalse(
|
||||||
|
got_last_umode, "Got RPL_UMODELIST after the list ended."
|
||||||
|
)
|
||||||
|
if msg.params[1] == "*":
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_UMODELIST, params=["user", "*", *umode_pat]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
msg, command=RPL_UMODELIST, params=["user", *umode_pat]
|
||||||
|
)
|
||||||
|
got_last_umode = True
|
||||||
|
|
||||||
|
for token in msg.params[-1].split(" "):
|
||||||
|
name = re.match(capturing_re, token).group("name")
|
||||||
|
self.assertNotIn(name, seen_umodes, f"Duplicate umode {name}")
|
||||||
|
seen_umodes.add(name)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"noextmsg", seen_chmodes, "'noextmsg' chmode not supported/advertised"
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"invisible", seen_umodes, "'invisible' umode not supported/advertised"
|
||||||
|
)
|
||||||
|
|
||||||
|
unknown_chmodes = {m for m in seen_chmodes if "/" not in m} - CHMODES
|
||||||
|
unknown_umodes = {m for m in seen_umodes if "/" not in m} - UMODES
|
||||||
|
self.assertFalse(
|
||||||
|
unknown_chmodes, fail_msg="Got unknown unvendored chmodes: {got}"
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
unknown_umodes, fail_msg="Got unknown unvendored umodes: {got}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OverlyStrictNamedModesTestCase(_NamedModeTestMixin, cases.BaseServerTestCase):
|
||||||
|
"""Stronger tests, that assert the server only sends PROP and never MODE.
|
||||||
|
Passing these tests is not required to"""
|
||||||
|
|
||||||
|
ALLOW_MODE_REPLY = False
|
@ -11,7 +11,7 @@ from irctest.patma import ANYSTR, StrRe
|
|||||||
|
|
||||||
|
|
||||||
class NamesTestCase(cases.BaseServerTestCase):
|
class NamesTestCase(cases.BaseServerTestCase):
|
||||||
def _testNames(self, symbol: bool, allow_trailing_space: bool):
|
def _testNames(self, symbol):
|
||||||
self.connectClient("nick1")
|
self.connectClient("nick1")
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
@ -31,10 +31,7 @@ class NamesTestCase(cases.BaseServerTestCase):
|
|||||||
"nick1",
|
"nick1",
|
||||||
*(["="] if symbol else []),
|
*(["="] if symbol else []),
|
||||||
"#chan",
|
"#chan",
|
||||||
StrRe(
|
StrRe("(nick2 @nick1|@nick1 nick2)"),
|
||||||
"(nick2 @nick1|@nick1 nick2)"
|
|
||||||
+ (" ?" if allow_trailing_space else "")
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,59 +44,20 @@ class NamesTestCase(cases.BaseServerTestCase):
|
|||||||
@cases.mark_specifications("RFC1459", deprecated=True)
|
@cases.mark_specifications("RFC1459", deprecated=True)
|
||||||
def testNames1459(self):
|
def testNames1459(self):
|
||||||
"""
|
"""
|
||||||
|
https://modern.ircdocs.horse/#names-message
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
||||||
"""
|
"""
|
||||||
self._testNames(symbol=False, allow_trailing_space=True)
|
self._testNames(symbol=False)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812", "Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
def testNames2812(self):
|
def testNames2812(self):
|
||||||
"""
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNames(symbol=True, allow_trailing_space=True)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Bahamut", "irc2"], "Bahamut and irc2 send a trailing space in RPL_NAMREPLY"
|
|
||||||
)
|
|
||||||
def testNamesModern(self):
|
|
||||||
"""
|
"""
|
||||||
https://modern.ircdocs.horse/#names-message
|
https://modern.ircdocs.horse/#names-message
|
||||||
"""
|
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
||||||
self._testNames(symbol=True, allow_trailing_space=False)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812", "Modern")
|
|
||||||
def testNames2812Secret(self):
|
|
||||||
"""The symbol sent for a secret channel is `@` instead of `=`:
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
||||||
https://modern.ircdocs.horse/#rplnamreply-353
|
|
||||||
"""
|
"""
|
||||||
self.connectClient("nick1")
|
self._testNames(symbol=True)
|
||||||
self.sendLine(1, "JOIN #chan")
|
|
||||||
# enable secret channel mode
|
|
||||||
self.sendLine(1, "MODE #chan +s")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.sendLine(1, "NAMES #chan")
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[0],
|
|
||||||
command=RPL_NAMREPLY,
|
|
||||||
params=["nick1", "@", "#chan", StrRe("@nick1 ?")],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[1],
|
|
||||||
command=RPL_ENDOFNAMES,
|
|
||||||
params=["nick1", "#chan", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.connectClient("nick2")
|
|
||||||
self.sendLine(2, "JOIN #chan")
|
|
||||||
namreplies = [msg for msg in self.getMessages(2) if msg.command == RPL_NAMREPLY]
|
|
||||||
self.assertNotEqual(len(namreplies), 0)
|
|
||||||
for msg in namreplies:
|
|
||||||
self.assertMessageMatch(
|
|
||||||
msg, command=RPL_NAMREPLY, params=["nick2", "@", "#chan", ANYSTR]
|
|
||||||
)
|
|
||||||
|
|
||||||
def _testNamesMultipleChannels(self, symbol):
|
def _testNamesMultipleChannels(self, symbol):
|
||||||
self.connectClient("nick1")
|
self.connectClient("nick1")
|
||||||
|
@ -42,21 +42,14 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
|
|
||||||
# 'alice' is claimed, so 'Alice' is reserved and Bob cannot take it:
|
# case change: both alice and bob should get a successful nick line
|
||||||
self.sendLine(2, "NICK Alice")
|
|
||||||
ms = self.getMessages(2)
|
|
||||||
self.assertEqual(len(ms), 1)
|
|
||||||
self.assertMessageMatch(ms[0], command=ERR_NICKNAMEINUSE)
|
|
||||||
|
|
||||||
# but alice can change case to 'Alice'; both alice and bob should get
|
|
||||||
# a successful NICK line
|
|
||||||
self.sendLine(1, "NICK Alice")
|
self.sendLine(1, "NICK Alice")
|
||||||
ms = self.getMessages(1)
|
ms = self.getMessages(1)
|
||||||
self.assertEqual(len(ms), 1)
|
self.assertEqual(len(ms), 1)
|
||||||
self.assertMessageMatch(ms[0], nick="alice", command="NICK", params=["Alice"])
|
self.assertMessageMatch(ms[0], command="NICK", params=["Alice"])
|
||||||
ms = self.getMessages(2)
|
ms = self.getMessages(2)
|
||||||
self.assertEqual(len(ms), 1)
|
self.assertEqual(len(ms), 1)
|
||||||
self.assertMessageMatch(ms[0], nick="alice", command="NICK", params=["Alice"])
|
self.assertMessageMatch(ms[0], command="NICK", params=["Alice"])
|
||||||
|
|
||||||
# no responses, either to the user or to friends, from a no-op nick change
|
# no responses, either to the user or to friends, from a no-op nick change
|
||||||
self.sendLine(1, "NICK Alice")
|
self.sendLine(1, "NICK Alice")
|
||||||
@ -197,27 +190,3 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
|||||||
self.sendLine(2, "USER u s e r")
|
self.sendLine(2, "USER u s e r")
|
||||||
reply = self.getRegistrationMessage(2)
|
reply = self.getRegistrationMessage(2)
|
||||||
self.assertMessageMatch(reply, command=RPL_WELCOME)
|
self.assertMessageMatch(reply, command=RPL_WELCOME)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
def testLabeledNick(self):
|
|
||||||
"""
|
|
||||||
InspIRCd up to 3.16.1 used the new nick as source of NICK changes
|
|
||||||
|
|
||||||
https://github.com/inspircd/inspircd/issues/2067
|
|
||||||
|
|
||||||
https://github.com/inspircd/inspircd/commit/83f01b36a11734fd91a4e7aad99c15463858fe4a
|
|
||||||
"""
|
|
||||||
self.connectClient(
|
|
||||||
"alice",
|
|
||||||
capabilities=["batch", "labeled-response"],
|
|
||||||
skip_if_cap_nak=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "@label=abc NICK alice2")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
nick="alice",
|
|
||||||
command="NICK",
|
|
||||||
params=["alice2"],
|
|
||||||
tags={"label": "abc", **ANYDICT},
|
|
||||||
)
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
|
|
||||||
from irctest import cases, runner, scram
|
from irctest import cases, runner, scram
|
||||||
from irctest.numerics import ERR_SASLFAIL, RPL_LOGGEDIN, RPL_SASLMECHS
|
from irctest.numerics import ERR_SASLFAIL
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
|
|
||||||
|
|
||||||
@ -48,37 +48,11 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command=RPL_LOGGEDIN,
|
command="900",
|
||||||
params=[ANYSTR, ANYSTR, "jilles", ANYSTR],
|
params=[ANYSTR, ANYSTR, "jilles", ANYSTR],
|
||||||
fail_msg="Unexpected reply to correct SASL authentication: {msg}",
|
fail_msg="Unexpected reply to correct SASL authentication: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
|
||||||
def testPlainFailure(self):
|
|
||||||
"""PLAIN authentication with incorrect username/password."""
|
|
||||||
self.controller.registerUser(self, "jilles", "sesame")
|
|
||||||
self.addClient()
|
|
||||||
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
|
|
||||||
self.sendLine(1, "AUTHENTICATE PLAIN")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="AUTHENTICATE",
|
|
||||||
params=["+"],
|
|
||||||
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
|
|
||||||
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
|
|
||||||
)
|
|
||||||
# password 'millet'
|
|
||||||
self.sendLine(1, "AUTHENTICATE amlsbGVzAGppbGxlcwBtaWxsZXQ=")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command=ERR_SASLFAIL,
|
|
||||||
params=[ANYSTR, ANYSTR],
|
|
||||||
fail_msg="Unexpected reply to incorrect SASL authentication: {msg}",
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.skipUnlessHasMechanism("PLAIN")
|
||||||
def testPlainNonAscii(self):
|
def testPlainNonAscii(self):
|
||||||
@ -187,11 +161,11 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
|
self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=False)
|
||||||
self.sendLine(1, "AUTHENTICATE FOO")
|
self.sendLine(1, "AUTHENTICATE FOO")
|
||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
while m.command == RPL_SASLMECHS:
|
while m.command == "908": # RPL_SASLMECHS
|
||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command=ERR_SASLFAIL,
|
command="904",
|
||||||
fail_msg="Did not reply with 904 to “AUTHENTICATE FOO”: {msg}",
|
fail_msg="Did not reply with 904 to “AUTHENTICATE FOO”: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases, runner
|
||||||
from irctest.numerics import ERR_ERRONEUSNICKNAME
|
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
|
|
||||||
|
|
||||||
@ -54,23 +53,15 @@ class Utf8TestCase(cases.BaseServerTestCase):
|
|||||||
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
||||||
|
|
||||||
self.addClient()
|
self.addClient()
|
||||||
self.sendLine(2, "NICK bar")
|
self.sendLine(2, "NICK foo")
|
||||||
self.clients[2].conn.sendall(b"USER username * * :i\xe8rc\xe9\r\n")
|
self.clients[2].conn.sendall(b"USER username * * :i\xe8rc\xe9\r\n")
|
||||||
|
|
||||||
d = b""
|
d = self.clients[2].conn.recv(1024)
|
||||||
while True:
|
if b" FAIL " in d or b" 468 " in d: # ERR_INVALIDUSERNAME
|
||||||
try:
|
|
||||||
buf = self.clients[2].conn.recv(1024)
|
|
||||||
except TimeoutError:
|
|
||||||
break
|
|
||||||
if d and not buf:
|
|
||||||
break
|
|
||||||
d += buf
|
|
||||||
if b"FAIL " in d or b"ERROR " in d or b"468 " in d: # ERR_INVALIDUSERNAME
|
|
||||||
return # nothing more to test
|
return # nothing more to test
|
||||||
self.assertIn(b" 001 ", d)
|
self.assertIn(b" 001 ", d)
|
||||||
|
|
||||||
self.sendLine(2, "WHOIS bar")
|
self.sendLine(2, "WHOIS foo")
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
|
|
||||||
def testNonutf8Username(self):
|
def testNonutf8Username(self):
|
||||||
@ -79,56 +70,14 @@ class Utf8TestCase(cases.BaseServerTestCase):
|
|||||||
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
||||||
|
|
||||||
self.addClient()
|
self.addClient()
|
||||||
self.sendLine(2, "NICK bar")
|
self.sendLine(2, "NICK foo")
|
||||||
self.clients[2].conn.sendall(b"USER \xe8rc\xe9 * * :readlname\r\n")
|
self.sendLine(2, "USER 😊😊😊😊😊😊😊😊😊😊 * * :realname")
|
||||||
|
m = self.getRegistrationMessage(2)
|
||||||
d = b""
|
if m.command in ("FAIL", "468"): # ERR_INVALIDUSERNAME
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
buf = self.clients[2].conn.recv(1024)
|
|
||||||
except TimeoutError:
|
|
||||||
break
|
|
||||||
if d and not buf:
|
|
||||||
break
|
|
||||||
d += buf
|
|
||||||
if b"FAIL " in d or b"ERROR " in d or b"468 " in d: # ERR_INVALIDUSERNAME
|
|
||||||
return # nothing more to test
|
return # nothing more to test
|
||||||
self.assertIn(b"001 ", d)
|
|
||||||
|
|
||||||
self.sendLine(2, "WHOIS bar")
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
|
|
||||||
class ErgoUtf8NickEnabledTestCase(cases.BaseServerTestCase):
|
|
||||||
@staticmethod
|
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
|
||||||
return cases.TestCaseControllerConfig(
|
|
||||||
ergo_config=lambda config: config["server"].update(
|
|
||||||
{"casemapping": "precis"},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Ergo")
|
|
||||||
def testUtf8NonAsciiNick(self):
|
|
||||||
"""Ergo accepts certain non-ASCII UTF8 nicknames if PRECIS is enabled."""
|
|
||||||
self.connectClient("Işıl")
|
|
||||||
self.joinChannel(1, "#test")
|
|
||||||
|
|
||||||
self.connectClient("Claire")
|
|
||||||
self.joinChannel(2, "#test")
|
|
||||||
|
|
||||||
self.sendLine(1, "PRIVMSG #test :hi there")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
self.getMessage(2), nick="Işıl", params=["#test", "hi there"]
|
m,
|
||||||
|
command="001",
|
||||||
)
|
)
|
||||||
|
self.sendLine(2, "WHOIS foo")
|
||||||
|
self.getMessages(2)
|
||||||
class ErgoUtf8NickDisabledTestCase(cases.BaseServerTestCase):
|
|
||||||
@cases.mark_specifications("Ergo")
|
|
||||||
def testUtf8NonAsciiNick(self):
|
|
||||||
"""Ergo rejects non-ASCII nicknames in its default configuration."""
|
|
||||||
self.addClient(1)
|
|
||||||
self.sendLine(1, "USER u s e r")
|
|
||||||
self.sendLine(1, "NICK Işıl")
|
|
||||||
self.assertMessageMatch(self.getMessage(1), command=ERR_ERRONEUSNICKNAME)
|
|
||||||
|
@ -60,7 +60,7 @@ class BaseWhoTestCase:
|
|||||||
"*", # no chan
|
"*", # no chan
|
||||||
StrRe("~?" + self.username),
|
StrRe("~?" + self.username),
|
||||||
StrRe(host_re),
|
StrRe(host_re),
|
||||||
StrRe(r"(My.Little.Server|\*)"),
|
"My.Little.Server",
|
||||||
"coolNick",
|
"coolNick",
|
||||||
flags,
|
flags,
|
||||||
StrRe(realname_regexp(self.realname)),
|
StrRe(realname_regexp(self.realname)),
|
||||||
@ -76,7 +76,7 @@ class BaseWhoTestCase:
|
|||||||
"#chan",
|
"#chan",
|
||||||
StrRe("~?" + self.username),
|
StrRe("~?" + self.username),
|
||||||
StrRe(host_re),
|
StrRe(host_re),
|
||||||
StrRe(r"(My.Little.Server|\*)"),
|
"My.Little.Server",
|
||||||
"coolNick",
|
"coolNick",
|
||||||
flags + "@",
|
flags + "@",
|
||||||
StrRe(realname_regexp(self.realname)),
|
StrRe(realname_regexp(self.realname)),
|
||||||
@ -87,7 +87,7 @@ class BaseWhoTestCase:
|
|||||||
class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoStar(self):
|
def testWhoStar(self):
|
||||||
if self.controller.software_name in ("Bahamut",):
|
if self.controller.software_name in ("Bahamut", "Sable"):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -118,7 +118,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoNick(self, mask):
|
def testWhoNick(self, mask):
|
||||||
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -148,7 +148,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
ids=["username", "realname-mask", "hostname"],
|
ids=["username", "realname-mask", "hostname"],
|
||||||
)
|
)
|
||||||
def testWhoUsernameRealName(self, mask):
|
def testWhoUsernameRealName(self, mask):
|
||||||
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -201,7 +201,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoNickAway(self, mask):
|
def testWhoNickAway(self, mask):
|
||||||
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -235,7 +235,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoNickOper(self, mask):
|
def testWhoNickOper(self, mask):
|
||||||
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -274,7 +274,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoNickAwayAndOper(self, mask):
|
def testWhoNickAwayAndOper(self, mask):
|
||||||
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -308,7 +308,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
@pytest.mark.parametrize("mask", ["#chan", "#CHAN"], ids=["exact", "casefolded"])
|
@pytest.mark.parametrize("mask", ["#chan", "#CHAN"], ids=["exact", "casefolded"])
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoChan(self, mask):
|
def testWhoChan(self, mask):
|
||||||
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -336,7 +336,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
"#chan",
|
"#chan",
|
||||||
StrRe("~?" + self.username),
|
StrRe("~?" + self.username),
|
||||||
StrRe(host_re),
|
StrRe(host_re),
|
||||||
StrRe(r"(My.Little.Server|\*)"),
|
"My.Little.Server",
|
||||||
"coolNick",
|
"coolNick",
|
||||||
"G@",
|
"G@",
|
||||||
StrRe(realname_regexp(self.realname)),
|
StrRe(realname_regexp(self.realname)),
|
||||||
@ -351,7 +351,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
"#chan",
|
"#chan",
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
StrRe(r"(My.Little.Server|\*)"),
|
"My.Little.Server",
|
||||||
"otherNick",
|
"otherNick",
|
||||||
"H",
|
"H",
|
||||||
StrRe("[0-9]+ .*"),
|
StrRe("[0-9]+ .*"),
|
||||||
@ -398,7 +398,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
chan,
|
chan,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
StrRe(r"(My.Little.Server|\*)"),
|
"My.Little.Server",
|
||||||
"coolNick",
|
"coolNick",
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
@ -413,7 +413,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
chan,
|
chan,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
StrRe(r"(My.Little.Server|\*)"),
|
"My.Little.Server",
|
||||||
"otherNick",
|
"otherNick",
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
@ -479,7 +479,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
StrRe("~?myusernam"),
|
StrRe("~?myusernam"),
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
StrRe(r"(My.Little.Server|\*)"),
|
"My.Little.Server",
|
||||||
"coolNick",
|
"coolNick",
|
||||||
StrRe("H@?"),
|
StrRe("H@?"),
|
||||||
ANYSTR, # hopcount
|
ANYSTR, # hopcount
|
||||||
@ -632,7 +632,7 @@ class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
class WhoInvisibleTestCase(cases.BaseServerTestCase):
|
class WhoInvisibleTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoInvisible(self):
|
def testWhoInvisible(self):
|
||||||
if self.controller.software_name in ("Bahamut",):
|
if self.controller.software_name in ("Bahamut", "Sable"):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self.connectClient("evan", name="evan")
|
self.connectClient("evan", name="evan")
|
||||||
|
@ -8,7 +8,6 @@ import pytest
|
|||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import (
|
from irctest.numerics import (
|
||||||
ERR_NOSUCHNICK,
|
|
||||||
RPL_AWAY,
|
RPL_AWAY,
|
||||||
RPL_ENDOFWHOIS,
|
RPL_ENDOFWHOIS,
|
||||||
RPL_WHOISACCOUNT,
|
RPL_WHOISACCOUNT,
|
||||||
@ -57,7 +56,6 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
|
|||||||
[m.command for m in self.getMessages(1)],
|
[m.command for m in self.getMessages(1)],
|
||||||
fail_msg="OPER failed",
|
fail_msg="OPER failed",
|
||||||
)
|
)
|
||||||
self.getMessages(1) # make sure we did get all oper-up messages
|
|
||||||
|
|
||||||
self.sendLine(1, "WHOIS nick2")
|
self.sendLine(1, "WHOIS nick2")
|
||||||
|
|
||||||
@ -97,9 +95,7 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
|
|||||||
params=[
|
params=[
|
||||||
"nick1",
|
"nick1",
|
||||||
"nick2",
|
"nick2",
|
||||||
# trailing space was required by the RFCs, and Modern explicitly
|
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1)"),
|
||||||
# allows it
|
|
||||||
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1) ?"),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
elif m.command == RPL_WHOISSPECIAL:
|
elif m.command == RPL_WHOISSPECIAL:
|
||||||
@ -220,25 +216,6 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
|
|||||||
whois_user.params[3], [nick, username, "~" + username, realname]
|
whois_user.params[3], [nick, username, "~" + username, realname]
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812")
|
|
||||||
@cases.xfailIfSoftware(["Sable"], "https://github.com/Libera-Chat/sable/issues/101")
|
|
||||||
def testWhoisMissingUser(self):
|
|
||||||
"""Test WHOIS on a nonexistent nickname."""
|
|
||||||
self.connectClient("qux", name="qux")
|
|
||||||
self.sendLine("qux", "WHOIS bar")
|
|
||||||
messages = self.getMessages("qux")
|
|
||||||
self.assertEqual(len(messages), 2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[0],
|
|
||||||
command=ERR_NOSUCHNICK,
|
|
||||||
params=["qux", "bar", ANYSTR],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[1],
|
|
||||||
command=RPL_ENDOFWHOIS,
|
|
||||||
params=["qux", "bar", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"away,oper",
|
"away,oper",
|
||||||
[(False, False), (True, False), (False, True)],
|
[(False, False), (True, False), (False, True)],
|
||||||
|
@ -7,7 +7,6 @@ The WHOSWAS command (`RFC 1459
|
|||||||
TODO: cross-reference Modern
|
TODO: cross-reference Modern
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -145,8 +144,6 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
time.sleep(1) # Ergo may take a little while to record the nick as free
|
|
||||||
|
|
||||||
self.connectClient("nick2", ident="ident3")
|
self.connectClient("nick2", ident="ident3")
|
||||||
self.sendLine(3, "QUIT :bye")
|
self.sendLine(3, "QUIT :bye")
|
||||||
try:
|
try:
|
||||||
@ -154,9 +151,6 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.controller.software_name == "Sable":
|
|
||||||
time.sleep(1) # may take a little while to record the historical user
|
|
||||||
|
|
||||||
self.sendLine(1, whowas_command)
|
self.sendLine(1, whowas_command)
|
||||||
|
|
||||||
messages = self.getMessages(1)
|
messages = self.getMessages(1)
|
||||||
|
@ -38,6 +38,7 @@ class Capabilities(enum.Enum):
|
|||||||
MESSAGE_TAGS = "message-tags"
|
MESSAGE_TAGS = "message-tags"
|
||||||
MULTILINE = "draft/multiline"
|
MULTILINE = "draft/multiline"
|
||||||
MULTI_PREFIX = "multi-prefix"
|
MULTI_PREFIX = "multi-prefix"
|
||||||
|
NAMED_MODES = "draft/named-modes"
|
||||||
SERVER_TIME = "server-time"
|
SERVER_TIME = "server-time"
|
||||||
SETNAME = "setname"
|
SETNAME = "setname"
|
||||||
STS = "sts"
|
STS = "sts"
|
||||||
|
@ -65,7 +65,7 @@ def get_install_steps(*, software_config, software_id, version_flavor):
|
|||||||
install_steps = [
|
install_steps = [
|
||||||
{
|
{
|
||||||
"name": f"Checkout {name}",
|
"name": f"Checkout {name}",
|
||||||
"uses": "actions/checkout@v4",
|
"uses": "actions/checkout@v3",
|
||||||
"with": {
|
"with": {
|
||||||
"repository": software_config["repository"],
|
"repository": software_config["repository"],
|
||||||
"ref": ref,
|
"ref": ref,
|
||||||
@ -94,7 +94,7 @@ def get_build_job(*, software_config, software_id, version_flavor):
|
|||||||
cache = [
|
cache = [
|
||||||
{
|
{
|
||||||
"name": "Cache dependencies",
|
"name": "Cache dependencies",
|
||||||
"uses": "actions/cache@v4",
|
"uses": "actions/cache@v3",
|
||||||
"with": {
|
"with": {
|
||||||
"path": f"~/.cache\n${{ github.workspace }}/{path}\n",
|
"path": f"~/.cache\n${{ github.workspace }}/{path}\n",
|
||||||
"key": "3-${{ runner.os }}-"
|
"key": "3-${{ runner.os }}-"
|
||||||
@ -123,10 +123,10 @@ def get_build_job(*, software_config, software_id, version_flavor):
|
|||||||
"run": "cd ~/; mkdir -p .local/ go/",
|
"run": "cd ~/; mkdir -p .local/ go/",
|
||||||
},
|
},
|
||||||
*cache,
|
*cache,
|
||||||
{"uses": "actions/checkout@v4"},
|
{"uses": "actions/checkout@v3"},
|
||||||
{
|
{
|
||||||
"name": "Set up Python 3.11",
|
"name": "Set up Python 3.11",
|
||||||
"uses": "actions/setup-python@v5",
|
"uses": "actions/setup-python@v4",
|
||||||
"with": {"python-version": 3.11},
|
"with": {"python-version": 3.11},
|
||||||
},
|
},
|
||||||
*install_steps,
|
*install_steps,
|
||||||
@ -160,7 +160,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
|
|||||||
downloads.append(
|
downloads.append(
|
||||||
{
|
{
|
||||||
"name": "Download build artefacts",
|
"name": "Download build artefacts",
|
||||||
"uses": "actions/download-artifact@v4",
|
"uses": "actions/download-artifact@v3",
|
||||||
"with": {"name": f"installed-{software_id}", "path": "~"},
|
"with": {"name": f"installed-{software_id}", "path": "~"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -195,10 +195,10 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
|
|||||||
"runs-on": "ubuntu-22.04",
|
"runs-on": "ubuntu-22.04",
|
||||||
"needs": needs,
|
"needs": needs,
|
||||||
"steps": [
|
"steps": [
|
||||||
{"uses": "actions/checkout@v4"},
|
{"uses": "actions/checkout@v3"},
|
||||||
{
|
{
|
||||||
"name": "Set up Python 3.11",
|
"name": "Set up Python 3.11",
|
||||||
"uses": "actions/setup-python@v5",
|
"uses": "actions/setup-python@v4",
|
||||||
"with": {"python-version": 3.11},
|
"with": {"python-version": 3.11},
|
||||||
},
|
},
|
||||||
*downloads,
|
*downloads,
|
||||||
@ -212,7 +212,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
|
|||||||
"name": "Install irctest dependencies",
|
"name": "Install irctest dependencies",
|
||||||
"run": script(
|
"run": script(
|
||||||
"python -m pip install --upgrade pip",
|
"python -m pip install --upgrade pip",
|
||||||
"pip install pytest pytest-xdist pytest-timeout -r requirements.txt",
|
"pip install pytest pytest-xdist -r requirements.txt",
|
||||||
*(
|
*(
|
||||||
software_config["extra_deps"]
|
software_config["extra_deps"]
|
||||||
if "extra_deps" in software_config
|
if "extra_deps" in software_config
|
||||||
@ -223,11 +223,8 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
|
|||||||
{
|
{
|
||||||
"name": "Test with pytest",
|
"name": "Test with pytest",
|
||||||
"timeout-minutes": 30,
|
"timeout-minutes": 30,
|
||||||
"env": {
|
|
||||||
"IRCTEST_DEBUG_LOGS": "${{ runner.debug }}",
|
|
||||||
},
|
|
||||||
"run": (
|
"run": (
|
||||||
f"PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' "
|
f"PYTEST_ARGS='--junit-xml pytest.xml' "
|
||||||
f"PATH=$HOME/.local/bin:$PATH "
|
f"PATH=$HOME/.local/bin:$PATH "
|
||||||
f"{env}make {test_id}"
|
f"{env}make {test_id}"
|
||||||
),
|
),
|
||||||
@ -235,7 +232,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
|
|||||||
{
|
{
|
||||||
"name": "Publish results",
|
"name": "Publish results",
|
||||||
"if": "always()",
|
"if": "always()",
|
||||||
"uses": "actions/upload-artifact@v4",
|
"uses": "actions/upload-artifact@v3",
|
||||||
"with": {
|
"with": {
|
||||||
"name": f"pytest-results_{test_id}_{version_flavor.value}",
|
"name": f"pytest-results_{test_id}_{version_flavor.value}",
|
||||||
"path": "pytest.xml",
|
"path": "pytest.xml",
|
||||||
@ -254,7 +251,7 @@ def upload_steps(software_id):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Upload build artefacts",
|
"name": "Upload build artefacts",
|
||||||
"uses": "actions/upload-artifact@v4",
|
"uses": "actions/upload-artifact@v3",
|
||||||
"with": {
|
"with": {
|
||||||
"name": f"installed-{software_id}",
|
"name": f"installed-{software_id}",
|
||||||
"path": "~/artefacts-*.tar.gz",
|
"path": "~/artefacts-*.tar.gz",
|
||||||
@ -315,10 +312,10 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
|
|||||||
# this job then
|
# this job then
|
||||||
"if": "success() || failure()",
|
"if": "success() || failure()",
|
||||||
"steps": [
|
"steps": [
|
||||||
{"uses": "actions/checkout@v4"},
|
{"uses": "actions/checkout@v3"},
|
||||||
{
|
{
|
||||||
"name": "Download Artifacts",
|
"name": "Download Artifacts",
|
||||||
"uses": "actions/download-artifact@v4",
|
"uses": "actions/download-artifact@v3",
|
||||||
"with": {"path": "artifacts"},
|
"with": {"path": "artifacts"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
2
mypy.ini
2
mypy.ini
@ -1,5 +1,5 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
python_version = 3.8
|
python_version = 3.7
|
||||||
warn_return_any = True
|
warn_return_any = True
|
||||||
warn_unused_configs = True
|
warn_unused_configs = True
|
||||||
|
|
||||||
|
25
patches/inspircd_mainloop.patch
Normal file
25
patches/inspircd_mainloop.patch
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
When a client registers (ie. sends USER+NICK), InspIRCd does not
|
||||||
|
immediately answers with 001. Instead it waits for the next iteration
|
||||||
|
of the main loop to call `DoBackgroundUserStuff`.
|
||||||
|
|
||||||
|
However, this main loop executes only once a second. This is usually
|
||||||
|
fine, but makes irctest considerably slower, as irctest uses hundreds
|
||||||
|
of very short-lived connections.
|
||||||
|
|
||||||
|
This patch removes the frequency limitation of the main loop to make
|
||||||
|
InspIRCd more responsive.
|
||||||
|
|
||||||
|
diff --git a/src/inspircd.cpp b/src/inspircd.cpp
|
||||||
|
index 5760e631b..1da0285fb 100644
|
||||||
|
--- a/src/inspircd.cpp
|
||||||
|
+++ b/src/inspircd.cpp
|
||||||
|
@@ -680,7 +680,7 @@ void InspIRCd::Run()
|
||||||
|
* timing using this event, so we dont have to
|
||||||
|
* time this exactly).
|
||||||
|
*/
|
||||||
|
- if (TIME.tv_sec != OLDTIME)
|
||||||
|
+ if (true)
|
||||||
|
{
|
||||||
|
CollectStats();
|
||||||
|
CheckTimeSkip(OLDTIME, TIME.tv_sec);
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
[tool.black]
|
[tool.black]
|
||||||
target-version = ['py38']
|
target-version = ['py37']
|
||||||
exclude = 'irctest/scram/*'
|
exclude = 'irctest/scram/*'
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
|
@ -29,6 +29,7 @@ markers =
|
|||||||
message-tags
|
message-tags
|
||||||
draft/multiline
|
draft/multiline
|
||||||
multi-prefix
|
multi-prefix
|
||||||
|
draft/named-modes
|
||||||
server-time
|
server-time
|
||||||
setname
|
setname
|
||||||
sts
|
sts
|
||||||
|
@ -134,9 +134,9 @@ software:
|
|||||||
path: ergo
|
path: ergo
|
||||||
prefix: ~/go
|
prefix: ~/go
|
||||||
pre_deps:
|
pre_deps:
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '^1.22.0'
|
go-version: '^1.21.0'
|
||||||
- run: go version
|
- run: go version
|
||||||
separate_build_job: false
|
separate_build_job: false
|
||||||
build_script: |
|
build_script: |
|
||||||
@ -148,7 +148,7 @@ software:
|
|||||||
name: InspIRCd
|
name: InspIRCd
|
||||||
repository: inspircd/inspircd
|
repository: inspircd/inspircd
|
||||||
refs: &inspircd_refs
|
refs: &inspircd_refs
|
||||||
stable: v3.17.1
|
stable: v3.15.0
|
||||||
release: null
|
release: null
|
||||||
devel: master
|
devel: master
|
||||||
devel_release: insp3
|
devel_release: insp3
|
||||||
@ -158,7 +158,13 @@ software:
|
|||||||
separate_build_job: true
|
separate_build_job: true
|
||||||
build_script: &inspircd_build_script |
|
build_script: &inspircd_build_script |
|
||||||
cd $GITHUB_WORKSPACE/inspircd/
|
cd $GITHUB_WORKSPACE/inspircd/
|
||||||
|
|
||||||
|
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
|
||||||
|
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
|
||||||
|
|
||||||
|
wget https://raw.githubusercontent.com/progval/inspircd-contrib/namedmodes/4.0/m_ircv3_namedmodes.cpp -O src/modules/m_ircv3_namedmodes.cpp
|
||||||
./configure --prefix=$HOME/.local/inspircd --development
|
./configure --prefix=$HOME/.local/inspircd --development
|
||||||
|
|
||||||
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
||||||
make install
|
make install
|
||||||
irc2:
|
irc2:
|
||||||
@ -230,7 +236,7 @@ software:
|
|||||||
name: ngircd
|
name: ngircd
|
||||||
repository: ngircd/ngircd
|
repository: ngircd/ngircd
|
||||||
refs:
|
refs:
|
||||||
stable: acf8409c60ccc96beed0a1f990c4f9374823c0ce # three months ahead of v27
|
stable: 0714466af88d71d6c395629cd7fb624b099507d4 # two years ahead of rel-26.1
|
||||||
release: null
|
release: null
|
||||||
devel: master
|
devel: master
|
||||||
devel_release: null
|
devel_release: null
|
||||||
@ -249,7 +255,7 @@ software:
|
|||||||
name: Sable
|
name: Sable
|
||||||
repository: Libera-Chat/sable
|
repository: Libera-Chat/sable
|
||||||
refs:
|
refs:
|
||||||
stable: e9701e5e8d0c4f278ddd61ce7285f4918ecf99e9
|
stable: ff1179512a79eba57ca468a5f83af84ecce08a5b
|
||||||
release: null
|
release: null
|
||||||
devel: master
|
devel: master
|
||||||
devel_release: null
|
devel_release: null
|
||||||
@ -300,8 +306,8 @@ software:
|
|||||||
name: UnrealIRCd 6
|
name: UnrealIRCd 6
|
||||||
repository: unrealircd/unrealircd
|
repository: unrealircd/unrealircd
|
||||||
refs:
|
refs:
|
||||||
stable: a68625454078641ce984eeb197f7e02b1857ab6c # 6.1.7.1
|
stable: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7
|
||||||
release: a68625454078641ce984eeb197f7e02b1857ab6c # 6.1.7.1
|
release: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7
|
||||||
devel: unreal60_dev
|
devel: unreal60_dev
|
||||||
devel_release: null
|
devel_release: null
|
||||||
path: unrealircd
|
path: unrealircd
|
||||||
@ -343,16 +349,16 @@ software:
|
|||||||
separate_build_job: true
|
separate_build_job: true
|
||||||
path: anope
|
path: anope
|
||||||
refs:
|
refs:
|
||||||
stable: "2.0.14"
|
stable: "2.0.9"
|
||||||
release: "2.1.1"
|
release: "2.0.9"
|
||||||
devel: "2.1"
|
devel: "2.0.9"
|
||||||
devel_release: "2.0"
|
devel_release: "2.0.9"
|
||||||
build_script: |
|
build_script: |
|
||||||
cd $GITHUB_WORKSPACE/anope/
|
cd $GITHUB_WORKSPACE/anope/
|
||||||
sudo apt-get install ninja-build --no-install-recommends
|
cp $GITHUB_WORKSPACE/data/anope/* .
|
||||||
mkdir build && cd build
|
CFLAGS=-O0 ./Config -quick
|
||||||
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local/ -DPROGRAM_NAME=anope -DUSE_PCH=ON -GNinja ..
|
make -C build -j 4
|
||||||
ninja install
|
make -C build install
|
||||||
|
|
||||||
dlk:
|
dlk:
|
||||||
name: Dlk
|
name: Dlk
|
||||||
@ -389,7 +395,7 @@ software:
|
|||||||
run: pip install limnoria cryptography pyxmpp2-scram
|
run: pip install limnoria cryptography pyxmpp2-scram
|
||||||
devel:
|
devel:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install git+https://github.com/progval/Limnoria.git@master cryptography pyxmpp2-scram
|
run: pip install git+https://github.com/ProgVal/Limnoria.git@testing cryptography pyxmpp2-scram
|
||||||
devel_release: null
|
devel_release: null
|
||||||
|
|
||||||
sopel:
|
sopel:
|
||||||
|
Reference in New Issue
Block a user